Skip to content

fix(pages): preserve document import order#2560

Open
james-elicx wants to merge 5 commits into
mainfrom
codex/fix-pages-document-import-order-28938793088-v2
Open

fix(pages): preserve document import order#2560
james-elicx wants to merge 5 commits into
mainfrom
codex/fix-pages-document-import-order-28938793088-v2

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • load Pages Router root modules in Next.js order: _document_app → page
  • preserve that ordering in both the dev SSR module runner and generated production server entry
  • add focused dev-runtime and generated-entry regressions ported from the pinned Next.js suite

Failure addressed

From Actions run 28938793088:

  • test/e2e/app-document-import-order/app-document-import-order.test.tsroot components should be imported in order _document > _app > page

The two test/e2e/app-document/rendering.test.ts nonce/crossOrigin failures are already owned by #2044; I commented there with the exact failing tests and intentionally did not duplicate that work.

Next.js oracle

Pinned Next.js v16.3.0-canary.80 at 9dd6d677b63b249584c8ff0bccffcc6a65288683.

Validation

  • vp test run tests/entry-templates.test.ts tests/pages-router.test.ts — 395 passed
  • vp check packages/vinext/src/entries/pages-server-entry.ts packages/vinext/src/server/dev-server.ts tests/entry-templates.test.ts tests/pages-router.test.ts — passed
  • git diff --check — passed
  • REPO="$(pwd)" NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref-v16.3.0-canary.80" ./scripts/run-targeted-nextjs-e2e.sh test/e2e/app-document-import-order/app-document-import-order.test.ts — 1 passed, 1 upstream Turbopack skip

Notes

  • No Bonk requested yet.
  • Concurrent unstaged Pages data-props edits in the shared worktree were excluded from this commit.

@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2560
npm i https://pkg.pr.new/create-vinext-app@2560
npm i https://pkg.pr.new/vinext@2560

commit: 3460f36

@github-actions

github-actions Bot commented Jul 8, 2026

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

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 3460f36 against base 617bffa 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 126.6 KB 126.6 KB ⚫ +0.0%
Client entry size (gzip) vinext 120.6 KB 120.6 KB ⚫ +0.0%
Dev server cold start vinext 2.61 s 2.63 s ⚫ +1.0%
Production build time vinext 2.73 s 2.72 s ⚫ -0.3%
RSC entry closure size (gzip) vinext 98.5 KB 98.5 KB ⚫ +0.0%
Server bundle size (gzip) vinext 165.1 KB 165.1 KB ⚫ +0.0%

View detailed results and traces

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

@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): preserve document import order

Overall this is a well-scoped fix. The _document > _app > page ordering is correct in both the dev SSR runner and the generated production entry, and it matches Next.js module-evaluation order. The error-propagation change (removing the fail-open try/catch around root-module loads) is intentional, tested, and correctly degrades on the 404 path via the outer connect error handler (next(e) at packages/vinext/src/index.ts:5284) → 500. Nice porting of the Next.js app-document-import-order cases.

I found one behavioral divergence worth addressing before merge.

1. Data requests now eagerly import _document (parity regression)

In the dev SSR handler, loadPagesRootComponents() is called unconditionally on the main render path:

packages/vinext/src/server/dev-server.ts:813

const { DocumentComponent, AppComponent } = await loadPagesRootComponents(
  runner,
  pagesDir,
  matcher,
);

This runs for every page request, including isDataReq (/_next/data/<buildId>/<page>.json) requests, which reach this line and only short-circuit later (the isDataReq early returns are at lines 1508/1602-ish, all after 813).

Previously, _document was loaded lazily inside the HTML-render branch (old dev-server.ts line ~1863), which sits after all the isDataReq early returns. So in the old code a data request never evaluated _document; now it always does.

This diverges from Next.js, where _document is server-HTML-render-only and is never evaluated for data requests. Consequences:

  • A _document with import side effects now runs on data requests.
  • A throwing _document now 500s data requests that previously succeeded (the _next/data JSON path).

_document isn't used for the data-request response at all (it's only consumed in the HTML streamPageToResponse branch), so this is pure wasted work plus a side-effect/error-surface divergence.

Suggested fix: keep _app eager (it participates in readiness / getInitialProps and the __NEXT_DATA__ payload, matching the old ordering rationale), but defer _document loading to the HTML-render branch, or gate the _document portion on !isDataReq. That preserves the _document > _app > page ordering for HTML renders while restoring the "data requests don't touch _document" behavior. Worth a ported test asserting a throwing _document does not break a /_next/data/*.json request (Next.js oracle: data requests never evaluate _document).

Minor

  • loadPagesRootComponents returns appAssetPath but the two callers on the main render path (:813) destructure only { DocumentComponent, AppComponent }; only renderErrorPage uses appAssetPath. Fine as-is, just noting the asymmetry.

Everything else (ordering assertions in tests/entry-templates.test.ts, propagation tests in tests/pages-router.test.ts, prod parity via the generated entry — prod-server.ts doesn't load _document/_app itself) looks correct. Verified locally: the new order + propagation tests pass.

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2560.

Summary

The PR correctly fixes _document > _app > page import ordering in both the dev SSR runner and the generated production entry, matching Next.js module-evaluation order. The error-propagation change (removing fail-open try/catch around root-module loads) is intentional and well-tested — the 404 path degrades safely to a 500 via the outer connect error handler.

I found one behavioral divergence worth addressing:

  • Data requests now eagerly import _document (dev-server.ts:813). The unconditional loadPagesRootComponents() call runs for isDataReq requests too, which reach that line before their early returns. Previously _document was loaded lazily in the HTML-render branch, after all data-request short-circuits, so data requests never touched _document. This diverges from Next.js (where _document is HTML-render-only): a side-effecting or throwing _document now affects /_next/data/*.json requests. Suggested fix: keep _app eager but defer/gate _document loading to the non-data HTML path.

I also verified prod parity (prod-server delegates to the correctly-ordered generated entry) and ran the new ordering + propagation tests locally — all pass.

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.

Review: fix(pages): preserve document import order

The head commit fec1a9b addresses the parity concern raised in my prior review — data requests no longer eagerly evaluate _document. This is a clean, well-tested fix. Verified locally: tests/entry-templates.test.ts (42 passed) and tests/pages-router.test.ts (357 passed), plus vp check on both modified source files.

What's correct

  • Ordering fix (both runtimes). The generated production entry now emits docImportCode → appImportCode → pageImports (pages-server-entry.ts:293-295), and the dev runner loads _document_app → page via loadPagesRootComponents + the subsequent importModule(runner, route.filePath) (dev-server.ts:815-826). Since these are static ESM imports (or const X = null fallbacks when the file is absent), textual order determines module-evaluation order. Matches Next.js.

  • Data-request gate is safe. loadPagesRootComponents(..., { includeDocument: !isDataReq }) at dev-server.ts:817 runs before the data-request short-circuits (main envelope return at :1631-1677, plus the redirect/notFound returns at :739/:940/:1098/:1550), all of which precede the HTML render at :1935 where DocumentComponent is consumed. So DocumentComponent === null on data paths is never read. _app stays eager (participates in readiness/__NEXT_DATA__), matching the old rationale. The new test "does not evaluate _document for a dev data request" (throwing _document + /_next/data/*.json → 200) locks this in.

  • Error-propagation change is deliberate and covered. Removing the fail-open try/catch around root-module loads means a throwing _document/_app now surfaces. On the 500 path this is caught by the fallback at dev-server.ts:2070-2076; on the 404/normal paths it propagates to the outer next(e) at index.ts:5284 → 500. New tests assert this for both a normal route (throwing _document) and a cold-start 404 (throwing _app). This is dev-only (prod delegates to the generated entry) and matches Next.js fail-fast on broken root modules.

  • findFileWithExts refactor is equivalent and slightly more robust — it returns the resolved absolute path and imports it directly, instead of the old existence-check-then-extensionless-import pattern. Same dottedExtensions/existsSync iteration order.

Minor (non-blocking)

  • Redundant _app lookup on the HTML render path. loadPagesRootComponents already computes and returns appAssetPath, but the main-path caller destructures only { DocumentComponent, AppComponent } (dev-server.ts:817) and then recomputes it at :1724 (AppComponent ? findFileWithExts(...) : null). One extra findFileWithExts per HTML render — harmless, but you could thread the returned appAssetPath through to drop the second lookup.

  • renderErrorPage now evaluates _document/_app even when no error-page candidate exists. loadPagesRootComponents runs unconditionally at :2109 before the candidate loop, so a 404 that falls through to buildDefaultPagesNotFoundResponse() (:2243) now evaluates root modules where it previously didn't. Behaviorally this only matters when a root module throws, which the new cold-start _app test asserts should 500 — so it's intended and covered. Noting it only because it's a wider surface than the "matched error page" case.

Nothing blocking. LGTM.

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2560.

Summary

The head commit fec1a9b fix(pages): skip document for data requests resolves the parity concern from the earlier review — data requests no longer eagerly evaluate _document. The PR is solid and well-tested.

Verified:

  • tests/entry-templates.test.ts — 42 passed
  • tests/pages-router.test.ts — 357 passed
  • vp check on both modified source files — clean

Correct behavior confirmed:

  • _document → _app → page ordering in both the generated production entry (pages-server-entry.ts:293-295) and the dev runner (dev-server.ts:815-826), enforced by static ESM import order.
  • The includeDocument: !isDataReq gate is safe — DocumentComponent is null on data paths but never consumed before the data-request short-circuits return.
  • Error-propagation change (removed fail-open try/catch) is deliberate, matches Next.js fail-fast, and is caught/handled appropriately (500-path fallback + outer next(e)).
  • findFileWithExts refactor is equivalent and slightly more robust.

Two minor, non-blocking notes:

  • Redundant _app lookup on the HTML render path (appAssetPath is returned by the helper but recomputed at :1724).
  • renderErrorPage now evaluates root modules unconditionally before the candidate loop — intended and covered by the new cold-start _app test, just a wider surface.

No blocking issues found.

github run

@james-elicx james-elicx marked this pull request as ready for review July 8, 2026 17:24
@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): preserve document import order

Reviewed at head 3460f36. This is a clean, well-scoped fix. The _document > _app > page ordering is now correct in both the dev SSR runner and the generated production entry, and it matches the Next.js oracle. Verified locally: tests/entry-templates.test.ts (42 passed) + tests/pages-router.test.ts (357 passed), and vp check on both modified source files is clean.

What's correct

  • Ordering matches Next.js. Confirmed against test/e2e/app-document-import-order/app-document-import-order.test.ts, which asserts side effects fire in ['_document', '_app', 'page'] order. The generated entry emits docImportCode → appImportCode → pageImports (pages-server-entry.ts:293-295) and the dev runner loads _document_app (loadPagesRootComponents) then the page (importModule(runner, route.filePath) at dev-server.ts:826). Since these are static ESM imports (or const X = null fallbacks), textual order = module-evaluation order.

  • Data-request _document gate is correct. loadPagesRootComponents(..., { includeDocument: !isDataReq }) (dev-server.ts:817) runs before every isDataReq short-circuit, and the _next/data JSON 404 exits at :753 before ever reaching renderErrorPage, so a data request never evaluates _document. DocumentComponent === null on data paths is never consumed. The new "does not evaluate _document for a dev data request" test (throwing _document + /_next/data/*.json → 200) locks this in. This resolves the parity concern from the earlier review round.

  • Fail-fast propagation is deliberate, parity-correct, and covered. Removing the fail-open try/catch around root-module loads means a throwing _document/_app now surfaces instead of being swallowed. This matches Next.js (broken root modules fail the render, they don't silently degrade). Note the behavioral consequence in renderErrorPage: loadPagesRootComponents at :2109 is now outside the per-candidate try/catch, so a throwing _app turns a 404 into a 500 rather than continuing to the default 404 fallback. That is the intended fail-fast behavior and is asserted by the new "propagates a throwing _app import on a cold-start missing dev route" test.

  • Dev/prod parity holds. prod-server.ts and cloudflare/worker-entry.ts don't load _document/_app themselves — they delegate to the generated entry, which now carries the corrected order. No separate fix needed there.

  • findFileWithExts refactor is equivalent. Same dottedExtensions iteration order as the old findFileWithExtensions existence check, but returns the resolved absolute path and imports it directly. The _document load also switches from an extensionless runner.import(docPath) to the resolved-with-extension path — more robust, and covered by the new dev import-order test using a real _document.tsx.

Minor (non-blocking)

  • Redundant _app lookup on the HTML render path. loadPagesRootComponents already resolves and returns appAssetPath, but the main-path caller destructures only { DocumentComponent, AppComponent } (dev-server.ts:817) and then recomputes it at :1724. One extra findFileWithExts per HTML render — harmless; you could thread the returned appAssetPath through to drop the second lookup.

  • apiImports moved relative to root modules in the generated entry. Old order was pageImports, apiImports, appImportCode, docImportCode; new order is docImportCode, appImportCode, pageImports, apiImports. So API-route modules now evaluate after _document/_app/page instead of before. API routes are independent endpoints with no expected side-effect coupling to root modules, so this is inert in practice — noting only for completeness.

Nothing blocking. LGTM.

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2560.

Summary

Reviewed at head 3460f36. The PR correctly fixes _document > _app > page import ordering in both the dev SSR runner and the generated production entry, verified against the Next.js oracle test (app-document-import-order, which asserts ['_document', '_app', 'page']).

Verified locally:

  • tests/entry-templates.test.ts — 42 passed
  • tests/pages-router.test.ts — 357 passed
  • vp check on both modified source files — clean

Correct behavior confirmed:

  • Static ESM import order = evaluation order in both runtimes (pages-server-entry.ts:293-295, dev-server.ts:817-826).
  • The includeDocument: !isDataReq gate is safe — data requests never evaluate _document (JSON 404 exits at :753 before renderErrorPage), resolving the earlier parity concern.
  • Fail-fast propagation (removed fail-open try/catch) matches Next.js and is covered by new tests — including the deliberate behavioral change where a throwing _app turns a 404 into a 500 in renderErrorPage (loads are now outside the per-candidate try/catch).
  • Dev/prod parity holds — prod-server.ts and worker-entry.ts delegate to the corrected generated entry.
  • findFileWithExts refactor is equivalent and slightly more robust.

Two minor, non-blocking notes:

  • Redundant _app lookup on the HTML render path (appAssetPath returned by the helper but recomputed at :1724).
  • apiImports now evaluate after root modules in the generated entry (inert — API routes have no side-effect coupling to _document/_app).

No blocking issues found.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Independent Codex review — NO FINDINGS

Reviewed exact head 3460f36c4b8496320b22a098b7a64468c7f25963. Scope: custom Pages _document/_app/page import-order parity in dev SSR and the generated production entry, including preservation of the separate nonce/crossOrigin ownership boundary. Targeted Next.js v16.3.0-canary.80 oracle: test/e2e/app-document-import-order/app-document-import-order.test.ts1 passed, with 1 upstream Turbopack skip.

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