Commit Graph

50 Commits

Author SHA1 Message Date
Jerry Zhao ef78685beb test(next-config): build the cacheHandler file:// fixture with pathToFileURL (#2163) 2026-06-18 21:39:37 +01:00
Nathan Nguyen 10b5086f4d fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next

App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior.

The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks.

* fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan

* test(app-router): add full-chain scroll intent integration test

* fix(app-router): match legacy scroll focus parity

* fix(app-router): preserve latest scroll navigation intent

* fix(app-router): scope scroll and error recovery

* fix(app-router): gate navigation failure recovery

* fix(app-router): match navigation failure handling

* fix(app-router): align scroll completion parity

* fix(app-router): retain latest failure target

* fix(app-router): preserve route error boundaries

* fix(app-router): clear discarded failure targets

* fix(app-router): align failure recovery boundaries

* fix(app-router): disarm refused recovery targets

* fix(app-router): preserve failure target ownership

* fix(app-router): match boundary recovery parity

---------

Co-authored-by: James <james@eli.cx>
2026-06-14 23:59:28 +01:00
James Anderson 3d9639510e fix(build): honour experimental.lightningCssFeatures include/exclude (#1498) (#1664)
vinext silently ignored `experimental.lightningCssFeatures` because the
options were never plumbed into Vite's CSS pipeline. `light-dark()` and
other modern CSS features were still lowered to polyfills even when the
user explicitly listed them under `exclude`.

Resolve `experimental.useLightningcss` and convert the dash-case feature
names from `experimental.lightningCssFeatures` into the numeric bitmask
expected by lightningcss (mirroring Next.js's `featureNamesToMask` in
`packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts`
and `crates/next-core/src/next_config.rs`). When `useLightningcss` is on,
forward `transformer: 'lightningcss'` plus the resolved `include`/`exclude`
masks to Vite's `css.lightningcss` bridge.

Adds a regression test that builds a fixture with
`exclude: ['light-dark']` and asserts the function survives untouched in
the emitted CSS, plus an inverse `include: ['light-dark']` case and unit
coverage for the feature-name → bitmask helper.

Ported from Next.js:
test/e2e/app-dir/experimental-lightningcss-features/experimental-lightningcss-features.test.ts
2026-06-13 00:39:58 +00:00
James Anderson a84a038fe6 fix(config): share CommonJS module fallback (#1990)
* fix(config): share CommonJS module fallback

* fix(config): harden CommonJS retry detection
2026-06-13 00:13:22 +01:00
James Anderson 5e19a2dd38 fix(config): honor resolve extensions and improve webpack config processing (#1959)
* fix(config): honor resolve extensions

* fix(config): preserve resolve extension semantics

* fix(config): preserve resolve extensions fallback

* fix(config): preserve resolver environment semantics

* fix(config): treat Vite preview as production
2026-06-12 22:55:06 +00:00
Nathan Nguyen e018a08708 fix(pages-router): restore scroll across reload history traversal (#1905)
* fix(pages-router): restore scroll across reload history traversal

Pages Router pages with experimental.scrollRestoration currently expose window.next.router before the popstate runtime is installed and only keep Vinext scroll coordinates in the in-memory history state. After a reload, a back or forward traversal can run before the runtime is ready and has no durable scroll snapshot for the target history entry.

The violated invariant is that the Pages Router owns browser scroll restoration whenever the Next.js scroll restoration flag is enabled, with one stable history key per entry and routeChangeComplete emitted after the new page commits.

Resolve the Next.js config flag into process.env.__NEXT_SCROLL_RESTORATION, install the Pages Router runtime during next/router evaluation, persist Next-style sessionStorage scroll snapshots per history key, and render navigations through a stable commit boundary before emitting routeChangeComplete.

Regression coverage ports Next.js reload-scroll-backforward-restoration and verifies manual restoration plus exact scroll positions across reload, back, and forward traversal.

* fix(pages-router): sequence routeChangeComplete after scroll restoration and settle on render failures

* fix(router): guard against missing window.history in stampInitialHistoryState

* fix(router): defer hash scroll until after routeChangeComplete

Move hash scrolling out of renderPagesRouterElement's commit callback
and into the post-routeChangeComplete path, matching Next.js ordering:
x/y restoration happens during render commit, hash scrolling happens
after the completion event.

Also fix E2E test expectRouteChangeComplete to use one-shot event
listeners instead of checking for stale text, and add listener cleanup
to the fixture to prevent accumulation.

* fix(test): remove post-push deadlock and throw on missing events in E2E helper

expectRouteChangeComplete() now throws if window.next.router.events is
absent instead of silently resolving. Remove the redundant
expectRouteChangeComplete call after pushWithPagesRouter in the third
test — the awaited router.push already resolves after routeChangeComplete,
so registering a listener after it would deadlock.

* fix(test): replace as any casts with type-safe window.next narrowing in E2E scroll tests

* fix(router): narrow renderPagesRouterElement scroll param, drop dead hash branch

* fix(router): reset x/y scroll on hash routes before routeChangeComplete and cancel pending render on supersede

- Pass {x:0, y:0} through commit boundary for all scrollable
  navigations, not just hash-less ones (hash scroll deferred to after
  routeChangeComplete as before).
- Cancel pending render-commit Promise when a newer navigation starts,
  preventing hang when the first root.render() commit is dropped by
  React re-reconciliation.
- Add regression tests for both edge cases.

* fix(router): use SSR-safe layout effect in commit boundary and pin shallow scroll ordering

Address bonk review on #1905:
- swap the commit boundary's raw useLayoutEffect for the SSR-safe
  useNonWarningLayoutEffect pattern from shims/image.tsx so Pages Router
  SSR renders no longer log React's useLayoutEffect warning
- document that the default-noop commit boundary intentionally also wraps
  SSR and initial hydration
- document the intentional shallow-scroll-before-routeChangeComplete
  ordering and add a unit test pinning it

* fix(router): keep history-state scroll restore on popstate when scrollRestoration flag is off

Without experimental.scrollRestoration, popstate must still honour the
__vinext_scrollX/Y saved into history state on push, since a soft popstate
re-renders content the browser's native restoration can't position. Also
document why the popstate-time sessionStorage snapshot depends on
history.scrollRestoration = "manual".

* fix(router): guard render-commit canceller against late commits from superseded trees

* fix(router): defer current-history-key commit until beforePopState allows navigation

When beforePopState cancels a popstate the app stays on the previous
history entry, but _currentHistoryKey had already been updated to the
target key, skewing subsequent scroll-position bookkeeping. Move the
key commit below the beforePopState early-return, matching the
deliberate deferral of _lastPathnameAndSearch.

* chore(router): note intentional eager scroll snapshot; fix fixture link increment

- Document that the outgoing-entry scroll snapshot is deliberately written
  before the beforePopState gate: on cancellation the app stays on
  currentKey, so the saved value remains that entry's live scroll.
- Coerce id to a number in the pages-scroll-restoration fixture Link so
  the href increments numerically instead of string-concatenating.

* fix(router): keep manual scroll restoration opt-in Pages-Router-only

Gate installManualScrollRestoration on the document not being an App
Router one (window.next.appDir === true): with the now-global
__NEXT_SCROLL_RESTORATION define, an App Router app that value-imports
next/router for compat would otherwise have its native browser scroll
restoration disabled at module eval. Upstream only flips
history.scrollRestoration inside the Pages Router constructor, which
never runs on App Router documents.

Also document the hash-only popstate path discarding the per-entry
__next_scroll_<key> snapshot as intentional upstream parity (change()'s
onlyAHashChange branch passes null to this.set and only scrollToHash
runs), and add unit coverage for both behaviors.

* ci: run pages-scroll-restoration E2E project in the CI matrix

Add the missing E2E matrix entry so the ported Next.js regression spec
(reload-scroll-backforward-restoration.spec.ts) actually executes in CI,
matching the shape of the sibling single-shard Pages Router entries.

Also document why the soft-popstate scroll fallback differs between
manual restoration on ({ x: 0, y: 0 }) and off (null -> native auto
restoration handles unstamped entries), per review.

* refactor(router): fold App Router gate into manualScrollRestoration constant

The popstate handler's sessionStorage save/read branches gated on the
module-level constant, which lacked the window.next?.appDir check that
installManualScrollRestoration used, leaving those branches relying
solely on the __N state filter to stay inert on App Router documents.
Fold the appDir check into the constant so every consumer is
Pages-Router-only by the same gate; the App Router bootstrap stamps
window.next.appDir before any client module evaluates, so the
module-eval-time check is reliable.

* test(pages-scroll-restoration): default getServerSideProps id to string "0"

Route params always arrive as strings, so the numeric 0 fallback was
inconsistent with the string values the component relies on
(Number(id) + 1 href, id === "error" check). Default to "0" for type
consistency.

* test(pages-scroll-restoration): align fixture deps and vinext import with pages-basic

Import vinext via the workspace package name instead of reaching into
dist by relative path, and declare the same react/react-dom/vinext
dependencies as the other fixtures so the fixture is self-describing
and the pnpm-lock.yaml importer entry is populated.

* fix(router): snapshot outgoing scroll on hash-only pushes

Upstream Next.js snapshots the outgoing entry's scroll into
__next_scroll_<key> inside Router.push() itself, before change()'s
onlyAHashChange short-circuit, so hash-only pushes still capture the
departed entry's position. Our hash-only push branch returned before
saveScrollPosition() ran, so a later back-popstate to that entry
restored {x: 0, y: 0} instead of the position the user had reached.

Save the outgoing scroll before updateHistory mints the new key,
matching the full-navigation push path and upstream ordering.

* docs(router): clarify manual scroll-target fallback chain precedence

* test(shims): tag scrollRestoration App Router test as eval-order guard

* docs(router): note missing-snapshot fallback intent at forcedScroll default

---------

Co-authored-by: James <james@eli.cx>
2026-06-12 11:31:33 +01:00
Nathan Nguyen fd1023321b fix(router): support experimental gesture push (#1909)
* fix(router): support experimental gesture push

Vinext did not expose useRouter().experimental_gesturePush when Next.js apps enabled experimental.gestureTransition. Upstream gesture-transition fixtures call the method inside a held async transition, so the target route never became visible before the canonical router.push completed.

The fix resolves the config flag into the same __NEXT_GESTURE_TRANSITION define used by Next.js, exposes the optional router method only under that flag, and lets gesture navigation use the existing RSC navigation pipeline with an immediate approved visible commit. The ported e2e fixture preserves the upstream held-transition flow and target/static/dynamic assertions.

* test(router): update navigate mock assertions to expect default transition commit mode

* fix(router): address ask-bonk review comments on gesture push

- track scheduled app router navigation in experimental_gesturePush for
  parity with push/replace, so a refresh() issued in the same task skips
  its redundant re-fetch
- document the synchronous visible-commit mode's null pendingRouterState
  invariant in the browser navigation controller
- add a unit test asserting experimental_gesturePush is only attached
  when the __NEXT_GESTURE_TRANSITION define is set

* style(tests): fix formatting in shims.test.ts

* docs(router): clarify gesture push normalization and flushSync commit paths

* test(router): fix unbound-method lint in shims test and document gesture spec coverage

* docs(router): clarify gesture define boolean form and spec held-state rationale

* docs(test): note gestureTransition is fixture-wide for the gesture spec only

* fix(router): no-op experimental_gesturePush before the navigation runtime is ready

Upstream gesturePush early-returns when getCurrentAppRouterState() is null,
so a gesture dispatched before hydration is a no-op. Previously vinext fell
through navigateClientSide's non-runtime branch and performed a real history
push. Guard on the runtime's navigate function (the same readiness check
navigateClientSide uses) and document that the un-awaited
void navigateClientSide(...) deliberately matches push/replace's
fire-and-forget rejection shape.

* docs(router): note gesturePush guard/dispatch runtime reads have no TOCTOU window

* docs(test): document gesture fixture's double-navigation divergence from upstream

---------

Co-authored-by: James <james@eli.cx>
2026-06-12 03:14:24 +01:00
Jerry Zhao 6d9d16da1e test(next-config): make CJS config fixture deterministic on Windows (#1922) 2026-06-11 23:02:30 +01:00
James Anderson 89a12bb203 fix(build): don't path-resolve bare specifier aliases for esmExternals (#1507) (#1843) 2026-06-08 19:21:57 +01:00
James Anderson de2ad5278f fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) (#1841)
* fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552)

* test(app-router): cover reactMaxHeadersLength config resolution

* refactor(app-router): prioritize React preloads in Link header; cover buildAppPageLinkHeader

Address review feedback on #1841:
- Order React preload hints before font preloads so render-critical entries
  survive a tight reactMaxHeadersLength cap.
- Rename the buildAppPageHtmlResponse option to linkHeader (it now carries the
  combined header, not just fonts).
- Add unit coverage for buildAppPageLinkHeader (cap, 0-disables, ordering,
  whole-entry truncation).
2026-06-08 19:18:21 +01:00
James Anderson a843ea2cfa fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828) (#1834)
* fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828)

Oversized fetch (client-invoked) server-action requests were rejected with
a bare 413 Payload Too Large response, which bypasses the client error
boundary and surfaces the wrong status/content-type. Next.js instead throws
the body-exceeded error before the action runs and, for fetch actions, emits
a 500 Flight response carrying the rejected action result so the nearest
error boundary catches it.

Mirror that: when an RSC fetch action body exceeds the configured limit
(content-length pre-check or stream read), render a Flight stream with
returnValue { ok: false } and no page root, returning status 500. The
no-JS progressive form path keeps its 413 (no client boundary to catch it).

Closes #1828

* fix(actions): use verbatim bodySizeLimit string in body-exceeded error (#1828)

Address bonk review: the body-exceeded error message must contain the
verbatim serverActions.bodySizeLimit config string (e.g. "2mb") to match
Next.js byte-for-byte — the upstream suites assert the log message literally
('Body exceeded 2mb limit'). The previous reconstruction from the parsed byte
count produced '2 MB', diverging on casing and spacing.

Thread serverActionsBodySizeLimitLabel (the verbatim config value, defaulting
to "1mb") through next-config -> index -> app-rsc-entry -> the RSC action
handler options, and use it directly in createBodyExceededError. Removes the
lossy formatBodySizeLimit reconstructor. Tests assert the verbatim string.

* fix(actions): default bodySizeLimit label to Next.js' '1 MB' literal (#1828)

Address bonk re-review nit: when serverActions.bodySizeLimit is unset, Next.js
emits 'Body exceeded 1 MB limit.' (its defaultBodySizeLimit = '1 MB' literal).
Default serverActionsBodySizeLimitLabel to '1 MB' (was '1mb') so the
unconfigured-case message matches upstream byte-for-byte too.
2026-06-08 19:13:16 +01:00
Divanshu Chauhan (divkix) 5a1ea0d967 fix(config): avoid duplicate CJS global shims (#1771)
* fix(config): avoid duplicate CJS global shims

* fix(config): guard require injection too

The createRequire ESM polyfill (const require = createRequire(import.meta.url))
commonly appears alongside the __dirname/__filename polyfills and hits the same
duplicate-const Rolldown parse error. Skip the createRequire import and the
const require line when the user already declares require. Add a regression
test covering the scenario.

---------

Co-authored-by: James <james@eli.cx>
2026-06-05 21:21:56 +00:00
James Anderson 49f3905b7b fix(config): keep packages externalized when loading TypeScript next.config (#1710)
PR #1701 added `resolve.noExternal: true` while loading a TypeScript
next.config so a baseUrl-local file could shadow an installed package of the
same name. That de-externalizes every installed package and forces it through
Vite's module runner, which breaks CJS config plugins that call `require` /
`require.resolve` at runtime — e.g. `@next/mdx`, which failed the
app-router-playground production build with "require is not defined".

This was not caught pre-merge because deploy-examples is skipped for fork PRs.

Drop the blanket `noExternal: true`. Installed packages stay externalized (so
CJS config plugins keep working), and the native Vite 8 `resolve.tsconfigPaths`
resolver still resolves baseUrl-local bare imports that have no installed
package of the same name (the real Next.js tsconfig-extends fixture). The only
behavior we give up is shadowing an installed package with a baseUrl-local file
of the same name, which no Next.js fixture exercises.

- Update the shadowing test to assert the installed package wins.
- Add a regression test for an @next/mdx-style CJS plugin that calls
  `require.resolve` at runtime (red before this change).
- Document the package-precedence behavior in the README limitations entry.
2026-06-01 09:58:09 +00:00
Nathan Nguyen 5991406324 fix(config): resolve next.config baseUrl imports from tsconfig (#1701)
* fix(config): resolve next.config baseUrl imports from tsconfig

next.config.ts could resolve tsconfig paths aliases but not bare imports covered by compilerOptions.baseUrl. This broke configs that inherit baseUrl through tsconfig extends and import local modules such as "bar" from next.config.ts.

Carry the effective baseUrl out of the tsconfig reader and add a narrow Vite runner resolver for bare specifiers so local files resolve before normal package fallback. Add a regression ported from the Next.js tsconfig-extends deploy fixture.

* fix(config): preserve baseUrl priority over packages

next.config.ts imports that matched both a local tsconfig baseUrl target and an installed package resolved to the package first. That violated the TypeScript and Next lookup order where baseUrl local resolution wins before package fallback.

Vite's module runner externalized installed bare packages before resolveId hooks ran, so the baseUrl resolver only handled missing packages. Keep config imports inside the resolver pipeline, run the baseUrl resolver before package resolution, and scope it to project-owned importers so package dependencies are not rewritten through the app baseUrl.

Adds focused regression coverage for local-over-package shadowing, package fallback, node_modules importer isolation, and existing CJS/ESM package imports from next.config.ts.

* fix(config): keep baseUrl lookup scoped to TypeScript configs

next.config.mjs could inherit TypeScript baseUrl package shadowing because the config runner noExternal override and baseUrl resolver were enabled for every config file type. That leaked Next's TypeScript config transpilation semantics into non-TypeScript config loading.

Gate the baseUrl resolver and noExternal runner override behind TypeScript config extensions while keeping existing alias loading behavior unchanged. Add a regression for mjs package shadowing and update the tsconfig helper comment to name baseUrl explicitly.

* refactor(config): resolve next.config baseUrl via native Vite 8 tsconfigPaths

Replace the hand-rolled tsconfigBaseUrlResolverPlugin with Vite 8's native
resolve.tsconfigPaths (oxc-resolver tsconfig: 'auto'), which already mirrors
Next.js's SWC paths + baseUrl handling: follows extends, resolves baseUrl-local
bare imports before package fallback, and scopes the app baseUrl to
project-owned importers via per-importer tsconfig discovery.

Gate it behind the existing Vite-major check (matching index.ts). Vite 7 has no
native equivalent, so baseUrl bare imports in next.config.ts are a documented
Vite 7/8 capability gap; paths aliases still work on both via resolve.alias.

Extract getViteMajorVersion into utils/vite-version.ts so next-config.ts and
index.ts share one detection path without a circular import.

---------

Co-authored-by: James <james@eli.cx>
2026-06-01 10:34:00 +01:00
Nathan Nguyen 66519412c3 fix(app-router): reuse pending prefetched RSC payloads (#1704) 2026-05-31 20:08:13 +01:00
Divanshu Chauhan (divkix) 19dfd5e009 feat(config): add experimental.appShells config plumbing and validation (#1692) 2026-05-31 18:22:15 +01:00
Divanshu Chauhan (divkix) 00f585969d feat: recognize Sparkle experimental flags (varyParams, optimisticRouting, cachedNavigations) (#1655)
* feat(check): recognize Sparkle experimental flags in CONFIG_SUPPORT

* test(check): add tests for Sparkle experimental flags

* feat(config): warn when cachedNavigations is set without cacheComponents

* test: address ask-bonk review on cachedNavigations tests

- Remove redundant mockRestore() calls (afterEach handles it)
- Add negative test cases for cachedNavigations not set / false
- Match existing swcEnvOptions test style (warn.mock.calls.find())
2026-05-29 11:27:42 +01:00
James Anderson cd62cb2ebb fix(app-router): honor experimental.staleTimes in client prefetch cache (#1490) (#1660)
* fix(app-router): honor experimental.staleTimes in client prefetch cache (#1490)

The App Router's client-side prefetch cache previously used a hard-coded
30s TTL and ignored `experimental.staleTimes` from next.config. As a
result, users could not extend (or shorten) how long prefetched route
segments were considered fresh, breaking parity with Next.js'
`test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts`.

Surface `experimental.staleTimes.{dynamic,static}` through
`ResolvedNextConfig` and inject it into the client bundle as
`process.env.__NEXT_CLIENT_ROUTER_{DYNAMIC,STATIC}_STALETIME` (matching
the Next.js variable names plumbed by `build/define-env.ts`). The
prefetch cache TTL in `shims/navigation.ts` now reads the static value
at module load time, converting seconds to ms.

Defaults mirror Next.js (`dynamic: 0`, `static: 300`). When the env var
is absent — primarily in unit tests that import the module outside the
plugin's `define` pipeline — we fall back to vinext's historical 30s
default rather than the Next.js 300s so existing tests keep passing.

Refs: .nextjs-ref/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts
Refs: .nextjs-ref/packages/next/src/build/define-env.ts

* review: address bot feedback on staleTimes PR

- Remove invokeLoaderSideEffects scope creep (belongs to #1500 / PR #1657)
- Reject negative staleTimes at resolution time alongside Number.isFinite,
  matching the seconds < 0 guard in resolvePrefetchCacheTtl
- Drop unused __NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME define; vinext does
  not yet distinguish prefetch kinds, so the value was never consumed
2026-05-28 20:59:44 +01:00
James Anderson 90a09df893 fix(build): invoke webpack loader side effects to land process.env mutations (#1500) (#1657)
Some Next.js webpack loaders mutate `process.env.X = ...` at compile
time, expecting the value to be visible to other modules during the
same build. vinext doesn't run the webpack loader pipeline, so those
env mutations never happened and downstream code (defines,
`NEXT_PUBLIC_*`, server-side reads) saw `undefined`.

Walk the rules collected from the user's `webpack(config)` function
and invoke each referenced loader once with a dummy source. Loaders
that only mutate `process.env` get a chance to land their changes in
the shared Node process. Failures are swallowed since vinext doesn't
use the loader's transform output. Well-known framework loaders
(next-babel-loader, @next/mdx, next/dist/build/webpack/*) are skipped
to avoid pulling in webpack-runtime-dependent code at probe time.
2026-05-28 20:57:21 +01:00
Nathan Nguyen 7be428dbc3 feat(app-router): support inline CSS parity (#1595)
* feat(app-router): support inline CSS parity

App Router production builds ignored Next.js experimental.inlineCss, so HTML responses kept stylesheet links, next/font CSS lived in a second style tag, and Next-style RSC requests with RSC: 1 plus _rsc could return HTML.

The missing invariant was that inlineCss changes the production CSS delivery boundary: non-RSC HTML gets inlined style resources, RSC payloads keep CSS bodies out, and client navigations must not reintroduce duplicate stylesheet resources for CSS already present inline.

Resolve the config flag into the generated RSC entry, collect emitted CSS assets into an inline manifest for Node and Workers production, rewrite matching React stylesheet links into hoistable style tags, merge next/font CSS into the first inline style with a fallback path, and recognize Next-style RSC requests.

Regression coverage now ports the upstream app-inline-css behavior at production-browser level and adds focused tests for request normalization, stream rewriting, font merging, and fallback emission.

* feat(app-router): preserve inline CSS import ordering

Inline CSS font merging could prepend generated font rules before a stylesheet import. CSS imports must stay at the beginning of their stylesheet, so that ordering caused imported styles to be ignored when experimental.inlineCss and local font CSS were combined.

Keep import-sensitive stylesheet preambles untouched and let the existing font fallback style carry the font rules instead. Add stream-helper coverage for the import case.

* feat(app-router): harden inline CSS review edges

Inline CSS rewriting only saw complete link tags in a single decoded chunk and treated rel as an exact value. That left valid stylesheet links un-inlined when rel had multiple tokens or when streaming split the tag across chunks.

The production manifest injection also assumed the RSC worker bundle was always at dist/server/index.js, so custom rscOutDir builds missed the inline CSS manifest.

Buffer incomplete link tags across stream flushes, match stylesheet as a rel token, and inject the manifest into the configured RSC output entry. Add regressions for the streaming and custom-output cases.

* feat(app-router): match inline CSS cleanup semantics

Client navigation cleanup only pruned links whose rel was exactly stylesheet and whose precedence marker used data-precedence. That diverged from the server inline CSS rewrite, so tokenized rel values or legacy precedence links could survive after navigation as duplicate stylesheet resources.

The fix moves the cleanup into a testable helper, reuses shared HTML token-list matching for server and browser paths, accepts both precedence marker forms, and only buffers split link tags when an inline CSS manifest is present.

Regression coverage now covers browser cleanup and non-inlineCSS streaming chunk granularity.

* feat(app-router): harden inline CSS HTML rewriting

Inline CSS rewriting could prepend SSR font CSS ahead of namespace-bearing stylesheets and could treat stylesheet-like text inside script bodies as real link tags. That violates CSS ordering constraints and corrupts streamed script text when experimental.inlineCss is enabled.

The stream rewrite now treats @namespace as an unsafe preamble, skips link replacement inside raw-text and comment regions, and buffers incomplete rewrite boundaries across flushes. Regression coverage pins namespace fallback, complete script text, split script text, and unterminated script text.

* test(e2e): specify junction type for fs.symlink to support Windows

* refactor(app-router): drop write-only __PAGE__ wire marker

Every outgoing App Router payload carried `__PAGE__: true` in its wire
metadata, but nothing in the runtime ever read it. Its only consumer was
an inline-css E2E assertion checking that a dynamic navigation returned an
RSC payload rather than HTML, so the marker added permanent per-response
wire overhead with no behavioural effect.

The `__route` key is already present on every payload and proves the same
thing: the response carries page metadata. Remove the marker key, its type
field, and its re-export, and point the unit and E2E assertions at
`__route` instead.

* perf(app-router): skip inline-css manifest scan on the streaming hot path

The tick-buffered SSR transform recomputed `Object.keys(manifest).length`
on every flush through `rewriteInlineCssStylesheetLinks`, even when inline
CSS was disabled and the manifest empty. Streaming flushes are a hot path,
so this ran per chunk of every response for no benefit.

Compute the manifest-present flag once at transform creation and reuse it
to gate both the split-link boundary buffering and the link rewrite, so
non-inline-css responses skip the rewrite entirely. Also document the
shared-lastIndex constraint on the module-level rewrite regexes and the
startup/build-time population contract for `__VINEXT_INLINE_CSS__`.

* fix(app-router): forward SSR script nonce to inline-css <style> tags

Sites running `Content-Security-Policy: style-src 'nonce-…'` saw the
inlined `<style>` blocks blocked at parse time and rendered unstyled
when `experimental.inlineCss` was enabled. The `<link rel="stylesheet">`
tags the feature replaces aren't subject to inline-style CSP, but the
new inline blocks are — and React Fizz typically doesn't put a nonce on
the `<link>`, so the rewrite path had no nonce to carry over.

Thread the SSR-time `scriptNonce` through `createTickBufferedTransform`
into `rewriteInlineCssStylesheetLinks`, and prefer the link's own nonce
when present (otherwise fall back to the SSR nonce). Add unit coverage
for the SSR-only, link-only, and missing-nonce cases.

Also replace `html.toLowerCase().lastIndexOf("<link")` in
`splitTrailingIncompleteLinkTag` with a forward `g+i` regex scan so the
streaming hot path no longer allocates a lowercased copy of every flush
(can be tens of KB on large responses).

* chore(check): register experimental.inlineCss as supported

So `vp check` advertises App Router CSS inlining alongside the other
experimental flags. Notes that next/font CSS is folded into the first
inline `<style>` block.

---------

Co-authored-by: James <james@eli.cx>
2026-05-28 18:17:06 +00:00
James Anderson 9e049261e1 fix(otel): inject experimental.clientTraceMetadata into SSR head (#1644)
* fix(otel): inject experimental.clientTraceMetadata into SSR head (#1494)

When `experimental.clientTraceMetadata` is configured in next.config, read
the active OpenTelemetry context via the registered propagator and emit
`<meta name="..." content="...">` tags in the SSR HTML head for each
allow-listed key. Both App Router and Pages Router are covered (dev and
prod servers). `@opentelemetry/api` is resolved as an optional peer at
runtime, so apps without OTel get a zero-cost no-op.

Mirrors Next.js: packages/next/src/server/lib/trace/utils.ts and
packages/next/src/server/app-render/make-get-server-inserted-html.tsx.

* fix(otel): drop unused getOpenTelemetryTraceData export

knip flagged the helper as unused — it is only consumed internally by
`getClientTraceMetadataHTML`. Keep it module-private.

* fix(otel): normalize clientTraceMetadata allow-list to undefined-only

Pick one convention for the optional allow-list across the App Router
pipeline (was mixing `string[] | null` and `string[] | undefined`).
Drops the redundant `?? null` / `?? undefined` coercions and aligns
every server-side type with `ResolvedNextConfig.clientTraceMetadata`,
which is `string[] | undefined`.

Also adds a resolveNextConfig test asserting that an array passed via
experimental.clientTraceMetadata is preserved verbatim, non-string
entries are filtered, and unset / non-array values resolve to
undefined.
2026-05-28 17:44:59 +01:00
James Anderson 34c8669182 fix(config): implement compiler.define / compiler.defineServer (#1638)
* feat(config): implement compiler.define and compiler.defineServer (#1496)

Forward `compiler.define` to Vite's top-level `define` (applies to both
client and server bundles) and `compiler.defineServer` to non-client
environments only via a `configEnvironment` hook, matching Next.js
Webpack `DefinePlugin` semantics. Values are JSON-stringified at config
resolution time so they slot directly into Vite's substitution format.

Refs Next.js: packages/next/src/build/define-env.ts.

Fixes #1496

* fix(config): reject compiler.define keys that collide with built-ins

Mirrors Next.js (`packages/next/src/build/define-env.ts`) — throw when a
user-provided `compiler.define` or `compiler.defineServer` key would
silently overwrite an internal define (e.g. `process.env.NODE_ENV`,
`__NEXT_*`, `__VINEXT_*`) or when `define` and `defineServer` define the
same key. Without this, a misconfigured project would produce subtly
wrong output instead of failing fast.

Addresses /bigbonk review on #1638.
2026-05-28 15:45:55 +01:00
James Anderson f75845ef14 fix(pages-router): emit page scripts with defer in <head> by default (#1636)
* fix(pages-router): emit page scripts with defer in <head> by default

Next.js' Pages Router emits page scripts with the `defer` attribute in
`<head>` when `experimental.disableOptimizedLoading` is false (the
default). vinext emits `<script type="module">` (which already defers
implicitly), but upstream tests assert the literal `defer` attribute
and were failing against the deploy suite.

Add `defer` to module scripts emitted by `collectAssetTags` when
`experimental.disableOptimizedLoading` is not enabled. Surface the
flag in `ResolvedNextConfig` so prod-server respects an explicit
opt-out. Browser behaviour is unchanged (module scripts already
defer), but parity tests now pass.

Fixes #1519.

* test(e2e): update hydration regex to allow defer attribute

The script tag regex in `tests/e2e/cloudflare-pages-router/hydration.spec.ts`
required `type="module"` to be immediately followed by `src="…"`, but the
optimized-loading fix inserts a `defer` attribute between them. Relax the
regex to allow any whitespace-separated attributes between `type="module"`
and `src` so future attribute additions don't break it either.
2026-05-28 14:12:15 +01:00
Divanshu Chauhan (divkix) aae4b50fe8 fix(build): implement compiler.removeConsole option (#1611)
* fix(build): implement compiler.removeConsole option

Implements Next.js parity for compiler.removeConsole, which strips
console.* calls from the client bundle during production builds.

Behavior:
- compiler.removeConsole: true removes all console.<method>() calls
- compiler.removeConsole: { exclude: ['error'] } preserves excluded
  methods (case-insensitive)
- Shadowed console bindings (local vars, params, destructured) are
  preserved per Next.js SWC behavior
- Computed access console[prop]() is preserved
- Removed calls in expression statements become ; for clean output
- Removed calls in other contexts (JSX, arguments, returns) become void 0

Files:
- packages/vinext/src/plugins/remove-console.ts (new transform)
- packages/vinext/src/config/next-config.ts (types + resolution)
- packages/vinext/src/index.ts (Vite plugin registration)
- tests/remove-console.test.ts (24 unit tests)
- tests/next-config.test.ts (update default resolved config)

Fixes #1497

* fix(knip): un-export RemoveConsoleConfig type

Remove the declare -x ABBR_TIPS_PROMPT="\\n💡 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}"
declare -x ABBR_TIPS_REGEXES="(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*"
declare -x BUN_PTY_LIB="/\$bunfs/root/librust_pty_arm64-b7hv765h.dylib"
declare -x COLORTERM="truecolor"
declare -x COMMAND_MODE="unix2003"
declare -x CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include"
declare -x DEBIAN_FRONTEND="noninteractive"
declare -x DIVMEMORY_API_KEY="3b3f7180985460433de23604da244b8e4c1917f81f992199ebeb0ebc75e7a51e"
declare -x DIVMEMORY_WORKER_URL="https://divmemory.divkix.workers.dev"
declare -x DOTNET_CLI_TELEMETRY_OPTOUT="= 1"
declare -x DO_NOT_TRACK="= 1"
declare -x EDITOR="nano"
declare -x FORCE_COLOR="0"
declare -x FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT="= 1"
declare -x GATSBY_TELEMETRY_DISABLED="= 1"
declare -x GCM_INTERACTIVE="never"
declare -x GIT_ASKPASS=""
declare -x GIT_EDITOR="true"
declare -x GIT_SEQUENCE_EDITOR="true"
declare -x GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
declare -x GIT_TERMINAL_PROMPT="0"
declare -x GPG_TTY="/dev/ttys005"
declare -x HOME="/Users/divkix"
declare -x HOMEBREW_CELLAR="/opt/homebrew/Cellar"
declare -x HOMEBREW_NO_ANALYTICS="= 1"
declare -x HOMEBREW_PREFIX="/opt/homebrew"
declare -x HOMEBREW_REPOSITORY="/opt/homebrew"
declare -x INFOPATH="/opt/homebrew/share/info"
declare -x LANG="en_US.UTF-8"
declare -x LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib"
declare -x LOGNAME="divkix"
declare -x LaunchInstanceID="EB1B14BF-88FC-4DF6-A808-CFA3E9E33401"
declare -x MORPH_API_KEY="sk-sKIOR_4BfuniAInrzxgMoovjMKqlBxa2Ae6EC2oVOe44hOl-"
declare -x NEXT_TELEMETRY_DISABLED="= 1"
declare -x NODE_EXTRA_CA_CERTS="/Users/divkix/.factory/cache/certs/factory-cli-certs.pem"
declare -x OLDPWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x OPENCODE_ENABLE_EXA="true"
declare -x OSLogRateLimit="64"
declare -x PATH="/Users/divkix/.factory/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/divkix/.antigravity-ide/antigravity-ide/bin:/Users/divkix/.antigravity/antigravity/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/imagemagick-full/bin:/opt/homebrew/opt/ffmpeg-full/bin:/opt/homebrew/opt/ruby/bin:/Users/divkix/.bun/bin:/bin:/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home/bin:/opt/homebrew/opt/python/libexec/bin:/opt/homebrew/opt/python/bin:/opt/homebrew/opt/curl/bin:/opt/homebrew/opt/libpq/bin:/Users/divkix/go/bin:/Users/divkix/.local/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/Users/divkix/.lmstudio/bin:/Users/divkix/.orbstack/bin:/Applications/Warp.app/Contents/Resources/bin"
declare -x PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@3/lib/pkgconfig"
declare -x POWERSHELL_TELEMETRY_OPTOUT="= 1"
declare -x PWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x PYTHONUNBUFFERED="1"
declare -x SECURITYSESSIONID="186b0"
declare -x SHELL="/opt/homebrew/bin/fish"
declare -x SHLVL="2"
declare -x SSH_ASKPASS=""
declare -x SSH_AUTH_SOCK="/var/run/com.apple.launchd.VER64w7Euk/Listeners"
declare -x SSH_SOCKET_DIR="~/.ssh"
declare -x STARSHIP_CONFIG="/Users/divkix/.config/starship.toml"
declare -x STARSHIP_SESSION_KEY="5594177922001814"
declare -x STARSHIP_SHELL="fish"
declare -x SUDO_ASKPASS=""
declare -x TERM="dumb"
declare -x TERM_PROGRAM="WarpTerminal"
declare -x TERM_PROGRAM_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x TMPDIR="/var/folders/l6/72dpyfhj03x4z9lvndxkdbxr0000gn/T/"
declare -x USER="divkix"
declare -x VSCODE_TELEMETRY_LEVEL="= off"
declare -x WARP_CLIENT_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x WARP_CLI_AGENT_PROTOCOL_VERSION="1"
declare -x WARP_HONOR_PS1="1"
declare -x WARP_IS_LOCAL_SHELL_SESSION="1"
declare -x WARP_USE_SSH_WRAPPER="1"
declare -x XPC_FLAGS="0x0"
declare -x XPC_SERVICE_NAME="0"
declare -x __ABBR_TIPS_KEYS="l la ll lla g gcl gph gphf gpl gplr gst gcm gcam gco gca gcan gitweb gwp gwl reload c df less www untar bu oc fastdl a__cat a__ls a__rmd"
declare -x __ABBR_TIPS_VALUES="ls ls -a ls -l ls -la git git clone git push git push -f git pull git pull --rebase git status git commit -m git commit -am git commit git commit -a --amend git commit --amend -a --no-edit open (git remote get-url origin) git worktree prune -v git worktree list exec fish \"clear && printf '\\\\e[3J'\" df -h more python3 -m http.server 8000 tar -zxvf fisher update && brew update && brew upgrade opencode aria2c -x16 -s16 -k1M bat lsd --icon-theme=unicode trash"
declare -x __CFBundleIdentifier="dev.warp.Warp-Stable"
declare -x __CF_USER_TEXT_ENCODING="0x1F5:0:0" keyword from  — it is only
used internally within  and knip flagged it as an
unused export.

* ci: trigger CI re-run

Previous E2E failures (font-google, error-interactive, hooks) appear
to be flaky/unrelated to the removeConsole changes.

* fix(remove-console): address bonk review feedback

- Document client-only scoping as a known parity gap from Next.js
- Correct the node_modules skip comment (Next.js does apply to deps)
- Hoist SCOPE_ENTERING_TYPES to a module-level Set for O(1) lookup
- Push a scope for CatchClause params so `catch (console)` shadows
- Document the var-hoisting limitation in BlockStatement scope handling
- Add resolution tests covering all removeConsole config shapes

* fix(remove-console): gate to build + cover remaining shadow patterns

- Set `apply: "build"` on the plugin so console stripping no longer fires
  in `next dev`. Matches Next.js, which documents removeConsole as a
  production-only feature.
- Extend `checkBinding` to recognize AssignmentPattern, ArrayPattern, and
  RestElement so destructured defaults, array destructuring, and rest
  elements correctly shadow `console`.
- Mark FunctionDeclaration / ClassDeclaration `id` in the enclosing scope
  before pushing the function's own scope (function and class declarations
  hoist their name into the surrounding scope).
- Mark named FunctionExpression `id` in the function's own scope (named
  function expressions bind their name only inside their body).
- Add 10 tests covering all newly-supported shadow patterns plus a
  CatchClause regression test.

---------

Co-authored-by: James <james@eli.cx>
2026-05-27 19:16:34 +01:00
Divanshu Chauhan (divkix) 6a806ecee1 feat(instrumentation): support nextConfig.instrumentationClientInject (#1416)
* feat(instrumentation): support nextConfig.instrumentationClientInject (#1326)

Add support for the Next.js instrumentationClientInject config option that
lets users inject client bootstrap modules for side effects and an optional
onRouterTransitionStart hook ahead of the user's instrumentation-client file.

Changes:
- New generateInstrumentationClientInjectModule() generates a virtual ESM
  module with side-effect imports in array order + composed hook fan-out.
- Accept instrumentationClientInject in NextConfig / ResolvedNextConfig.
- New vinext:instrumentation-client-inject Vite plugin with resolveId/load
  hooks; virtual module string is precomputed once in config() for efficiency.
- Unit tests cover empty injects, single/multi specifiers, hook ordering,
  and empty-module fallback when no user file exists.

* test(instrumentation): fix oxfmt formatting and assertion indentation

The test expected 2-space indent for the generated code, but the
implementation produces 4-space indent within if blocks. Also fixes
oxfmt quote style and line wrapping.

* fix(ci): lint warnings and missing field in test helper

- Replace unnecessary escaped quotes inside template literals to fix
  eslint(no-useless-escape) warnings in instrumentation-client-inject.ts
- Add missing instrumentationClientInject field to makeResolved() test
  helper in next-config.test.ts to fix TS2322 type errors

* Address review feedback for instrumentationClientInject.

Make clientInjectModule self-clearing on config re-eval, filter non-string inject specifiers, document dual resolution paths, and add escaping coverage.

Signed-off-by: Divanshu Chauhan <divkix@divkix.me>

* test(instrumentation): add plugin pipeline integration tests and fix inject resolution

Add client-environment tests for instrumentationClientInject virtual module
wiring, omit the alias when injects are active so resolveId/load run, reload
next.config when root changes, and pre-resolve relative inject paths. Share
empty-module path via INSTRUMENTATION_CLIENT_EMPTY_MODULE and simplify tests.

* fix(instrumentation): resolve empty-module.js in production builds

CI E2E builds failed because INSTRUMENTATION_CLIENT_EMPTY_MODULE pointed
at empty-module.ts, which is not shipped in dist. Prefer .js when present.

* test(instrumentation): address PR review comments for client inject

- Revert nextConfigRoot guard changes in config loading to avoid buildId divergence risk
- Add cache-busting to dynamic ESM module imports in tests and document the reason
- Add integration test case for empty-inject and missing user-file fallback path

---------

Signed-off-by: Divanshu Chauhan <divkix@divkix.me>
2026-05-27 11:13:53 +01:00
Nathan Nguyen 4b6b438b94 fix(app-router): stream generated metadata for non-html bots (#1585)
* fix(app-router): stream generated metadata for non-html bots

Dynamic App Router metadata was always folded into the route head. That diverged from Next.js for normal browser-like requests, where generated metadata is sent through a body outlet while html-limited bots still receive blocking head metadata.

Track whether the matched route uses generateMetadata, thread next.config htmlLimitedBots into the generated RSC entry, and choose head versus body placement at page element construction.

* test(app-router): cover streaming metadata bot edge cases

Generated metadata must keep the default html-limited bot list when serialized config contains a falsy regex source. Empty config strings previously produced an empty regex, which treated every user agent as blocking and moved generated metadata back into the head.

Add regression coverage for the default Twitterbot path and the streaming body serializer, then match Next.js by falling back to the default bot regex for any falsy htmlLimitedBots value.

* fix(app-router): validate html-limited bot regex config

Invalid serialized htmlLimitedBots values could reach request handling and throw while building the bot matcher. That puts a config error on the request path and recompiles the same matcher for repeated requests.

Validate serialized regex sources while resolving next.config and reuse compiled bot matchers in the streaming metadata helper. The focused tests cover invalid config, falsy fallback, and matcher reuse.

* refactor(config): narrow next config option reads

Config resolution had several local assertions around experimental options, Turbopack aliases, output mode, and Sass options. Those assertions made the boundary wider than necessary in the code adjacent to htmlLimitedBots handling.

Read optional records, strings, arrays, and body-size inputs through typed narrowing helpers so the resolved config path keeps the same semantics without spreading unchecked asserted values.

* fix(config): keep bot regex helper out of server graph

Config resolution imported the streaming metadata server module to validate htmlLimitedBots. In RSC-backed App Router integration tests, that cross-layer import can pull server runtime modules through config resolution and surface rsc reference-validation errors.

Move the shared html-limited bot matcher cache to a neutral utils module and import it from config and the server streaming metadata helper. This keeps config validation independent from the server runtime graph while preserving the cached matcher behavior.
2026-05-26 12:07:07 +01:00
Divanshu Chauhan (divkix) c1c51fdbed fix(config): warn on deprecated experimental.rootParams (#1417)
has been stable since Next.js stabilized the feature upstream.
- Emit a deprecation warning when users still set
- Matches Next.js upstream wording for parity

Closes #1325
2026-05-22 09:12:09 +01:00
James Anderson 9415a238f4 feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time

Wire `assetPrefix` from next.config through to the Vite build so every
emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors
Next.js semantics: `assetPrefix` controls asset URLs only, independent
of `basePath` (which still controls route URLs).

* Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via
  a new `normalizeAssetPrefix` helper that trims trailing slashes,
  accepts absolute URLs (`https://cdn.example.com[/sub]`) and path
  prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable
  values.
* New `packages/vinext/src/utils/asset-prefix.ts` module collects the
  shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix`
  (emitted URL prefix), `assetPrefixPathname` (path portion for runtime
  matching), and the canonical `_next/static` directory constant.
* In the Vite plugin, set `build.assetsDir` so on-disk layout matches the
  URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for
  absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's
  default `base + url` URL emission so `assetPrefix` and `basePath` stay
  independent — routes continue to anchor at `base = basePath + '/'`.
* Persist `assetPrefix` in the embedded `vinextConfig` for the Pages
  Router server and in a new `vinext-runtime-config.json` sibling for
  the App Router prod server (the RSC entry only embeds `__basePath`).

@see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix

* feat(server): serve assetPrefix-prefixed assets at runtime

Update the production server to resolve incoming asset requests under
`<assetPrefix>/_next/static/<filename>` (path-prefix form) and
`/_next/static/<filename>` (absolute-URL form) in addition to the
historical `/assets/` URLs. Both routers (Pages and App) share a new
`resolveAppRouterAssetPath` helper that returns the lookup path on disk
or `null` to fall through.

* App Router prod server loads `assetPrefix` from the new
  `vinext-runtime-config.json` sibling written by the Vite plugin.
* Pages Router prod server reads `assetPrefix` from the embedded
  `vinextConfig` and threads it through the existing
  `stripBasePath` lookup.
* StaticFileCache marks any path under `_next/static/` as content-hashed
  (alongside the existing `assets/` rule) so emitted bundles get the
  long-lived `immutable` cache headers. Same change in the slow-path
  branch of `tryServeStatic`.

Cloudflare Workers needs no code change: the ASSETS binding serves
`<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...`
because the on-disk layout already mirrors the URL. Absolute-URL
prefixes are expected to be served by the CDN, so they don't reach the
worker.

* test(asset-prefix): add unit + e2e build tests for next.config assetPrefix

Covers the surface area added in the previous two commits:

* Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL,
  trailing-slash trimming, validation), `resolveAssetsDir` /
  `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout),
  `assetPrefixPathname` (runtime matching), and
  `resolveAppRouterAssetPath` (request → on-disk lookup translation).
* End-to-end build tests that exercise `vinext build` against the
  `tests/fixtures/app-basic` app patched with each `assetPrefix` shape:
    - Path prefix (`/custom-asset-prefix`): SSR HTML references
      `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches,
      and fetching the bootstrap URL from the prod server returns 200.
    - Absolute URL (`https://cdn.example.com`): emitted asset URLs are
      fully qualified; no `/assets/` leaks.
    - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes
      stay under `/app/` while asset URLs live under `/cdn-prefix/` —
      proving the two configs stay independent.
    - Unset (default): historical `/assets/` layout is preserved.

Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts

* test(next-config): add assetPrefix field to makeResolved helper

ResolvedNextConfig requires assetPrefix; the test helper was missing
it which broke the type check.

* fix(config): replace polynomial regex with loop (CodeQL)

CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "")
as polynomial-time backtracking on uncontrolled input. Swap both for
explicit while loops with linear-time semantics. Same fix applied to
the URL pathname trim in assetPrefixPathname().

* refactor(prod-server): drop dead null fallback; tighten hashed-asset detection

- Remove the `?? null` after resolveAppRouterAssetPath; the helper
  already returns null on miss, so the fallback (and its obsolete
  comment) were no-ops.
- Add explicit startsWith("_next/static/") / startsWith("/_next/static/")
  prefix checks before the substring fallback in both static-file-cache
  and prod-server. The substring check stays so assetPrefix-prefixed
  layouts (e.g. "cdn/_next/static/") are still recognised.

* fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed

precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix`
is configured the build writes hashed assets to a different subdirectory
(`<prefix>/_next/static/` for path prefixes, `_next/static/` for
absolute URLs) and the precompression step walked an empty directory,
silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds
with assetPrefix would then serve uncompressed assets.

Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix)
at the plugin call site, default to `"assets"` for backward compat,
and add focused tests for both assetPrefix layouts.

* refactor: drop redundant isHashed startsWith; harden test cleanup against build failures

- prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")`
  check; `pathname` always has a leading slash so the existing
  `includes("/_next/static/")` already covers it.
- static-file-cache.ts: keep both `startsWith("_next/static/")` and
  `includes("/_next/static/")` because `relativePath` has no leading
  slash — the prefix and substring checks cover different layouts
  (absolute-URL prefix vs path-prefix). Clarify the comment.
- tests/asset-prefix.test.ts: register the tmp-dir cleanup with the
  shared `cleanups` array synchronously inside `buildFixtureWithConfig`,
  right after `mkdtempSync`. Previously the cleanup was only pushed
  after the build returned, so a thrown `createBuilder`/`buildApp`
  would leak the tmp dir.

* refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON

Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts)
and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry.
The runtime-config.json sidecar was unnecessary — the App Router entry
template already inlines config values as compile-time constants, so
assetPrefix should be inlined the same way.

Changes:
- Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts.
- Emit `export const __assetPrefix = <json>` from the generated RSC
  entry alongside the existing `__basePath` / `__configHeaders` /
  `__expireTime` constants. Single-line declaration to stay within the
  "thin generated entry" guideline.
- Thread `nextConfig.assetPrefix` through the generateRscEntry call site
  in index.ts.
- App Router prod-server reads `rscModule.__assetPrefix` (with a string
  fallback for older builds) instead of `vinext-runtime-config.json`.
- Delete the vinext-runtime-config.json write in index.ts. Nothing else
  in the repo reads this file (verified via rg on packages/ tests/
  examples/).

Pages Router behaviour is unchanged: it already gets assetPrefix from
the embedded `vinextConfig` exported by pages-server-entry.ts.

Addresses james-elicx review feedback on #1311.

* fix(ssr): emit bootstrap as <script src> via bootstrapModules

The App Router SSR pipeline previously passed only `bootstrapScriptContent`
to React's renderToReadableStream, producing inline
`<script>import("/path/to/entry.js")</script>` tags. The URL was therefore
inaccessible to anything that reads `script.attribs.src` — including the
Next.js asset-prefix fixture test "bundles should return 200 on served
assetPrefix" which iterates script tags looking for a `src` attribute
prefixed by the configured assetPrefix.

Extract the bootstrap URL from the plugin-rsc-generated content and pass
it as `bootstrapModules: [url]` so React emits a real
`<script type="module" src="url">` tag. The existing
`extractModulePreloadHtml` helper already did the same extraction for
the modulepreload <link>, so the URL was already exposed (just renamed
`extractBootstrapModuleUrl` and lifted out of the head-injection helper
to share between the bootstrap and the preload tags).

The inline `bootstrapScriptContent` is no longer needed — it was only
carrying the dynamic-import call that bootstrapModules now performs
natively. Passing both would load the bootstrap module twice.

`bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits
a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`)
that the browser fetches as a module from the dev server; in prod it's the
hashed bundle URL, optionally prefixed by `assetPrefix`.

Mirrors Next.js's app-render path which combines both
`bootstrapScriptContent` (for session state) and `bootstrapScripts`
(for the actual chunk URL). See
.nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx.
We use `bootstrapModules` instead of `bootstrapScripts` because vinext's
chunks are native ES modules (Vite output) — a `type="module"` tag is the
correct loader.

Tests updated where they asserted on the old `import("URL")` shape:
tests/asset-prefix.test.ts, tests/app-router.test.ts,
tests/e2e/cloudflare-workers/navigation.spec.ts.

Verified all related suites pass locally (asset-prefix, app-router,
script-nonce, script-preload, use-client-page-pathname, document,
prerender, ISR, fetch-cache, entry-templates, precompress).

* fix(build): use resolveAssetsDir for Pages Router client entry fallback scan

The Pages Router Cloudflare Workers post-build step scans the on-disk
assets directory for the client entry chunk when the SSR manifest
lookup doesn't surface one. The directory path was hardcoded to
`<clientDir>/assets/`, which is the Vite default — but when
`assetPrefix` is configured, the chunk lives under
`<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL
prefix). The fallback would silently miss the entry and the worker
would ship without its client bootstrap reference.

Use the shared `resolveAssetsDir(assetPrefix)` helper that already
drives `build.assetsDir` and the prod-server lookup, so this scan
works for every supported asset layout.

Addresses round-4 reviewer feedback on #1311 (low-priority follow-up
item — only fires when the SSR manifest lookup fails).

* feat(config): mirror Next.js basePath → assetPrefix fallback

Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured
and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531).
Without this, an app deployed under a basePath serves its routes
correctly but emits its assets from `<basePath>/assets/...` (Vite's
default `base + assetsDir` composition) rather than from the Next.js-
canonical `<basePath>/_next/static/...`.

Add the same fallback in resolveNextConfig, gated by Next.js's own
conditions:
  - basePath !== ""   (skip when basePath is unset)
  - basePath !== "/"  (Next.js rejects this earlier; we mirror the
                       guard so we never produce assetPrefix === "/")
  - assetPrefix === "" (user did not opt out)

The fallback feeds the rest of the asset-prefix pipeline (Vite's
`build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server
URL→file resolver) so no other code paths need to change.

Tests:
 - tests/next-config.test.ts: 5 unit tests for the four edge cases of
   the fallback condition.
 - tests/asset-prefix.test.ts: new end-to-end test that builds with
   `basePath: "/app"` (no assetPrefix), starts the prod-server, and
   asserts the bootstrap <script src> lands at /app/_next/static/...
   (and that the legacy /app/assets/... layout is gone).
 - tests/pages-router.test.ts: updated the "preserves basePath on
   backfilled SSR manifest entries" test to expect the new
   /docs/_next/static/ layout (was /docs/assets/). The manifest
   assertion is relaxed to startsWith("docs/") because Vite's raw
   SSR manifest now emits some duplicate-prefixed entries
   ("docs/docs/_next/static/...") alongside the backfilled ones; both
   forms anchor under basePath so the prod-server's URL→file lookup
   remains correct — and the user-visible HTML assertions in the same
   test are the source of truth.

Addresses round-4 reviewer feedback on #1311.

* fix(server): Pages Router must not strip basePath before asset lookup

The Pages Router prod-server stripped `basePath` from the request
pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke
the asset lookup whenever the assetPrefix path-prefix branch was active
with a path that happened to equal basePath.

Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's
`basePath → assetPrefix` fallback, an app configured with
`basePath: "/docs"` (and no explicit `assetPrefix`) now resolves
`assetPrefix = "/docs"`. Emitted asset URLs are
`/docs/_next/static/…` (correct). But the Pages Router branch did:

  const staticLookupPath = stripBasePath(pathname, basePath);
  // "/docs/_next/static/x.js" → "/_next/static/x.js"
  resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs");
  // path-prefix branch needs pathname to start with "/docs/" → null → 404

The App Router branch already passes the un-stripped `pathname` to the
same helper (see line ~1140). Match that behaviour. `staticLookupPath`
is still computed and used downstream by non-asset paths
(image-optimization endpoint, page routing), where the basePath-stripped
form is correct.

Test coverage:
 - tests/pages-router.test.ts: extended the existing "preserves basePath
   on backfilled SSR manifest entries" test to FETCH every emitted asset
   URL and assert HTTP 200 (previously it only inspected HTML for the
   URL string). This is the missing assertion that would have caught
   the bug the first time.
 - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test
   that builds an inline pages/ fixture with `basePath: "/docs"`,
   starts the prod-server, collects every script/link URL from the SSR
   HTML, and asserts every URL serves a 200 + correct content-type.
   Confirmed to fail on `bac8425` before this fix and pass after.

Addresses round-5 reviewer feedback on #1311.

* refactor(ssr): accept either quote style in bootstrap URL regex

plugin-rsc currently emits the bootstrap as `import("<url>")` with
double quotes, but the contract isn't documented and a future version
could switch to single quotes. Tighten the URL extraction regex to
`import(["'])([^"']+)\1` so either form works.

Addresses non-blocking round-5 review feedback on #1311.

* docs(ssr): note CSP implications of bootstrapModules switch

The round-3 commit `3a2c886` switched the App Router SSR bootstrap from
inline `bootstrapScriptContent` (`<script>import(…)</script>`) to
React's `bootstrapModules` option (`<script type="module" src=…>`).
That has user-visible CSP consequences worth documenting at the call
site so app authors hit them in code search rather than at runtime:

  - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap
    (improvement — inline imports previously required it).
  - Apps that restrict script sources need `'self'` for same-origin,
    or the CDN origin when assetPrefix is an absolute URL.
  - React still threads nonce through to the emitted <script> tag, so
    nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps
    working unchanged.

Addresses non-blocking round-5 review feedback on #1311.
2026-05-19 11:53:49 +01:00
James Anderson e69721f392 fix(config): provide CJS globals when loading next.config.ts (#1278)
* fix(config): provide CJS globals when loading next.config.ts

Next.js's `next.config.ts` loader transpiles the file to CommonJS via SWC
and evaluates it through Node's `Module._compile`, which exposes the CJS
globals (`__filename`, `__dirname`, `module`, `exports`, `require`) even
when the source uses ESM syntax. vinext previously loaded the config
through Vite's ESM-only module runner, so configs that read `__dirname`,
called `require()`, or used `module.exports` threw `ReferenceError`s
during build.

Inject the same globals via a small Vite `transform` plugin that runs
only on the `.ts`/`.mts`/`.cts` config entry point. Reassigning
`module.exports` is preserved by exposing the wrapper object as a named
export and reading it back in `unwrapConfig`.

Reference: packages/next/src/build/next-config-ts/{transpile-config,require-hook}.ts

* fix(config): guard CJS-globals injection on static reference check

Skip cjsGlobalsInjectorPlugin's transform entirely when the next.config
source contains no bareword reference to __filename, __dirname, require,
module, or exports. The vast majority of next.config.ts files are pure
ESM (every upstream `test/e2e/app-dir/next-config-ts` fixture except
node-api-cjs is) and pay no cost from the transform.

Substring matching is intentional: a false positive (e.g. the word
`module` inside a comment or `node:module` specifier) costs at most one
wasted, idempotent transform, while a false negative would be a
correctness regression. Bareword `\b` boundaries already exclude
`requireSomething` and `myModule` style identifiers, and `\bexports\b`
does not match `export default`.

* fix(config): skip module.exports wrapper when source uses ESM-only

Statically detect whether a `next.config.ts` source assigns to
`module.exports` (via `module.exports =`, `module.exports.foo =`, or
`module.exports[...] =`) with a new `reassignsModuleExports` helper.
Only emit the `__vinext_cjs_exports` named export and its companion
initial-reference export when that check matches. Pure-ESM configs —
the overwhelming majority — get a slightly smaller injected preamble
and a simpler unwrap path that just reads the ESM `default` export.

The previous sentinel-on-module.exports approach tagged the initial
exports object with a `Symbol.for("vinext:cjs-initial")` property on
every config load and checked for it during unwrap. That worked but
ran unconditionally and left a stray symbol-keyed property on the
final exports object whenever the user reassigned. The new shape only
pays for the wrapper when the source plausibly needs it, and keeps a
referential-identity check (`module.exports !== __vinext_cjs_initial_exports`)
as belt-and-braces so that a `reassignsModuleExports` false positive
inside a string or comment still falls back to the ESM default
instead of returning an empty `{}`.
2026-05-16 20:10:23 +01:00
James Anderson 9252ca2966 fix(css): pass sassOptions from next.config to Vite preprocessor (#1277)
* fix(css): pass sassOptions from next.config to Vite preprocessor

Next.js threads `sassOptions` from `next.config.js` into sass-loader,
destructuring `prependData` / `additionalData` / `implementation` and
forwarding the rest (e.g. `includePaths`, `loadPaths`) as Sass options.
vinext was dropping this entirely, so SCSS fixtures that rely on
`additionalData` to define variables, `includePaths`/`loadPaths` to
resolve partials, or a custom `implementation` all failed to build
with `Undefined variable` / `Can't find stylesheet to import`.

Resolve `sassOptions` from next.config into `ResolvedNextConfig`, then
translate it onto Vite's `css.preprocessorOptions.scss` (and `.sass`)
shape:

  - prependData precedence matches Next.js (prependData || additionalData)
  - legacy includePaths is merged into modern loadPaths (Vite uses
    modern Sass, which dropped includePaths)
  - implementation and any remaining Sass options are passed through

Affected upstream fixtures:
  - test/e2e/app-dir/scss/basic-module-include-paths
  - test/e2e/app-dir/scss/basic-module-additional-data
  - test/e2e/app-dir/scss/basic-module-prepend-data

Refs: cloudflare/vinext CI run 25951111875
Source: packages/next/src/build/webpack/config/blocks/css/index.ts

* fix(css): address sass passthrough review feedback

- Drop `export` on `VitePreprocessorOptions` so Knip stops flagging it
  as unused (CI Check blocker).
- Use truthy-OR (`prependData || additionalData`) to match Next.js's
  `sassPrependData || sassAdditionalData` exactly — empty-string
  `prependData` now falls through to `additionalData` instead of
  overriding it.
- Widen `additionalData` type to accept Vite's function form
  (`(source, filename) => string | Promise<string>`) and drop the
  inaccurate `as string` cast.
- Add tests covering the empty-`prependData` fallthrough and the
  function-form `additionalData` passthrough.
2026-05-16 19:45:03 +01:00
James Anderson 1f2030b8d2 fix(config): load CJS next.config.js under "type": "module" (#1275)
`createRequire` of a `.js` file in a `"type": "module"` package still gets
resolved as ESM by Node (extension + parent type wins over the require()
caller), so the existing CJS fallback in loadNextConfig threw `require is
not defined` for every plain `module.exports = {}` config after `vinext
init`. Worked around in the Nextjs deploy suite by rewriting every
fixture's config from CJS to ESM in-place via scripts/cjs-to-esm-config.mjs.

Copy the source to a sibling temp `.cjs` (extension overrides the parent
type field) and require that instead. Relative imports inside the config
still resolve against the original directory. Drop the deploy-suite
rewrite step and its 145-line shim.
2026-05-16 18:36:22 +01:00
James Anderson eccee1843e fix(deploy-suite): apply tsconfig compilerOptions.paths when loading next.config.ts (#1215)
Next.js's own loader (packages/next/src/build/next-config-ts/transpile-config.ts)
reads compilerOptions.paths + baseUrl from tsconfig.json and passes them to SWC
so next.config.ts can import via tsconfig aliases like:

    import { foo } from '@/foo'

vinext's loadNextConfig() calls Vite's runnerImport(), but did not configure any
resolve.alias entries, so the same imports fail with:

    [vinext] Failed to load next.config.ts:
      Cannot find module '@/foo' imported from '<dir>/next.config.ts'

This is a parity gap surfaced by the Next.js deploy test suite.

Fix: extract a small tsconfig-paths loader (config/tsconfig-paths.ts) that
reads compilerOptions.paths (following extends, honoring baseUrl) and returns
absolute Vite resolve.alias entries. loadNextConfig now passes those aliases
into runnerImport's inlineConfig.

Affected suites (from run 25870737355):

  - e2e/app-dir/next-config-ts/import-alias-paths-only/
  - e2e/app-dir/next-config-ts/import-alias-paths-with-baseurl/
  - e2e/app-dir/next-config-ts/import-js-extensions-cjs/
  - e2e/app-dir/next-config-ts/import-js-extensions-esm/
  - e2e/app-dir/next-config-ts-native-ts/dynamic-import-esm/
  - e2e/app-dir/next-config-ts-native-mts/import-from-node-modules/

Verification:
  - 9 new unit tests for loadTsconfigPathAliasesForRoot
  - 4 new unit tests covering loadNextConfig + tsconfig paths (ported from
    Next.js's import-alias-paths-only / import-alias-paths-with-baseurl
    fixtures), including extends + jsconfig fallback
  - Reproduced against .nextjs-ref/test/e2e/app-dir/next-config-ts/
    import-alias-paths-only/ — config now loads with `foo === "foo"`
  - vp check (fmt + lint + types) clean
  - bash -n scripts/*.sh clean

Refs: cloudflare/vinext#actions/runs/25870737355
2026-05-14 19:36:21 +01:00
Nathan Nguyen 671d782062 fix(cache): scope use cache keys by deployment id (#1092)
* fix(cache): scope use cache keys by deployment id

Shared "use cache" entries only used the vinext build ID as their cache-key seed. That diverges from the upstream Next.js change for cacheHandlers and can let a Cloudflare Worker deployment reuse entries from another deployment when the deployment identity changes independently of the build fallback.

The cache runtime now resolves a deployment-aware key seed at execution time, with request-scoped Worker version metadata or NEXT_DEPLOYMENT_ID taking precedence over the build ID. Generated Wrangler config includes the CF_VERSION_METADATA binding, and the App Router Worker entry threads it into the runtime without process-global request races.

Tests cover deployment ID precedence, concurrent deployment scope isolation, build ID fallback, and generated Worker config wiring.

* fix(config): resolve deployment id for cache seeds

Deployment-aware "use cache" keys had runtime Worker and NEXT_DEPLOYMENT_ID support, but vinext did not resolve the Next.js deploymentId config field. That left config-defined deployment identity out of the build-time cache seed.

Resolve deploymentId from next.config.js before NEXT_DEPLOYMENT_ID, validate the same string shape Next.js documents, and feed the resolved value into the __VINEXT_DEPLOYMENT_ID define. The process-wide cache-runtime setter is also documented as a fallback rather than the per-request API.

Tests cover config precedence, env fallback, empty config values, and invalid deploymentId shapes.

* refactor(cache): carry deployment id through request context

The Worker version metadata path still needs request-scoped state, but cache-runtime should not own a cache-specific deployment ALS or public-looking setter.

Move runtime deployment ID storage into the unified request-context layer. The Worker boundary now writes deploymentId into request runtime context, cache-runtime reads that generic request metadata before falling back to inlined deployment/build IDs, and the tests cover direct runtime scopes plus inheritance into unified request contexts.

* refactor(cache): reuse request context for deployment id

* test(app-elements): expect artifact compatibility metadata

* refactor(cache): inline deployment id seed

* chore: rerun ci
2026-05-08 21:41:46 +01:00
Nathan Nguyen f45fce00d5 fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings

Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses.

Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules.

Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known.

* fix(isr): address cache life review follow-ups

* chore: address latest review follow-ups

* ci: pin vite-plus setup version

* Revert "ci: pin vite-plus setup version"

This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1.

* fix: avoid blocking ISR page streams on cache metadata

* fix: preserve headers for speculative cacheLife probes

* fix: preserve prerender cacheLife metadata

* fix: preserve legacy ISR cache metadata behavior

* fix: preserve prerender seed revalidate context

* fix: harden app page cache policy metadata

* fix: resolve app router prerender conflict
2026-05-02 19:54:25 +01:00
Divanshu Chauhan (divkix) ae0440b4f7 fix: support enablePrerenderSourceMaps config with default true (#973)
* fix: support enablePrerenderSourceMaps config with default true (#960)

Adds the `enablePrerenderSourceMaps` config option matching Next.js behavior.
Defaults to `true` so prerender stack traces resolve through sourcemaps to
original source files. Error results in prerender output now include full
stack traces when enabled. Node.js sourcemap support is enabled before the
prerender phase in both `cli.ts` (build) and `deploy.ts` (deploy).

Config parity: silently accepts the option; recognized as "supported" in
`vinext check` output.

* refactor: address review comments for enablePrerenderSourceMaps (#960)

- prerender.ts: simplify getErrorMessageWithStack and fix comment
  (V8's Error.stack always prepends message, remove startsWith guard)
- cli.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled
- deploy.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled
- next-config.test.ts: move enablePrerenderSourceMaps tests to own describe
  block and remove redundant explicit-true test

* fix(prerender): simplify getErrorMessageWithStack and add test coverage

Simplify the error stack handling in getErrorMessageWithStack by removing
the redundant nullish coalescing with empty string. The expression
`err.stack || err.message` handles all cases correctly.

Add test coverage proving that enablePrerenderSourceMaps: true (default)
includes stack traces in prerender errors. New test fixture page
`error-throw.tsx` throws during render to exercise the error path.

* fix(prerender): address review feedback - update snapshot, add config comment

- Regenerate entry-templates.test.ts.snap after adding error-throw fixture page
- Add comment clarifying enablePrerenderSourceMaps default in prerender test

* feat(config): add enablePrerenderSourceMaps config option

Adds support for the experimental.enablePrerenderSourceMaps config
option that Next.js 15+ introduced for debugging prerender issues.

Also includes merged changes from main:
- hashSalt: support experimental.outputHashSalt + NEXT_HASH_SALT env
- swcEnvOptions warning: warn about unsupported SWC polyfill option

All 75 next-config tests passing.

Closes #960
2026-05-01 22:41:12 +01:00
Divanshu Chauhan (divkix) 721be81709 fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#978)
* fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#754)

Add support for Next.js experimental.outputHashSalt config option and
NEXT_HASH_SALT environment variable. When set, the salt is mixed into
content-addressed build output filenames via Rollup/Rolldown's
output.hashSalt option, producing different hashes without modifying
source code — useful for cache-busting after CDN poisoning.

Config resolution: concatenates config salt + env var salt (matching
Next.js behavior: config first, then env). When neither is set, no
salt is applied and behavior is unchanged.

* ci: re-trigger CI (Windows create-next-app flake, STATUS_STACK_BUFFER_OVERRUN)

* fix: use augmentChunkHash for outputHashSalt instead of output.hashSalt

The output.hashSalt option is webpack-specific and is silently ignored by
Rollup/Rolldown. Use the augmentChunkHash plugin hook instead, which is
supported by both bundlers and actually affects chunk content hashes.

Changes:
- Remove hashSalt parameter from getClientOutputConfigForVite()
- Add vinext:hash-salt plugin with augmentChunkHash hook
- Add experimental.outputHashSalt to CONFIG_SUPPORT map

* refactor: simplify hash-salt plugin comments

- Simplify comment about augmentChunkHash (just Rolldown, not Rollup)
- Add scope-gap note about CSS/static asset hashing not being salted
2026-05-01 08:00:54 +01:00
Divanshu Chauhan (divkix) 155be501e1 fix: accept experimental.swcEnvOptions config without erroring (#977)
* fix: accept experimental.swcEnvOptions config without erroring (#774)

Next.js added experimental.swcEnvOptions for SWC preset-env polyfill injection.
vinext uses Vite for transforms, so polyfill injection is not applicable, but
the config key should be accepted without errors. Add:
- CONFIG_SUPPORT entry in check.ts (unsupported with detail)
- Warning during config resolution in next-config.ts
- Tests for analyzeConfig detection and dot-notation matching

* refactor: align swcEnvOptions messaging and add test coverage

- Change runtime warning from "not yet supported" to "not applicable"

- Update CONFIG_SUPPORT detail for consistent messaging

- Add test coverage for swcEnvOptions warning in next-config.test.ts

* fix(check): update test assertion for swcEnvOptions message

The CONFIG_SUPPORT message for experimental.swcEnvOptions was updated
to indicate it's not applicable since vinext uses Vite instead of SWC.
This updates the corresponding test assertion to match the new message.

Fixes CI failure in tests/check.test.ts
2026-04-30 08:53:58 +01:00
Divanshu Chauhan (divkix) e0854972eb fix: support file:// URLs in cacheHandler config path (#944)
* fix: support file:// URLs in cacheHandler config path (#728)

- Add cacheHandler and cacheMaxMemorySize to NextConfig type
- Add cacheHandler to ResolvedNextConfig with file:// URL handling
- Create resolveCacheHandlerPathToFilesystem() helper using fileURLToPath()
- Convert file:// URLs from import.meta.resolve() to filesystem paths
  during config resolution to prevent broken path concatenation

Matches Next.js commit 9a0214a1 which added the same file:// URL handling
for ESM projects using import.meta.resolve() for the cache handler path.

* fix: drop unused export on resolveCacheHandlerPathToFilesystem

* fix: resolve cacheMaxMemorySize and add cacheHandler tests

- Add cacheMaxMemorySize to ResolvedNextConfig and resolve it from input config,
  preventing silent config swallowing
- Add 7 tests for resolveNextConfig cacheHandler: file:// URL resolution,
  absolute/relative path passthrough, null/undefined defaults
- Also add cacheMaxMemorySize resolution and default tests
2026-04-29 18:32:39 +01:00
Gentrit Biba fa03a06bea feat: optimize barrel imports for RSC-incompatible packages (#138)
* feat: add vinext:optimize-imports plugin for barrel import rewriting

Barrel imports like `import { Slot } from "radix-ui"` cause RSC crashes
because Vite eagerly evaluates all re-exported sub-packages, some of which
call React.createContext() — unavailable in the react-server condition.

This plugin rewrites barrel imports into direct sub-module imports on the
server, matching Next.js's optimizePackageImports behavior:

- Parses barrel entry files to build export maps (namespace, named,
  default re-exports, and import-then-export patterns)
- resolveId hook handles pnpm strict hoisting via Vite's own resolver
- Only runs on server environments; client uses Vite's dep optimizer
- Lazy-initialized Set<string> for O(1) package lookups
- Respects experimental.optimizePackageImports from next.config
- Includes Next.js default package list plus radix-ui

Closes cloudflare/vinext#100
Closes cloudflare/vinext#137

* fix: add missing packages to DEFAULT_OPTIMIZE_PACKAGES

Add the full effect ecosystem (17 packages) and all react-icons/*
sub-packages (32 packages) to match Next.js's built-in defaults.

* fix: address bonk review — entryPathCache, buildStart init, Windows path fix, trailing semicolons, fixture-based transform tests

* fix: correct bySource grouping for mixed namespace+named imports, clear barrelCaches on rebuild

- Key bySource map on `${source}::${isNamespace}` instead of just source
  so that a namespace re-export and a named re-export from the same sub-module
  produce two separate import statements rather than one corrupted one.
  Caught by the new 'produces separate statements for namespace and named
  imports from the same source' test.
- Clear barrelCaches.exportMapCache and barrelCaches.subpkgOrigin in buildStart
  alongside entryPathCache so stale barrel analysis doesn't survive rebuilds.
- Add regression test for the mixed namespace+named grouping bug.

* fix: resolve relative barrel re-export paths to absolute, emit default imports for default re-exports

Barrels like lodash-es and lucide-react use relative re-export paths
(e.g. `export { chunk } from './chunk.js'`). Writing those verbatim caused
Vite to resolve './chunk.js' against the importing user file rather than
the barrel package directory.

Fix: resolve any relative source path against the barrel entry's directory
before emitting the replacement import statement, producing an absolute
filesystem path that Vite resolves correctly regardless of where the
importing file lives.

Also fix: `export { default as X }` re-exports now produce `import X from`
(a default import) instead of `import { default as X }` (invalid syntax
that only works via ASI / bundler leniency).

Tests updated to assert absolute paths and added a dedicated test for
default re-export handling.

* refactor: extract barrel optimization to plugins/optimize-imports.ts, add Windows path fix and react-server TODO

- Move all barrel types, helpers, DEFAULT_OPTIMIZE_PACKAGES, and the
  vinext:optimize-imports IIFE from index.ts into a new
  packages/vinext/src/plugins/optimize-imports.ts file with a factory
  function createOptimizeImportsPlugin(getNextConfig, getRoot)
- Forward-slash normalize resolved absolute paths so emitted import
  statements use '/' separators on Windows (path.sep join)
- Add TODO comment in resolveExportsValue noting that the 'react-server'
  condition should be added in a future pass
- Add test for ImportDefaultSpecifier: import MyFoo from 'pkg' rewrites
  to import MyFoo from '<abs>/sub' (22 tests total, all passing)

* fix(optimize-imports): address sixth-pass review comments

- Fix astName() to throw on unexpected nodes instead of String(value)
- Normalize all resolvePackageEntry return values to forward slashes (Windows fix)
- Fix originalName falsy guard: && → !== undefined
- Pre-build quotedPackages in buildStart to avoid per-file template literal allocations
- Add first-wins comment on subpkgOrigin registration
- Add clarifying comment to lucide-react test fixture

* fix(optimize-imports): remove EnvironmentPluginContext cast, expand subpkgOrigin comment

Vite augments rolldown's MinimalPluginContext with `environment: Environment`
via declaration merging, so both resolveId and transform have `this.environment`
typed without any cast. Drop the EnvironmentPluginContext interface and the
`as unknown as EnvironmentPluginContext` cast in the transform handler, using
direct `this.environment` access consistently with resolveId.

Also expand the subpkgOrigin first-wins comment to make explicit why first-wins
is safe: the sub-package specifier resolves to the same path regardless of which
barrel's importer context is used.

* fix(optimize-imports): address seventh-pass review nits

- Fix test imports: vitest → vite-plus/test, Plugin from vite → vite-plus,
  following the convention used by 82 of 85 test files in this repo
- Add TODO comment for extensionless absolute paths produced when barrel
  sources lack file extensions (Vite resolves these correctly in practice)
- Expand subpkgOrigin first-wins comment to explicitly note the nested
  node_modules edge case and why it is out of scope for the default list

* fix(optimize-imports): address james-elicx review comments

- Support wildcard re-exports (export * from './sub') by recursively
  parsing sub-modules and merging their exports into the barrel map
- Add circular-reference guard to prevent infinite loops in recursive
  export map building
- Refactor buildBarrelExportMap into buildExportMapFromFile helper
  with shared recursive logic
- Use switch statements for spec.type handling instead of if-else chains
- Remove non-null assertions and type assertions; use optional chaining
  and typeof guards throughout
- Extract resolvePackageInfo helper to separate concerns in
  resolvePackageEntry
- Tests now import directly from optimize-imports.ts instead of via
  the full vinext() plugin array
- Remove _buildBarrelExportMap re-export from index.ts (no longer needed
  since the plugin has its own file)
- Fix type assertion for optimizePackageImports in next-config.ts using
  a proper type-guard filter
- Apply README nit: 'is always optimized' → 'are always optimized'
- Add tests for wildcard export resolution, no-overwrite semantics,
  and circular re-export safety

* fix(optimize-imports): address ask-bonk review comments

- Resolve relative sources to absolute paths inside buildExportMapFromFile
  so nested wildcard re-exports (e.g. antd-style components/) resolve against
  the correct sub-module directory instead of the barrel root
- Add react-server condition to resolveExportsValue (before node/import) for
  RSC-compatible entry points in packages like react and react-dom
- Add NOTE comment in resolvePackageEntry documenting that only exports["."]
  is checked, not subpath exports
- Eliminate double readFile call in buildBarrelExportMap by passing the
  already-read content to buildExportMapFromFile via new initialContent param
- Update subpkgOrigin guard to check for absolute paths (not startsWith ".")
  since relative sources are now stored as absolute paths in the export map
- Add test for nested subdirectory wildcard path resolution correctness
- Update existing unit tests to expect absolute paths in export map entries

* fix(optimize-imports): only prefer react-server condition in RSC environment

SSR renders with the full React runtime and must not resolve react-server
export condition entries. Thread preferReactServer (env.name === 'rsc')
through resolveExportsValue and resolvePackageEntry, and key entryPathCache
by environment prefix to keep RSC and SSR barrel entries separate.

* fix(optimize-imports): address review nits — TODO comment, type annotation, test names, wildcard dir test

- Add TODO comment on allResolved bail-out suggesting debug logging
- Remove redundant ': string[]' annotation on optimizePackageImports (inferred)
- Rename namespace re-export test to use generic X/barrel names instead of Slot/lucide-react
- Update syntax-error test to match new empty-map-on-parse-error behavior (not null)
- Add test for directory-style 'export * from "./components"' where components/index.js exists

* fix(optimize-imports): address review nits — wildcard extensions, double-read, readFile hoisting, subpkg registration guard

- Add /index.mjs, /index.tsx, and .tsx to wildcard candidate list for ESM-first and TypeScript-first packages
- Pass candidateContent to buildExportMapFromFile in wildcard loop to avoid reading each sub-module file twice
- Add comment clarifying the no-op resolveEntry callback pattern
- Hoist readFileSafe closure outside the import-declaration for-loop (one allocation per transform, not per barrel import)
- Gate subpkgOrigin registration loop with registeredBarrels Set so 50 files importing the same barrel don't each iterate the full export map

* fix(optimize-imports): remove redundant double-cache, extract PluginCtx type alias

- Remove the redundant exportMapCache.set() in buildBarrelExportMap — buildExportMapFromFile
  already stores the result under the same key; the extra set was a no-op but misleading
- Extract PluginCtx type alias for the environment cast so all three hook sites share one
  definition and the verbose inline cast doesn't repeat across resolveId and transform

* fix(optimize-imports): hoist readFileSafe to module level, add namespace import test

- Move readFileSafe from handler scope to module level since it captures
  nothing from the per-file closure; avoids a per-call function allocation
- Add test verifying import * as Pkg from 'barrel' is left unchanged
  (ImportNamespaceSpecifier sets allResolved = false — can't optimize a
  full namespace import to a sub-module)

* fix(optimize-imports): make barrel resolution async, add debug logging, update tests

* fix(optimize-imports): address review feedback — extensions, env-keyed subpkgOrigin, debug gate, graceful astName

* fix: scope registeredBarrels key to env to prevent RSC registration blocking SSR

When RSC and SSR resolve the same barrel entry path (common — most packages
have no react-server export condition), the bare barrelEntry key in
registeredBarrels caused RSC's registration to suppress SSR's inner loop
entirely. SSR's subpkgOrigin map was left empty, making resolveId depend
solely on the cross-env fallback — fragile coupling that could silently break.

Change the registeredBarrels key from barrelEntry to `${envKey}:barrelEntry`
so each environment runs the inner loop and populates its own subpkgOrigin
map independently. Matches the existing entryPathCache key pattern.

Add a regression test covering the shared-barrel-entry scenario.

* fix: handle inline export declarations in barrel sub-modules (date-fns)

date-fns barrels use `export * from "./formatDistanceToNow.js"` where the
sub-module directly declares `export function formatDistanceToNow(...) {}`.
buildExportMapFromFile only handled ExportNamedDeclaration with a source
(re-exports) or specifiers (local re-bindings) — it silently skipped inline
declarations (export function/class/const/let/var), leaving the name absent
from the export map and causing the plugin to log "skipping: could not
resolve specifier" and leave the barrel import unchanged.

Handle the declaration branch: FunctionDeclaration and ClassDeclaration
contribute their id.name; VariableDeclaration iterates all declarators.
The source for inline declaration entries is the sub-module file itself.

Also extend AstBodyNode.declaration typing from `unknown` to the minimal
shape covering all three declaration kinds.

Add three unit tests covering export function, export const (multiple
declarators), and export class; all 38 tests pass.

* refactor: remove DEBUG-gated logging from optimize-imports plugin

* refactor: always emit skip log without DEBUG env gating

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-03-21 15:18:54 +00:00
James Anderson 70e7553ae0 fix: throw errors when next config file loading fails (#593)
* fix: throw errors when next config parse fails

* add unit test
2026-03-19 18:24:05 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
James Anderson 23f9e1e13d feat: propagate next config serverExternalPackages (#526)
* feat: propagate next config serverExternalPackages

* .
2026-03-13 13:44:06 +00:00
James Anderson 735a443169 chore: upgrade vitest to 4.1 (#512)
* chore: upgrade vitest to 4.1

* fix: clear console.warn spy state before asserting call counts

Vitest 4 changed vi.spyOn to return the same mock when called on an
already-intercepted function. This means console.warn call records from
earlier tests accumulate in the spy, causing toHaveBeenCalledTimes(2) to
see 4 calls instead of 2.

Adding mockClear() after each spy creation resets the call count so the
assertions only reflect calls made within the test itself.
2026-03-13 05:53:39 +00:00
Divanshu Chauhan (divkix) ddf4d337d5 docs/runtime: warn on external rewrites, sanitize Content-Disposition (#418)
* fix: warn on external rewrites and sanitize Content-Disposition

Add a startup warning when next.config contains rewrites targeting
external origins (e.g. https://...), alerting that credential headers
(cookie, authorization, x-api-key, proxy-authorization) are forwarded.

Sanitize contentDispositionType in image optimization and prod server
to only allow "attachment" — any other value defaults to "inline",
preventing header injection via arbitrary config values.

Closes #414

* fix: improve external rewrite warning and add Content-Disposition sanitization test

Use proper singular/plural in the external rewrite warning message
instead of "rewrite(s)". Add test assertions verifying the warning
includes the source pattern and arrow separator. Add a test confirming
that invalid contentDispositionType values default to "inline".

* fix: correct external rewrite warning to reflect actual header forwarding

proxyExternalRequest copies all request headers, not just credential
headers. Updated warning text to accurately state this and suggest
server-side fetch as an alternative. Tightened test assertion for
exact plural form.

* fix: address PR review feedback on external rewrite warning

Clarify why API routes are preferable (control over forwarded headers),
tighten test assertion to prevent false singular/plural match.

Refs: #430

* fix: refine external rewrite warning wording and add source path assertions

Mention route handlers alongside API routes in the warning suggestion,
and assert that source paths (/proxy1, /proxy2, /proxy3) appear in the
multi-rewrite warning message.

* fix: strip credential headers from outbound proxy requests

External rewrites via proxyExternalRequest now strip authorization,
cookie, proxy-authorization, and x-api-key headers before forwarding
to third-party origins. This prevents leaking user credentials to
external services.

Updated the warning message to reflect that credentials are stripped
(not forwarded) and updated tests to assert the new behavior.

Closes #430

* fix: align app-router credential stripping test with implementation

The integration test in app-router.test.ts was missed when c4a128d added
credential header stripping to proxyExternalRequest. Assertions now
correctly expect credentials to be undefined (stripped), matching the
unit test in shims.test.ts and the actual implementation.

* fix: keep Next.js rewrite forwarding

* fix: restore Next.js external rewrite header forwarding
2026-03-11 16:57:28 +00:00
James Anderson 0faf62ac4c feat: support generateBuildId in next.config and inject build ID at runtime (#408)
* feat: support generateBuildId in next.config and inject build ID at runtime

Resolves generateBuildId() from next.config, falling back to a random UUID (ad-blocker-safe) when not configured or when the function returns null. The resolved ID is injected as process.env.__VINEXT_BUILD_ID via Vite define and embedded into __NEXT_DATA__ for Pages Router (dev, prod, and static export). NextRequest gains a buildId accessor for middleware deployment-skew detection.

* regen snaps

* refactor: extract safeUUID helper and drop buildId param from createSSRHandler

Address review nits: deduplicate the UUID+retry loop into a safeUUID() helper,
and read process.env.__VINEXT_BUILD_ID directly in dev-server instead of
threading it as an explicit parameter through createSSRHandler.

* regen snaps

* fix: stabilize entry-templates snapshot by setting generateBuildId in pages-basic fixture

The random UUID from resolveBuildId was embedded in the server entry snapshot,
making it fail on every run. Set generateBuildId to a fixed string in the
pages-basic test fixture so the snapshot is deterministic.

* fix: move buildId to NextURL to match Next.js API surface

Next.js exposes buildId on nextUrl (request.nextUrl.buildId), not directly
on NextRequest. Move the getter to NextURL and add a pass-through on
NextRequest for convenience. Update tests to assert both paths.
2026-03-10 13:59:00 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
Nathan Nguyen cb66cc793c feat(next-intl): auto-detect config without createNextIntlPlugin (#366)
* fix: resolve NEXT_DATA type conflict with next package types

next/dist/client/index.d.ts declares Window.__NEXT_DATA__ as NEXT_DATA,
which conflicts with our inline type in global.d.ts. The vinext-specific
extensions (__vinext, __pageModule, __appModule) aren't in NEXT_DATA,
causing TS2339 errors.

Extract vinext extensions into a VinextNextData interface and cast at
the two usage sites (client/entry.ts, shims/link.tsx) instead of
re-declaring __NEXT_DATA__ with a conflicting type.

* feat(next-intl): auto-detect next-intl config without createNextIntlPlugin

next-intl's createNextIntlPlugin() crashes in vinext because it calls
require('next/package.json') to check the Next.js version, and next
isn't installed. Instead, vinext now detects next-intl in project
dependencies and registers the next-intl/config alias automatically.

Detection probes i18n/request.{ts,tsx,js,jsx} then
src/i18n/request.{ts,tsx,js,jsx}, matching next-intl's own discovery
order. Explicit aliases from webpack/turbopack config take precedence.

Also adds a targeted warning when config loading fails due to
next-intl/plugin, telling users the wrapper is unnecessary.

Closes #202

* fix: address review feedback

- Update check.ts: next-intl status "partial" → "supported"
- Extend VinextNextData from NEXT_DATA in shims/internal/utils.ts
  instead of duplicating fields
- Add JSDoc note about monorepo resolution walk-up behavior
2026-03-09 14:24:41 +00:00
Georgiy Slobodenyuk 8a4929dd63 feat: support experimental.serverActions.bodySizeLimit from next.config (#338)
* feat: support experimental.serverActions.bodySizeLimit from next.config

* fix: address PR review feedback for parseBodySizeLimit

- Accept bare number strings (e.g. "1048576") as bytes, matching Next.js bytes library behavior
- Add tb and pb unit support
- Throw on zero/negative values to match Next.js validation behavior
- Remove unreachable type guard (value is always string at that point)
- Update tests: add tb/pb/bare-number/validation cases, fix "10tb" assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: warn on invalid bodySizeLimit string instead of silently falling back

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:02:14 +00:00
Nathan Nguyen 032a2afb41 fix: preserve next.config plugin aliases for next-intl (#342)
* fix: preserve next.config plugin aliases

* refactor: probe wrapped webpack config once

* fix: tighten next.config alias warnings
2026-03-08 15:30:05 +00:00
Nathan Nguyen d056e50467 fix(config): pass correct Next.js phase to function-form configs + instrumentation test coverage (#273)
* fix(config): pass correct Next.js phase to function-form configs

unwrapConfig() hardcoded "phase-development-server" regardless of
whether vinext was running in dev or build mode. Apps that branch on
the phase argument in next.config.ts received wrong config during
production builds.

Map Vite's env.command to the correct Next.js phase constant:
- "serve" → "phase-development-server"
- "build" → "phase-production-build"

* test: add instrumentation module test coverage

The instrumentation module had zero tests. Add 11 tests covering:

- findInstrumentationFile: file discovery across root and src/,
  priority ordering, null when absent
- runInstrumentation: register() lifecycle, onRequestError handler
  storage, graceful handling of empty modules, error isolation
  when ssrLoadModule fails
- reportRequestError: handler invocation with correct args,
  no-op without handler, error isolation when handler throws

* fix(test): use shared helper for App Router static export test

The static-export test had its own startFixtureServer that loaded the
fixture's vite.config.ts via configFile. This caused RSC plugin timing
issues on Node 25 / macOS — the App Router dev server returned 404 for
all pages. The Pages Router section (same approach) worked because it
doesn't use the RSC plugin.

Switch the App Router section to use the shared startFixtureServerHelper
from helpers.ts, which uses configFile: false with explicit plugin
registration. This matches how app-router.test.ts (193 passing tests)
creates its dev server and avoids the RSC timing issue.

* fix: address PR review feedback

- Import PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
  from shims/constants.ts instead of hardcoding string literals in
  index.ts and config/next-config.ts
- Add afterEach cleanup for temp directories in instrumentation tests

* refactor: use phase constants in tests, simplify pure function test setup

- Replace hardcoded phase strings in next-config.test.ts with
  PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
- Remove unnecessary vi.resetModules() from findInstrumentationFile
  tests — it's a pure function with no module-level state to reset

* refactor: use shared startFixtureServer helper in static export tests

Remove the local startFixtureServer function — the shared helper from
tests/helpers.ts does the same thing. The Pages Router fixture's
vite.config.ts is just `vinext()` with no special options, so the
shared helper (which uses configFile: false + vinext plugin) works
identically.
2026-03-06 16:31:16 +00:00