Prefetch SSG Data (#10127)
* Prefetch SSG Data * Update packages/next/client/page-loader.js Co-Authored-By: JJ Kasper <jj@jjsweb.site> * Revert router.ts * Revert link.tsx * undo change * mimmic existing * simplify * Prefetch href and asPath * fix load * dedupe prefetchAs * Inject script tag on hover * comment prefetchAs * minify code * introduce lazy files * Add some breathing room * correct default type * Prefetch non-dynamic data * Prefetch dynamic route data * Fix size test * Humanize code * add tests * Disable code * Only generate modern version in modern mode * Extract function helper * add comments * Filter out dynamic route to simplify manifest size * add test Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
parent
8f01a4ae83
commit
990eda2c88
13 changed files with 443 additions and 5 deletions
|
@ -1,6 +1,7 @@
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import ciEnvironment from 'ci-info'
|
import ciEnvironment from 'ci-info'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
import devalue from 'devalue'
|
||||||
import escapeStringRegexp from 'escape-string-regexp'
|
import escapeStringRegexp from 'escape-string-regexp'
|
||||||
import findUp from 'find-up'
|
import findUp from 'find-up'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
@ -28,6 +29,7 @@ import { recursiveReadDir } from '../lib/recursive-readdir'
|
||||||
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
|
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
|
||||||
import {
|
import {
|
||||||
BUILD_MANIFEST,
|
BUILD_MANIFEST,
|
||||||
|
CLIENT_STATIC_FILES_PATH,
|
||||||
EXPORT_DETAIL,
|
EXPORT_DETAIL,
|
||||||
EXPORT_MARKER,
|
EXPORT_MARKER,
|
||||||
PAGES_MANIFEST,
|
PAGES_MANIFEST,
|
||||||
|
@ -851,6 +853,11 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
||||||
JSON.stringify(prerenderManifest),
|
JSON.stringify(prerenderManifest),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
await generateClientSsgManifest(prerenderManifest, {
|
||||||
|
distDir,
|
||||||
|
buildId,
|
||||||
|
isModern: !!config.experimental.modern,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const prerenderManifest: PrerenderManifest = {
|
const prerenderManifest: PrerenderManifest = {
|
||||||
version: 2,
|
version: 2,
|
||||||
|
@ -863,6 +870,8 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
||||||
JSON.stringify(prerenderManifest),
|
JSON.stringify(prerenderManifest),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
// No need to call this fn as we already emitted a default SSG manifest:
|
||||||
|
// await generateClientSsgManifest(prerenderManifest, { distDir, buildId })
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsWriteFile(
|
await fsWriteFile(
|
||||||
|
@ -961,3 +970,36 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
||||||
|
|
||||||
await telemetry.flush()
|
await telemetry.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateClientSsgManifest(
|
||||||
|
prerenderManifest: PrerenderManifest,
|
||||||
|
{
|
||||||
|
buildId,
|
||||||
|
distDir,
|
||||||
|
isModern,
|
||||||
|
}: { buildId: string; distDir: string; isModern: boolean }
|
||||||
|
) {
|
||||||
|
const ssgPages: Set<string> = new Set<string>([
|
||||||
|
...Object.entries(prerenderManifest.routes)
|
||||||
|
// Filter out dynamic routes
|
||||||
|
.filter(([, { srcRoute }]) => srcRoute == null)
|
||||||
|
.map(([route]) => route),
|
||||||
|
...Object.keys(prerenderManifest.dynamicRoutes),
|
||||||
|
])
|
||||||
|
|
||||||
|
const clientSsgManifestPaths = [
|
||||||
|
'_ssgManifest.js',
|
||||||
|
isModern && '_ssgManifest.module.js',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(f => path.join(`${CLIENT_STATIC_FILES_PATH}/${buildId}`, f as string))
|
||||||
|
const clientSsgManifestContent = `self.__SSG_MANIFEST=${devalue(
|
||||||
|
ssgPages
|
||||||
|
)};self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
|
||||||
|
clientSsgManifestPaths.forEach(clientSsgManifestPath =>
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(distDir, clientSsgManifestPath),
|
||||||
|
clientSsgManifestContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -147,6 +147,23 @@ export default class BuildManifestPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the runtime ssg manifest file as a lazy-loaded file dependency.
|
||||||
|
// We also stub this file out for development mode (when it is not
|
||||||
|
// generated).
|
||||||
|
const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
|
||||||
|
|
||||||
|
const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
|
||||||
|
assetMap.lowPriorityFiles.push(ssgManifestPath)
|
||||||
|
compilation.assets[ssgManifestPath] = new RawSource(srcEmptySsgManifest)
|
||||||
|
|
||||||
|
if (this.modern) {
|
||||||
|
const ssgManifestPathModern = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.module.js`
|
||||||
|
assetMap.lowPriorityFiles.push(ssgManifestPathModern)
|
||||||
|
compilation.assets[ssgManifestPathModern] = new RawSource(
|
||||||
|
srcEmptySsgManifest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
assetMap.pages = Object.keys(assetMap.pages)
|
assetMap.pages = Object.keys(assetMap.pages)
|
||||||
.sort()
|
.sort()
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import { parse } from 'url'
|
||||||
import mitt from '../next-server/lib/mitt'
|
import mitt from '../next-server/lib/mitt'
|
||||||
|
import { isDynamicRoute } from './../next-server/lib/router/utils/is-dynamic'
|
||||||
|
import { getRouteMatcher } from './../next-server/lib/router/utils/route-matcher'
|
||||||
|
import { getRouteRegex } from './../next-server/lib/router/utils/route-regex'
|
||||||
|
|
||||||
function hasRel(rel, link) {
|
function hasRel(rel, link) {
|
||||||
try {
|
try {
|
||||||
|
@ -18,6 +22,7 @@ const relPrefetch =
|
||||||
|
|
||||||
const hasNoModule = 'noModule' in document.createElement('script')
|
const hasNoModule = 'noModule' in document.createElement('script')
|
||||||
|
|
||||||
|
/** @param {string} route */
|
||||||
function normalizeRoute(route) {
|
function normalizeRoute(route) {
|
||||||
if (route[0] !== '/') {
|
if (route[0] !== '/') {
|
||||||
throw new Error(`Route name should start with a "/", got "${route}"`)
|
throw new Error(`Route name should start with a "/", got "${route}"`)
|
||||||
|
@ -62,6 +67,16 @@ export default class PageLoader {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/** @type {Promise<Set<string>>} */
|
||||||
|
this.promisedSsgManifest = new Promise(resolve => {
|
||||||
|
if (window.__SSG_MANIFEST) {
|
||||||
|
resolve(window.__SSG_MANIFEST)
|
||||||
|
} else {
|
||||||
|
window.__SSG_MANIFEST_CB = () => {
|
||||||
|
resolve(window.__SSG_MANIFEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise for the dependencies for a particular route
|
// Returns a promise for the dependencies for a particular route
|
||||||
|
@ -76,6 +91,89 @@ export default class PageLoader {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} href the route href (file-system path)
|
||||||
|
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
|
||||||
|
*/
|
||||||
|
getDataHref(href, asPath) {
|
||||||
|
const getHrefForSlug = (/** @type string */ path) =>
|
||||||
|
`${this.assetPrefix}/_next/data/${this.buildId}${
|
||||||
|
path === '/' ? '/index' : path
|
||||||
|
}.json`
|
||||||
|
|
||||||
|
const { pathname: hrefPathname, query } = parse(href, true)
|
||||||
|
const { pathname: asPathname } = parse(asPath)
|
||||||
|
|
||||||
|
const route = normalizeRoute(hrefPathname)
|
||||||
|
|
||||||
|
let isDynamic = isDynamicRoute(route),
|
||||||
|
interpolatedRoute
|
||||||
|
if (isDynamic) {
|
||||||
|
const dynamicRegex = getRouteRegex(route)
|
||||||
|
const dynamicGroups = dynamicRegex.groups
|
||||||
|
const dynamicMatches =
|
||||||
|
// Try to match the dynamic route against the asPath
|
||||||
|
getRouteMatcher(dynamicRegex)(asPathname) ||
|
||||||
|
// Fall back to reading the values from the href
|
||||||
|
// TODO: should this take priority; also need to change in the router.
|
||||||
|
query
|
||||||
|
|
||||||
|
interpolatedRoute = route
|
||||||
|
if (
|
||||||
|
!Object.keys(dynamicGroups).every(param => {
|
||||||
|
let value = dynamicMatches[param]
|
||||||
|
const repeat = dynamicGroups[param].repeat
|
||||||
|
|
||||||
|
// support single-level catch-all
|
||||||
|
// TODO: more robust handling for user-error (passing `/`)
|
||||||
|
if (repeat && !Array.isArray(value)) value = [value]
|
||||||
|
|
||||||
|
return (
|
||||||
|
param in dynamicMatches &&
|
||||||
|
// Interpolate group into data URL if present
|
||||||
|
(interpolatedRoute = interpolatedRoute.replace(
|
||||||
|
`[${repeat ? '...' : ''}${param}]`,
|
||||||
|
repeat
|
||||||
|
? value.map(encodeURIComponent).join('/')
|
||||||
|
: encodeURIComponent(value)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
interpolatedRoute = '' // did not satisfy all requirements
|
||||||
|
|
||||||
|
// n.b. We ignore this error because we handle warning for this case in
|
||||||
|
// development in the `<Link>` component directly.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDynamic
|
||||||
|
? interpolatedRoute && getHrefForSlug(interpolatedRoute)
|
||||||
|
: getHrefForSlug(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} href the route href (file-system path)
|
||||||
|
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
|
||||||
|
*/
|
||||||
|
prefetchData(href, asPath) {
|
||||||
|
const { pathname: hrefPathname } = parse(href, true)
|
||||||
|
const route = normalizeRoute(hrefPathname)
|
||||||
|
return this.promisedSsgManifest.then(
|
||||||
|
(s, _dataHref) =>
|
||||||
|
// Check if the route requires a data file
|
||||||
|
s.has(route) &&
|
||||||
|
// Try to generate data href, noop when falsy
|
||||||
|
(_dataHref = this.getDataHref(href, asPath)) &&
|
||||||
|
// noop when data has already been prefetched (dedupe)
|
||||||
|
!document.querySelector(
|
||||||
|
`link[rel="${relPrefetch}"][href^="${_dataHref}"]`
|
||||||
|
) &&
|
||||||
|
// Inject the `<link rel=prefetch>` tag for above computed `href`.
|
||||||
|
appendLink(_dataHref, relPrefetch, 'fetch')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
loadPage(route) {
|
loadPage(route) {
|
||||||
return this.loadPageScript(route).then(v => v.page)
|
return this.loadPageScript(route).then(v => v.page)
|
||||||
}
|
}
|
||||||
|
@ -206,6 +304,10 @@ export default class PageLoader {
|
||||||
register()
|
register()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} route
|
||||||
|
* @param {boolean} [isDependency]
|
||||||
|
*/
|
||||||
prefetch(route, isDependency) {
|
prefetch(route, isDependency) {
|
||||||
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
|
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
|
||||||
// License: Apache 2.0
|
// License: Apache 2.0
|
||||||
|
@ -215,6 +317,7 @@ export default class PageLoader {
|
||||||
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
|
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
let url
|
let url
|
||||||
if (isDependency) {
|
if (isDependency) {
|
||||||
url = route
|
url = route
|
||||||
|
|
|
@ -703,9 +703,12 @@ export default class Router implements BaseRouter {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](
|
Promise.all([
|
||||||
toRoute(pathname)
|
this.pageLoader.prefetchData(url, asPath),
|
||||||
).then(() => resolve(), reject)
|
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](
|
||||||
|
toRoute(pathname)
|
||||||
|
),
|
||||||
|
]).then(() => resolve(), reject)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
test/integration/preload-viewport/next.config.js
Normal file
5
test/integration/preload-viewport/next.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
generateBuildId() {
|
||||||
|
return 'test-build'
|
||||||
|
},
|
||||||
|
}
|
5
test/integration/preload-viewport/pages/ssg/basic.js
Normal file
5
test/integration/preload-viewport/pages/ssg/basic.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export function getStaticProps() {
|
||||||
|
return { props: { message: 'hello world' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ message }) => <p id="content">{message}</p>
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function getStaticProps({ params }) {
|
||||||
|
return { props: { message: `hello ${params.slug.join(' ')}` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return {
|
||||||
|
paths: ['/ssg/catch-all/one', '/ssg/catch-all/one/two'],
|
||||||
|
fallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ message }) => <p id="content">{message || 'loading'}</p>
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function getStaticProps({ params }) {
|
||||||
|
return { props: { message: `hello ${params.slug1} ${params.slug2}` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return {
|
||||||
|
paths: ['/ssg/dynamic-nested/one/two'],
|
||||||
|
fallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ message }) => <p id="content">{message || 'loading'}</p>
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function getStaticProps({ params }) {
|
||||||
|
return { props: { message: `hello ${params.slug}` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return { paths: ['/ssg/dynamic/one'], fallback: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ message }) => <p id="content">{message || 'loading'}</p>
|
65
test/integration/preload-viewport/pages/ssg/fixture/index.js
Normal file
65
test/integration/preload-viewport/pages/ssg/fixture/index.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<main>
|
||||||
|
<h1>SSG Data Prefetch Fixtures</h1>
|
||||||
|
<p>
|
||||||
|
<Link href="/ssg/basic">
|
||||||
|
<a>Non-dynamic route</a>
|
||||||
|
</Link>
|
||||||
|
: this is a normal Next.js page that does not use dynamic routing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link href="/ssg/dynamic/[slug]" as="/ssg/dynamic/one">
|
||||||
|
<a>Dynamic Route (one level) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with a single dynamic segment that{' '}
|
||||||
|
<strong>was returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link href="/ssg/dynamic/[slug]" as="/ssg/dynamic/two">
|
||||||
|
<a>Dynamic Route (one level) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with a single dynamic segment that{' '}
|
||||||
|
<strong>was not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link
|
||||||
|
href="/ssg/dynamic-nested/[slug1]/[slug2]"
|
||||||
|
as="/ssg/dynamic-nested/one/two"
|
||||||
|
>
|
||||||
|
<a>Multi Dynamic Route (two levels) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with two dynamic segments that{' '}
|
||||||
|
<strong>were returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link
|
||||||
|
href="/ssg/dynamic-nested/[slug1]/[slug2]"
|
||||||
|
as="/ssg/dynamic-nested/foo/bar"
|
||||||
|
>
|
||||||
|
<a>Multi Dynamic Route (two levels) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with two dynamic segments that{' '}
|
||||||
|
<strong>were not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link href="/ssg/catch-all/[...slug]" as="/ssg/catch-all/one">
|
||||||
|
<a>Catch-All Route (one level) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with one segment that{' '}
|
||||||
|
<strong>was returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link href="/ssg/catch-all/[...slug]" as="/ssg/catch-all/foo">
|
||||||
|
<a>Catch-All Route (one level) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with one segment that{' '}
|
||||||
|
<strong>was not returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link href="/ssg/catch-all/[...slug]" as="/ssg/catch-all/one/two">
|
||||||
|
<a>Catch-All Route (two levels) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with two segments that{' '}
|
||||||
|
<strong>were returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link href="/ssg/catch-all/[...slug]" as="/ssg/catch-all/foo/bar">
|
||||||
|
<a>Catch-All Route (two levels) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with two segments that{' '}
|
||||||
|
<strong>were not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
)
|
|
@ -0,0 +1,89 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<main>
|
||||||
|
<h1>Mismatched SSG Data Prefetch Fixtures</h1>
|
||||||
|
<p>
|
||||||
|
<Link href="/ssg/dynamic/[slug]?slug=one" as="/ssg/fixture/mismatch">
|
||||||
|
<a>Dynamic Route (one level) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with a single dynamic segment that{' '}
|
||||||
|
<strong>was returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link href="/ssg/dynamic/[slug]?slug=two" as="/ssg/fixture/mismatch">
|
||||||
|
<a>Dynamic Route (one level) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with a single dynamic segment that{' '}
|
||||||
|
<strong>was not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/dynamic-nested/[slug1]/[slug2]',
|
||||||
|
query: { slug1: 'one', slug2: 'two' },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Multi Dynamic Route (two levels) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with two dynamic segments that{' '}
|
||||||
|
<strong>were returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/dynamic-nested/[slug1]/[slug2]',
|
||||||
|
query: { slug1: 'foo', slug2: 'bar' },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Multi Dynamic Route (two levels) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Dynamic Page with two dynamic segments that{' '}
|
||||||
|
<strong>were not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/catch-all/[...slug]',
|
||||||
|
query: { slug: ['one'] },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Catch-All Route (one level) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with one segment that{' '}
|
||||||
|
<strong>was returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/catch-all/[...slug]',
|
||||||
|
query: { slug: ['foo'] },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Catch-All Route (one level) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with one segment that{' '}
|
||||||
|
<strong>was not returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/catch-all/[...slug]',
|
||||||
|
query: { slug: ['one', 'two'] },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Catch-All Route (two levels) — Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with two segments that{' '}
|
||||||
|
<strong>were returned</strong> from <code>getStaticPaths</code>.<br />
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/ssg/catch-all/[...slug]',
|
||||||
|
query: { slug: ['foo', 'bar'] },
|
||||||
|
}}
|
||||||
|
as="/ssg/fixture/mismatch"
|
||||||
|
>
|
||||||
|
<a>Catch-All Route (two levels) — Not Prerendered</a>
|
||||||
|
</Link>
|
||||||
|
: this is a Catch-All Page with two segments that{' '}
|
||||||
|
<strong>were not returned</strong> from <code>getStaticPaths</code>.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
)
|
|
@ -9,6 +9,8 @@ import {
|
||||||
} from 'next-test-utils'
|
} from 'next-test-utils'
|
||||||
import webdriver from 'next-webdriver'
|
import webdriver from 'next-webdriver'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { readFile } from 'fs-extra'
|
||||||
|
import { parse } from 'url'
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
|
||||||
|
|
||||||
|
@ -244,4 +246,78 @@ describe('Prefetching Links in viewport', () => {
|
||||||
const calledPrefetch = await browser.eval(`window.calledPrefetch`)
|
const calledPrefetch = await browser.eval(`window.calledPrefetch`)
|
||||||
expect(calledPrefetch).toBe(true)
|
expect(calledPrefetch).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should correctly omit pre-generaged dynamic pages from SSG manifest', async () => {
|
||||||
|
const content = await readFile(
|
||||||
|
join(appDir, '.next', 'static', 'test-build', '_ssgManifest.js'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
let self = {}
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
eval(content)
|
||||||
|
expect([...self.__SSG_MANIFEST].sort()).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"/ssg/basic",
|
||||||
|
"/ssg/catch-all/[...slug]",
|
||||||
|
"/ssg/dynamic-nested/[slug1]/[slug2]",
|
||||||
|
"/ssg/dynamic/[slug]",
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefetch data files', async () => {
|
||||||
|
const browser = await webdriver(appPort, '/ssg/fixture')
|
||||||
|
await waitFor(2 * 1000) // wait for prefetching to occur
|
||||||
|
|
||||||
|
const links = await browser.elementsByCss('link[rel=prefetch][as=fetch]')
|
||||||
|
|
||||||
|
const hrefs = []
|
||||||
|
for (const link of links) {
|
||||||
|
const href = await link.getAttribute('href')
|
||||||
|
hrefs.push(href)
|
||||||
|
}
|
||||||
|
hrefs.sort()
|
||||||
|
|
||||||
|
expect(hrefs.map(href => parse(href).pathname)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"/_next/data/test-build/ssg/basic.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/foo.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/foo/bar.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/one.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/one/two.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic-nested/foo/bar.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic-nested/one/two.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic/one.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic/two.json",
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefetch data files when mismatched', async () => {
|
||||||
|
const browser = await webdriver(appPort, '/ssg/fixture/mismatch')
|
||||||
|
await waitFor(2 * 1000) // wait for prefetching to occur
|
||||||
|
|
||||||
|
const links = await browser.elementsByCss('link[rel=prefetch][as=fetch]')
|
||||||
|
|
||||||
|
const hrefs = []
|
||||||
|
for (const link of links) {
|
||||||
|
const href = await link.getAttribute('href')
|
||||||
|
hrefs.push(href)
|
||||||
|
}
|
||||||
|
hrefs.sort()
|
||||||
|
|
||||||
|
expect(hrefs.map(href => parse(href).pathname)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"/_next/data/test-build/ssg/catch-all/foo.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/foo/bar.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/one.json",
|
||||||
|
"/_next/data/test-build/ssg/catch-all/one/two.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic-nested/foo/bar.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic-nested/one/two.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic/one.json",
|
||||||
|
"/_next/data/test-build/ssg/dynamic/two.json",
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -80,7 +80,7 @@ describe('Production response size', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// These numbers are without gzip compression!
|
// These numbers are without gzip compression!
|
||||||
const delta = responseSizesBytes - 231 * 1024
|
const delta = responseSizesBytes - 232 * 1024
|
||||||
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
||||||
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
||||||
})
|
})
|
||||||
|
@ -100,7 +100,7 @@ describe('Production response size', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// These numbers are without gzip compression!
|
// These numbers are without gzip compression!
|
||||||
const delta = responseSizesBytes - 164 * 1024
|
const delta = responseSizesBytes - 165 * 1024
|
||||||
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
||||||
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue