Commit Graph

1132 Commits

Author SHA1 Message Date
James Anderson 875f314c11 fix(deploy-suite): handle missing optional deps in test fixture configs (#1213)
Three classes of missing-dep build failures in the Next.js deploy harness:

1. `@next/bundle-analyzer` (test/e2e/app-dir/metadata-{font,edge,dynamic-routes})
   These fixtures wrap the require in `if (process.env.ANALYZE)`, but our
   CJS→ESM converter hoisted `require('mod')(args)` to a static top-level
   `import`, which unconditionally tried to resolve the package and failed
   the build even when ANALYZE was unset.

   Fix: rewrite `const X = require('mod')(args)` to an inline
   `(await import('mod')).default(args)` instead of hoisting. This preserves
   CJS lazy semantics — the import only runs when the surrounding code does.

2. `webpack` (test/e2e/app-dir/next-config/next.config.js)
   Top-level `require('webpack').sources.RawSource` — Next.js's own monorepo
   has webpack at the workspace root, but the test fixture's package.json
   doesn't declare it. Inject `webpack@^5.99.0` into the harness-deps block.

3. `ipaddr.js` (test/e2e/type-module-interop, test/e2e/app-dir/app-middleware)
   vinext's image-config shim imports `ipaddr.js`, which is in vinext's
   `dependencies`. But vinext is installed via `file:.vinext-local-package`,
   so pnpm keeps its transitive deps under `.vinext-local-package/node_modules`
   and Node's ESM resolver can't see them from `<test-app>/dist/...` at
   prerender time. Inject `ipaddr.js` into the harness-deps list (spec sourced
   from the vinext catalog).

Affected suites:
- test/e2e/app-dir/metadata-font
- test/e2e/app-dir/metadata-edge
- test/e2e/app-dir/metadata-dynamic-routes
- test/e2e/app-dir/next-config
- test/e2e/type-module-interop
- test/e2e/app-dir/app-middleware (prerender failure)

Verified:
- `bash -n scripts/*.sh`
- `node --check scripts/cjs-to-esm-config.mjs`
- Round-tripped each of the four fixture next.config.js files through the
  patched converter; output is syntactically valid and loads without error
  when ANALYZE is unset (and fails with the expected ERR_MODULE_NOT_FOUND
  when ANALYZE=1, matching CJS lazy semantics).

Refs: workflow run 25870737355
2026-05-14 19:24:06 +00:00
James Anderson f91d478d9f test(link-navigation): sequence link.js and importActual to fix flake (#1218)
The "clicking an RSC Link starts app-router navigation inside a React
transition" test ran link.js's dynamic import and vi.importActual("react")
in parallel via Promise.all. When importActual won the race, "react" got
cached as the actual module before the doMock factory ran, so link.tsx's
transitive `import React from "react"` resolved to the unmocked module.
That made React.startTransition the real implementation rather than the
spy, causing toHaveBeenCalledTimes(1) to occasionally see 0.

Sequencing the imports — load link.js first (with the doMock factory in
effect), then grab React via importActual — eliminates the race.

Refs: https://github.com/cloudflare/vinext/actions/runs/25878626751/job/76052351860
2026-05-14 19:01:55 +00:00
James Anderson 040eef55ae fix(deploy-suite): inject jiti when test app uses TypeScript config files (#1211)
Test apps using `next.config.ts`, `postcss.config.{ts,mts,cts}`, or
`.postcssrc.ts` fail at `vinext build` time with:

  Failed to load PostCSS config: 'tsx' or 'jiti' is required for the
  TypeScript configuration files. Make sure it is installed
  Cannot find package 'jiti' imported from
    .../vite/dist/node/chunks/node.js

Next.js auto-installs one of jiti/tsx as part of its own build pipeline
when it detects a TypeScript config file. Once vinext replaces Next.js
as the deploy target, the test app's package.json no longer lists
either, and Vite's bundled PostCSS / Tailwind / etc. config loaders
fail to load `*.config.ts` files.

Fix: extend the harness-deps injector in scripts/e2e-deploy.sh to scan
the test app root for `*.config.{ts,mts,cts}` files (and the dotfile
`.postcssrc.ts` variants) and inject `jiti` into devDependencies
when present. jiti is the lighter option (no native deps) and matches
what Vite's internal loader prefers.

Pinned to `^2.6.1` to match the version already resolved transitively
in this workspace via `@tailwindcss/node`. Both postcss-load-config and
Vite require `jiti >=1.21.0`; `^2.6.1` satisfies that.

Affected suites (~20 build-time failures in run 25870737355):
- test/e2e/app-dir/next-config-ts/*
- test/e2e/app-dir/next-config-ts-native-mts/*
- test/e2e/app-dir/next-config-ts-native-ts/*
- test/e2e/postcss-config-ts/

Verified locally:
- `bash -n scripts/*.sh` passes
- Manually exercised the embedded Node block against synthetic test
  apps with next.config.ts, postcss.config.mts, .postcssrc.ts, and a
  negative control (next.config.js only). jiti injection fires for all
  three positives and stays out for the negative.

Refs: https://github.com/cloudflare/vinext/actions/runs/25870737355
2026-05-14 18:46:22 +00: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
James Anderson 289ab18b14 fix(deploy-suite): bump pinned sass below ^1.70.0 for Vite 8 (#1214)
The Next.js scss e2e fixtures pin `sass: '1.54.0'` (and some scss-modules
fixtures pin `sass-embedded: '1.75.0'`) via createNextDescribe's
`dependencies` option. Vite 8's built-in `vite:css` preprocessor calls
`sass.initAsyncCompiler()`, an API added in dart-sass 1.70.0 (Feb 2024).
Vite 8 declares `sass@^1.70.0` / `sass-embedded@^1.70.0` as optional
peers, but the fixture's older pin wins during install.

Result: every test/e2e/app-dir/scss/* and test/e2e/app-dir/scss-modules/*
suite fails at vinext build time with:

  TypeError: [sass] sass.initAsyncCompiler is not a function

(see run 25870737355, ~31 deploy-script failures from this cluster).

Fix: in the deploy harness's dependency-injection step, after a test
app's package.json has been augmented with vinext + Vite plugins, check
for a `sass` or `sass-embedded` entry pinned below `^1.70.0` (parsed
from the leading `major.minor` of common semver specs) and rewrite it to
`^1.70.0`. Non-version specs (workspace:, file:, git+, tags) are left
alone. This is a no-op for test apps that don't depend on sass.

Verified locally by running the patched harness against the multi-page
scss fixture: the resolved pnpm path goes from `sass@1.54.0` to
`sass@1.99.0`, and the build advances past the SCSS preprocessor (a
separate JSX-in-Pages-Router error then trips it, tracked as its own
cluster).
2026-05-14 18:32:14 +00:00
James Anderson aa5ec73ada docs(vinext): clarify middleware.ts deprecation warning is non-fatal (#1212)
The deploy-suite investigation in run 25870737355 flagged this warning
as a build-failure root cause. It is not. middleware.ts is still fully
supported in Next.js 16 and in vinext; the message is purely a
deprecation notice (console.warn), and none of the 26 build failures in
the cluster are caused by it. The real failures are unrelated (wasm
imports, missing CSS modules, URLPattern, server-only validation, and
prerender param bugs) and belong to other clusters.

This commit only updates the JSDoc to:

- Explain why the warning may appear 2-3 times per build (Vite calls
  the plugin config hook once per environment).
- Note that Next.js itself emits the same message via Log.warnOnce, and
  point to where parity would live.
- Add an inline comment warning future readers not to change this
  console.warn to a throw / process.exit.

No behaviour change. The warning is intentional and stays a warning.

Refs: https://github.com/cloudflare/vinext/actions/runs/25870737355
2026-05-14 19:31:02 +01:00
James Anderson bc8cee2385 fix(deploy-suite): extract CJS->ESM converter to standalone script (#1209)
* fix(deploy-suite): extract CJS->ESM converter to standalone script

The CJS->ESM next.config converter was inlined into scripts/e2e-deploy.sh
via 'node -e ...' with bash single-quote escapes ('"'"'). The escaping
rebalanced quotes such that later code (a comment containing '(e.g.') sat
outside any single-quoted bash region, causing 'bash -n' to fail with:

  scripts/e2e-deploy.sh: line 490: syntax error near unexpected token `('

The script died before doing anything in every deploy-suite shard, producing
2480 identical 'Custom deploy script failed: undefined (2)' failures and a
misleading 9.5% pass rate (almost all 'passes' were 'should skip next deploy'
no-ops). See run 25869359604.

- Extract the converter to scripts/cjs-to-esm-config.mjs (same regex
  pipeline, no behavioural change).
- Replace the inlined 'node -e' block with a plain 'node ./script.mjs'
  invocation.
- Add a 'bash -n scripts/*.sh' gate to the Check job so a future regression
  here can't ship green.

* Apply suggestion from @james-elicx
2026-05-14 16:04:09 +00:00
James Anderson 843df83de9 ci(deploy-suite): default to all suites and disable test retries (#1208)
The Next.js deploy suite defaulted to only running app router tests with
3 attempts per test (run-tests.js DEFAULT_NUM_RETRIES = 2). Switch the
default suite-filter to 'all' so nightly runs cover pages router too,
and pass --retries 0 so failures surface on the first attempt instead
of masking flakes behind retries.
2026-05-14 15:27:24 +00:00
James Anderson a6d9826613 fix(shims): add runtime default export to next/app (#1204)
* fix(shims): add runtime default export to next/app

Pages Router fixtures using the canonical `pages/_app.js` pattern
(`import App from 'next/app'; export default class MyApp extends App`)
currently fail to build under vinext with:

  [MISSING_EXPORT] "default" is not exported by
  "node_modules/.../vinext/dist/shims/app.js"

The existing shim only re-exported a type as default. This adds a real
runtime class component matching Next.js's behaviour:

- `App` extends React.Component, renders `<Component {...pageProps} />`.
- `App.getInitialProps(appContext)` returns `{ pageProps }`, invoking the
  wrapped page's `getInitialProps` if defined.
- `App.origGetInitialProps` is preserved alongside `getInitialProps`.
- AppProps, AppContext, AppInitialProps types are exported.

Ported from:
  https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx

This unblocks 51 build failures in the Next.js deploy suite that all
trace back to the missing default export.

* fix(shims): expose App class + types in next-shims.d.ts; document null coercion

Address two nits from the PR #1204 bonk review:

1. `next-shims.d.ts` still declared the old minimal `AppProps`-only shape.
   External `.d.ts` consumers (editor, type checks against the published
   package) couldn't see the new runtime class, `AppContext`, or
   `AppInitialProps`. Update the `declare module 'next/app'` block to
   declare the full surface added in the shim:
     - default `class App<P, CP, S>` extending `React.Component` with
       `static origGetInitialProps` / `static getInitialProps` and a
       `render(): React.ReactNode` method
     - `AppContext` and `AppInitialProps<P>` type aliases
     - Expanded `AppProps<P>` shape (router, __N_SSG/__N_SSP flags)

2. `appGetInitialProps` silently coerces a null/undefined return from
   `Component.getInitialProps` to `{}`. Next.js's `loadGetInitialProps`
   throws in that case. Add an inline comment at the coercion site
   noting the divergence, linking to the upstream source, and giving
   future maintainers a bread crumb when they're debugging Pages Router
   pages that render with mysteriously empty props.
v0.0.50
2026-05-14 16:14:44 +01:00
James Anderson 36d2ec560d fix(shims): add withRouter HOC to next/router (#1203)
* fix(shims): add withRouter HOC to next/router

Add the `withRouter` higher-order component export to
`packages/vinext/src/shims/router.ts`, ported from
`packages/next/src/client/with-router.tsx` in Next.js.

`withRouter` is used primarily by class components (which cannot call
hooks) to receive the Pages Router `router` instance as a prop. Real
Next.js apps and the Next.js deploy test suite import it from
`next/router`; without this export, bundlers fail with
`[MISSING_EXPORT] "withRouter" is not exported by ".../shims/router.js"`,
which is currently the cause of 9 build failures in the deploy suite.

The implementation mirrors Next.js's HOC: it calls `useRouter()` and
forwards the router as a `router` prop, forwards `getInitialProps` and
`origGetInitialProps` from the composed component, and sets a
`withRouter(<Name>)` displayName in development.

Also exports the `WithRouterProps` and `ExcludeRouterProps` types
(commonly imported by typed Pages Router apps) and adds matching
`declare module "next/router"` entries.

* fix(shims): match Next.js spread order in withRouter and tidy tests

Address bonk review on PR #1203:

1. (Correctness) Match the Next.js prop-spread order in `withRouter`.
   Next.js's with-router.tsx renders
     <ComposedComponent router={useRouter()} {...props} />
   so the injected `router` is placed FIRST and `{...props}` is
   spread after — a user-passed `router` prop wins by last-spread.
   The previous implementation spread `props` first and then
   re-assigned `router`, which silently swallowed any user-passed
   `router` prop. Now matches Next.js exactly.

   Added a regression test that passes a sentinel `router` prop and
   asserts the user-passed value reaches the composed component.

2. (Cleanup) Extracted the repeated globalThis.window mock and
   vi.resetModules() teardown in the withRouter describe block into
   beforeEach/afterEach. Cuts ~60 lines from tests/shims.test.ts.
2026-05-14 16:13:18 +01:00
James Anderson a713463894 fix(shims): add unstable_cacheLife and unstable_cacheTag to next/cache (#1202)
Next.js exports unstable_cacheLife and unstable_cacheTag as deprecation
aliases that emit a one-time console.error and delegate to the stabilized
cacheLife / cacheTag. Several Next.js apps in the deploy suite still
import the unstable_ names, so vinext apps fail to build with
[MISSING_EXPORT] errors. Match Next.js's behavior exactly: same warning
text, same one-time-per-process gating, same delegation.
2026-05-14 16:12:21 +01:00
James Anderson a39047432d fix: expose window.next global for Next.js parity (#1201)
* fix: expose window.next global for Next.js parity

Next.js publishes window.next from both the Pages Router client bootstrap
(packages/next/src/client/next.ts) and the App Router bootstrap
(packages/next/src/client/app-bootstrap.ts). Userland code and the Next.js
deploy test suite read window.next.router directly (push, replace,
events.on, etc.), and Next.js's own client code reads
window.next.__pendingUrl and window.next.__internal_src_page for App
Router navigation bookkeeping.

vinext did not install this global, which surfaced as ~422 console errors
and 30+ runtime failures in the Next.js deploy test suite, with
"TypeError: Cannot read properties of undefined (reading 'router')" the
most cited.

Adds an installWindowNext helper in client/window-next.ts that both router
bootstraps invoke:

- Pages Router (shims/router.ts) installs window.next.router pointing at
  the Router singleton at module load.
- App Router (server/app-browser-entry.ts) installs
  window.next = { appDir: true, router: appRouterInstance } before
  hydration starts, mirroring app-bootstrap.ts.

The helper merges fields on repeat calls so hybrid Pages+App setups
behave like Next.js (the App Router bootstrap wins for router/appDir,
matching Next.js's load order). __pendingUrl and __internal_src_page
remain undefined until the navigation paths populate them; that matches
Next.js's behavior on initial load.

* fix(window-next): address review — drop unused exports, wire real version, remove module fallback

- Drop `export` from `AppRouterPublicInstance` and `WindowNext` types
  (knip flagged them as unused; they're only used within window-next.ts).
- Drop `getWindowNext()` and its export — tests read `window.next`
  directly, which is the same surface the runtime callers use.
- Wire up `process.env.__NEXT_VERSION` via Vite `define` in index.ts,
  reading the vinext package version from packages/vinext/package.json
  at plugin load. This mirrors Next.js's own DefinePlugin substitution
  (packages/next/src/client/next.ts line 5) so library code that branches
  on `process.env.__NEXT_VERSION` sees a real value and
  `window.next.version` lands the package version instead of the
  `"vinext"` fallback.
- Drop the module-level `installedNext` fallback. `installWindowNext`
  now reads and writes through `window.next` directly so deleting
  `window.next` from tests/userland fully resets state — no stale
  reference can be resurrected on the next call.
2026-05-14 15:08:48 +01:00
James Anderson 3f4e594e1c fix(deploy-suite): create .next/trace before build to avoid ENOENT noise on build failure (#1200)
The Next.js deploy test harness (test/lib/next-modes/base.ts) calls
lstat('.next/trace') during destroy() and throws ENOENT if the file
doesn't exist. vinext doesn't produce a .next/trace (it's a Next.js
telemetry file), so the deploy script creates an empty one as a stub.

That stub was created AFTER 'vinext build' (line ~520), so any run where
the build (or any earlier step like 'pnpm install' / 'vinext init')
failed left .next/trace missing. The test harness then logged ENOENT for
every failed test — 303 lines per deploy-suite workflow run.

Move the stub creation to immediately after the cleanup trap is
installed, before any step that can fail. The cleanup trap runs on EXIT,
so debug artifacts are still preserved; this just ensures .next/trace is
present for the test harness regardless of whether the build succeeded.

Note: VITE_GIT_HOOKS=0 used because vp check --fix errors out when no
JS/TS files are staged (only a .sh file is changed here).
2026-05-14 13:31:08 +00:00
James Anderson 747bb0ec76 ci: add Next.js deploy suite harness (#1189)
* ci: add Next.js Pages Router deploy suite harness

Add the workflow, scripts, and manifest generator for running the
Next.js Pages Router deploy test suite against vinext.

Triggers:
- Nightly cron (06:00 UTC) against main
- Manual dispatch with configurable vinext-ref, next-ref, and concurrency

Extracted from #922.

* refactor: align deploy suite harness with Next.js adapter testing docs

- Rename scripts to match Next.js convention: e2e-deploy.sh, e2e-logs.sh,
  e2e-cleanup.sh
- Split workflow into build + test jobs (build once, shard 16 ways) per
  the documented adapter testing pattern
- Generalize workflow from pages-only to configurable: add suite-filter
  input (pages/app/all, defaults to pages)
- Add --filter flag to manifest generator (replaces the pages-hardcoded
  version)
- Accept ADAPTER_DIR (Next.js docs convention) alongside VINEXT_DIR
- Rename workflow file to nextjs-deploy-suite.yml
- Use matrix.group strings ("1/16") instead of shard integers, matching
  the docs example

* ci: default suite-filter to app

* ci: bump Next.js default to v16.2.6, move cron to 02:00 UTC

* address review: add report job, clean up cache paths, document compat shims

- Add a report job that aggregates .results.json across all shards into
  a GitHub Actions summary (pass/fail/skip counts, failed test details,
  passed suite list) and uploads a JSON report artifact
- Upload test results from every shard (not just on failure)
- Remove redundant next.js/ from cache paths (already under .)
- Add comment explaining why IS_TURBOPACK_TEST=1 is needed
- Clean up stale PID file in e2e-cleanup.sh
- Add header comments to all scripts explaining their purpose and contract
- Document the hardcoded warning messages and .next/trace shim
- Document the YAML catalog parser assumptions
- Fix [[ ]] -> [ ] inconsistency in run-nextjs-deploy-suite.sh

* ci: add temporary PR trigger to test workflow [remove before merge]

* fix: address deploy script failures found in CI and local testing

Bugs found and fixed:

- IMMUTABLE_ASSET_TOKEN marker renamed to NEXT_SUPPORTS_IMMUTABLE_ASSETS
  to match what next-deploy.ts parseIdsFromCliOuput() actually parses.
  Without this, every test fails with 'Failed to get supportsImmutableAssets'.

- Deploy script now injects "type": "module" into test app package.json
  and generates a vite.config.ts. Without type:module, Rolldown emits .mjs
  files but the RSC plugin's cross-environment imports expect .js. This
  mirrors what `vinext init` does (steps 3 and 5).

- Added vp (Vite+) fallback to run_pnpm() for environments where pnpm
  and corepack are not available but vp is.

- Cleanup script now uses process group kills (kill -TERM -PID) and a
  port-based lsof fallback to handle orphaned child processes from
  vp/pnpm exec wrappers.

- Error cleanup trap now dumps the last 80 lines of the build log and
  last 40 lines of the server log to stderr, so failures are visible
  in CI output (previously only showed file sizes).

- Test job uses run-install: false since the workspace is already built
  in the cache from the build job.

Tested locally: deploy script succeeds, curl returns valid HTML, logs
script outputs all three required markers (BUILD_ID, DEPLOYMENT_ID,
NEXT_SUPPORTS_IMMUTABLE_ASSETS), cleanup kills the server.

* fix: use vinext init in deploy script, add next.config.js to CJS rename list

Replace the manual type:module / vite.config.ts / CJS rename logic in
e2e-deploy.sh with a single `vinext init --skip-check --force` call.
This runs after pnpm install (so deps are available) and before
vinext build.

Also add next.config.js to the CJS_CONFIG_FILES list in project.ts.
Without this, vinext init adds type:module but doesn't rename CJS
next.config.js files, causing 'module is not defined in ES module
scope' errors. This was the root cause of the CI failures — Next.js
test fixtures use module.exports in next.config.js.

All 61 init tests pass.

* fix: convert CJS next.config.js to ESM instead of renaming to .cjs

Next.js doesn't support next.config.cjs, so we can't rename it. Instead,
convert module.exports/require() to export default/import in-place after
vinext init adds "type": "module".

Also revert adding next.config.js to CJS_CONFIG_FILES — vinext init
should not rename it since it's not a generic config file.

Tolerate pnpm 10+ ERR_PNPM_IGNORED_BUILDS exit code 1 on install (the
install completes, packages are in node_modules, but pnpm exits non-zero
due to unapproved build scripts). Verify node_modules/vinext exists
as a safety check.

Tested against the real Next.js actions-streaming fixture: CJS
next.config.js converted, app builds, server starts, HTTP 200.

* fix: emit both IMMUTABLE_ASSET_TOKEN and NEXT_SUPPORTS_IMMUTABLE_ASSETS

v16.2.x parses IMMUTABLE_ASSET_TOKEN, canary renamed it to
NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat.

* fix: enable JSX in .js files, convert CJS next.config.ts to ESM

Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files.
Next.js allows JSX in .js files (Babel/SWC handle it transparently),
but Vite 8's built-in vite:oxc plugin excludes .js files by default
(exclude: /\.js$/) AND infers lang: 'js' from the extension (which
disables JSX parsing). Neither can be fixed via config alone.

The plugin runs with enforce: 'pre' before vite:oxc, transforming .js
files with OXC using lang: 'jsx' so JSX is compiled before vite:oxc
sees the code. This unblocks ~60 test suites in the deploy suite.

Also extend the deploy script's CJS-to-ESM config converter to handle
next.config.ts files (not just .js), fixing ~6 more test failures.

Tested: 1284 tests pass (jsx-in-js + shims + routing + app-router),
zero regressions.

* ci: trigger clean workflow run

* fix: improve CJS-to-ESM converter for next.config files

The naive regex converter missed two patterns:
- require('mod')(args) produced invalid ESM (import X from 'mod'(args))
- require() calls inside function bodies were not converted

New converter handles:
- module.exports = X → export default X
- const X = require('mod') → import X from 'mod'
- const X = require('mod')(args) → import _X from 'mod'; const X = _X(args)
- require('mod') in expressions → (await import('mod')).default

Tested against bundle-analyzer, path, and inline require patterns.

* .

* Apply suggestion from @james-elicx

* fix: handle destructured requires in CJS converter, read port from file in cleanup

- Add pattern for const { a, b } = require('mod') → import { a, b } from 'mod'
- Read port from PORT_FILE in cleanup_on_error trap instead of using $PORT
  variable which may be unset if the script fails before port allocation
- Add TODO noting remaining CJS edge cases (dynamic require, require.resolve)

* Apply suggestions from code review

Co-authored-by: James Anderson <james@eli.cx>

* Apply suggestion from @james-elicx
2026-05-14 13:01:01 +00:00
James Anderson 81d5426ee6 fix: enable JSX in plain .js files (Next.js compat) (#1199)
* fix: enable JSX in plain .js files (Next.js compat)

Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files.
Next.js allows JSX in .js files (Babel/SWC handle it transparently),
but Vite 8's built-in vite:oxc plugin excludes .js files by default
(exclude: /\.js$/) AND infers lang: "js" from the extension (which
disables JSX parsing). Neither can be fixed via config alone.

The plugin runs with enforce: "pre" before vite:oxc, transforming .js
files with OXC using lang: "jsx" so JSX is compiled before vite:oxc
sees the code.

Tested: 972 tests pass (jsx-in-js + shims + routing), zero regressions.

* fix: suppress rules-of-hooks lint for Playwright fixture use()

The oxlint-disable-next-line only covered the function signature line,
not the use() call deeper in the function body. Use block-scoped
disable/enable to suppress the entire fixture definition.
2026-05-14 13:49:33 +01:00
Stephen Zhou 9126344cd8 fix: use loader for client reference (#1141)
* fix: split client chunk

* add test

* fix ci deps

* install deps

* use loader

* tweaks
2026-05-14 13:18:58 +01:00
Nathan Nguyen 6cf4148ffd fix(link): keep auto prefetch from fetching dynamic app routes (#1177)
* fix(link): keep auto prefetch from fetching dynamic app routes

App Router Link treated null, undefined, and auto prefetch as the same full RSC prefetch used by prefetch={true}. That violates Next.js's public prefetch contract and over-fetches dynamic route payloads as soon as links enter the viewport.

The broken assumption was that prefetch only needed a boolean enabled state. Split Link prefetch into disabled, auto, and full modes, emit a small browser route manifest, and allow automatic full RSC prefetch only for matched static app routes while preserving explicit full prefetch.

Add focused tests for the mode mapping and generated route manifest.

* test(link): opt interception prefetch fixture into full mode

The interception-context E2E asserts that full RSC prefetch cache keys stay separate by source context. After auto prefetch stops full-fetching dynamic routes, that fixture needs to request the full prefetch mode explicitly.
2026-05-14 09:25:53 +01:00
한만욱 275f8bdd73 fix(shims): add placeholder useRouter bfcacheId (#1192) 2026-05-14 08:21:28 +00:00
Nathan Nguyen e534e9bdc3 fix(app-router): promote mounted parallel slot preservation (#1191)
* fix(app-router): promote mounted parallel slot preservation

Mounted slots were still preserved by treating missing AppElements slot keys as proof during promoted same-root commits. That kept legacy wire absence as semantic authority after layout persistence moved into the planner.

The planner snapshot now carries mounted parallel slot ownership and approves preservation only when the slot owner layout is in the retained same-root layout prefix. Approved commits pass that decision to mergeElements, while unknown-root fallbacks keep the legacy absent-slot path fenced.

* test(app-router): lock traverse slot cleanup precedence

Traverse commits already clear absent slots after copying planner-approved preserved ids. That ordering means a mounted parallel slot can be approved by the planner and still be removed when the traverse payload omits it.

Cover that precedence explicitly so the mounted-slot preservation slice keeps navigation slot preservation and traversal cleanup as separate contracts.

* fix(app-router): align traverse slot preservation metadata

Traverse commits clear absent slots, so a planner proposal should not name mounted slot ids that the visible reducer will remove. That made preserveElementIds semantically wider than the commit contract.

Split current-root commit preservation by lane so traverse commits preserve the retained layout prefix only, while ordinary current-root commits still preserve mounted slots owned by retained layouts. Cover both the pure planner decision and browser-state approval boundary.
2026-05-14 09:21:12 +01:00
MK (fengmk2) 9d5a2e9a77 chore(deps): bump vite-plus to 0.1.21 (#1190)
- Bump vite, vite-plus, vitest catalog entries to 0.1.21
- Refresh pnpm-lock.yaml
- Regenerate AGENTS.md via vp config postinstall
- Drop unused imports (PHASE_PRODUCTION_BUILD, patternToNextFormat) flagged by upgraded lint rules
- Move react-hooks/rules-of-hooks suppression in tests/e2e/fixtures.ts to the use(errors) call site
- Add react/no-children-prop and react/no-danger-with-children suppressions on the intentional precedence test in tests/head.test.ts
2026-05-14 09:14:31 +01:00
Arpit Jain e33feeb946 ci(benchmarks): pin contents: read (#1188)
The benchmarks workflow uploads the artifact via
actions/upload-artifact (writes only to the run's artifact store, not
the repo) and POSTs results to benchmarks.vinext.workers.dev using
secrets.BENCHMARK_UPLOAD_TOKEN. The GitHub token is only used for the
checkout, so contents: read is sufficient. Top-level style matches
the rest of the repo.
2026-05-13 12:10:46 +01:00
Nathan Nguyen ee261e33eb fix(head): preserve dangerous html during client sync (#1180)
* fix(head): preserve dangerous html during client sync

Client-managed next/head tags currently drop dangerouslySetInnerHTML when hydration or navigation rebuilds vinext-owned head nodes. That diverges from Next.js and can leave SSR-injected script, style, or noscript content empty after the client projection runs.

The client path treated raw HTML as unsafe even though the SSR path already accepts the developer opt-in. Reuse one typed prop application helper for client head nodes and set innerHTML from dangerouslySetInnerHTML with the same precedence as Next.js.

Add regression coverage for client raw HTML application, empty __html fallback, and precedence over children.

* fix(head): handle array children and add children-only test coverage

Address review feedback:
- Handle Array.isArray(children) in _applyHeadPropsToElement by joining
  array elements, matching Next.js reactElementToDOM behavior
- Add test for children-only path (no dangerouslySetInnerHTML) to ensure
  the extracted helper didn't regress the common case
- Add test for array children joining

* fix(head): resolve SSR/client parity gap for empty dangerouslySetInnerHTML

Address second review: dangerouslySetInnerHTML: { __html: '' } was
handled inconsistently between SSR and client. The SSR path used a
truthiness check (if (html)) which skipped empty strings, falling
through to children. The client path checked the prop object (truthy),
so empty __html correctly took precedence.

Fix: restructure headChildToHTML to check dangerouslySetInnerHTML before
children using != null, making both paths consistent with Next.js
semantics where dangerouslySetInnerHTML always wins when present.

* fix(head): unify SSR/client content handling via getDangerouslySetInnerHTML

Address third review round:
- SSR: add Array.isArray(children) handling to headChildToHTML, matching
  the client path for cases like createElement('title', null, 'A', 'B')
- Client: replace truthiness check with getDangerouslySetInnerHTML as
  the gatekeeper (matching SSR pattern). This fixes silent content wipes
  for malformed props like dangerouslySetInnerHTML: 'string' or
  dangerouslySetInnerHTML: {} (missing __html key)
- Both paths now use the exact same entry point and condition shape:
  getDangerouslySetInnerHTML() != null
- Update tests: malformed dangerouslySetInnerHTML: {} now correctly
  falls through to children on both SSR and client

---------

Co-authored-by: James <james@eli.cx>
2026-05-13 11:34:32 +01:00
Nathan Nguyen 2d6978f1f9 fix(link): align app-router prefetch scheduling with intent (#1149)
* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* test: tighten link prefetch coverage

* test(link): tighten prefetch decision coverage

* test(link): address prefetch review notes
2026-05-13 11:33:30 +01:00
James Anderson a27193fcaf chore: upgrade pnpm 10 to 11 and enable frozenLockfile (#1185)
* chore: upgrade pnpm 10 to 11 and enable frozenLockfile

- Bump packageManager from pnpm@10.32.1 to pnpm@11.1.1
- Migrate .npmrc settings to pnpm-workspace.yaml (pnpm 11 only reads auth/registry from .npmrc)
  - auto-install-peers=false -> autoInstallPeers: false
  - Remove playground save-exact=true (minor pnpm add preference, not needed for builds)
- Convert onlyBuiltDependencies list to allowBuilds map (pnpm 11 format)
- Add frozenLockfile: true and preferFrozenLockfile: true
- Delete .npmrc files (contained no auth settings)

* fix: handle pnpm 11 strictDepBuilds, preserve autoInstallPeers, remove minimumReleaseAgeExclude

- Add unrs-resolver: false to allowBuilds (reviewed, deny build scripts)
- Keep autoInstallPeers: false to match existing lockfile
- Remove minimumReleaseAgeExclude entirely
- Append allowBuilds to scaffolded project's pnpm-workspace.yaml after
  create-next-app so subsequent pnpm steps don't abort
2026-05-13 09:15:02 +00:00
Nathan Nguyen d85791c2fb feat(app-router): promote segment reset semantics (#1187)
* feat(app-router): promote segment reset semantics

Implements #726-CORE-11/12 from #726.

Add typed App Router segment state-key derivation, wire template/loading/error/access boundaries to semantic reset keys, and preserve pathname fallback semantics for legacy boundary callers.

Cover dynamic params, route groups, catch-all keys, template reset ownership, and boundary reset behavior.

* fix(app-router): treat empty boundary reset keys as absent

Empty segment identities can be produced for root, route-group-only, and default slot paths. Normalize empty reset keys to null so pathname fallback continues to clear caught boundary state.

* fix(app-router): scope route reset keys to branches

Use typed segment state identities and full visible route-state paths for route-wide wrappers so unrelated branches do not collide on the same leaf value.

Add hostile unit and browser tests for dynamic and static branch collisions.
2026-05-13 10:02:02 +01:00
Yoinky3000 493e72d256 fix(app-router): catch redirect diges, render loading.tsx during cross-route navigation and invalid with calling refresh() (#1152)
* fix(app-router): catch server redirect digest, render loading.tsx across navigation

* Update page.tsx

* fix(app-router): tighten RedirectBoundary digest handling and dedupe nav-signal check

Addresses code-review findings on PR #1152:

- RedirectBoundary.reset() now clears both `redirect` and `redirectType`
  so the state never sits in a half-cleared (`null`, "replace") shape.
- getDerivedStateFromError re-throws on malformed `NEXT_REDIRECT;` digests
  (empty URL segment) instead of silently swallowing the error. Mirrors
  the server-side guard in parseNextRedirectDigest (next-error-digest.ts:51).
- Document the `error.handled` branch as a Next.js parity placeholder so
  it's clear it is not dead code, just unused by vinext today.
- Extract `getErrorDigest` / `isNavigationSignalError` into a shared
  utils/navigation-signal.ts, removing the duplicated digest classification
  in shims/error-boundary.tsx and server/dev-error-overlay.tsx.
- Update stale `snapshotRscResponse` reference in the cached-route comment
  to the current body.tee + createFromFetch flow.
- Strengthen the delayed-redirect-under-loading E2E test to also assert
  the loading.tsx fallback paints during the 50ms server delay.
- Format fix on tests/fixtures/app-basic/app/page.tsx that was failing
  the CI Check job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(app-router): invalidate client RSC caches on router.refresh()

router.refresh() previously only re-fetched the current URL via the
"refresh" navigationKind, leaving the visited-response and prefetch
caches untouched for sibling routes. After a session change (e.g.
authClient.signOut() → router.push("/") → router.refresh()), a
subsequent <Link> click to a previously visited auth-gated route
would render the stale cached RSC payload and bypass the server's
redirect() entirely.

Matches Next.js's refresh-reducer.ts, whose header comment is
explicit: "During a refresh, we invalidate the segment cache but not
the route cache. The segment cache contains the actual RSC data
which needs to be re-fetched." Next does this via an O(1) version
counter (invalidateSegmentCacheEntries → currentSegmentCacheVersion++);
vinext's bounded LRU is small enough that Map.clear() on the
visited-response + prefetch caches is equivalent in observable
behavior.

Pre-existing in upstream vinext (verified against 8edc010); only
surfaced after the redirect-digest fix earlier in this PR, because
before that the unhandled NEXT_REDIRECT digest aborted the page
render outright and masked the stale-cache reuse.

- Expose clearClientNavigationCaches as window.__VINEXT_CLEAR_NAV_CACHES__
- router.refresh() calls it synchronously before scheduling the RSC
  re-fetch inside React.startTransition. Synchronous ordering matters:
  if the clear ran inside the transition, prefetches kicked off during
  the transition's renders could repopulate stale entries into the
  just-cleared cache.
- Unit test pins the clear-before-navigate ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(app-router): address review — remove HandleRedirect reset, fix boundary nesting, add malformed digest test

Address ask-bonk review comments from all three rounds:

1. Remove reset() from HandleRedirect — calling reset() inside startTransition
   clears the boundary state, re-rendering children and re-mounting the page
   that threw redirect(). For deterministic redirects (auth guards) this
   creates an infinite loop. Next.js intentionally does not call reset() in
   HandleRedirect (redirect-boundary.tsx); the boundary stays in its 'caught'
   state rendering null until router.push()/replace() mounts fresh children.

2. Move RedirectBoundary inside Suspense (loading boundary) — Next.js nesting
   is Error > Loading > AccessFallback > Redirect > content. The previous
   placement had RedirectBoundary outside Suspense, which unmounted the
   loading fallback during redirect transitions. Now RedirectBoundary wraps
   the page slot directly, and Suspense wraps both.

3. Add comment documenting dual-format digest parsing — the parts.length >= 5
   branching handles both vinext 3/4-part and Next.js 5-part digests, which
   was non-obvious to reviewers.

4. Add test for malformed-digest re-throw guard — verifies that
   getDerivedStateFromError re-throws when the URL segment is empty
   (e.g. NEXT_REDIRECT;push;) rather than silently swallowing the error.

* fix(app-router): correct nesting-order comment, add error.handled test

Address ask-bonk round-4 review:

1. Fix the boundary nesting comment in app-page-route-wiring.tsx — it
   incorrectly claimed the order matches Next.js's
   Error > Loading > AccessFallback > Redirect > content. The actual
   vinext order is Error > AccessFallback > Loading > Redirect > content;
   the AccessFallback positioning is a pre-existing divergence.

2. Add test for the error.handled branch in RedirectErrorBoundary —
   verifies the Next.js parity placeholder returns null state when a
   redirect error is already marked as handled by an outer boundary.

* refactor: use shared header constants instead of string literals

Replace two hardcoded header strings in the visited-response cache
snapshot with the already-imported VINEXT_MOUNTED_SLOTS_HEADER and
VINEXT_PARAMS_HEADER constants from server/headers.ts.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-05-12 16:48:15 +00:00
James Anderson 996d9b4f3d refactor: extract internal HTTP header names into shared constants (#1184)
* refactor: extract internal HTTP header names into shared constants

Internal header name strings (x-vinext-*, x-middleware-*, x-action-*,
RSC protocol headers, etc.) were scattered across ~30 files as raw
string literals. This made typos possible and renames difficult.

Add packages/vinext/src/server/headers.ts as the single source of
truth for all ~40 internal header name constants, organized into
categories: vinext-proprietary, RSC protocol, Next.js compat,
server action response, middleware protocol, flight headers, and
the INTERNAL_HEADERS security blocklist.

Update all consumers to import from the shared module. Existing
re-exports (VINEXT_RSC_MOUNTED_SLOTS_HEADER, VINEXT_RSC_RENDER_MODE_HEADER
from app-rsc-cache-busting.ts; FLIGHT_HEADERS from app-middleware.ts;
INTERNAL_HEADERS from request-pipeline.ts) are preserved for backward
compatibility.

* refactor: un-export header constants only used in INTERNAL_HEADERS array

knip flagged 6 constants as unused exports. They are only referenced
within the same file by the INTERNAL_HEADERS array, so drop the export
keyword and keep them as module-scoped const declarations.

* refactor: address PR review comments

- server-action-not-found.ts: use rename-import instead of intermediate
  const alias (import { X as Y } pattern)
- request-pipeline.ts: use 'export { INTERNAL_HEADERS } from' re-export
  pattern instead of aliased import + re-assignment, matching the style
  used by other re-exports in this PR

* refactor: import VINEXT_RSC_RENDER_MODE_HEADER from headers.js directly

Move the import in app-rsc-request-normalization.ts from the re-export
in app-rsc-cache-busting.js to the canonical headers.js module.

* refactor: migrate shims/link.tsx to import from headers.js directly

Move VINEXT_MOUNTED_SLOTS_HEADER import from the deprecated re-export
in app-rsc-cache-busting.ts to the canonical headers.ts module. This
was the last production consumer of the VINEXT_RSC_MOUNTED_SLOTS_HEADER
alias, so remove that deprecated export entirely.
2026-05-12 17:17:10 +01:00
dependabot[bot] e9d1749b95 chore(deps): bump next from 16.2.5 to 16.2.6 (#1183)
* bump nextjs

* .

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 14:18:00 +00:00
Nathan Nguyen ac75d43e7e fix(app-router): preserve UI during action refresh (#1147)
* fix(app-router): preserve UI during action refresh

Server action responses currently always render a page payload, and router.refresh can put BrowserRoot into a pending state that mounts loading.tsx while the same route refresh resolves. That diverges from Next.js, where non-revalidating actions can skip page rendering and refresh/revalidation should preserve the current page UI.

The server action path now omits the root payload when no data was invalidated, suppresses loading boundaries for action rerenders, and carries the same suppress-loading variant through RSC request, cache-busting, and ISR cache keys. The browser action callback now resolves no-root action payloads directly and avoids pending router state for refresh.

Regression coverage includes the no-rerender action payload, loading-boundary suppression, cache key variants, cache-busting compatibility, request normalization, and an app-router Playwright case that catches route loading mounts after action plus router.refresh.

* fix(app-router): carry semantic RSC render modes

Replace the cross-layer suppress-loading boolean with AppRscRenderMode so browser requests, request normalization, rendering, cache-busting, Vary, and ISR keys all carry the same semantic payload variant.

Decode server action responses before clearing client navigation caches so no-root return-only action results keep visited response and prefetch caches intact while redirects, root payloads, and legacy bare payloads still invalidate.

Add coverage for refresh() inside server actions and for normal versus preserve-current-UI ISR RSC cache keys.

* fix(app-router): default helper render mode to navigation

* docs: add review-requested comments for structural discriminator and ISR key format

* fix: add explicit null case to parseAppRscRenderMode switch for exhaustiveness check

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 15:09:45 +01:00
Nathan Nguyen 11cc64a3af fix(app-router): preserve useActionState form state after hydration (#1162)
* fix(app-router): preserve useActionState form state after hydration

Progressive multipart server actions executed before the initial HTML render were dropping React form state. That meant useActionState hydrated from the JavaScript initializer instead of the server action result after a no-JS form submission.

The progressive action path now decodes React form state, carries it through the App Router render pipeline, passes it to React's server and client hydration APIs, and marks form-state HTML responses as uncacheable so action results cannot be replaced by stale ISR HTML.

Adds targeted unit coverage for action decoding, SSR forwarding, cache policy, cache read bypass, and a Playwright no-JS useActionState regression.

* fix(app-router): keep progressive action result type internal

Knip reported ProgressiveServerActionResult as an unused exported type. The type is only part of this module's implementation detail for the progressive action return path, so keep it module-local.

* fix(app-router): separate progressive action cache bypass

* chore(app-router): tighten form state bootstrap wiring
2026-05-12 14:25:30 +01:00
dependabot[bot] 477411ae1b chore(deps): bump next-intl from 4.9.1 to 4.9.2 (#1160)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.9.1 to 4.9.2.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.9.1...v4.9.2)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 13:24:21 +00:00
Nathan Nguyen 04140e47a3 feat(app-router): promote same-layout ancestor persistence (#1167)
* feat(app-router): promote same-layout ancestor persistence

App Router soft commits previously preserved absent non-slot entries through the merge layer and kept root-layout hard-navigation policy split between browser state and planner. That made same-layout ancestor persistence implicit and allowed unknown root identity to behave like proof of reuse.

The planner now returns explicit preserveElementIds for proven same-root transitions, browser state stores layoutIds from wire metadata, and visible commits preserve only planner-approved ancestors while leaving parallel-slot persistence semantics intact.

Adds coverage for layout id metadata, unknown-root fallback, stale element removal, and visible layout persistence.

* fix(app-router): validate layout persistence proofs

App Router payload metadata treated every string in __layoutIds as planner preservation authority. That let a bad producer, stale payload, or future refactor pass a page, slot, or malformed key as proof for layout ancestor reuse.

The violated invariant is that layout persistence proof must be validated before it reaches the commit planner. Parse __layoutIds through the AppElements wire-key codec and accept only layout keys.

Also add dynamic segment browser coverage for /blog/[slug] navigation, proving param-sensitive layout and page content update while the layout client counter persists.

* fix(app-router): scope boundary layout proofs

HTTP access fallback payloads derived layout metadata from the full route definition. Layout-level access fallbacks can render only ancestor layouts, so the old proof could let the planner preserve child layouts that were absent from the response.

Derive boundary layout metadata from the rendered layout module subset and mark the root boundary unknown when no route layout is rendered.

* docs(app-router): document layout id ordering

Document the ancestor-first layoutIds invariant at the planner snapshot boundary so future payload producers do not break same-layout prefix persistence.
2026-05-12 14:19:21 +01:00
Nathan Nguyen 682b3f15d7 fix(dynamic): preserve noSSR initial delay state (#1171)
dynamic() currently renders the ssr:false server loading component with pastDelay=true. That disagrees with Next.js noSSR behavior and makes loading components that suppress UI before pastDelay render the opposite state during SSR.

The ssr:false server placeholder now passes pastDelay=false while the existing SSR-enabled and client Suspense fallback paths keep their current loading props. Tests cover both the raw prop contract and the observable null-until-delay loading behavior.
2026-05-12 14:09:58 +01:00
Nathan Nguyen 2e6d2b75c7 fix(app-router): preserve omitted redirect type context (#1172)
redirect() without an explicit type currently reaches shared digest parsing as an empty sentinel, but the parser resolves that sentinel to push before non-action callers can apply their own default. That makes SSR and route-handler redirect handling inherit the Server Action history behavior.

Preserve the omitted type as null at the digest boundary and resolve it to push only when packaging Server Action redirects. Add focused parser coverage for omitted, explicit, and non-empty raw type segments.
2026-05-12 14:08:37 +01:00
Nathan Nguyen 142f128f72 fix(headers): make draft mode status live (#1174)
* fix(headers): make draft mode status live

draftMode().isEnabled captured the initial cookie state when the request API object was created. That diverged from Next.js, where DraftMode.isEnabled is a getter over mutable provider state, so reading the same object after enable() or disable() reflects the updated draft mode status.

Keep vinext's existing request cookie map as the draft mode provider and expose isEnabled as a getter over that map. Add focused regression tests for same-object enable and disable reads.

* remove unnecessary headersContext local const — state is ALS-isolated per request

* merge duplicate draftMode isEnabled tests into existing enable/disable tests

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 13:07:38 +00:00
Nathan Nguyen d602a8fe6c fix: handle basePath hash-only page navigations (#1173) 2026-05-12 14:06:15 +01:00
Nathan Nguyen e3e4a05175 fix(app-router): preserve config headers on middleware redirects (#1175)
App Router middleware redirects skipped next.config.js headers because config header application only happened during final response finalization. Redirect responses bypass that finalizer, so matching config headers disappeared whenever middleware short-circuited with a redirect.

Apply the matched config header layer specifically for middleware redirects using the pre-middleware request context, then merge the redirect response over it so middleware headers keep precedence. Non-redirect middleware responses continue through finalization, with regression coverage to prevent duplicate additive headers.
2026-05-12 13:55:29 +01:00
Nathan Nguyen 08da5c32e9 fix(app-router): error truncated progressive RSC streams (#1176) 2026-05-12 13:53:04 +01:00
Nathan Nguyen 4b3af442a1 fix(dynamic): accept Next.js dynamic overload forms (#1179)
vinext previously treated the first next/dynamic argument as a callable loader. That broke valid Next.js call shapes such as a direct import promise and an options object with loader because React.lazy eventually tried to call a non-function value.

The shim now normalizes the public dynamic input before rendering: promises become loader functions, function loaders stay unchanged, and options objects merge with the second options argument. Focused tests cover the legacy promise form and the options-object loader form against SSR output.
2026-05-12 13:51:09 +01:00
Deepam Goyal 97f878ac94 fix: remove duplicate next-shims.d.ts from fixtures (#1170)
* fix: remove duplicate next-shims.d.ts from fixtures

Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com>

* fix: use type intersections

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* fix: remove duplicate next-shims.d.ts from examples

Apply the same consolidation from fixtures to examples/realworld-api-rest
and examples/pages-router-cloudflare. Point their tsconfigs at the shared
packages/vinext/src/shims/next-shims.d.ts instead of keeping local copies.

* fix: declare Link value before default export in next/link shim

The deleted fixture copies all had `const Link: ComponentType<LinkProps>`
before the default export. Without it the default import resolves to `any`
(masked by skipLibCheck). Add the missing value declaration.

---------

Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-05-12 11:24:55 +00:00
Nathan Nguyen 826bab4530 feat: implement next web vitals hook (#1161) 2026-05-11 17:15:12 +01:00
Nathan Nguyen 07be32258f fix(router): preserve filesystem routes before afterFiles rewrites (#1166)
* fix(router): preserve filesystem routes before afterFiles rewrites

afterFiles rewrites were evaluated before App and Pages filesystem route matches in several runtime paths. That let a rewrite override an existing non-dynamic page, which diverges from Next.js route ordering.

The fix checks the page/app match first and only applies afterFiles rewrites when no non-dynamic route wins, while still allowing afterFiles to run before dynamic routes. Regression coverage exercises App RSC handler, Pages dev/prod, and generated Worker wiring.

* test(router): align chained afterFiles rewrite expectations

The chained rewrite fixture expected an afterFiles rewrite to override a concrete /intermediate page. That is the route-ordering behavior this branch fixes.

Update the fixture to cover both intended contracts: middleware can chain into afterFiles when no page file wins, and concrete page files are not overridden by afterFiles rewrites.

* refactor(router): remove route-ordering type assertions

Validate generated Pages route metadata at the boundary instead of asserting its shape, and make the app handler test fixture route matching explicit.

* test(router): cover afterFiles order in Pages Worker

Add a built Cloudflare Pages Router regression where a concrete page route must win before an afterFiles rewrite. Align custom Pages Worker entries with the generated worker route-match gate.
2026-05-11 17:13:51 +01:00
Nathan Nguyen eb3295b8c4 fix(dynamic): pass complete loading props (#1163)
vinext only passed isLoading, pastDelay, and error to next/dynamic loading components. Next.js includes retry and timedOut, so loading UIs that destructure those props observed an incompatible shape. The shim assumed the loading prop contract was limited to the values it currently used.

This aligns the runtime and ambient shim declarations with Next.js by passing the full loading props shape, preserving current delay defaults, and wiring client retry through a replaceable lazy component. The regression test covers the SSR loading object shape.
2026-05-11 16:51:52 +01:00
Nathan Nguyen a75ec7c3f8 fix(app-router): append RSC vary headers in finalizer (#1165)
App Router responses can vary by RSC navigation headers. Route handlers returned custom responses through the outer finalizer without the shared RSC Vary set, so caches could reuse response variants across navigation modes.

The finalizer now merges the shared VINEXT_RSC_VARY_HEADER for non-redirect, non-static-file App Router responses. Existing page helpers that already set it are deduped by the shared Vary merge helper.

Tests cover route handler finalization, custom Vary preservation, duplicate avoidance, wildcard Vary, and static-file signal exclusion.
2026-05-11 16:45:26 +01:00
Nathan Nguyen 71acce4243 fix(headers): reuse request API promise identity (#1164)
headers() and cookies() decorated a fresh Promise on every call even when they returned the same request headers or cookie store. React.use() tracks thenables by identity, so repeated calls in one request could look like uncached request API promises.

Cache the decorated thenable by the sealed request API object, matching Next.js's WeakMap-shaped contract while preserving vinext's existing middleware invalidation path. Add regression coverage for stable same-context identity and fresh identities after middleware request header overrides.
2026-05-11 16:45:19 +01:00
Nathan Nguyen 77c4d9ce84 feat(app-router): plan root-boundary navigation decisions (#1114)
* feat(app-router): plan root-boundary navigation decisions

* test(app-router): cover unknown visible root planning

* test(app-router): cover stale root-boundary precedence

* fix(app-router): thread target href into navigation approval

Root-boundary hard-navigation planning could derive its decision URL from the pending route snapshot even though the browser executor navigated to the controller target href. That left planner traces able to disagree with the actual hard navigation target.

Require targetHref at visible commit approval boundaries, pass the controller target through navigation and same-url server-action approvals, and record it in lifecycle traces. The focused app-browser tests now assert the planner trace carries the executor URL for hard navigation decisions.

* docs(app-router): note route manifest planner follow-up

The planner input already carries routeManifest for the issue 726 route-graph slices, but CORE-07/08 always passes null and does not read it. Without a comment, the reserved field looks accidentally unused.

Document that routeManifest becomes non-null in CORE-09 and loosen the disposition test helper so render identity can diverge from navigation identity in future coverage.
2026-05-11 13:30:45 +01:00
Divanshu Chauhan (divkix) 3a9046dce2 fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections (#1153)
* fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections

Ports Next.js commit 5452439f3db2a78967178ca4180b27fb48393a19 (PR #91686)
to vinext's next/image shim and server-side image config.

Changes:
- Add isPrivateIp() helper in shims/image-config.ts (IPv4 + IPv6, no deps)
- Update validateRemoteUrl in shims/image.tsx to reject literal private-IP
  hostnames unless dangerouslyAllowLocalIP is true
- Wire dangerouslyAllowLocalIP through next-config type, Vite define,
  global.d.ts, and server-side ImageConfig
- Add unit tests for isPrivateIp and component-level private-IP guard

Closes #1065

* fix(image): use ipaddr.js for correct private-IP classification

Addresses review feedback on the SSRF guard:
- Replaces the hand-rolled isPrivateIp with ipaddr.js (same library Next.js
  uses), eliminating bugs in IPv4/IPv6 range checks
- Fixes incorrect 2001:2f::/32 check (not a real range) and adds all missing
  non-unicast ranges: CGNAT, multicast, reserved, benchmarking, teredo,
  documentation, discard, NAT64
- Correctly handles ::ffff: IPv4-mapped addresses via isIPv4MappedAddress()
- Adds explanatory comments for best-effort client-side guard and
  dangerouslyAllowLocalIP server-side config
- Adds comments in tests explaining the vi.resetModules() pattern

* fix(image): externalize ipaddr.js to prevent SSR crash

ipaddr.js is a CJS UMD module that assigns to `this.ipaddr` at the
bottom of its wrapper. When Vite's SSR module runner loads it with
`noExternal: true`, the UMD wrapper executes with `this = undefined`
(strict mode in ESM context), producing:

  TypeError: Cannot set properties of undefined (setting 'ipaddr')

This crashed every page render that imports `next/image` (via the
image-config.ts shim). The image-optimization-parity tests failed
with 500 errors.

Fix: add `ipaddr.js` to `ssr.external` in all 3 Vite environment
configs (top-level, RSC, SSR) so Node's native loader handles the
CJS module correctly.
2026-05-11 13:21:02 +01:00
James Anderson 0c4e528554 chore: build vinext on postinstall (#1159) 2026-05-11 09:46:31 +01:00
Nathan Nguyen 207246320e test: harden ecosystem fixture startup (#1146)
Co-authored-by: James <james@eli.cx>
2026-05-11 07:23:14 +00:00
Nathan Nguyen a0737e971b feat(router): expose RouteManifest default and segment boundary facts (#1136)
* feat(router): expose RouteManifest default and segment boundary facts

RouteManifest slot facts only exposed whether a default existed, and boundary facts were limited to layout-aligned outcomes. That left later #726 planner work without graph-owned IDs for default-rendering slot bindings or layoutless route-group error segments.

Add explicit default entries keyed by semantic slot ID, thread default IDs through slot declarations and default-rendering bindings, and materialize segment-level error boundaries even when no sibling layout owns that segment.

Validation: vp test run tests/app-route-graph.test.ts; vp test run tests/app-route-graph.test.ts tests/routing.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts; vp run vinext#build.

* ci: rerun flaked checks

* ci: rerun flaked checks again

* fix(router): avoid duplicate RouteManifest error boundary writes

RouteManifest now exposes layoutless segment error boundaries, but the segment pass also revisited layout-bearing error.tsx files that the layout pass had already written.

Skip segment error entries with an owning layout so layout-owned boundaries remain single-sourced, while layoutless route-group boundaries still materialize as ownerless facts.

Validation: vp test run tests/app-route-graph.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts.
2026-05-11 08:21:22 +01:00