mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Fixes an issue in self-hosted and Vercel Adapter that causes some routes that prerender static pages to include more route params than expected when running in Cache Components. For all Next.js environments any route matching a root param not already in the cache will result in a prerender that includes all params regardless of whether they were omitted from generateStaticParams. For Vercel Adapter environments any route without a fallback that has an empty shell during the build will prerender misses with all params regardless of whether they were omitted from generateStaticParams. This change fixes both issues and introduces a comprehensive test matrix to catch regressions in these kinds of prerender decisions in the future closes NAR-882
231 lines
7.7 KiB
TypeScript
231 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
import { use } from 'react'
|
|
|
|
// Interactive variant switcher: every dimension of the route matrix is a
|
|
// control whose checked state derives from the server-rendered scenario
|
|
// (the current URL), and changing a control navigates to the equivalent
|
|
// URL with the new dimension value.
|
|
//
|
|
// Navigation is a FULL page load (window.location, not router.push): the
|
|
// badge paint scripts describe initial-HTML render provenance, and client
|
|
// navigations re-render content from flight data without re-executing
|
|
// them, which breaks the UI's coherence. This whole fixture is a test of
|
|
// initial HTML loading, so every control change reloads the document.
|
|
//
|
|
// - lang and category are RADIOS between their prerendered value (the one
|
|
// generateStaticParams enumerates in the partial/fully matrices) and a
|
|
// canonical non-prerendered value. A current value that is neither (e.g.
|
|
// a probe-generated unique value) reads as non-prerendered.
|
|
// - the matrix is a three-way RADIO.
|
|
// - the branch (root params) and shell topology are CHECKBOXES.
|
|
//
|
|
// Every combination is navigable — including dynamic-param with root
|
|
// params, which lands on a tombstone page explaining that the combination
|
|
// cannot exist.
|
|
//
|
|
// This component unwraps params with use(), so it inherits the params'
|
|
// disposition exactly like the previous server-component links did: on
|
|
// empty-shell pages it renders in the deferred region; on non-empty pages
|
|
// it must be wrapped in its own Suspense boundary so the page shell stays
|
|
// param-free.
|
|
const MATRICES = [
|
|
'partial-static-param',
|
|
'fully-static-param',
|
|
'dynamic-param',
|
|
] as const
|
|
|
|
const PRERENDERED_LANG = 'en'
|
|
const NOT_PRERENDERED_LANG = 'fr'
|
|
const PRERENDERED_CATEGORY = 'shoes'
|
|
const NOT_PRERENDERED_CATEGORY = 'toys'
|
|
const PRERENDERED_ID = '1'
|
|
const NOT_PRERENDERED_ID = '2'
|
|
|
|
type Matrix = (typeof MATRICES)[number]
|
|
type Branch = 'with-root-param' | 'without-root-param'
|
|
|
|
export function VariantControls({
|
|
params,
|
|
matrix,
|
|
tree,
|
|
branch,
|
|
}: {
|
|
params: Promise<{ lang: string; category: string; id: string }>
|
|
matrix: Matrix
|
|
tree: string
|
|
branch: Branch
|
|
}) {
|
|
const { lang, category, id } = use(params)
|
|
|
|
const current = { matrix, branch, tree, lang, category, id }
|
|
|
|
function hrefFor(dims: typeof current) {
|
|
// Trees that only exist in one place (e.g. empty-shell-dynamic in the
|
|
// standalone app) map to their closest equivalent when any dimension
|
|
// changes.
|
|
const targetTree =
|
|
dims.tree === tree && dims.matrix === matrix && dims.branch === branch
|
|
? dims.tree
|
|
: dims.tree === 'empty-shell-dynamic'
|
|
? 'empty-shell'
|
|
: dims.tree
|
|
|
|
// Every route follows the same positional scheme —
|
|
// /<scenario>/<root-param>/<shell>/<lang>/<category>/<id> — the
|
|
// with/without-root-param distinction lives entirely in where each
|
|
// branch's first layout sits (above [lang] or at [lang]).
|
|
return `/${dims.matrix}/${dims.branch}/${targetTree}/${dims.lang}/${dims.category}/${dims.id}`
|
|
}
|
|
|
|
function push(patch: Partial<typeof current>) {
|
|
window.location.assign(hrefFor({ ...current, ...patch }))
|
|
}
|
|
|
|
const langLabel = branch === 'with-root-param' ? 'root param' : 'lang'
|
|
const langIsPrerendered = lang === PRERENDERED_LANG
|
|
const categoryIsPrerendered = category === PRERENDERED_CATEGORY
|
|
|
|
return (
|
|
<nav
|
|
style={{
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
marginTop: 12,
|
|
borderTop: '1px dashed #94a3b8',
|
|
paddingTop: 8,
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: 16,
|
|
}}
|
|
>
|
|
<fieldset style={{ border: '1px solid #cbd5e1', padding: 6 }}>
|
|
<legend>{langLabel}</legend>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-lang"
|
|
checked={langIsPrerendered}
|
|
onChange={() => push({ lang: PRERENDERED_LANG })}
|
|
/>{' '}
|
|
prerendered ({PRERENDERED_LANG})
|
|
</label>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-lang"
|
|
checked={!langIsPrerendered}
|
|
onChange={() => push({ lang: NOT_PRERENDERED_LANG })}
|
|
/>{' '}
|
|
non-prerendered (
|
|
{lang === PRERENDERED_LANG ? NOT_PRERENDERED_LANG : lang})
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset style={{ border: '1px solid #cbd5e1', padding: 6 }}>
|
|
<legend>category</legend>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-category"
|
|
checked={categoryIsPrerendered}
|
|
onChange={() => push({ category: PRERENDERED_CATEGORY })}
|
|
/>{' '}
|
|
prerendered ({PRERENDERED_CATEGORY})
|
|
</label>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-category"
|
|
checked={!categoryIsPrerendered}
|
|
onChange={() => push({ category: NOT_PRERENDERED_CATEGORY })}
|
|
/>{' '}
|
|
non-prerendered (
|
|
{category === PRERENDERED_CATEGORY
|
|
? NOT_PRERENDERED_CATEGORY
|
|
: category}
|
|
)
|
|
</label>
|
|
</fieldset>
|
|
|
|
{/* Only the fully-static matrix enumerates an id value, so id
|
|
prerender-ness is only a real dimension there — the radios stay
|
|
visible but disabled elsewhere to teach exactly that. */}
|
|
<fieldset
|
|
style={{ border: '1px solid #cbd5e1', padding: 6 }}
|
|
disabled={matrix !== 'fully-static-param'}
|
|
title={
|
|
matrix !== 'fully-static-param'
|
|
? 'id is never prerenderable outside the fully-static-param matrix'
|
|
: undefined
|
|
}
|
|
>
|
|
<legend>id</legend>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-id"
|
|
checked={id === PRERENDERED_ID}
|
|
onChange={() => push({ id: PRERENDERED_ID })}
|
|
/>{' '}
|
|
prerendered ({PRERENDERED_ID})
|
|
</label>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-id"
|
|
checked={id !== PRERENDERED_ID}
|
|
onChange={() => push({ id: NOT_PRERENDERED_ID })}
|
|
/>{' '}
|
|
non-prerendered ({id === PRERENDERED_ID ? NOT_PRERENDERED_ID : id})
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset style={{ border: '1px solid #cbd5e1', padding: 6 }}>
|
|
<legend>matrix</legend>
|
|
{MATRICES.map((variantMatrix) => (
|
|
<label key={variantMatrix} style={{ display: 'block' }}>
|
|
<input
|
|
type="radio"
|
|
name="variant-matrix"
|
|
checked={matrix === variantMatrix}
|
|
onChange={() => push({ matrix: variantMatrix })}
|
|
/>{' '}
|
|
{variantMatrix}
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
|
|
<fieldset style={{ border: '1px solid #cbd5e1', padding: 6 }}>
|
|
<legend>structure</legend>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={branch === 'with-root-param'}
|
|
onChange={(event) =>
|
|
push({
|
|
branch: event.target.checked
|
|
? 'with-root-param'
|
|
: 'without-root-param',
|
|
})
|
|
}
|
|
/>{' '}
|
|
root params
|
|
</label>
|
|
<label style={{ display: 'block' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={tree !== 'non-empty-shell'}
|
|
onChange={(event) =>
|
|
push({
|
|
tree: event.target.checked ? 'empty-shell' : 'non-empty-shell',
|
|
})
|
|
}
|
|
/>{' '}
|
|
empty shell
|
|
</label>
|
|
</fieldset>
|
|
</nav>
|
|
)
|
|
}
|