Skip to content

Add experimental parameter matching APIs - #97393

Open
gnoff wants to merge 89 commits into
codex/root-param-shell-cachefrom
jstory/unstable-matcher
Open

gnoff wants to merge 89 commits into
codex/root-param-shell-cachefrom
jstory/unstable-matcher

Conversation

@gnoff

@gnoff gnoff commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Add per-parameter matching directives behind experimental.paramMatching, with Cache Components enabled. generateStaticParams remains responsible for concrete build-time prerenders; matching configuration controls how requests with novel parameter values are handled.

This lets an application close a prefix to its build-time values, require blocking generation for the next parameter, and serve immediate fallback UI for another. Unconfigured parameters retain inference from the generated shells, and an unconfigured tail without examples remains dynamic.

Stack

The prerequisites are ordered bottom to top:

  1. Use structural React keys during server rendering #98944: use structural server React keys and remove postponed-state param interpolation; this supersedes the closed Preserve array values for catch-all fallback parameters #98890 catch-all placeholder change.
  2. Generalize the build-time generator work-unit store name #98891: generalize the gSP work-unit store to build-time-generator and retain the originating function name for diagnostics, without changing existing gSP behavior.
  3. Keep cached root parameters out of generic fallback shells #98950: prune seeded RDC entries that depend on unknown roots and suspend fresh root-dependent cache fills independently, including nested caches. This replaces the closed Suspend unresolved root parameter reads inside public caches #98892.
  4. This PR: add the opt-in matching API and its validation/runtime integration, including the root-fallback query-key correction formerly isolated in Prefix fallback root query keys in adapter outputs #98893.

The prerequisites can land without committing to this API. The closed-parameter prediction fix #98889 and the earlier dev-validation prerequisites #98511 and #98512 have already merged. Opt-in diagnostic logging remains separately committed and can be removed independently.

The prerequisites remain independently reviewable above #98944. This PR preserves its existing history through integration merges; generic generator diagnostics live in #98891, while this layer supplies the matching API's name and integration coverage. The superseded catch-all array conversion is not reintroduced.

Example

// next.config.ts
export default {
  cacheComponents: true,
  experimental: { paramMatching: true },
}

// app/[lang]/layout.tsx
export const experimental_paramMatching = {
  lang: 'not-found',
}

// app/[lang]/catalog/[top]/items/[bottom]/page.tsx
export const experimental_paramMatching = {
  top: 'blocking',
  bottom: 'fallback',
}

export function generateStaticParams() {
  return [{ lang: 'en', top: 't1', bottom: 'b1' }]
}

The build prerenders /en/catalog/t1/items/b1. A novel lang returns 404; /en/catalog/t2/items/b2 blocks on generation; /en/catalog/t1/items/b2 can receive fallback UI immediately.

The generated form, export async function experimental_generateParamMatching(), returns the same partial object and takes no arguments. It can load matching policy from an external configuration system and runs independently of gSP's per-parent-value enumeration. Each unique module is evaluated once per route evaluation, not once globally per build: a shared layout can run again for another page. Repeated occurrences of the same module within one route's parallel tree share the result. It uses the same build-time work-unit context as gSP: public use cache helpers work, while request-only/private caching reports the normal unsupported-context error. The context explicitly identifies experimental_generateParamMatching, so an invalid headers() call names that export rather than gSP. The same applies to private caching, revalidation, and unavailable root parameters; matching generators are not told to obtain concrete params from parent gSP calls.

Ordinary TypeScript object literals work without as const. For optional strict mode checking, use satisfies ParamMatching imported from next. ParamMatching<'lang' | 'top'> additionally constrains keys when writing an explicit annotation. Generated types still reject keys outside the module's visible parameters; runtime validation rejects invalid modes and malformed JavaScript or generated exports.

Layering and validation

Fragments are assignment-merged from layouts toward each page. Descendants replace only supplied keys. A module may configure only parameters at or above its own segment: [lang]/layout.tsx cannot configure a later top parameter.

The merged explicit policy must follow not-found → blocking → fallback → dynamic. Every parameter preceding an explicit not-found must also be explicitly not-found, directly or through inheritance. Next.js never silently closes an unconfigured prefix or rewrites inherited intent:

// Ancestor
{ lang: 'blocking' }

// Descendant: invalid after merging
{ category: 'not-found' }

// Descendant: explicitly overrides the affected prefix
{ lang: 'not-found', category: 'not-found' }

Omitting lang entirely is also invalid in that example. A configuration on the [lang] layout can supply it to all descendants.

Parallel branches sharing a matcher must agree and produce a coherent combined policy in URL-parameter order, independent of route groups. Routes sharing a parameter definition must all explicitly configure/inherit its not-found policy if any of them does. Explicitly dynamic parameters cannot appear in gSP output or have prerendered parameters beneath them.

Only after explicit configuration is validated are holes filled by inference, constrained by the explicit boundaries.

Static shells, hints, and development

An inferred empty shell may result in blocking behavior instead of a servable fallback. Explicit fallback requires a nonempty shell and cannot silently become blocking. Blocking also validates a reachable useful shell; when no example reaches the boundary, the generic shell provides that validation. instant = false opts out of shell validation, not best-effort hint collection.

An explicit fallback moves the required validation to that more generic shell. For example, top: 'fallback' requires useful UI with both top and bottom unknown, even if gSP also supplies t1/b1. Once that shell passes, descendant prerenders do not repeat the empty-shell requirement; making more params known does not add a new unknown-data boundary.

Redundant blocking renders are skipped only for API-configured routes when a descendant covers them. Unconfigured routes retain their existing build renders and hint collection. A blocking boundary without an example still gets a best-effort generic render; this does not make that result a servable fallback.

Dev preserves required-or-completed shell selection and adds explicit fallback boundaries to the unknown parameter set. For example, with a t1/b1 prerender and top: 'fallback', validation treats both parameters as unknown even for /t1/b1. Ordinary dev requests remain dynamic renders. Dev supplies exact closed-parameter names from the effective policy, while production persists them in renderer manifest metadata. The shared transport-tree builder marks only those dynamic nodes. For { lang: 'not-found', slug: 'fallback' }, only [lang] is marked, even though the generic matcher has fallback: false; [slug] remains open. Successful document loads, live navigations, and prefetched trees therefore disable optimistic prediction consistently. No dev prerender-manifest entry or new adapter routing directive is needed.

Deployment and diagnostics

Closed-prefix output uses the existing fallback-false adapter contract, with specialized matchers admitting the open suffix beneath each generated prefix. The function output keeps its original source-page entrypoint. Synthetic alias entrypoints and the alternate app-path-routes manifest have been removed; no new adapter/proxy contract is introduced. Local output/launcher checks do not replace a real grouped-function deployment check.

Explicit root fallback matching makes a previously unreachable adapter case possible: a servable fallback whose root parameter is still unknown. For app/[lang]/layout.tsx with lang: 'fallback', the fallback's query allowlist must contain nxtPlang, matching the routing destination, rather than the logical name lang. This correction now lives with the API instead of in #98893. The real param-matching-root-params fixture checks the exact route and non-empty key list for HTML, RSC, and segment outputs, and confirms the HTML output has a servable fallback. The synthetic manifest-editing fixture and direct internal-emitter invocation have been removed.

Initially, output: export requires every parameter to be explicitly not-found. This avoids silently changing matching semantics and leaves room for future client-only fallback support.

Diagnostics print effective matching decisions and deployment patterns only when both experimental.paramMatching and NEXT_PRIVATE_DEBUG_PARAM_MATCHING=1 are enabled.

Verification and remaining work

  • Latest review follow-up is included in 19d60ae82c0 and 9dd593abb6d. The explicit type-fixture project passes all three checks in both Turbopack dev and production, including rejection of invalid parameter keys. The three build-contract suites pass all 28 production tests; matching unit coverage passes all 98 tests. Repository TypeScript, changed-file ESLint/formatting, and the routing README language check pass. Verification used a full bootstrap with a fresh native Turbopack build. Comments now describe normalized-prefix identity, per-route generator evaluation, and the purpose of build-artifact assertions. Explicit fallback shell validation and removal of synthetic adapter aliases are documented above.

  • Root fallback query-key follow-up: the genuine API fixture now requires a servable /[lang] fallback, matches its exact routing rule, and requires ['nxtPlang'] on HTML, RSC, and segment outputs. Temporarily removing only the prefix conversion fails that assertion with ['lang'] while the seven behavior tests still pass. Restoring it passes all eight fixture tests; the existing blocking-query fixture also passes (11 production Webpack tests combined). Development Webpack also passes (six tests, two production-only skips). Full bootstrap, repository TypeScript checking, and changed-file lint pass. This checks the real build's adapter contract, not a live deployment; the separate intercepted-parameter (nxtI) investigation is deferred.

  • Root-cache coverage now uses ordinary builds and requests only. The prerequisite fixture no longer exposes shell-debugging APIs. A real revalidateTag request in this layer forces a root-fallback shell to regenerate, and checks that cached root-dependent UI remains deferred while independent cached content is refreshed. Both fixtures pass together in production Webpack (11 tests) and development Webpack (seven tests, four build/ISR-only skips). Removing just the fresh-fill handling, while retaining RDC pruning, makes normal root-fallback requests and the revalidation regression return opaque placeholder content; restoring it makes them pass.

  • Generator-name follow-up: all nine new production Webpack diagnostic fixtures pass. The existing matching-generator and gSP-error suites also pass (13 production tests), and the existing matching-generator suite passes in dev (seven tests, one prefetch-only skip). Full bootstrap, repository types, and changed-file lint pass. An initial fixture-only import error was corrected before the successful diagnostic run.

  • Restack verification on the combined Use structural React keys during server rendering #98944-based stack: full build and repository type check pass, along with 222 focused unit tests (four snapshots), 43 production Webpack tests, and 26 development Webpack tests (nine snapshots). Existing mode-specific skips are unchanged. That run used the native build available at the time. Subsequent simplification verification rebuilt native Turbopack locally and passed the production file-tracing suite (six tests, five snapshots).

  • Node-local hint coverage passes with both bundlers: eight generator-fixture tests in production, and seven in dev with the prefetch-only case skipped. The mixed closed-prefix/open-suffix fixture verifies only [lang] is marked, novel slugs still render, novel languages return 404, and the renderer manifest identifies the exact closed subset. With inlining disabled, a prefetched navigation uses no additional requests and still marks only [lang], proving the existing response merge preserves the transport-tree hints. Closed-parameter bits are not duplicated in the build-measured prefetch-hints.json tree; createUniformHintTree remains unchanged from the stack's base. The legacy fixture covers the inlined path and passes all eight production tests with both bundlers. Both fixtures also pass together in dev (12 passed, four prefetch-dependent cases skipped per bundler). The three unchanged Cache Components prediction regressions passed with both production bundlers in the preceding node-local-hint verification.

  • Regression fixtures demonstrate before/after failures for missing closed prefixes, public cached generators, successful dev closure hints, and widened object-literal types. Runtime invalid-mode/key checks and private-cache rejection remain covered.

  • The six focused production Webpack suites pass (29 tests), covering generators, types, prefix validation, blocking/partial-prefetch behavior, cached root parameters, and parallel route groups. The main production Turbopack API suite passes (26 tests).

  • The main API and existing cache-components-dev-fallback-validation suites pass together in Turbopack development (26 tests, 9 snapshots).

  • Full bootstrap build and repository TypeScript checks pass. Both touched unit suites pass in the broad unit run; that run has three unrelated macOS /var versus /private/var path-comparison failures in runTypeScriptCli.test.ts.

  • The four decoded-value expectations in this PR's new fixture were incorrect. Freshly built canary 34433fd12ee reproduces encoded server values without matching configuration, and its existing prerender-encoding test explicitly expects them. The follow-up corrects those expectations while retaining encoded spaces, literal percent signs, unconfigured controls, and repeated requests; no routing implementation is changed. All 25 runnable dev routing cases pass (six existing production-only skips). Production passes 27 cases and still fails four: the two percent-sign URLs return 500 before the value assertion, expected routing 404s log an internal error, and fully closed-route revalidation fails. Those HTTP/error assertions are unchanged. These remain open issues, not a claim that the API is ready to merge.

  • Actual grouped-function deployment verification is blocked locally because VERCEL_ADAPTER_TEST_TOKEN and VERCEL_ADAPTER_TEST_TEAM are not configured.

@gnoff
gnoff force-pushed the jstory/unstable-matcher branch 2 times, most recently from fcdac58 to c5d86cc Compare August 16, 2026 04:16
@gnoff
gnoff changed the base branch from canary to codex/prerender-param-policy-model August 16, 2026 04:16
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch 5 times, most recently from e07682c to af86527 Compare August 17, 2026 02:19
@github-actions

Copy link
Copy Markdown
Contributor

Stats in progress

Commit: af86527
View workflow run

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

🔴 3 regressions, 1 improvement

Metric Canary PR Change Trend
node_modules Size 554 MB 555 MB 🔴 +483 kB (+0%) ▁▁▁▁▁
Webpack Build Time 25.052s 25.783s 🔴 +731ms (+3%) █▃▁▆▃
Webpack Build Time (cached) 24.736s 25.295s 🔴 +559ms (+2%) █▆▃█▅
Turbo Build Time (cached) 3.221s 2.956s 🟢 265ms (-8%) █▃▃▂▁
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 813ms 812ms █▁▁▁▁
Cold (Ready in log) 811ms 800ms █▁▂▁▁
Cold (First Request) 1.332s 1.291s █▂▃▂▁
Warm (Listen) 813ms 813ms █▁▁▁▁
Warm (Ready in log) 805ms 793ms █▁▁▁▁
Warm (First Request) 1.282s 1.252s ▁▅▅▄▄
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 865ms 814ms ▇▇▅█▇
Cold (Ready in log) 824ms 815ms ▆▆▂█▂
Cold (First Request) 3.504s 3.479s ▄▄▂█▂
Warm (Listen) 864ms 813ms █████
Warm (Ready in log) 821ms 817ms █▄▄█▄
Warm (First Request) 3.498s 3.497s █▃▃█▄

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 6.232s 5.974s █▂▃▁▁
Cached Build 3.221s 2.956s 🟢 265ms (-8%) █▃▃▂▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 25.052s 25.783s 🔴 +731ms (+3%) █▃▁▆▃
Cached Build 24.736s 25.295s 🔴 +559ms (+2%) █▆▃█▅
node_modules Size 554 MB 555 MB 🔴 +483 kB (+0%) ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
050icza-xjz0i.js gzip 5.73 kB N/A -
07jdby0ue616s.js gzip 450 B N/A -
0bjdc8muo74n5.js gzip 8.71 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0eo3xvq-zd3j2.js gzip 65.6 kB N/A -
0rci1f3or1a19.js gzip 13.3 kB N/A -
1_2x714--ii1i.js gzip 8.76 kB N/A -
1-3y752pkth5-.js gzip 10 kB N/A -
1-nfv1877mihd.js gzip 156 B N/A -
12bso2e_5x4zx.js gzip 157 B N/A -
140af-gch2905.js gzip 157 B N/A -
1469bmgz0r8ob.js gzip 154 B N/A -
15_1l6mmqyrws.js gzip 156 B N/A -
15xlq53c28l8-.js gzip 159 B N/A -
16loy0anq4cfq.js gzip 154 B N/A -
19gb-1_5udxfb.js gzip 13.2 kB N/A -
1cywsbjgohnj9.js gzip 167 B N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1qij_v1qfn8w5.js gzip 71.6 kB N/A -
1tf1phijqlx9j.js gzip 220 B 220 B
1uzabyd1120a1.js gzip 8.71 kB N/A -
1y91ypv-32-2c.js gzip 150 B N/A -
2-ufv8lc-g7gg.js gzip 10.6 kB N/A -
21kmjy_10x14f.js gzip 8.81 kB N/A -
28dhc6t85q1_p.js gzip 8.78 kB N/A -
2f-ilvczue-tp.js gzip 9.46 kB N/A -
2f1u17u5c8iny.js gzip 8.79 kB N/A -
2ikltg_8iegxw.js gzip 10.3 kB N/A -
2ixp3xn2_vz0r.js gzip 3.57 kB N/A -
2r1yklvxhy9fu.js gzip 154 B N/A -
2vxi673cz1-t4.js gzip 8.79 kB N/A -
37zg4vafpzsi1.js gzip 155 B N/A -
38-q43pzktqhs.js gzip 1.46 kB N/A -
3f977fytahn5a.js gzip 7.53 kB N/A -
3nbojhxiy1qv_.js gzip 13.7 kB N/A -
3npru_5qbg6lt.js gzip 155 B N/A -
3r9mg09sag_yr.js gzip 47 kB N/A -
41u5s3oe2-erp.js gzip 2.29 kB N/A -
445s_9hf8o7ao.js gzip 8.76 kB N/A -
4486in8mnn3zl.js gzip 154 B N/A -
turbopack-0d..9ejy.js gzip 3.74 kB 3.74 kB
0-6bcj16ji2wf.js gzip N/A 10.6 kB -
07ryk0jced-sc.js gzip N/A 8.78 kB -
0bck4f999fyqy.js gzip N/A 71.7 kB -
0cib1uxlom68m.js gzip N/A 154 B -
0jwho9fkrb_t3.js gzip N/A 2.29 kB -
0kfnsdg9gjq_a.js gzip N/A 155 B -
0roh390ijzxa5.js gzip N/A 8.75 kB -
0vex9w55ursqj.js gzip N/A 8.79 kB -
11zw3t1q0obkt.js gzip N/A 46.9 kB -
13fdp6hysi38e.js gzip N/A 7.53 kB -
17oe55cu76cd7.js gzip N/A 450 B -
1dmmbm4sdub-n.js gzip N/A 160 B -
1drww5xikb-c-.js gzip N/A 9.46 kB -
1g60xde_dv17t.js gzip N/A 8.79 kB -
1i8agvlvgn56i.js gzip N/A 155 B -
1q_2qy9pu40_5.js gzip N/A 153 B -
1t5nnab0mb-xk.js gzip N/A 167 B -
1uzv47btzam64.js gzip N/A 5.73 kB -
1vy7n7wxv_rh5.js gzip N/A 8.81 kB -
2_y_gel90e96i.js gzip N/A 153 B -
2-0i7pl900-ou.js gzip N/A 8.71 kB -
2-kcbngm7ik7y.js gzip N/A 8.75 kB -
238feb_ximsbi.js gzip N/A 155 B -
29p5-xa4jmdhg.js gzip N/A 13.7 kB -
2is1q248dqrlh.js gzip N/A 151 B -
2mg811to9eum0.js gzip N/A 155 B -
2nsrcvgnlgkby.js gzip N/A 3.56 kB -
2sxo-ur6g5fuc.js gzip N/A 65.6 kB -
2ugc69z0t0ypz.js gzip N/A 1.46 kB -
36dn8i-_3dnq5.js gzip N/A 10 kB -
3fljpmwcjxqhx.js gzip N/A 10.3 kB -
3gz44skqdlsrb.js gzip N/A 8.71 kB -
3j8es5as0tfa4.js gzip N/A 154 B -
3mycjgbtvwtnm.js gzip N/A 155 B -
3skgwg3_u6i_e.js gzip N/A 13.2 kB -
3y9tus7kb5su0.js gzip N/A 13.3 kB -
4379ebyrlu8xj.js gzip N/A 159 B -
Total 401 kB 401 kB ✅ -7 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 1.06 kB 1.06 kB
Total 1.06 kB 1.06 kB
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 877 B 873 B
Total 877 B 873 B ✅ -4 B
Build Cache
Canary PR Change
00000001.sst gzip 10.2 MB 14.2 MB 🔴 +4.04 MB (+40%)
00000002.sst gzip 13.4 MB 13.1 MB 🟢 337 kB (-3%)
00000003.sst gzip 14.8 MB 14.1 MB 🟢 747 kB (-5%)
00000004.sst gzip 14.4 MB 14.1 MB 🟢 302 kB (-2%)
00000005.sst gzip 12.8 MB 2.8 MB 🟢 10 MB (-78%)
00000006.sst gzip 2.8 MB 10.2 MB 🔴 +7.4 MB (+264%)
00000007.sst gzip 59 B 59 B
00000008.meta gzip 89 B 89 B
00000009.meta gzip 298 kB 298 kB
00000010.meta gzip 298 kB 298 kB
00000011.meta gzip 298 kB 298 kB
00000012.sst gzip 51.4 kB 51.2 kB
00000013.sst gzip 1.82 MB 1.75 MB 🟢 66.1 kB (-4%)
00000014.sst gzip 59 B 59 B
00000015.meta gzip 116 B 116 B
00000016.meta gzip 327 kB 328 kB
00000017.meta gzip 407 kB 405 kB
00000018.sst gzip 52 kB 52 kB
00000019.sst gzip 1.4 MB 1.38 MB 🟢 18.5 kB (-1%)
00000020.sst gzip 59 B 59 B
00000021.meta gzip 116 B 116 B
00000022.meta gzip 327 kB 328 kB
00000023.meta gzip 373 kB 373 kB
00000024.sst gzip 51.9 kB 52.1 kB
00000025.sst gzip 1.4 MB 1.38 MB 🟢 19.1 kB (-1%)
00000026.sst gzip 59 B 59 B
00000027.meta gzip 116 B 116 B
00000028.meta gzip 327 kB 328 kB
00000029.meta gzip 373 kB 373 kB
00000030.sst gzip 51.6 kB 51.3 kB
00000031.sst gzip 1.4 MB 1.38 MB 🟢 18.9 kB (-1%)
00000032.sst gzip 59 B 59 B
00000033.meta gzip 116 B 116 B
00000034.meta gzip 327 kB 328 kB
00000035.meta gzip 373 kB 373 kB
00000036.sst gzip 51.9 kB 51.9 kB
00000037.sst gzip 1.4 MB 1.38 MB 🟢 19.1 kB (-1%)
00000038.sst gzip 59 B 59 B
00000039.meta gzip 116 B 116 B
00000040.meta gzip 327 kB 328 kB
00000041.meta gzip 373 kB 373 kB
CURRENT gzip 94 B 94 B
LOG gzip 683 B 674 B 🟢 9 B (-1%)
Total 80.7 MB 80.5 MB ✅ -137 kB

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 66.1 kB N/A -
4191.HASH.js gzip 169 B N/A -
7920-HASH.js gzip 4.67 kB N/A -
9784-HASH.js gzip 5.63 kB N/A -
b1ad9f4c-HASH.js gzip 63.5 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 253 B 253 B
main-HASH.js gzip 40.2 kB 40.2 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
3577.HASH.js gzip N/A 168 B -
578-HASH.js gzip N/A 66.3 kB -
8590-HASH.js gzip N/A 5.61 kB -
9750-HASH.js gzip N/A 4.68 kB -
a8984546-HASH.js gzip N/A 63.5 kB -
Total 242 kB 242 kB ⚠️ +68 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 193 B
_error-HASH.js gzip 181 B 182 B
css-HASH.js gzip 334 B 331 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 253 B
head-HASH.js gzip 349 B 351 B
hooks-HASH.js gzip 382 B 384 B
image-HASH.js gzip 581 B 582 B
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.48 kB 2.48 kB
routerDirect..HASH.js gzip 317 B 318 B
script-HASH.js gzip 384 B 386 B
withRouter-HASH.js gzip 316 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.95 kB 7.96 kB ⚠️ +4 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 129 kB 129 kB
page.js gzip 294 kB 294 kB
Total 423 kB 423 kB ✅ -648 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 616 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 46.1 kB 45.6 kB 🟢 503 B (-1%)
edge-runtime..pack.js gzip 842 B 842 B
Total 47.7 kB 47.2 kB ✅ -505 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 717 B 718 B
Total 717 B 718 B ⚠️ +1 B
Build Cache
Canary PR Change
0.pack gzip 4.83 MB 4.82 MB 🟢 5.19 kB (0%)
index.pack gzip 123 kB 124 kB
index.pack.old gzip 124 kB 123 kB
Total 5.08 MB 5.07 MB ✅ -5.88 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 376 kB 376 kB
app-page-exp..prod.js gzip 207 kB 207 kB
app-page-tur...dev.js gzip 375 kB 376 kB
app-page-tur..prod.js gzip 206 kB 206 kB
app-page-tur...dev.js gzip 372 kB 372 kB
app-page-tur..prod.js gzip 204 kB 204 kB
app-page.run...dev.js gzip 372 kB 372 kB
app-page.run..prod.js gzip 204 kB 204 kB
app-route-ex...dev.js gzip 83.2 kB 83.2 kB
app-route-ex..prod.js gzip 56.2 kB 56.2 kB
app-route-tu...dev.js gzip 83.2 kB 83.2 kB
app-route-tu..prod.js gzip 56.2 kB 56.2 kB
app-route-tu...dev.js gzip 82.8 kB 82.8 kB
app-route-tu..prod.js gzip 56 kB 56 kB
app-route.ru...dev.js gzip 82.8 kB 82.8 kB
app-route.ru..prod.js gzip 55.9 kB 55.9 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 132 kB 132 kB
dev-validati...dev.js gzip 132 kB 132 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 46.5 kB 46.5 kB
pages-api-tu..prod.js gzip 34.7 kB 34.7 kB
pages-api.ru...dev.js gzip 46.5 kB 46.5 kB
pages-api.ru..prod.js gzip 34.7 kB 34.7 kB
pages-turbo....dev.js gzip 55.3 kB 55.3 kB
pages-turbo...prod.js gzip 40.2 kB 40.2 kB
pages.runtim...dev.js gzip 55.3 kB 55.3 kB
pages.runtim..prod.js gzip 40.2 kB 40.2 kB
server.runti..prod.js gzip 66.6 kB 66.9 kB
use-cache-pr...dev.js gzip 72.7 kB 72.7 kB
use-cache-pr...dev.js gzip 72.7 kB 72.7 kB
use-cache-pr...dev.js gzip 70.9 kB 70.9 kB
use-cache-pr...dev.js gzip 70.9 kB 70.9 kB
Total 4.11 MB 4.11 MB ⚠️ +1.82 kB
📝 Changed Files (9 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • server.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/37cb5ddcfb0c7f8079cd39af6df631c28a821780/next

Commit: 37cb5dd

@github-actions

Copy link
Copy Markdown
Contributor

Tests in progress

Commit: af86527
View workflow run

@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from a376314 to ed49b3f Compare August 17, 2026 02:54
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from ed49b3f to aef873b Compare August 17, 2026 03:23
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch 2 times, most recently from 37cb5dd to 1aca829 Compare August 17, 2026 15:07
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Failing test suites

Commit: 9dd593a | About building and testing Next.js

pnpm test-start-experimental-turbo test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (turbopack) (Experimental) (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > handles an expected matcher miss without a cache-generation invariant error (DD)
  • param-matching-routing > revalidation > revalidates a seeded output without changing route admission: /en/closed/known (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › handles an expected matcher miss without a cache-generation invariant error

expect(received).not.toMatch(expected)

Expected pattern: not /invariant:|Error:/i
Received string:      "Error: Internal: NoFallbackError

  at n (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:1220)
  at responseGenerator (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:4878)
  "
  at Object.toMatch (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:113:51)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

● param-matching-routing › revalidation › revalidates a seeded output without changing route admission: /en/closed/known

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 404

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:124:24

pnpm test-start test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > handles an expected matcher miss without a cache-generation invariant error (DD)
  • param-matching-routing > revalidation > revalidates a seeded output without changing route admission: /en/closed/known (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › handles an expected matcher miss without a cache-generation invariant error

expect(received).not.toMatch(expected)

Expected pattern: not /invariant:|Error:/i
Received string:      "Error: Internal: NoFallbackError

  at n (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:1220)
  at responseGenerator (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:4878)
  "
  at Object.toMatch (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:113:51)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

● param-matching-routing › revalidation › revalidates a seeded output without changing route admission: /en/closed/known

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 404

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:134:18
  at retry (lib/next-test-utils.ts:940:14)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:133:9

pnpm test-deploy test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > client route discovery > does not poison an admitted open suffix after prefetching a 404: /en/catalog/nav-top/items/nav-bottom (DD)
  • param-matching-routing > client route discovery > does not reuse an admitted prefix when navigation must be a routing 404 (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: "space%20here/100%25"
Received: ""

  13 |     expect(res.status).toBe(200)
  14 |     const $ = cheerio.load(await res.text())
> 15 |     expect($('#params').text()).toBe(params)
     |                                 ^
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)
  17 |     return $('#generation').text()
  18 |   }

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:15:33)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › client route discovery › does not poison an admitted open suffix after prefetching a 404: /en/catalog/nav-top/items/nav-bottom

The same expected substring was sent multiple times by the server:

Catalog

Choose a more specific substring to assert on.

  165 |             .click()
  166 |         })
> 167 |         await act(
      |               ^
  168 |           async () => {
  169 |             await browser
  170 |               .elementByCss(`input[data-link-accordion="${pathname}"]`)

  at act (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:167:15)

● param-matching-routing › client route discovery › does not reuse an admitted prefix when navigation must be a routing 404

The same expected substring was sent multiple times by the server:

Catalog

Choose a more specific substring to assert on.

  192 |         },
  193 |       })
> 194 |       await act(
      |             ^
  195 |         async () => {
  196 |           await browser
  197 |             .elementByCss(

  at Object.act (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:194:13)

pnpm test-deploy test/e2e/app-dir/param-matching-root-params/param-matching-root-params.test.ts (job)

  • param-matching-root-params > keeps unknown roots out of a fallback shell regenerated after invalidation (DD)
Expand output

● param-matching-root-params › keeps unknown roots out of a fallback shell regenerated after invalidation

expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 0

  73 |     expect($('#nested-lang').text()).toBe('nested:IT')
  74 |     expect($('#direct-lang').text()).toBe('it')
> 75 |     expect($('#nested-pending').length).toBe(1)
     |                                         ^
  76 |     expect($('#outer-pending').length).toBe(1)
  77 |     expect($('#nested-lang').closest('[hidden]').length).toBe(1)
  78 |     expect($('#cache-prefix').closest('[hidden]').length).toBe(1)

  at Object.toBe (e2e/app-dir/param-matching-root-params/param-matching-root-params.test.ts:75:41)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

pnpm test-deploy-experimental-turbo test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (turbopack) (Experimental) (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > client route discovery > does not poison an admitted open suffix after prefetching a 404: /en/catalog/nav-top/items/nav-bottom (DD)
  • param-matching-routing > client route discovery > does not reuse an admitted prefix when navigation must be a routing 404 (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: "space%20here/100%25"
Received: ""

  13 |     expect(res.status).toBe(200)
  14 |     const $ = cheerio.load(await res.text())
> 15 |     expect($('#params').text()).toBe(params)
     |                                 ^
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)
  17 |     return $('#generation').text()
  18 |   }

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:15:33)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › client route discovery › does not poison an admitted open suffix after prefetching a 404: /en/catalog/nav-top/items/nav-bottom

The same expected substring was sent multiple times by the server:

Catalog

Choose a more specific substring to assert on.

  165 |             .click()
  166 |         })
> 167 |         await act(
      |               ^
  168 |           async () => {
  169 |             await browser
  170 |               .elementByCss(`input[data-link-accordion="${pathname}"]`)

  at act (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:167:15)

● param-matching-routing › client route discovery › does not reuse an admitted prefix when navigation must be a routing 404

The same expected substring was sent multiple times by the server:

Catalog

Choose a more specific substring to assert on.

  192 |         },
  193 |       })
> 194 |       await act(
      |             ^
  195 |         async () => {
  196 |           await browser
  197 |             .elementByCss(

  at Object.act (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:194:13)

pnpm test-deploy-experimental-turbo test/e2e/app-dir/param-matching-root-params/param-matching-root-params.test.ts (turbopack) (Experimental) (job)

  • param-matching-root-params > keeps unknown roots out of a fallback shell regenerated after invalidation (DD)
Expand output

● param-matching-root-params › keeps unknown roots out of a fallback shell regenerated after invalidation

expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 0

  73 |     expect($('#nested-lang').text()).toBe('nested:IT')
  74 |     expect($('#direct-lang').text()).toBe('it')
> 75 |     expect($('#nested-pending').length).toBe(1)
     |                                         ^
  76 |     expect($('#outer-pending').length).toBe(1)
  77 |     expect($('#nested-lang').closest('[hidden]').length).toBe(1)
  78 |     expect($('#cache-prefix').closest('[hidden]').length).toBe(1)

  at Object.toBe (e2e/app-dir/param-matching-root-params/param-matching-root-params.test.ts:75:41)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

pnpm test-start test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > handles an expected matcher miss without a cache-generation invariant error (DD)
  • param-matching-routing > revalidation > revalidates a seeded output without changing route admission: /en/closed/known (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › handles an expected matcher miss without a cache-generation invariant error

expect(received).not.toMatch(expected)

Expected pattern: not /invariant:|Error:/i
Received string:      "Error: Internal: NoFallbackError

  at q (../.next/server/chunks/734.js:22:1220)
  at responseGenerator (../.next/server/chunks/734.js:22:4878)
  "
  at Object.toMatch (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:113:51)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

● param-matching-routing › revalidation › revalidates a seeded output without changing route admission: /en/closed/known

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 404

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:124:24

pnpm test-start-turbo test/e2e/app-dir/param-matching-routing/param-matching-routing.test.ts (turbopack) (job)

  • param-matching-routing > serves an admitted path through its real handler: /es/docs/space%20here/100%25 (DD)
  • param-matching-routing > serves an admitted path through its real handler: /open/docs/space%20here/100%25 (DD)
  • param-matching-routing > handles an expected matcher miss without a cache-generation invariant error (DD)
  • param-matching-routing > revalidation > revalidates a seeded output without changing route admission: /en/closed/known (DD)
Expand output

● param-matching-routing › serves an admitted path through its real handler: /es/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › serves an admitted path through its real handler: /open/docs/space%20here/100%25

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:41:7

● param-matching-routing › handles an expected matcher miss without a cache-generation invariant error

expect(received).not.toMatch(expected)

Expected pattern: not /invariant:|Error:/i
Received string:      "Error: Internal: NoFallbackError

  at n (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:1220)
  at responseGenerator (../.next/server/chunks/ssr/[root-of-the-server]__0wqqc2npd6qpi._.js:2:4878)
  "
  at Object.toMatch (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:113:51)
  at Object.gatedBody (lib/gate/runtime.ts:280:7)

● param-matching-routing › revalidation › revalidates a seeded output without changing route admission: /en/closed/known

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 404

  11 |   async function render(pathname: string, params: string) {
  12 |     const res = await next.fetch(pathname)
> 13 |     expect(res.status).toBe(200)
     |                        ^
  14 |     const $ = cheerio.load(await res.text())
  15 |     expect($('#params').text()).toBe(params)
  16 |     expect($('#root-not-found, #nested-not-found').length).toBe(0)

  at toBe (e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:13:24)
  at e2e/app-dir/param-matching-routing/param-matching-routing.test.ts:124:24

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

Warning

No stats were collected for Webpack because its stats job did not complete (it failed, was cancelled, or timed out). The results below only cover the bundlers that finished.

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 554 MB 554 MB 🔴 +450 kB (+0%) ▁▁▁▁▁
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 813ms 812ms ▁▂▂▂▂
Cold (Ready in log) 805ms 799ms ▁▁▂▂▁
Cold (First Request) 1.326s 1.318s ▁▂▂▂▁
Warm (Listen) 813ms 813ms ▁▂▂▂▂
Warm (Ready in log) 810ms 811ms ▁▁▂▁▂
Warm (First Request) 1.321s 1.334s ▄▅▅▅▅

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.688s 5.804s ▁▃▃▁▁
Cached Build 2.959s 2.981s ▁▃▄▁▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
node_modules Size 554 MB 554 MB 🔴 +450 kB (+0%) ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
0-ygxg4edo90y.js gzip 150 B N/A -
00p2c2wj212x3.js gzip 155 B N/A -
01vjfb7f6m12-.js gzip 155 B N/A -
050icza-xjz0i.js gzip 5.73 kB N/A -
07jdby0ue616s.js gzip 450 B N/A -
0bjdc8muo74n5.js gzip 8.71 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0ddqsrtxulkx0.js gzip 168 B N/A -
0rci1f3or1a19.js gzip 13.3 kB N/A -
1_2x714--ii1i.js gzip 8.76 kB N/A -
1-3y752pkth5-.js gzip 10 kB N/A -
19gb-1_5udxfb.js gzip 13.2 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1j97foemny6je.js gzip 153 B N/A -
1t1r5ct8uyoa7.js gzip 71.6 kB N/A -
1tf1phijqlx9j.js gzip 220 B 220 B
1uzabyd1120a1.js gzip 8.71 kB N/A -
1yji5jif2phft.js gzip 155 B N/A -
2-ufv8lc-g7gg.js gzip 10.6 kB N/A -
21kmjy_10x14f.js gzip 8.81 kB N/A -
25ia1zomyic4a.js gzip 159 B N/A -
28dhc6t85q1_p.js gzip 8.78 kB N/A -
2eztkb8qx_2tl.js gzip 156 B N/A -
2f-ilvczue-tp.js gzip 9.46 kB N/A -
2f1u17u5c8iny.js gzip 8.79 kB N/A -
2ikltg_8iegxw.js gzip 10.3 kB N/A -
2ixp3xn2_vz0r.js gzip 3.57 kB N/A -
2kims73jfrel-.js gzip 155 B N/A -
2ll2e0ryjz_ro.js gzip 155 B N/A -
2v8dcmz7cu2hz.js gzip 156 B N/A -
2vxi673cz1-t4.js gzip 8.79 kB N/A -
3-s5mc_uukwxe.js gzip 65.6 kB N/A -
30dzo1on6iyzf.js gzip 154 B N/A -
30ru3h2exh52x.js gzip 46.9 kB N/A -
38-q43pzktqhs.js gzip 1.46 kB N/A -
3f977fytahn5a.js gzip 7.53 kB N/A -
3h7ul6xddqjr3.js gzip 157 B N/A -
3nbojhxiy1qv_.js gzip 13.7 kB N/A -
41u5s3oe2-erp.js gzip 2.29 kB N/A -
445s_9hf8o7ao.js gzip 8.76 kB N/A -
turbopack-0d..9ejy.js gzip 3.74 kB 3.74 kB
0-6bcj16ji2wf.js gzip N/A 10.6 kB -
07ryk0jced-sc.js gzip N/A 8.78 kB -
0em9x2z24624i.js gzip N/A 153 B -
0gmtlup9ai5-v.js gzip N/A 156 B -
0jwho9fkrb_t3.js gzip N/A 2.29 kB -
0roh390ijzxa5.js gzip N/A 8.75 kB -
0tnqbstf54jds.js gzip N/A 168 B -
0vex9w55ursqj.js gzip N/A 8.79 kB -
1_iu9fa4-5eeu.js gzip N/A 151 B -
13fdp6hysi38e.js gzip N/A 7.53 kB -
17oe55cu76cd7.js gzip N/A 450 B -
186ulb4oejwp2.js gzip N/A 160 B -
18nkmwg0i-cre.js gzip N/A 155 B -
1c_5ayxbnh9m0.js gzip N/A 156 B -
1drww5xikb-c-.js gzip N/A 9.46 kB -
1g60xde_dv17t.js gzip N/A 8.79 kB -
1umpctldnyrmf.js gzip N/A 71.7 kB -
1uzv47btzam64.js gzip N/A 5.73 kB -
1vy7n7wxv_rh5.js gzip N/A 8.81 kB -
1x5-tqu49a74h.js gzip N/A 155 B -
2-0i7pl900-ou.js gzip N/A 8.71 kB -
2-kcbngm7ik7y.js gzip N/A 8.75 kB -
28bc8dop-tuzu.js gzip N/A 155 B -
29p5-xa4jmdhg.js gzip N/A 13.7 kB -
29y9nf5et8m57.js gzip N/A 156 B -
2d4muvzp1coq7.js gzip N/A 13.2 kB -
2nsrcvgnlgkby.js gzip N/A 3.56 kB -
2ugc69z0t0ypz.js gzip N/A 1.46 kB -
2xww-wmxsmsl5.js gzip N/A 159 B -
36dn8i-_3dnq5.js gzip N/A 10 kB -
3af363oh3ex9y.js gzip N/A 46.9 kB -
3fljpmwcjxqhx.js gzip N/A 10.3 kB -
3gz44skqdlsrb.js gzip N/A 8.71 kB -
3otcauqycd0hf.js gzip N/A 65.6 kB -
3wdu9xjwj7f69.js gzip N/A 156 B -
3xim9_hulwdh_.js gzip N/A 155 B -
3y9tus7kb5su0.js gzip N/A 13.3 kB -
Total 401 kB 401 kB ⚠️ +14 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 1.05 kB 1.06 kB
Total 1.05 kB 1.06 kB ⚠️ +1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 878 B 876 B
Total 878 B 876 B ✅ -2 B
Build Cache
Canary PR Change
00000001.sst gzip 14.9 MB 12.2 MB 🟢 2.74 MB (-18%)
00000002.sst gzip 12.4 MB 13.7 MB 🔴 +1.29 MB (+10%)
00000003.sst gzip 14.1 MB 15.4 MB 🔴 +1.25 MB (+9%)
00000004.sst gzip 14.1 MB 14.3 MB 🔴 +228 kB (+2%)
00000005.sst gzip 2.8 MB 10.2 MB 🔴 +7.43 MB (+265%)
00000006.sst gzip 10.2 MB 2.8 MB 🟢 7.42 MB (-73%)
00000007.sst gzip 59 B 59 B
00000008.meta gzip 89 B 89 B
00000009.meta gzip 298 kB 298 kB
00000010.meta gzip 298 kB 298 kB
00000011.meta gzip 298 kB 298 kB
00000012.sst gzip 51.6 kB 52.5 kB 🔴 +940 B (+2%)
00000013.sst gzip 1.82 MB 1.77 MB 🟢 47.8 kB (-3%)
00000014.sst gzip 59 B 59 B
00000015.meta gzip 116 B 116 B
00000016.meta gzip 328 kB 328 kB
00000017.meta gzip 407 kB 406 kB
00000018.sst gzip 52.4 kB 53.4 kB 🔴 +1.03 kB (+2%)
00000019.sst gzip 1.4 MB 1.39 MB 🟢 6.67 kB (0%)
00000020.sst gzip 59 B 59 B
00000021.meta gzip 116 B 116 B
00000022.meta gzip 328 kB 328 kB
00000023.meta gzip 373 kB 374 kB
00000024.sst gzip 52.5 kB 53.5 kB 🔴 +975 B (+2%)
00000025.sst gzip 1.39 MB 1.39 MB 🟢 6.6 kB (0%)
00000026.sst gzip 59 B 59 B
00000027.meta gzip 116 B 116 B
00000028.meta gzip 328 kB 328 kB
00000029.meta gzip 373 kB 374 kB
00000030.sst gzip 51.7 kB 52.7 kB 🔴 +1.06 kB (+2%)
00000031.sst gzip 1.39 MB 1.39 MB 🟢 6.67 kB (0%)
00000032.sst gzip 59 B 59 B
00000033.meta gzip 116 B 116 B
00000034.meta gzip 328 kB 328 kB
00000035.meta gzip 374 kB 374 kB
00000036.sst gzip 52.4 kB 53.3 kB 🔴 +903 B (+2%)
00000037.sst gzip 1.39 MB 1.39 MB 🟢 6.58 kB (0%)
00000038.sst gzip 59 B 59 B
00000039.meta gzip 116 B 116 B
00000040.meta gzip 328 kB 328 kB
00000041.meta gzip 373 kB 374 kB
CURRENT gzip 94 B 94 B
LOG gzip 672 B 671 B
Total 80.6 MB 80.6 MB ✅ -36.3 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 375 kB 375 kB
app-page-exp..prod.js gzip 206 kB 206 kB
app-page-tur...dev.js gzip 374 kB 375 kB
app-page-tur..prod.js gzip 206 kB 206 kB
app-page-tur...dev.js gzip 370 kB 371 kB
app-page-tur..prod.js gzip 204 kB 204 kB
app-page.run...dev.js gzip 371 kB 371 kB
app-page.run..prod.js gzip 204 kB 204 kB
app-route-ex...dev.js gzip 83.3 kB 83.3 kB
app-route-ex..prod.js gzip 56.3 kB 56.3 kB
app-route-tu...dev.js gzip 83.3 kB 83.3 kB
app-route-tu..prod.js gzip 56.3 kB 56.3 kB
app-route-tu...dev.js gzip 82.8 kB 82.8 kB
app-route-tu..prod.js gzip 56 kB 56 kB
app-route.ru...dev.js gzip 82.8 kB 82.8 kB
app-route.ru..prod.js gzip 56 kB 56 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 132 kB 132 kB
dev-validati...dev.js gzip 132 kB 132 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 46.5 kB 46.5 kB
pages-api-tu..prod.js gzip 34.7 kB 34.7 kB
pages-api.ru...dev.js gzip 46.5 kB 46.5 kB
pages-api.ru..prod.js gzip 34.6 kB 34.6 kB
pages-turbo....dev.js gzip 55.3 kB 55.3 kB
pages-turbo...prod.js gzip 40.2 kB 40.2 kB
pages.runtim...dev.js gzip 55.3 kB 55.3 kB
pages.runtim..prod.js gzip 40.2 kB 40.2 kB
server.runti..prod.js gzip 66.6 kB 66.9 kB
use-cache-pr...dev.js gzip 72.7 kB 72.7 kB
use-cache-pr...dev.js gzip 72.7 kB 72.7 kB
use-cache-pr...dev.js gzip 71 kB 71 kB
use-cache-pr...dev.js gzip 70.9 kB 70.9 kB
Total 4.11 MB 4.11 MB ⚠️ +1.98 kB
📝 Changed Files (9 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • server.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/1cad2abe18a5e6fa72545f6d01ff0bee840d21f8/next

Commit: 1cad2ab

@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from 1aca829 to 73146ab Compare August 17, 2026 18:53
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from 73146ab to c3175eb Compare August 18, 2026 14:59
Base automatically changed from codex/prerender-param-policy-model to canary August 18, 2026 19:31
gnoff added a commit that referenced this pull request Aug 18, 2026
## Summary

This is the behavior-preserving base of a four-PR stack that introduces
explicit prerender matching policy without coupling the foundational
refactor to the proposed API.

The build currently uses `PrerenderedRoute` values for two related but
distinct concepts:

- a logical request matcher that says which parameterized URL shapes the
route can handle
- a build-time render candidate that may or may not become a persisted
prerender artifact

A candidate is not guaranteed to be an output. It can be rendered only
to validate a shell, discarded, and still leave behind a matcher that
tells future requests to block.

The new relationship is:

```text
logical pathname matcher
  -> zero or more render candidates
    -> zero or more persisted artifacts
```

## Concrete example

Consider `/[top]/items/[bottom]`:

```ts
export function generateStaticParams() {
  return [{ top: 't1', bottom: 'b1' }]
}
```

Static-path generation may consider three shapes:

| Shape | Purpose |
| --- | --- |
| `/[top]/items/[bottom]` | A generic shell candidate and logical
matcher |
| `/t1/items/[bottom]` | A shell after resolving `top` |
| `/t1/items/b1` | A concrete build-time prerender |

Suppose the generic candidate renders an allowed empty shell. The build
should discard that candidate artifact and use blocking behavior for the
generic matcher. It should not remove `/[top]/items/[bottom]` from the
valid matcher set, and it should not discard the concrete `/t1/items/b1`
artifact.

This is why the build needs two sets:

- route matchers, which describe valid request shapes
- prerender candidates, whose render results determine whether an
artifact is retained and can refine inferred fallback behavior

## Variants compatibility

Variants will make pathname-only candidate maps insufficient. Several
variant combinations can share `/t1/items/b1` as their logical pathname
while writing distinct artifacts under variant-specific output paths.

This PR keeps the route matcher keyed by logical pathname but retains
every candidate associated with it. Candidate finalization can therefore
evaluate each variant artifact independently without changing the route
tree's matcher set.

## Behavior preservation

This PR does not add a user-facing API or change `generateStaticParams`
semantics:

- a usable static shell remains a fallback prerender
- an allowed empty shell is discarded and represented by a blocking
matcher
- a route that requires a non-empty shell still fails validation
- unresolved matchers remain gated by route-level PPR support
- the most-specific shell continues to supply first-writer-wins metadata
such as prefetch hints

Render results can decide whether a candidate artifact is published and
can refine inferred matcher behavior, but they do not remove the logical
route matcher itself.

## Stack plan

1. **#97431 — model prerenders as render candidates:** land the
behavior-preserving matcher/candidate separation and post-render
finalization first.
2. **#97393 — add the experimental matcher API:** add `unstable_matcher`
and `unstable_generateMatcher`, policy aggregation, validation, and
local diagnostics.
3. **#97426 — test complex route shapes:** add test-only coverage for
catch-alls, optional catch-alls, root parameters, and parallel slots.
4. **#97427 — test foreground policy behavior:** add test-only coverage
showing blocking misses generate before responding while fallback misses
return the shared shell immediately.

This layering lets the internal model be reviewed and landed
independently of the API design. The upper test PRs validate the final
behavior without increasing the implementation diff.

## Verification

- `pnpm --filter=next types`
- `pnpm test-start-turbo
test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/prefetch-inlining/prefetch-inlining.test.ts`

<!-- NEXT_JS_LLM -->
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch 2 times, most recently from 14a439e to 86f73ed Compare August 19, 2026 15:38
@gnoff
gnoff removed this pull request from stack #98894 September 19, 2026 22:03
@gnoff
gnoff added this pull request to stack #98946 September 19, 2026 22:03
A generic fallback shell can have an unresolved root parameter. Reading
that root directly already suspends, but reading it inside `use cache`
returned the opaque placeholder and could persist placeholder-derived UI
in the resume cache. For example, a cached `lang().toUpperCase()` could
render `%%DRP:LANG:...%%` for a later French request.

Propagate the fallback mask into public caches and abort the cache fill
when an unresolved root value is consumed. Share cancellation through
nested caches so an outer cache cannot retain the inner placeholder read.
Preserve tracked root dependencies while cancellation completes.

Add a real Cache Components fixture without paramMatching. Cold fallback
debug renders exercise direct and nested cached reads, followed by concrete
requests that must not reuse placeholder-derived content. All three tests
fail without this fix. Existing concrete-root cache coverage also passes.
Adapter routing forwards a root parameter such as lang as nxtPlang. The
servable-fallback branch used the unprefixed name in config.allowQuery,
unlike the existing blocking branch. That mismatch can discard the value
before the request reaches the function or partitions its cache entry.

Prefix the root names consistently. Add a production fixture that runs
the real emitter against real build artifacts and a servable-root-fallback
manifest input. Current inference emits blocking root entries, so the
test supplies that one input explicitly rather than depending on the new
paramMatching API. Its expected nxtPlang fails against the old lang output.

This changes only Next.js output and requires no adapter implementation
change. The contract test is not a live deployment/function-grouping test.
Integrate the updated build-time-generator prerequisite and initialize the
matching generator's functionName as experimental_generateParamMatching.
This keeps diagnostics tied to the exported API rather than reporting
generateStaticParams whenever a generator uses an unavailable operation.

For example, calling headers() in experimental_generateParamMatching now
reports that function's build-time scope. Root-param errors no longer
suggest that matching generators receive concrete values from parent gSP
calls, and private-cache and revalidation errors retain the same scope.

Add real build-error fixtures covering cookies, headers, connection, draft
mode, prefetch, navigation, private caching, revalidation, and root params.
The generic store support remains in the prerequisite PR; this layer only
supplies the matching API's name and its integration coverage.

Quick formatting and lint checks pass. The prerequisite's existing ten
gSP integration tests passed. Pushing before the new fixture verification
finishes so CI and the remaining local checks can run concurrently.
Correct the two new prefetch/navigation fixtures to use the public module
that actually exports unstable_prefetch and unstable_navigation. Importing
them from next/server stopped the fixture's TypeScript check before any
generator diagnostics could be exercised.

This changes only test imports; the framework implementation is unchanged.
Adapter routing forwards a root parameter such as lang as nxtPlang. The
servable-fallback branch used the unprefixed name in config.allowQuery,
unlike the existing blocking branch. That mismatch can discard the value
before the request reaches the function or partitions its cache entry.

Prefix the root names consistently. Add a production fixture that runs
the real emitter against real build artifacts and a servable-root-fallback
manifest input. Current inference emits blocking root entries, so the
test supplies that one input explicitly rather than depending on the new
paramMatching API. Its expected nxtPlang fails against the old lang output.

This changes only Next.js output and requires no adapter implementation
change. The contract test is not a live deployment/function-grouping test.
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from 59f762e to 114db84 Compare September 19, 2026 23:55
@gnoff
gnoff removed this pull request from stack #98946 September 19, 2026 23:55
@gnoff
gnoff added this pull request to stack #98951 September 19, 2026 23:55
Integrate #98950 and the rebased query-key prerequisite into the existing parameter-matching history. Replace shared cache cancellation with independently cancellable fallback-root fills and remove seeded entries only when they depend on roots that are unknown in the target shell.

Keep the API patch unchanged relative to its prerequisite branch and preserve its separately removable diagnostic commit. The resulting source tree is byte-identical to the already verified replay: 19 production Webpack checks pass across cached root matching, root-fallback behavior, and adapter query-key output. The new prerequisite separately passes its six production and four development regression cases.
@gnoff
gnoff force-pushed the jstory/unstable-matcher branch from 114db84 to a321933 Compare September 19, 2026 23:57
…uisite

Inherit the root-cache fixture cleanup without changing this layer's query-key behavior. Fresh unknown-root cache fills will be exercised through supported paramMatching requests, not shell-debugging overrides.
Inherit the prerequisite's ordinary-build assertions and remove debug-forced root fallback requests. This preserves the existing API history while separating build-cache reuse from runtime root-fallback revalidation coverage.
Expire tagged shell data through a Route Handler, then request a novel language. A new cached version proves the generic shell was regenerated; nested root reads and a cached component with its own Suspense boundary must remain dynamic holes and resume with the requested language. A second language reuses the regenerated language-independent shell.

The fixture uses normal paramMatching configuration, revalidateTag, and document requests, with no shell-debugging settings or forced render overrides. Removing only the fresh-fill handling while retaining RDC pruning makes the regression render an opaque root placeholder instead of the requested language. The complete implementation passes the production fixture.
Keep the nxtP root-query correction in the API layer that makes servable
root fallbacks reachable. For a [lang] layout configured as fallback, the
adapter output must allow nxtPlang, exactly as the routing rule forwards it.

Remove the synthetic manifest-editing fixture and its direct internal-emitter
invocation. Strengthen the real paramMatching build fixture to require a
servable root fallback and the exact non-empty query key list on its HTML,
RSC, and segment outputs. Removing the prefix fix fails this assertion with
lang instead of nxtPlang; restoring it passes all eight fixture tests.

Merge the latest root-cache prerequisite so the API PR can sit directly on
it without rewriting existing history. The separate interception-query
investigation is not part of this change.

Verification: 11 production Webpack tests, full bootstrap, repository
TypeScript checking, and changed-file lint pass.
@gnoff
gnoff removed this pull request from stack #98951 September 20, 2026 05:46
@gnoff
gnoff changed the base branch from codex/fallback-root-query-keys to codex/root-param-shell-cache September 20, 2026 05:46
@gnoff
gnoff added this pull request to stack #98956 September 20, 2026 05:46
Specialized prerender matchers execute their original page module. Remove the generated alias page files and deployment-only app-path-routes manifest; function grouping already filters the launcher manifest to the real page entrypoints included in the function.

Keep the parentFallbackMode correction: lang=not-found with an open catalog suffix must admit /en/catalog/t2/items/b2 even when only t1/b1 was built. Fully closed pages still retain their build-time allowlist.

Extend the existing adapter-output fixture to verify the original manifest is packaged, specialized matcher aliases are absent, and partially closed versus fully closed pages preserve their different parent fallback metadata.

Verification: focused production Webpack inheritance, matcher-output, and adapter-contract checks (3 passed); repository TypeScript and targeted lint checks. The preceding comparison also exercised the unchanged Vercel grouper and launcher with grouped and singleton functions, including fallback resume and RSC requests.
Remove the parameter-policy resolver check for combining dynamicParams with experimental parameter matching. Parameter matching requires Cache Components, whose existing segment-config validation already rejects dynamicParams, so there is no supported configuration that reaches this extra check.

Remove the unit case that bypassed module validation to exercise the redundant error. The existing cache-components-segment-configs fixture still verifies the real user-facing rejection. Verification: 98 static-paths unit tests and the production Webpack unsupported-config fixture pass.
Derive whether a route configures parameter matching from the optional policy itself instead of carrying a second boolean. An empty policy object remains distinct from an absent export, so opting in without overriding any parameter keeps its existing behavior.

Represent the dev-only not-found decision as devParamMatchingRejected. Preserve the reset before each matching attempt and the debug-prerender bypass; normal dev requests still render dynamically, regardless of blocking or fallback policies.

Verified with the existing routing and dev shell-validation fixtures (10 tests), a Next.js build, and the repository TypeScript check.
Rename the policy dictionary, resolver, validation helpers, and worker-result fields to paramMatching terminology. For example, resolveParamMatching merges a layout policy of lang: not-found with a page policy of top: blocking; it does not compile the emitted URL matchers.

Keep PrerenderRouteMatcher and the matcher diagnostics named for the actual route patterns they describe. The policy module becomes param-matching.ts, and its existing tracing snapshot follows that filename. This is a naming-only cleanup: policy resolution, validation, inference, and runtime behavior are unchanged.

Verified 108 unit tests, the existing production parameter-matching fixture (26 tests), dev export and generator fixtures (16 tests), repository typechecking, and six Turbopack tracing tests with five snapshots after a fresh native build.
Give the type fixture an explicit TypeScript project that excludes the test harness while checking generated development and production validators. This keeps ordinary export and invalid-key coverage from failing on missing Jest and harness dependencies in an isolated app.

Fix the routing README language warnings, clarify that generator results are shared per route evaluation rather than across the build, and describe closure identity as a normalized URL prefix. Explain which regressions the build-artifact assertions protect without changing matching or shell-validation behavior.

Verified with fresh native Turbopack: six type-fixture tests across dev and production, 28 production build-contract tests, 98 matching unit tests, repository TypeScript, and changed-file formatting, ESLint, and language checks.
The new routing fixture incorrectly expected decoded server params. A fresh canary build returns URI-encoded values for both single and catch-all params, and the existing prerender-encoding test explicitly asserts that behavior. Parameter matching does not change the encoding contract.

Correct the four expected values and document the baseline, retaining the encoded request URLs, unconfigured controls, repeated requests, and HTTP 200 assertions. All 25 runnable development routing cases now pass. Production passes 27 cases; two percent-sign requests still return 500, and the existing expected-404 logging and fully closed revalidation failures remain visible. No runtime implementation changes or test gates are added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants