Skip to content

fix(pages-router): allow server-only in API routes#2234

Merged
james-elicx merged 1 commit into
mainfrom
codex/fix-module-layer-parity-27858251903
Jun 22, 2026
Merged

fix(pages-router): allow server-only in API routes#2234
james-elicx merged 1 commit into
mainfrom
codex/fix-module-layer-parity-27858251903

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • treat Pages API routes as neutral server import-chain roots
  • allow direct and transitive server-only imports without weakening validation for ordinary Pages client modules
  • cover both direct and transitive Pages API cases in the local fixture suite

Next.js parity

The pinned Next.js v16.2.6 reference (ee6e79b1792a4d401ddf2480f40a83549fe8e722) permits server-only in Pages API routes and modules imported by them.

This fixes the 13 module-layer failures in deploy-suite run 27858251903, which all came from the same build abort while processing pages/api/server-only-edge.js.

Validation

  • vp test run tests/middleware-server-only.test.ts — 4/4 passed with one worker
  • independent exact-head review — clean

No broad local suite or raw Next.js E2E was run.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@pkg-pr-new

pkg-pr-new Bot commented Jun 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2234
npm i https://pkg.pr.new/vinext@2234

commit: a83a96c

@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared a83a96c against base cf9d601 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.8 KB 132.8 KB ⚫ -0.0%
Client entry size (gzip) vinext 118.7 KB 118.7 KB ⚫ -0.0%
Dev server cold start vinext 2.22 s 2.22 s ⚫ -0.0%
Production build time vinext 2.62 s 2.62 s ⚫ -0.1%
RSC entry closure size (gzip) vinext 92.2 KB 92.2 KB ⚫ +0.0%
Server bundle size (gzip) vinext 166.9 KB 166.9 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

2 similar comments
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(pages-router): allow server-only in API routes

I verified this change by reading the diff, tracing the taint-tracking logic, and running the test suite both with and without the fix wired up.

Verdict: Looks correct — ship it. Two minor (non-blocking) notes below.

What I verified

1. The fix is real and the test actually catches the regression. I temporarily stubbed isNeutralServerModule to always return false and re-ran tests/middleware-server-only.test.ts. The build failed with exactly the error this PR targets, for both the direct and transitive cases:

'server-only' cannot be imported in client build ('ssr' environment):
 imported by lib/api-server-only.ts
  imported by pages/api/server-only-edge.ts
   imported by virtual:vinext-server-entry
    imported by virtual:vinext-app-ssr-entry

Restoring the fix makes all 4 tests pass. So the new fixture genuinely exercises the new code path (not a trivially-passing test).

2. Taint propagation is correct for deep chains. The depth-1 case is seeded by isNeutralServerModule at the resolveId guard (line 118). Module X imported by the API route gets added to tainted via the this.resolve(...) + addTainted(resolved.id) path (lines 134-137). Beyond depth-1, the existing isTainted(importer) mechanism takes over, so transitive chains of arbitrary depth work. Confirmed by lib/api-server-only.ts (a transitive import) succeeding.

3. No weakening of the client-side server-only guard. I checked entries/pages-client-entry.ts:52-61 — Pages API routes are referenced in the client entry only as prefetch metadata (documentOnly: true), never imported as modules. API route code never enters the client graph, so vinext:validate-server-only-client-imports (index.ts:4392, client-environment-only) is untouched. The fix is correctly scoped to the SSR (non-RSC) environment only. No security regression.

4. isNeutralServerModule canonicalization is consistent. It calls canonicalizePageTransformPath(id) before isApiPage, which slices against canonicalPagesDir.length — matching the pattern already used at index.ts:4339-4343. Correct.

5. vp check passes on the changed files (format, lint, types clean).

Minor notes (non-blocking)

a. Stale doc comment. middleware-server-only.ts:38-42 still reads "The taint set is scoped to the middleware chain only…". After this change it also covers Pages API route chains. The summary paragraphs were updated to mention API routes, but this one wasn't. Worth a one-line tweak for future readers.

b. Test coverage only exercises runtime = "edge". The new fixture's API route sets export const runtime = "edge". Since isNeutralServerModule is runtime-agnostic (it only checks the pages/api path), a default Node-runtime API route takes the identical code path and would also pass — but it isn't covered explicitly. Not a correctness gap, just a coverage observation if you want belt-and-suspenders.

Parity note

I could not verify the Next.js v16.2.6 reference locally (.nextjs-ref is not present in this environment), so I'm taking the PR's parity claim at face value. The cited WEBPACK_LAYERS neutral/server-layer treatment of API routes is consistent with how Next.js handles server-only in non-client layers, and the behavior matches the documented intent.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2234.

Summary

I reviewed the PR by reading the diff, tracing the taint-tracking logic, and empirically verifying the fix and test by toggling the change on/off.

Verdict: Correct — ship it. No blocking issues found.

Key findings (verified, not assumed)

  1. The fix works and the test catches the regression. Stubbing isNeutralServerModule to return false reproduced the exact build failure the PR targets, for both the direct (pages/api/server-only-edge.ts) and transitive (lib/api-server-only.ts) cases. Restoring the fix makes all 4 tests pass.

  2. Taint propagation is sound for arbitrary-depth chains — depth-1 is seeded by the neutral-module guard; deeper hops ride the existing isTainted(importer) mechanism.

  3. No security regression. Pages API routes never enter the client graph (only referenced as prefetch metadata in pages-client-entry.ts), so the client-side validate-server-only-client-imports guard is untouched. The fix is correctly scoped to the SSR/non-RSC environment.

  4. vp check passes on the changed files.

Two minor, non-blocking notes

  • Stale doc comment: middleware-server-only.ts:38-42 still says the taint set is "scoped to the middleware chain only" — now also covers Pages API routes.
  • Test coverage only exercises runtime = "edge"; a Node-runtime API route takes the same path but isn't explicitly covered.

I flagged that I couldn't verify the Next.js v16.2.6 reference locally (no .nextjs-ref in this environment), so the parity claim is taken at face value, though it's consistent with documented webpack-layer behavior.

github run

@james-elicx james-elicx marked this pull request as ready for review June 21, 2026 19:20
@james-elicx james-elicx merged commit 1908494 into main Jun 22, 2026
58 checks passed
@james-elicx james-elicx deleted the codex/fix-module-layer-parity-27858251903 branch June 22, 2026 13:46
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.

1 participant