fix(app-router): reject interception routes in static export#2365
fix(app-router): reject interception routes in static export#2365james-elicx wants to merge 6 commits into
Conversation
|
/bigbonk review for issues |
commit: |
|
There was a problem hiding this comment.
Review: reject interception routes in static export
I verified the type usage, the error-message parity with Next.js, and the new test (which passes). The implementation is clean and the message matches Next.js (v16.2.6) exactly. However, there's one blocking correctness issue: the guard is added to a function the real export build never calls, so it does not actually fix the Next.js parity failure this PR targets.
Blocking: guard is in a dead code path for output: 'export' builds
staticExportApp() is only invoked by tests and re-exported as a public API from index.ts. The actual vinext build flow for output: 'export' is:
cli.ts:626—shouldPrerender = ... || resolvedNextConfig.output === "export"cli.ts:641—await runPrerender(...)run-prerender.ts:210—const mode = config.output === "export" ? "export" : "default"run-prerender.ts:269—await prerenderApp({ mode, routes, ... })← called directly, bypassingstaticExportApp()
So during a genuine static export build, hasInterceptionRoutes() is never reached, and a project with @modal/(.)page (the exact Next.js fixture shape) will attempt to prerender instead of failing with the build-time error. The Next.js test asserts nextBuild exits with code === 1 and stderr contains the message; the equivalent vinext build path would not produce that error.
The misleading comment at run-prerender.ts:229-230 reinforces this — it claims export builds are "handled by static-export.ts", but the CLI clearly routes export builds through runPrerender → prerenderApp.
Suggested fix: move (or duplicate) the interception check so it runs on the real export path. The cleanest spot is in run-prerender.ts right after const routes = await appRouter(...) (line 262), guarded by mode === "export", throwing the same message. Keeping the existing check in staticExportApp is fine for the public-API surface, but the export build must enforce it too. Consider extracting hasInterceptionRoutes to a shared module so both call sites use one implementation.
Test coverage gap (related to the above)
The new test calls staticExportApp() directly, so it passes even though the real build path is unguarded. To actually protect against regressions, add an assertion against the path the CLI uses — e.g. drive runPrerender (or prerenderApp with mode: "export") over the same fixture and assert it rejects. As written, the test gives false confidence that vinext build rejects interception routes.
Non-blocking notes
- Parity is otherwise good: error string matches Next.js
v16.2.6verbatim, and the fixture mirrorstest/e2e/app-dir/interception-routes-output-export(the omitted top-levelapp/default.tsxdoesn't affect interception detection). vp checkpasses on both changed files; the type access ofsiblingIntercepts/parallelSlots[].interceptingRoutesis sound (AppRouteGraphRouteis a superset ofAppRoute).
Once the guard is enforced on the runPrerender path with a test that exercises it, this is good to merge.
|
Review posted to PR #2365. SummaryI reviewed PR #2365 (reject interception routes in static export) and found one blocking issue: The guard is in a dead code path. The PR adds
The test passes but tests the wrong path — it calls What's correct:
Recommended fix: move/duplicate the check into |
|
/bigbonk review for issues |
|
/bigbonk review for issues |
|
/bigbonk review for issues |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
/bigbonk review for issues |
|
@james-elicx Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: reject interception routes in static export — blocking issue resolved ✅
This re-review follows up on my previous one, which flagged that the guard was added to a dead code path (staticExportApp(), which the real vinext build --output export flow never calls). The new commits (9ec5bd0, c03a71f, 1a4adf7) fix that correctly.
What's now correct
1. Guard is enforced on the real export path. assertNoStaticExportInterceptionRoutes() was extracted to a shared module (build/interception-static-export.ts) and is now called in run-prerender.ts:204-206, gated by mode === "export", before the prod server starts and before prerenderApp() runs. appRouter() is hoisted to line 203 and reused (no duplicate scan). This is exactly the path cli.ts drives for output: 'export', so the parity failure from run 28143992598 is genuinely fixed now.
2. Process-state leak avoided — and deliberately so. The VINEXT_PRERENDER env set/clear was moved from line ~169 to after the interception check (now line 242). This guarantees the validation throw happens before any global flag is set, so a rejected export build can't leak VINEXT_PRERENDER=1. The comment at 238-241 documents the ordering intent. Good defensive sequencing.
3. Test now exercises the real path. tests/prerender.test.ts adds a test that drives runPrerender({ output: 'export' }) over the exact Next.js fixture shape (@modal/(.)page), asserts it rejects with the verbatim message, AND asserts process.env.VINEXT_PRERENDER is unchanged after the throw. This closes the false-confidence gap from the prior review.
Parity verification
- Error string matches Next.js
v16.2.6verbatim:Intercepting routes are not supported with static export.(confirmed againsttest/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts, which assertsstderr.toContain(...)+code === 1). - Throwing in
runPrerenderpropagates to a non-zero CLI exit, matching Next.js's build-time error semantics. - Fixture mirrors the Next.js reference fixture shape.
Validation run locally
vp test run tests/prerender.test.ts -t "rejects App Router interception routes"→ passvp test run tests/app-router-static-export.test.ts -t "interception"→ passvp checkon all three changed source files → pass (format, lint, types clean).siblingIntercepts/parallelSlots[].interceptingRoutesaccess is sound onAppRoute.
Non-blocking nits
- The shared helper still keeps the redundant call in
staticExportApp()— that's fine (defensive for the public API surface), and both call sites use the one extracted implementation, so there's no divergence risk. - Minor: the
interception-static-export.tshelper has no JSDoc linking back to the Next.js fixture/test. A one-line// Ported from Next.js: ...reference (per AGENTS.md convention) would help future maintainers, but not required.
LGTM. The blocking issue is resolved and the fix is on the correct path with a test that exercises it.
|
Review posted to PR #2365. SummaryThe previous review's blocking issue has been resolved. The new commits ( What's now correct:
Parity verified:
Validation (run locally):
Only non-blocking nits remain (a redundant-but-harmless check in |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: reject interception routes in static export — verified, no new issues
I independently re-verified this PR after the earlier blocking issue (guard added to the dead staticExportApp() path) was fixed. The current state is correct and on the right code path. Ran the targeted tests, vp check, and git diff --check locally — all green.
What I verified
Guard is enforced on the real export build path. cli.ts routes output: 'export' builds through runPrerender → prerenderApp, never through staticExportApp(). The guard now lives at run-prerender.ts:204-206, gated by mode === "export", before the prod server starts and before prerenderApp(). This is the path the parity failure (run 28143992598) actually exercises.
No duplicate scan / perf regression from the hoist. appRouter() is hoisted to run-prerender.ts:203 and reused at line 264 (routes: appRoutes). appRouteGraph() caches the graph by appDir/pageExtensions (app-router.ts:64-66), so the App Router phase re-read hits the cache. For pages-only builds appDir is null → appRoutes is null → no behavior change.
Process-state ordering is correct. previousPrerenderFlag capture + VINEXT_PRERENDER = "1" was moved to line 242, after the interception throw at line 205. The finally (line 335-342) is paired with the right try, so a rejected export build can't leak VINEXT_PRERENDER. The prerender.test.ts test asserts this explicitly.
Type access is sound. appRouter() returns AppRouteGraphRoute[], a superset of AppRoute. siblingIntercepts and parallelSlots are required (non-optional) fields on AppRoute (app-route-graph.ts:128,134). The helper signature readonly AppRoute[] accepts it.
Parity. Error string Intercepting routes are not supported with static export. matches Next.js v16.2.6 verbatim. Throwing in runPrerender propagates to a non-zero CLI exit, matching Next.js's build-time code === 1 + stderr assertion. Both the helper and the new app-router-static-export.test.ts test carry the // Ported from Next.js: ... reference (the earlier-review nit is addressed).
Test coverage. Both interception shapes are covered: parallel-slot (@modal/(.)page) in prerender.test.ts + app-router-static-export.test.ts, and sibling (source/(.)target) in app-router-static-export.test.ts, with proper invalidateAppRouteCache() between cases.
Note (non-blocking)
The existing HTTP static-export test (static-export.test.ts:237) and the App Router export test (app-router-static-export.test.ts:24) now filter interception routes out of the shared app-basic fixture before calling staticExportApp(). This is necessary because that fixture contains ~20 interception/parallel-slot routes that the new guard would (correctly) reject. The filter is a reasonable workaround to keep those broad-coverage tests focused on non-interception export behavior — just flagging that the shared fixture's interception coverage for export is now exclusively in the dedicated new tests, which is fine.
Validation (local)
vitest run tests/prerender.test.ts -t "rejects App Router interception routes"→ passvitest run tests/app-router-static-export.test.ts -t "interception"→ passvitest run tests/static-export.test.ts→ 16 passvp checkon all 3 changed source files → clean (format, lint, types)git diff --check→ no whitespace errors
LGTM.
|
Review posted to PR #2365. SummaryI independently re-verified PR #2365. The earlier blocking issue (guard in the dead Verified:
One non-blocking note: the two broad existing tests now filter interception routes out of the shared All targeted tests pass, |
Summary
Next.js parity
Fixes the failure from run 28143992598 in:
test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.tsReference: https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts
Validation
vp test run tests/app-router-static-export.test.ts -t 'rejects interception routes' --maxWorkers=1vp check packages/vinext/src/build/static-export.ts tests/app-router-static-export.test.tsgit diff --checkFull suites are deferred to CI.