Ensure rewrites are resolved while prefetching (#22442)

This ensures we handle resolve rewrites during prefetching the same way we do during a client-transition. Previously if a rewritten source was used in an `href` neither the page bundle or SSG data if needed would be prefetched although would work correctly on a client transition. 


Fixes: https://github.com/vercel/next.js/issues/22441
This commit is contained in:
JJ Kasper 2021-02-24 09:37:13 -06:00 committed by GitHub
parent a78e904fc8
commit 9d2b0fc04a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 37 deletions

View file

@ -338,6 +338,34 @@ function prepareUrlAs(router: NextRouter, url: Url, as?: Url) {
}
}
function resolveDynamicRoute(
parsedHref: UrlObject,
pages: string[],
applyBasePath = true
) {
const { pathname } = parsedHref
const cleanPathname = removePathTrailingSlash(
denormalizePagePath(applyBasePath ? delBasePath(pathname!) : pathname!)
)
if (cleanPathname === '/404' || cleanPathname === '/_error') {
return parsedHref
}
// handle resolving href for dynamic routes
if (!pages.includes(cleanPathname!)) {
// eslint-disable-next-line array-callback-return
pages.some((page) => {
if (isDynamicRoute(page) && getRouteRegex(page).re.test(cleanPathname!)) {
parsedHref.pathname = applyBasePath ? addBasePath(page) : page
return true
}
})
}
parsedHref.pathname = removePathTrailingSlash(parsedHref.pathname!)
return parsedHref
}
export type BaseRouter = {
route: string
pathname: string
@ -915,7 +943,7 @@ export default class Router implements BaseRouter {
return false
}
parsed = this._resolveHref(parsed, pages) as typeof parsed
parsed = resolveDynamicRoute(parsed, pages) as typeof parsed
if (parsed.pathname !== pathname) {
pathname = parsed.pathname
@ -950,7 +978,7 @@ export default class Router implements BaseRouter {
pages,
rewrites,
query,
(p: string) => this._resolveHref({ pathname: p }, pages).pathname!,
(p: string) => resolveDynamicRoute({ pathname: p }, pages).pathname!,
this.locales
)
resolvedAs = rewritesResult.asPath
@ -1058,7 +1086,7 @@ export default class Router implements BaseRouter {
// it's not
if (destination.startsWith('/')) {
const parsedHref = parseRelativeUrl(destination)
this._resolveHref(parsedHref, pages, false)
resolveDynamicRoute(parsedHref, pages, false)
if (pages.includes(parsedHref.pathname)) {
const { url: newUrl, as: newAs } = prepareUrlAs(
@ -1419,33 +1447,6 @@ export default class Router implements BaseRouter {
return this.asPath !== asPath
}
_resolveHref(parsedHref: UrlObject, pages: string[], applyBasePath = true) {
const { pathname } = parsedHref
const cleanPathname = removePathTrailingSlash(
denormalizePagePath(applyBasePath ? delBasePath(pathname!) : pathname!)
)
if (cleanPathname === '/404' || cleanPathname === '/_error') {
return parsedHref
}
// handle resolving href for dynamic routes
if (!pages.includes(cleanPathname!)) {
// eslint-disable-next-line array-callback-return
pages.some((page) => {
if (
isDynamicRoute(page) &&
getRouteRegex(page).re.test(cleanPathname!)
) {
parsedHref.pathname = applyBasePath ? addBasePath(page) : page
return true
}
})
}
parsedHref.pathname = removePathTrailingSlash(parsedHref.pathname!)
return parsedHref
}
/**
* Prefetch page code, you may wait for the data during page rendering.
* This feature only works in production!
@ -1480,26 +1481,51 @@ export default class Router implements BaseRouter {
const pages = await this.pageLoader.getPageList()
parsed = this._resolveHref(parsed, pages, false) as typeof parsed
parsed = resolveDynamicRoute(parsed, pages, false) as typeof parsed
if (parsed.pathname !== pathname) {
pathname = parsed.pathname
url = formatWithValidation(parsed)
}
let route = removePathTrailingSlash(pathname)
let resolvedAs = asPath
if (process.env.__NEXT_HAS_REWRITES && asPath.startsWith('/')) {
let rewrites: any[]
;({ __rewrites: rewrites } = await getClientBuildManifest())
const rewritesResult = resolveRewrites(
addBasePath(addLocale(delBasePath(asPath), this.locale)),
pages,
rewrites,
parsed.query,
(p: string) => resolveDynamicRoute({ pathname: p }, pages).pathname!,
this.locales
)
if (rewritesResult.matchedPage && rewritesResult.resolvedHref) {
// if this directly matches a page we need to update the href to
// allow the correct page chunk to be loaded
route = rewritesResult.resolvedHref
pathname = rewritesResult.resolvedHref
parsed.pathname = pathname
url = formatWithValidation(parsed)
resolvedAs = rewritesResult.asPath
}
}
// Prefetch is not supported in development mode because it would trigger on-demand-entries
if (process.env.NODE_ENV !== 'production') {
return
}
const route = removePathTrailingSlash(pathname)
await Promise.all([
this.pageLoader._isSsg(url).then((isSsg: boolean) => {
return isSsg
? this._getStaticData(
this.pageLoader.getDataHref(
url,
asPath,
resolvedAs,
true,
typeof options.locale !== 'undefined'
? options.locale

View file

@ -95,7 +95,7 @@ describe('Build Output', () => {
expect(indexSize.endsWith('B')).toBe(true)
// should be no bigger than 63.9 kb
expect(parseFloat(indexFirstLoad)).toBeCloseTo(64, 1)
expect(parseFloat(indexFirstLoad)).toBeCloseTo(64.1, 1)
expect(indexFirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(err404Size) - 3.7).toBeLessThanOrEqual(0)
@ -104,7 +104,7 @@ describe('Build Output', () => {
expect(parseFloat(err404FirstLoad)).toBeCloseTo(67.1, 0)
expect(err404FirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(sharedByAll)).toBeCloseTo(63.7, 1)
expect(parseFloat(sharedByAll)).toBeCloseTo(63.8, 1)
expect(sharedByAll.endsWith('kB')).toBe(true)
if (_appSize.endsWith('kB')) {
@ -168,7 +168,7 @@ describe('Build Output', () => {
expect(parseFloat(indexSize)).toBeGreaterThanOrEqual(2)
expect(indexSize.endsWith('kB')).toBe(true)
expect(parseFloat(indexFirstLoad)).toBeLessThanOrEqual(66.8)
expect(parseFloat(indexFirstLoad)).toBeLessThanOrEqual(66.9)
expect(parseFloat(indexFirstLoad)).toBeGreaterThanOrEqual(60)
expect(indexFirstLoad.endsWith('kB')).toBe(true)
})

View file

@ -2,4 +2,12 @@ module.exports = {
generateBuildId() {
return 'test-build'
},
rewrites() {
return [
{
source: '/rewrite-me',
destination: '/ssg/dynamic/one',
},
]
},
}

View file

@ -0,0 +1,16 @@
export const getStaticProps = () => {
return {
props: {},
}
}
export const getStaticPaths = () => {
return {
paths: [{ params: { rest: ['one'] } }],
fallback: false,
}
}
export default function Page() {
return <p id="top-level-rest">Hello from [...rest]</p>
}

View file

@ -0,0 +1,9 @@
import Link from 'next/link'
export default function Page() {
return (
<Link href="/rewrite-me">
<a>to /rewrite-me</a>
</Link>
)
}

View file

@ -53,6 +53,31 @@ describe('Prefetching Links in viewport', () => {
}
})
it('should prefetch rewritten href with link in viewport onload', async () => {
let browser
try {
browser = await webdriver(appPort, '/rewrite-prefetch')
const links = await browser.elementsByCss('link[rel=prefetch]')
let found = false
for (const link of links) {
const href = await link.getAttribute('href')
if (href.includes('%5Bslug%5D')) {
found = true
break
}
}
expect(found).toBe(true)
const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`)
expect(hrefs.map((href) => new URL(href).pathname)).toEqual([
'/_next/data/test-build/ssg/dynamic/one.json',
])
} finally {
if (browser) await browser.close()
}
})
it('should prefetch with link in viewport when href changes', async () => {
let browser
try {
@ -337,6 +362,7 @@ describe('Prefetching Links in viewport', () => {
eval(content)
expect([...self.__SSG_MANIFEST].sort()).toMatchInlineSnapshot(`
Array [
"/[...rest]",
"/ssg/basic",
"/ssg/catch-all/[...slug]",
"/ssg/dynamic-nested/[slug1]/[slug2]",

View file

@ -81,6 +81,6 @@ describe('Production response size', () => {
const delta = responseSizesBytes / 1024
// Expected difference: < 0.5
expect(delta).toBeCloseTo(284.7, 0)
expect(delta).toBeCloseTo(285.3, 0)
})
})