Ensure path encoding is handled consistently for prerendered pages (#19135)
This ensures we handle encoding/decoding for SSG prerendered/fallback pages correctly. Since we only encode path delimiters when outputting to the disk we need to match this encoding when building the `ssgCacheKey` to look-up the prerendered pages. This also fixes non-ascii prerendered paths (e.g. 商業日語) not matching correctly. This does not resolve 👉 https://github.com/vercel/next.js/issues/10084 and further investigation will be needed before addressing non-ascii paths for non-SSG pages. The encoding output was tested against https://tst-encoding-l7amu5b9c.vercel.app/ to ensure the values will match correctly on Vercel. Closes: https://github.com/vercel/next.js/issues/17582 Closes: https://github.com/vercel/next.js/issues/17642 x-ref: https://github.com/vercel/next.js/pull/14717
This commit is contained in:
parent
e6c351859f
commit
1203b9082b
12 changed files with 624 additions and 25 deletions
|
@ -523,6 +523,7 @@ export default async function build(
|
|||
const hybridAmpPages = new Set<string>()
|
||||
const serverPropsPages = new Set<string>()
|
||||
const additionalSsgPaths = new Map<string, Array<string>>()
|
||||
const additionalSsgPathsEncoded = new Map<string, Array<string>>()
|
||||
const pageInfos = new Map<string, PageInfo>()
|
||||
const pagesManifest = JSON.parse(
|
||||
await promises.readFile(manifestPath, 'utf8')
|
||||
|
@ -640,8 +641,15 @@ export default async function build(
|
|||
ssgPages.add(page)
|
||||
isSsg = true
|
||||
|
||||
if (workerResult.prerenderRoutes) {
|
||||
if (
|
||||
workerResult.prerenderRoutes &&
|
||||
workerResult.encodedPrerenderRoutes
|
||||
) {
|
||||
additionalSsgPaths.set(page, workerResult.prerenderRoutes)
|
||||
additionalSsgPathsEncoded.set(
|
||||
page,
|
||||
workerResult.encodedPrerenderRoutes
|
||||
)
|
||||
ssgPageRoutes = workerResult.prerenderRoutes
|
||||
}
|
||||
|
||||
|
@ -841,8 +849,13 @@ export default async function build(
|
|||
// Append the "well-known" routes we should prerender for, e.g. blog
|
||||
// post slugs.
|
||||
additionalSsgPaths.forEach((routes, page) => {
|
||||
routes.forEach((route) => {
|
||||
defaultMap[route] = { page }
|
||||
const encodedRoutes = additionalSsgPathsEncoded.get(page)
|
||||
|
||||
routes.forEach((route, routeIdx) => {
|
||||
defaultMap[route] = {
|
||||
page,
|
||||
query: { __nextSsgPath: encodedRoutes?.[routeIdx] },
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -515,9 +515,13 @@ export async function buildStaticPaths(
|
|||
locales?: string[],
|
||||
defaultLocale?: string
|
||||
): Promise<
|
||||
Omit<UnwrapPromise<ReturnType<GetStaticPaths>>, 'paths'> & { paths: string[] }
|
||||
Omit<UnwrapPromise<ReturnType<GetStaticPaths>>, 'paths'> & {
|
||||
paths: string[]
|
||||
encodedPaths: string[]
|
||||
}
|
||||
> {
|
||||
const prerenderPaths = new Set<string>()
|
||||
const encodedPrerenderPaths = new Set<string>()
|
||||
const _routeRegex = getRouteRegex(page)
|
||||
const _routeMatcher = getRouteMatcher(_routeRegex)
|
||||
|
||||
|
@ -595,7 +599,18 @@ export async function buildStaticPaths(
|
|||
)
|
||||
}
|
||||
|
||||
prerenderPaths?.add(entry)
|
||||
// If leveraging the string paths variant the entry should already be
|
||||
// encoded so we decode the segments ensuring we only escape path
|
||||
// delimiters
|
||||
prerenderPaths.add(
|
||||
entry
|
||||
.split('/')
|
||||
.map((segment) =>
|
||||
escapePathDelimiters(decodeURIComponent(segment), true)
|
||||
)
|
||||
.join('/')
|
||||
)
|
||||
encodedPrerenderPaths.add(entry)
|
||||
}
|
||||
// For the object-provided path, we must make sure it specifies all
|
||||
// required keys.
|
||||
|
@ -617,6 +632,8 @@ export async function buildStaticPaths(
|
|||
|
||||
const { params = {} } = entry
|
||||
let builtPage = page
|
||||
let encodedBuiltPage = page
|
||||
|
||||
_validParamKeys.forEach((validParamKey) => {
|
||||
const { repeat, optional } = _routeRegex.groups[validParamKey]
|
||||
let paramValue = params[validParamKey]
|
||||
|
@ -647,8 +664,19 @@ export async function buildStaticPaths(
|
|||
.replace(
|
||||
replaced,
|
||||
repeat
|
||||
? (paramValue as string[]).map(escapePathDelimiters).join('/')
|
||||
: escapePathDelimiters(paramValue as string)
|
||||
? (paramValue as string[])
|
||||
.map((segment) => escapePathDelimiters(segment, true))
|
||||
.join('/')
|
||||
: escapePathDelimiters(paramValue as string, true)
|
||||
)
|
||||
.replace(/(?!^)\/$/, '')
|
||||
|
||||
encodedBuiltPage = encodedBuiltPage
|
||||
.replace(
|
||||
replaced,
|
||||
repeat
|
||||
? (paramValue as string[]).map(encodeURIComponent).join('/')
|
||||
: encodeURIComponent(paramValue as string)
|
||||
)
|
||||
.replace(/(?!^)\/$/, '')
|
||||
})
|
||||
|
@ -660,15 +688,24 @@ export async function buildStaticPaths(
|
|||
}
|
||||
const curLocale = entry.locale || defaultLocale || ''
|
||||
|
||||
prerenderPaths?.add(
|
||||
prerenderPaths.add(
|
||||
`${curLocale ? `/${curLocale}` : ''}${
|
||||
curLocale && builtPage === '/' ? '' : builtPage
|
||||
}`
|
||||
)
|
||||
encodedPrerenderPaths.add(
|
||||
`${curLocale ? `/${curLocale}` : ''}${
|
||||
curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage
|
||||
}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return { paths: [...prerenderPaths], fallback: staticPathsResult.fallback }
|
||||
return {
|
||||
paths: [...prerenderPaths],
|
||||
fallback: staticPathsResult.fallback,
|
||||
encodedPaths: [...encodedPrerenderPaths],
|
||||
}
|
||||
}
|
||||
|
||||
export async function isPageStatic(
|
||||
|
@ -683,8 +720,9 @@ export async function isPageStatic(
|
|||
isHybridAmp?: boolean
|
||||
hasServerProps?: boolean
|
||||
hasStaticProps?: boolean
|
||||
prerenderRoutes?: string[] | undefined
|
||||
prerenderFallback?: boolean | 'blocking' | undefined
|
||||
prerenderRoutes?: string[]
|
||||
encodedPrerenderRoutes?: string[]
|
||||
prerenderFallback?: boolean | 'blocking'
|
||||
isNextImageImported?: boolean
|
||||
}> {
|
||||
try {
|
||||
|
@ -760,11 +798,13 @@ export async function isPageStatic(
|
|||
}
|
||||
|
||||
let prerenderRoutes: Array<string> | undefined
|
||||
let encodedPrerenderRoutes: Array<string> | undefined
|
||||
let prerenderFallback: boolean | 'blocking' | undefined
|
||||
if (hasStaticProps && hasStaticPaths) {
|
||||
;({
|
||||
paths: prerenderRoutes,
|
||||
fallback: prerenderFallback,
|
||||
encodedPaths: encodedPrerenderRoutes,
|
||||
} = await buildStaticPaths(
|
||||
page,
|
||||
mod.getStaticPaths,
|
||||
|
@ -781,6 +821,7 @@ export async function isPageStatic(
|
|||
isAmpOnly: config.amp === true,
|
||||
prerenderRoutes,
|
||||
prerenderFallback,
|
||||
encodedPrerenderRoutes,
|
||||
hasStaticProps,
|
||||
hasServerProps,
|
||||
isNextImageImported,
|
||||
|
|
|
@ -110,9 +110,10 @@ export default async function exportPage({
|
|||
let query = { ...originalQuery }
|
||||
let params: { [key: string]: string | string[] } | undefined
|
||||
|
||||
let updatedPath = path
|
||||
let updatedPath = (query.__nextSsgPath as string) || path
|
||||
let locale = query.__nextLocale || renderOpts.locale
|
||||
delete query.__nextLocale
|
||||
delete query.__nextSsgPath
|
||||
|
||||
if (renderOpts.locale) {
|
||||
const localePathResult = normalizeLocalePath(path, renderOpts.locales)
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
NextPageContext,
|
||||
ST,
|
||||
} from '../utils'
|
||||
import escapePathDelimiters from './utils/escape-path-delimiters'
|
||||
import { isDynamicRoute } from './utils/is-dynamic'
|
||||
import { parseRelativeUrl } from './utils/parse-relative-url'
|
||||
import { searchParamsToUrlQuery } from './utils/querystring'
|
||||
|
@ -161,8 +160,16 @@ export function interpolateAs(
|
|||
interpolatedRoute!.replace(
|
||||
replaced,
|
||||
repeat
|
||||
? (value as string[]).map(escapePathDelimiters).join('/')
|
||||
: escapePathDelimiters(value as string)
|
||||
? (value as string[])
|
||||
.map(
|
||||
// these values should be fully encoded instead of just
|
||||
// path delimiter escaped since they are being inserted
|
||||
// into the URL and we expect URL encoded segments
|
||||
// when parsing dynamic route params
|
||||
(segment) => encodeURIComponent(segment)
|
||||
)
|
||||
.join('/')
|
||||
: encodeURIComponent(value as string)
|
||||
) || '/')
|
||||
)
|
||||
})
|
||||
|
@ -247,12 +254,15 @@ export function resolveHref(
|
|||
}
|
||||
}
|
||||
|
||||
function prepareUrlAs(router: NextRouter, url: Url, as: Url) {
|
||||
function prepareUrlAs(router: NextRouter, url: Url, as?: Url) {
|
||||
// If url and as provided as an object representation,
|
||||
// we'll format them into the string version here.
|
||||
const [resolvedHref, resolvedAs] = resolveHref(router.pathname, url, true)
|
||||
return {
|
||||
url: addBasePath(resolveHref(router.pathname, url)),
|
||||
as: as ? addBasePath(resolveHref(router.pathname, as)) : as,
|
||||
url: addBasePath(resolvedHref),
|
||||
as: addBasePath(
|
||||
as ? resolveHref(router.pathname, as) : resolvedAs || resolvedHref
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -592,7 +602,7 @@ export default class Router implements BaseRouter {
|
|||
* @param as masks `url` for the browser
|
||||
* @param options object you can define `shallow` and other options
|
||||
*/
|
||||
push(url: Url, as: Url = url, options: TransitionOptions = {}) {
|
||||
push(url: Url, as?: Url, options: TransitionOptions = {}) {
|
||||
;({ url, as } = prepareUrlAs(this, url, as))
|
||||
return this.change('pushState', url, as, options)
|
||||
}
|
||||
|
@ -603,7 +613,7 @@ export default class Router implements BaseRouter {
|
|||
* @param as masks `url` for the browser
|
||||
* @param options object you can define `shallow` and other options
|
||||
*/
|
||||
replace(url: Url, as: Url = url, options: TransitionOptions = {}) {
|
||||
replace(url: Url, as?: Url, options: TransitionOptions = {}) {
|
||||
;({ url, as } = prepareUrlAs(this, url, as))
|
||||
return this.change('replaceState', url, as, options)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
// escape delimiters used by path-to-regexp
|
||||
export default function escapePathDelimiters(segment: string): string {
|
||||
return segment.replace(/[/#?]/g, (char: string) => encodeURIComponent(char))
|
||||
export default function escapePathDelimiters(
|
||||
segment: string,
|
||||
escapeEncoded?: boolean
|
||||
): string {
|
||||
return segment.replace(
|
||||
new RegExp(`([/#?]${escapeEncoded ? '|%(2f|23|3f)' : ''})`, 'gi'),
|
||||
(char: string) => encodeURIComponent(char)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ import * as Log from '../../build/output/log'
|
|||
import { imageOptimizer } from './image-optimizer'
|
||||
import { detectDomainLocale } from '../lib/i18n/detect-domain-locale'
|
||||
import cookie from 'next/dist/compiled/cookie'
|
||||
import escapePathDelimiters from '../lib/router/utils/escape-path-delimiters'
|
||||
import { getUtils } from '../../build/webpack/loaders/next-serverless-loader/utils'
|
||||
|
||||
const getCustomRouteMatcher = pathMatch(true)
|
||||
|
@ -1429,6 +1430,33 @@ export default class Server {
|
|||
}`
|
||||
}
|
||||
|
||||
if (ssgCacheKey) {
|
||||
// we only encode path delimiters for path segments from
|
||||
// getStaticPaths so we need to attempt decoding the URL
|
||||
// to match against and only escape the path delimiters
|
||||
// this allows non-ascii values to be handled e.g. Japanese characters
|
||||
|
||||
// TODO: investigate adding this handling for non-SSG pages so
|
||||
// non-ascii names work there also
|
||||
ssgCacheKey = ssgCacheKey
|
||||
.split('/')
|
||||
.map((seg) => {
|
||||
try {
|
||||
seg = escapePathDelimiters(decodeURIComponent(seg), true)
|
||||
} catch (_) {
|
||||
// An improperly encoded URL was provided, this is considered
|
||||
// a bad request (400)
|
||||
const err: Error & { code?: string } = new Error(
|
||||
'failed to decode param'
|
||||
)
|
||||
err.code = 'DECODE_FAILED'
|
||||
throw err
|
||||
}
|
||||
return seg
|
||||
})
|
||||
.join('/')
|
||||
}
|
||||
|
||||
// Complete the response with cached data if its present
|
||||
const cachedData = ssgCacheKey
|
||||
? await this.incrementalCache.get(ssgCacheKey)
|
||||
|
@ -1608,10 +1636,10 @@ export default class Server {
|
|||
// `getStaticPaths`
|
||||
(isProduction ||
|
||||
!staticPaths ||
|
||||
// static paths always includes locale so make sure it's prefixed
|
||||
// with it
|
||||
!staticPaths.includes(
|
||||
`${locale ? '/' + locale : ''}${resolvedUrlPathname}`
|
||||
// we use ssgCacheKey here as it is normalized to match the
|
||||
// encoding from getStaticPaths along with including the locale
|
||||
query.amp ? ssgCacheKey.replace(/\.amp$/, '') : ssgCacheKey
|
||||
))
|
||||
) {
|
||||
if (
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
onDemandEntries: {
|
||||
// Make sure entries are not getting disposed.
|
||||
maxInactiveAge: 1000 * 60 * 60,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import getPaths from '../../paths'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Page(props) {
|
||||
const router = useRouter()
|
||||
|
||||
if (router.isFallback) {
|
||||
return 'Loading...'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="props">{JSON.stringify(props)}</p>
|
||||
<p id="router">
|
||||
{JSON.stringify({
|
||||
query: router.query,
|
||||
asPath: router.asPath,
|
||||
pathname: router.pathname,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = ({ params }) => {
|
||||
return {
|
||||
props: {
|
||||
random: Math.random(),
|
||||
params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = () => {
|
||||
return {
|
||||
paths: getPaths('/fallback-blocking'),
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import getPaths from '../../paths'
|
||||
|
||||
export default function Page(props) {
|
||||
const router = useRouter()
|
||||
|
||||
if (router.isFallback) {
|
||||
return 'Loading...'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="props">{JSON.stringify(props)}</p>
|
||||
<p id="router">
|
||||
{JSON.stringify({
|
||||
query: router.query,
|
||||
asPath: router.asPath,
|
||||
pathname: router.pathname,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = ({ params }) => {
|
||||
return {
|
||||
props: {
|
||||
random: Math.random(),
|
||||
params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = () => {
|
||||
return {
|
||||
paths: getPaths('/fallback-false'),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import getPaths from '../../paths'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Page(props) {
|
||||
const router = useRouter()
|
||||
|
||||
if (router.isFallback) {
|
||||
return 'Loading...'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="props">{JSON.stringify(props)}</p>
|
||||
<p id="router">
|
||||
{JSON.stringify({
|
||||
query: router.query,
|
||||
asPath: router.asPath,
|
||||
pathname: router.pathname,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = ({ params }) => {
|
||||
return {
|
||||
props: {
|
||||
random: Math.random(),
|
||||
params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = () => {
|
||||
return {
|
||||
paths: getPaths('/fallback-true'),
|
||||
fallback: true,
|
||||
}
|
||||
}
|
22
test/integration/prerender-fallback-encoding/paths.js
Normal file
22
test/integration/prerender-fallback-encoding/paths.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
export default function getPaths(pathPrefix) {
|
||||
return [
|
||||
// this will get turned into %2Fmy-post%2F
|
||||
{ params: { slug: '/my-post/' } },
|
||||
// this will get turned into %252Fmy-post%252F
|
||||
{ params: { slug: '%2Fmy-post%2F' } },
|
||||
// this will be passed through
|
||||
{ params: { slug: '+my-post+' } },
|
||||
// this will get turned into %3Fmy-post%3F
|
||||
{ params: { slug: '?my-post?' } },
|
||||
// ampersand signs
|
||||
{ params: { slug: '&my-post&' } },
|
||||
// non-ascii characters
|
||||
{ params: { slug: '商業日語' } },
|
||||
{ params: { slug: ' my-post ' } },
|
||||
{ params: { slug: encodeURIComponent('商業日語') } },
|
||||
`${pathPrefix}/%2Fsecond-post%2F`,
|
||||
`${pathPrefix}/%2Bsecond-post%2B`,
|
||||
`${pathPrefix}/%26second-post%26`,
|
||||
`${pathPrefix}/mixed-${encodeURIComponent('商業日語')}`,
|
||||
]
|
||||
}
|
355
test/integration/prerender-fallback-encoding/test/index.test.js
Normal file
355
test/integration/prerender-fallback-encoding/test/index.test.js
Normal file
|
@ -0,0 +1,355 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import cheerio from 'cheerio'
|
||||
import webdriver from 'next-webdriver'
|
||||
import {
|
||||
killApp,
|
||||
findPort,
|
||||
nextBuild,
|
||||
launchApp,
|
||||
nextStart,
|
||||
fetchViaHTTP,
|
||||
check,
|
||||
} from 'next-test-utils'
|
||||
|
||||
jest.setTimeout(1000 * 60 * 2)
|
||||
|
||||
const appDir = join(__dirname, '..')
|
||||
let app
|
||||
let appPort
|
||||
let buildId
|
||||
|
||||
// paths on the filesystem
|
||||
const prerenderedPaths = [
|
||||
'%2Fmy-post%2F',
|
||||
'%252Fmy-post%252F',
|
||||
'+my-post+',
|
||||
'%3Fmy-post%3F',
|
||||
'&my-post&',
|
||||
'商業日語',
|
||||
encodeURIComponent('商業日語'),
|
||||
' my-post ',
|
||||
'%2Fsecond-post%2F',
|
||||
'+second-post+',
|
||||
'&second-post&',
|
||||
'mixed-商業日語',
|
||||
]
|
||||
|
||||
// paths that should be requested in the URL
|
||||
const urlPaths = [
|
||||
'%2Fmy-post%2F',
|
||||
'%252Fmy-post%252F',
|
||||
'%2Bmy-post%2B',
|
||||
'%3Fmy-post%3F',
|
||||
'%26my-post%26',
|
||||
encodeURIComponent('商業日語'),
|
||||
encodeURIComponent(encodeURIComponent('商業日語')),
|
||||
'%20my-post%20',
|
||||
'%2Fsecond-post%2F',
|
||||
'%2Bsecond-post%2B',
|
||||
'%26second-post%26',
|
||||
`mixed-${encodeURIComponent('商業日語')}`,
|
||||
]
|
||||
|
||||
const modePaths = ['fallback-blocking', 'fallback-false', 'fallback-true']
|
||||
const pagesDir = join(appDir, '.next/server/pages')
|
||||
|
||||
function runTests(isDev) {
|
||||
if (!isDev) {
|
||||
it('should output paths correctly', async () => {
|
||||
for (const path of prerenderedPaths) {
|
||||
for (const mode of modePaths) {
|
||||
console.log('checking output', { path, mode })
|
||||
expect(await fs.exists(join(pagesDir, mode, path + '.html'))).toBe(
|
||||
true
|
||||
)
|
||||
expect(await fs.exists(join(pagesDir, mode, path + '.json'))).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle non-prerendered paths correctly', async () => {
|
||||
const prerenderedPaths = [
|
||||
'%2Fanother-post%2F',
|
||||
'+another-post+',
|
||||
'%3Fanother-post%3F',
|
||||
'&another-post&',
|
||||
'商業日語商業日語',
|
||||
]
|
||||
|
||||
const urlPaths = [
|
||||
'%2Fanother-post%2F',
|
||||
'%2Banother-post%2B',
|
||||
'%3Fanother-post%3F',
|
||||
'%26another-post%26',
|
||||
encodeURIComponent('商業日語商業日語'),
|
||||
]
|
||||
|
||||
for (const mode of modePaths) {
|
||||
for (let i = 0; i < urlPaths.length; i++) {
|
||||
const testSlug = urlPaths[i]
|
||||
const path = prerenderedPaths[i]
|
||||
|
||||
const res = await fetchViaHTTP(
|
||||
appPort,
|
||||
`/_next/data/${buildId}/${mode}/${testSlug}.json`
|
||||
)
|
||||
|
||||
if (mode === 'fallback-false') {
|
||||
expect(res.status).toBe(404)
|
||||
} else {
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const { pageProps: props } = await res.json()
|
||||
|
||||
expect(props.params).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
if (!isDev) {
|
||||
// we don't block on writing incremental data to the
|
||||
// disk so use check
|
||||
await check(
|
||||
() =>
|
||||
fs
|
||||
.exists(join(pagesDir, mode, path + '.html'))
|
||||
.then((res) => (res ? 'yes' : 'no')),
|
||||
'yes'
|
||||
)
|
||||
await check(
|
||||
() =>
|
||||
fs
|
||||
.exists(join(pagesDir, mode, path + '.json'))
|
||||
.then((res) => (res ? 'yes' : 'no')),
|
||||
'yes'
|
||||
)
|
||||
}
|
||||
|
||||
const browser = await webdriver(appPort, `/${mode}/${testSlug}`)
|
||||
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
|
||||
expect(browserRouter.pathname).toBe(`/${mode}/[slug]`)
|
||||
expect(browserRouter.asPath).toBe(`/${mode}/${testSlug}`)
|
||||
expect(browserRouter.query).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should respond with the prerendered pages correctly', async () => {
|
||||
for (let i = 0; i < urlPaths.length; i++) {
|
||||
const testSlug = urlPaths[i]
|
||||
|
||||
for (const mode of modePaths) {
|
||||
const res = await fetchViaHTTP(
|
||||
appPort,
|
||||
`/${mode}/${testSlug}`,
|
||||
undefined,
|
||||
{
|
||||
redirect: 'manual',
|
||||
}
|
||||
)
|
||||
|
||||
console.log('checking', { mode, testSlug })
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const $ = cheerio.load(await res.text())
|
||||
|
||||
expect(JSON.parse($('#props').text()).params).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
const router = JSON.parse($('#router').text())
|
||||
|
||||
expect(router.pathname).toBe(`/${mode}/[slug]`)
|
||||
expect(router.asPath).toBe(`/${mode}/${testSlug}`)
|
||||
expect(router.query).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should respond with the prerendered data correctly', async () => {
|
||||
for (const path of urlPaths) {
|
||||
for (const mode of modePaths) {
|
||||
const res = await fetchViaHTTP(
|
||||
appPort,
|
||||
`/_next/data/${buildId}/${mode}/${path}.json`,
|
||||
undefined,
|
||||
{
|
||||
redirect: 'manual',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const { pageProps: props } = await res.json()
|
||||
|
||||
expect(props.params).toEqual({
|
||||
slug: decodeURIComponent(path),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should render correctly in the browser for prerender paths', async () => {
|
||||
for (let i = 0; i < urlPaths.length; i++) {
|
||||
const testSlug = urlPaths[i]
|
||||
|
||||
for (const mode of modePaths) {
|
||||
const browser = await webdriver(appPort, `/${mode}/${testSlug}`)
|
||||
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
|
||||
expect(browserRouter.pathname).toBe(`/${mode}/[slug]`)
|
||||
expect(browserRouter.asPath).toBe(`/${mode}/${testSlug}`)
|
||||
expect(browserRouter.query).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should navigate client-side correctly with interpolating', async () => {
|
||||
for (const mode of modePaths) {
|
||||
const testSlug = urlPaths[0]
|
||||
const browser = await webdriver(appPort, `/${mode}/${testSlug}`)
|
||||
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
|
||||
expect(browserRouter.pathname).toBe(`/${mode}/[slug]`)
|
||||
expect(browserRouter.asPath).toBe(`/${mode}/${testSlug}`)
|
||||
expect(browserRouter.query).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
|
||||
for (const nextSlug of urlPaths) {
|
||||
if (nextSlug === testSlug) continue
|
||||
|
||||
await browser.eval(`(function() {
|
||||
window.next.router.push({
|
||||
pathname: '/${mode}/[slug]',
|
||||
query: { slug: '${decodeURIComponent(nextSlug)}' }
|
||||
})
|
||||
})()`)
|
||||
|
||||
await check(async () => {
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
return browserRouter.asPath === `/${mode}/${nextSlug}`
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should navigate client-side correctly with string href', async () => {
|
||||
for (const mode of modePaths) {
|
||||
const testSlug = urlPaths[0]
|
||||
const browser = await webdriver(appPort, `/${mode}/${testSlug}`)
|
||||
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
|
||||
expect(browserRouter.pathname).toBe(`/${mode}/[slug]`)
|
||||
expect(browserRouter.asPath).toBe(`/${mode}/${testSlug}`)
|
||||
expect(browserRouter.query).toEqual({
|
||||
slug: decodeURIComponent(testSlug),
|
||||
})
|
||||
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
|
||||
for (const nextSlug of urlPaths) {
|
||||
if (nextSlug === testSlug) continue
|
||||
|
||||
await browser.eval(`(function() {
|
||||
window.next.router.push('/${mode}/${nextSlug}')
|
||||
})()`)
|
||||
|
||||
await check(async () => {
|
||||
const browserRouter = JSON.parse(
|
||||
await browser.elementByCss('#router').text()
|
||||
)
|
||||
return browserRouter.asPath === `/${mode}/${nextSlug}`
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Fallback path encoding', () => {
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
await fs.remove(join(appDir, '.next'))
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
buildId = 'development'
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
runTests(true)
|
||||
})
|
||||
|
||||
describe('production mode', () => {
|
||||
beforeAll(async () => {
|
||||
await fs.remove(join(appDir, '.next'))
|
||||
appPort = await findPort()
|
||||
await nextBuild(appDir)
|
||||
|
||||
app = await nextStart(appDir, appPort)
|
||||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
runTests()
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue