Skip to content

fix(build): align Pages data export transforms - #2055

Merged
james-elicx merged 5 commits into
mainfrom
codex/pages-data-export-parity
Jun 16, 2026
Merged

fix(build): align Pages data export transforms#2055
james-elicx merged 5 commits into
mainfrom
codex/pages-data-export-parity

Conversation

@james-elicx

@james-elicx james-elicx commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

  • Align Pages Router data-export removal with Next.js for direct, aliased, forwarded, and mixed declarations.
  • Remove dependencies used exclusively by getServerSideProps, getStaticProps, and getStaticPaths while preserving shared client bindings.
  • Match Next.js diagnostics for client-compiled page export * declarations and mixed SSR/SSG exports.
  • Cover configured page extensions, MDX pages, API-route exclusions, nested routes, import attributes, destructuring, assignments, classes, lexical shadowing, and recursive helper graphs.

Context

Pages client builds previously handled only a subset of Next.js's page export transform. Some export forms retained server-only dependency chains, while other forms could drop sibling or shared bindings. This change ports the relevant behavior from Next.js's page export transforms and validates the emitted client output.

This overlaps with #2052's sibling-declarator fix, but includes that case as part of the broader transform parity work.

Next.js references

  • test/unit/babel-plugin-next-ssg-transform.test.ts
  • crates/next-custom-transforms/src/transforms/strip_page_exports.rs
  • packages/next/src/build/babel/plugins/next-page-disallow-re-export-all-exports.ts
  • packages/next/src/build/babel/loader/get-config.ts
  • packages/next/src/build/entries.ts
  • test/production/re-export-all-exports-from-page-disallowed/
  • test/production/mixed-ssg-serverprops-error/

Verification

  • vp check packages/vinext/src/plugins/strip-server-exports.ts packages/vinext/src/index.ts tests/build-optimization.test.ts tests/pages-server-exports-security.test.ts
  • vp test run tests/build-optimization.test.ts tests/pages-server-exports-security.test.ts tests/middleware-server-only.test.ts — 153 passed
  • vp run vinext#build
  • Independent cumulative diff review — clean after addressing findings

@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 28a61cf

@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

@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.

/bigbonk review — fix(build): align Pages data export transforms

Reviewed the transform port, the two plugin wirings in index.ts, and the new tests against the Next.js source in .nextjs-ref. The core strip-server-exports.ts transform is well-tested and the _app|_document|_error anchoring fix is a genuine parity improvement. Two issues worth addressing before merge — one is a deliberate-looking divergence from Next.js, the other is a perf regression in a hot path.


1. export * is rejected in API routes — Next.js does not do this (parity divergence)

packages/vinext/src/index.ts:4011-4032 — the new vinext:validate-page-exports plugin runs validatePageExports() on every page file under pages/, in every environment, with no API-route exclusion. The PR description and the code comment both state that "Next.js rejects export * from "..." in every Pages Router page file, including API routes." That is not what the Next.js source does.

In Next.js the disallow plugin is gated on !isServer && isPageFile:

// .nextjs-ref/packages/next/src/build/babel/loader/get-config.ts:192-200
const disallowExportAllItem =
  !isServer && isPageFile
    ? createConfigItem([require('../plugins/next-page-disallow-re-export-all-exports')], ...)
    : null

API routes never get a client compilation — createEntrypoints only routes them to onServer()/onEdgeServer():

// .nextjs-ref/packages/next/src/build/entries.ts:348-355
if (isAPIRoute(params.page)) {
  if (isEdgeRuntime(params.pageRuntime)) { params.onEdgeServer(); return }
  params.onServer(); return
}

So export * from "..." in pages/api/* builds fine in Next.js. The new test tests/pages-server-exports-security.test.ts:175 ("rejects export-all declarations in API page files") asserts the opposite of Next.js behavior and the upstream test (test/production/re-export-all-exports-from-page-disallowed/) only covers regular pages (about.js, contact.js, index.js) — no API route case.

This will break valid Next.js apps that use export * in an API route. Either:

  • exclude API routes (and _app/_document) from validate-page-exports to match Next.js, or
  • if this is an intentional defense-in-depth divergence, document it as a deliberate choice (per AGENTS.md, divergences must be deliberate and documented) and fix the comment + PR description, which currently misstate Next.js behavior.

2. Both transform handlers now realpathSync every module on every transform (perf regression)

packages/vinext/src/index.ts:4015-4032 and :4041-4067. The previous strip-server-exports plugin had filter: { id: /\.(tsx?|jsx?|mjs)$/ } and used the cheap id.startsWith(pagesDir) guard first. Both were removed:

  • vinext:validate-page-exports has no filter and no environment check, so its handler runs for every module across the client, SSR, and RSC environments.
  • vinext:strip-server-exports lost its extension filter.

Both now call canonicalize(stripViteModuleQuery(id)) unconditionally, and canonicalize is fs.realpathSync.native(...) (uncached syscall — build/ssr-manifest.ts:16). That is an extra syscall per module per environment, including for \0-prefixed virtual modules, CSS, and node_modules, none of which can be pages.

Suggested mitigations (cheap, no behavior change):

  • Re-add a filter: { id: /\.(tsx?|jsx?|mjs|mdx)$/ } to both plugins so the handler is skipped entirely for non-source modules.
  • Add an early if (id.startsWith("\0")) return null; before canonicalize.
  • For validate-page-exports, short-circuit on the cheap !/\bexport\s*\*/.test(code) check before doing the path work (this is the same check already inside validatePageExports).

Things that look correct

  • Anchoring _app/_document/_error to ^/(?:_app|_document|_error)(?:\.[^/]*)?$ correctly stops treating nested pages/admin/_error.tsx as special — matches Next.js, and the security fixture confirms its server deps are stripped.
  • The transform's dependency sweep, lexical-shadow handling, partial destructuring pruning, and mixed SSR/SSG conflict error all have solid, Next.js-aligned test coverage. vp test run tests/build-optimization.test.ts passes (141/141) locally.
  • validatePageExports correctly uses a broader \bexport\s*\* regex than the code.includes("export *") fast-path in stripServerExports, so newline-separated export\n*\nfrom is still caught by the dedicated plugin.

One minor follow-up to consider: recursive arrow-function helpers used only by server exports won't be swept, because the self-reference exclusion at strip-server-exports.ts:566-569 only applies to function/class kinds, not variable. Not a regression, just a parity gap if you want full coverage.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2055. Summary of findings:

Two issues to address

1. export * rejected in API routes — diverges from Next.js (correctness/parity)
The new vinext:validate-page-exports plugin runs on every page file in every environment with no API-route exclusion. Next.js gates its disallow-export-all plugin on !isServer && isPageFile, and API routes never get a client compilation (createEntrypoints only routes them to onServer()/onEdgeServer()). So export * in pages/api/* builds fine in Next.js. The new test and the PR comment both assert the opposite of actual Next.js behavior — verified against .nextjs-ref source. This can break valid Next.js apps. Either exclude API routes to match Next.js, or document it as a deliberate divergence and fix the misleading comment/description.

2. Perf regression — realpathSync per module per environment
Both transform handlers lost their filter: { id: /\.(tsx?|jsx?|mjs)$/ } and the cheap startsWith(pagesDir) guard, and now call canonicalize (uncached fs.realpathSync.native) unconditionally. validate-page-exports has no filter and no environment check, so it does a syscall for every module across client/SSR/RSC, including virtual modules, CSS, and node_modules. Suggested cheap mitigations: re-add the extension filter, early-return on \0 ids, and short-circuit on !/\bexport\s*\*/.

What's correct

  • The _app|_document|_error anchoring fix is a genuine parity improvement (nested _error.tsx now correctly treated as a regular page).
  • The transform's dependency sweep, shadow handling, destructuring pruning, and mixed SSR/SSG error are well-covered; all 141 tests in build-optimization.test.ts pass locally.

One minor note: recursive arrow-function helpers used only by server exports won't be swept (the self-reference exclusion only covers function/class kinds), but that's a pre-existing parity gap, not a regression.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Addressed the review findings in d616c14:

  • Export-star validation now runs only in the client graph and excludes API routes, including root pages/api.<ext> and nested API pages, matching Next.js entrypoint behavior.
  • Both transforms now reject virtual/unrelated modules before canonicalization and cache canonical paths for page candidates.
  • Recursive and mutually recursive helpers used only by data exports are swept while client-shared bindings remain preserved.
  • Added regression coverage and reran the affected suite: 153 tests passed, package build passed, and independent re-review was clean.

@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.

/bigbonk re-review — fix(build): align Pages data export transforms (d616c14)

Re-reviewed after the two findings from the prior review were addressed. Both are now resolved, and I verified the fixes against the Next.js source in .nextjs-ref and by running the affected suites locally (vp test run149 passed across build-optimization + pages-server-exports-security, plus middleware-server-only 4 passed; vp check clean on both source files).

Finding 1 (API-route export * parity) — resolved ✅

validate-page-exports now early-returns for API routes via isApiPage(canonicalId) (index.ts:4053), matching Next.js, where the disallow-export-all plugin is gated on !isServer && isPageFile and API routes never get a client compilation. The test was flipped from "rejects" to "allows export-all declarations in API page file" and is parameterized over both pages/api.ts and pages/api/index.ts (pages-server-exports-security.test.ts:181), covering the root-API-file edge case too. The misleading comment/description were corrected.

Finding 2 (per-module realpathSync perf regression) — resolved ✅

Both handlers now gate canonicalization behind cheap checks before any syscall:

  • \0 virtual-module early-return (index.ts:4042, :4072).
  • validate-page-exports: cheap code.includes("export") && code.includes("*") short-circuit, then isWithinPagesDirectory(modulePath) (string prefix) before canonicalize.
  • strip-server-exports: hasServerExportCandidate(code) fast-path, then the same string-prefix guard before canonicalize.
  • Canonical paths are memoized in pageTransformCanonicalPaths (bounded by page-module count, since only pages-dir candidates reach it).

Dropping the /\.(tsx?|jsx?|mjs)$/ filter in favor of fileMatcher.isPageFile is actually a correctness improvement: the old filter excluded configured pageExtensions like mdx, and the MDX fixture in the security test now exercises that path.

Recursive-helper parity gap — resolved ✅

The dependency sweep was reworked into a fixpoint (removableBindings seed → closureChanged growth over implementation ranges → pruneChanged shrink on live references). This is implementation-based rather than function/class-kind-based, so recursive and mutually-recursive arrow helpers used only by data exports are now swept (new tests at build-optimization.test.ts:2873, :2889), while client-shared bindings are still preserved (verified by the aliased/shared-binding tests).

Spot-checks that look correct

  • _app|_document|_error anchoring (^/(?:_app|_document|_error)(?:\.[^/]*)?$) — nested pages/admin/_error.tsx is treated as a regular page; the security fixture confirms its server deps + secret are stripped from client output.
  • _app/_document are not excluded from validate-page-exports (only API routes are), matching Next.js isPageFile gating.
  • Plugin reorder (server-only validation now runs after the data-export strip) is sound: the strip removes the import edge in the page module, so a server-only module re-exported only via getServerSideProps never enters the client graph. Confirmed by the index.tsx re-export fixture building cleanly.

Minor (non-blocking)

  • validatePageExports's code.includes("*") guard will parse files that contain a multiplication but no export-all; harmless, just an occasional extra parse.
  • pageTransformCanonicalPaths has no eviction, but it's bounded by the number of page candidates, so not a concern.

LGTM. No blocking issues.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Re-review posted to PR #2055. Summary:

Verdict: LGTM — no blocking issues

The two findings from the prior review (commit 6918345) are both resolved in d616c14, verified against .nextjs-ref source and by running the affected suites locally.

Finding 1 — API-route export * parity (resolved): validate-page-exports now excludes API routes via isApiPage(), matching Next.js's !isServer && isPageFile gating. The test was correctly flipped from "rejects" to "allows" and parameterized over pages/api.ts + pages/api/index.ts. Misleading comment/description fixed.

Finding 2 — per-module realpathSync perf regression (resolved): Both handlers now gate canonicalize behind cheap checks (\0 early-return, string code.includes short-circuits, isWithinPagesDirectory prefix check) and memoize canonical paths. Dropping the extension regex filter in favor of fileMatcher.isPageFile is a net correctness win (MDX pages now covered).

Bonus — recursive-helper parity gap (resolved): The dependency sweep was reworked into an implementation-based fixpoint, so recursive and mutually-recursive arrow helpers used only by data exports are now swept while shared client bindings are preserved. New tests cover both.

Verification: 149 tests passed (build-optimization + security), middleware-server-only 4 passed, vp check clean on both source files. Spot-checked the _app/_document/_error anchoring, plugin reorder soundness, and _app/_document validation gating — all correct.

Only two non-blocking minor notes (harmless extra parse from the code.includes("*") guard; unbounded-but-bounded canonical-path cache).

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Addressed the remaining export-all prefilter note: the plugin hook, validator, and strip transform now share a comment-aware hasExportAllCandidate() guard. It recognizes whitespace and comments between export and *, while avoiding AST parsing for ordinary multiplication expressions. Added focused regressions; 154 affected tests and the package build pass.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Comment thread packages/vinext/src/plugins/strip-server-exports.ts Fixed

@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.

/bigbonk re-review — fix(build): align Pages data export transforms (536a42c)

Re-reviewed after the export-all prefilter rework in commit 536a42c. The change is sound — no blocking issues. Verified by running the affected suites locally (vp test run tests/build-optimization.test.ts tests/pages-server-exports-security.test.ts150 passed; vp check clean on both source files).

New since last LGTM: shared hasExportAllCandidate() guard — looks correct ✅

The three call sites (validate-page-exports plugin hook in index.ts:4041, validatePageExports at strip-server-exports.ts:294, and stripServerExports at :317) now share one comment-aware regex:

/\bexport(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$))*\*/

I tested this directly against a range of inputs:

  • Real export-all forms all match: export * from, export\n*\nfrom, export /* c */ * from, export // c\n* from, tabs, multiple consecutive comments, CRLF, and export * as ns from. No real export * form is missed.
  • Multiplication no longer false-positives: export const area = width * height; and export {a} from "x"; const y = 2 * 3; correctly return false, which was the whole point of this commit (the old code.includes("export") && code.includes("*") guard would parse these).
  • export default a * b correctly returns false.
  • No ReDoS: pathological inputs (100k-char unterminated comment, 500k newlines without a star) complete in <10ms — the alternation is anchored on export and each branch consumes distinct character classes, so there's no catastrophic backtracking.

The remaining false positives (export * appearing only inside a string literal or comment) are harmless: they just trigger one extra parseAst, and validatePageExports only throws on an actual ExportAllDeclaration node (:301-305), matching Next.js's next-page-disallow-re-export-all-exports AST-visitor behavior. This is the same harmless-extra-parse note from the prior review, now narrowed further.

Spot-checks that remain correct

  • stripServerExports guard parity: the plugin only invokes it when hasServerExportCandidate(code) is true (index.ts:4068), so the || !hasExportAllCandidate(code) branch inside stripServerExports (:317) is effectively dead in the plugin path — but it's still exercised by direct unit tests, and export * rejection is owned by the separate validate-page-exports plugin, so there's no behavioral gap.
  • API-route / _app / _document / _error gating unchanged and still correct: isApiPage covers root pages/api.<ext> (stripExtension("/api") === "/api") and nested /api/*; the _app|_document|_error anchor (^/(?:_app|_document|_error)(?:\.[^/]*)?$) keeps nested pages/admin/_error.tsx as a regular page.
  • Canonical-path cache is still keyed after the isWithinPagesDirectory(modulePath) guard, so it stays bounded by page-candidate count.
  • New test (build-optimization.test.ts:2429) covers the multiplication-vs-export-all distinction and the comment/whitespace variants.

Verdict

LGTM. The two original findings (API-route export * parity, per-module realpathSync) and the recursive-helper gap remain resolved, and this follow-up is a clean, well-tested precision improvement to the cheap prefilter. No blocking issues.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2055. Summary:

Verdict: LGTM — no blocking issues

Re-reviewed the new commit 536a42c (narrow page export-all parsing), which landed after the prior LGTM. It introduces a shared comment-aware hasExportAllCandidate() regex replacing the cheap code.includes("export") && code.includes("*") guard across all three call sites.

What I verified:

  • Regex correctness — tested directly: matches all real export * forms (tabs, multiple comments, line comments, CRLF, export * as ns), correctly rejects export const x = a * b and export {a}; const y = 2*3. No real export-all form is missed.
  • No ReDoS — pathological inputs (100k unterminated comment, 500k newlines) complete in <10ms; no catastrophic backtracking.
  • Harmless false positivesexport * in a string/comment only triggers one extra parse; validatePageExports only throws on an actual ExportAllDeclaration AST node, matching Next.js.
  • Tests — 150 passed across build-optimization + pages-server-exports-security; vp check clean on both source files.
  • Prior findings stay resolved — API-route gating, _app/_document/_error anchoring, bounded canonical-path cache, and recursive-helper sweep all still correct.

The follow-up is a clean, well-tested precision improvement. No issues to address.

github run

@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.

/bigbonk review — fix(build): align Pages data export transforms (436fe62)

Re-reviewed after the CodeQL ReDoS fix in 436fe62, which replaces the comment-aware regex with a linear indexOf-based scanner in hasExportAllCandidate(). No blocking issues. Verified against the source, the prior findings, and by running the affected suites locally.

New since last LGTM: linear export-all scanner (436fe62) — correct ✅

The old regex /\bexport(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$))*\*/ was flagged by CodeQL (alert 206) for exponential backtracking on *//* repetitions. The replacement is a single forward pass:

  • Behavioral parity with the old regex — I diffed the new scanner against the old regex across 19 inputs (tabs, CRLF, single/multiple line + block comments, export * as ns, export*, $export, member-access .export, multiplication, unterminated comments). Zero mismatches. All real export * forms still match; export const x = a * b, export {a}; const y = 2 * 3, and export default a * b correctly return false.
  • Word-boundary handling matches \b — the explicit 0-9 / A-Z / _ / a-z char-code checks on the char before export and after it reproduce the regex's \b semantics, including treating $ as a boundary (so $export * is a harmless false-positive in both old and new — validatePageExports only throws on a real ExportAllDeclaration AST node, so no behavior change).
  • ReDoS resolved — the two CodeQL-style pathological inputs (200k *//*, 200k unterminated block comments) plus 500k newlines all complete in <15ms; the scanner only ever advances position/searchFrom, so there is no backtracking path.

Prior findings remain resolved

  • API-route export * parityvalidate-page-exports still early-returns via isApiPage(); I confirmed isApiPage handles root pages/api.tsx (stripExtension("/api") === "/api") and nested /api/*, while pages/admin/api.tsx is correctly a regular page.
  • _app/_document/_error anchoring^/(?:_app|_document|_error)(?:\.[^/]*)?$ verified: top-level _error excluded, nested pages/admin/_error.tsx treated as a regular page.
  • Per-module realpathSync perf — both handlers still gate canonicalization behind \0 early-return → cheap code short-circuit → isWithinPagesDirectory string prefix, with results memoized in pageTransformCanonicalPaths (bounded by page-candidate count).
  • Recursive-helper sweep — the implementation-based fixpoint (seed → closureChanged growth → pruneChanged shrink) still sweeps recursive/mutually-recursive arrow helpers; preserved by the shared-binding tests.

Verification

  • vp test run tests/build-optimization.test.ts tests/pages-server-exports-security.test.ts150 passed (the two "Build failed" stderr lines are the expected negative-path assertions).
  • vp check packages/vinext/src/plugins/strip-server-exports.ts packages/vinext/src/index.ts → clean (format + lint + types).
  • Destructuring-assignment edits (object-pattern parenthesization, array-position preservation, computed keys), import/export attributes, regex literals, and string-with-braces all have dedicated tests.

Verdict

LGTM. The ReDoS fix is a faithful, well-tested replacement of the regex with no behavioral change, and all earlier findings stay resolved.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2055.

Summary

Verdict: LGTM — no blocking issues.

I focused on the latest commit 436fe62, which replaces the CodeQL-flagged ReDoS regex in hasExportAllCandidate() with a linear indexOf-based scanner.

What I verified:

  • Behavioral parity — diffed the new scanner against the old regex across 19 inputs (comments, CRLF, export * as ns, multiplication, $export, member access, unterminated comments). Zero mismatches. All real export * forms still match; multiplication/export default a * b correctly don't.
  • ReDoS resolved — pathological inputs (200k *//*, 200k unterminated comments, 500k newlines) all complete in <15ms; the scanner only advances forward, so no backtracking.
  • Prior findings stay resolved — API-route gating (isApiPage handles root pages/api.tsx + nested), _app/_document/_error anchoring, per-module realpathSync guards + memoization, and the recursive-helper fixpoint sweep.
  • Tests/checks — 150 passed across build-optimization + pages-server-exports-security; vp check clean on both source files. Destructuring-assignment edits, import/export attributes, regex literals, and string-with-braces all have dedicated coverage.

The ReDoS fix is a faithful, well-tested replacement with no behavior change.

github run

@james-elicx
james-elicx marked this pull request as ready for review June 15, 2026 22:43
@james-elicx
james-elicx merged commit c845c0a into main Jun 16, 2026
56 checks passed
@james-elicx
james-elicx deleted the codex/pages-data-export-parity branch June 16, 2026 11:04
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