* fix(pages): render masked basePath error routes
Pages Router route misses under basePath returned generic HTML and masked client navigations to /404 or /_error could fetch the visible as-path instead of the error route. This diverged from Next.js when applications used next/router to preserve a friendly URL while rendering the error component.
The implementation assumed the browser URL and the component route could always collapse to one fetch target. Preserve the masked history URL, fetch the Pages error route as the HTML target, allow its 404 response to hydrate, and render pages/404 for production route misses with the correct status and client module metadata.
* test(pages): keep error navigation coverage out of vitest browser
Vitest integration CI does not install Playwright browsers, so launching Chromium from tests/pages-router.test.ts made the PR fail deterministically even though the upstream deploy harness passed.
Keep the browser-level verification in the upstream deploy-suite run and cover vinext locally at the lower boundaries: production 404 rendering through HTTP and Pages Router masked error-route navigation through the router shim. The generated Pages entry now also shares one SSR manifest module lookup helper between asset tag collection and client module URL resolution.
* fix(prerender): keep custom 404 out of generic page loop
* fix(pages): fall back to _error on route misses
* fix(pages): document the catch-all guard on matchRoute("/404")
The pattern check prevents a dynamic catch-all route (e.g. [...slug]) from
being used as the 404 fallback. Without it, matchRoute can return a
catch-all match and silently hijack the error rendering path.
* fix(pages): preserve error navigation rewrite order
* fix(pages): limit fallback rewrites to route misses
* fix(deploy): skip deferred error rendering for data requests
* fix(pages): preserve ISR response status
* fix(pages): keep module metadata in ISR regeneration
* fix(app-router): preserve history index on external state writes
External history.pushState and history.replaceState calls preserved vinext interception metadata but dropped the traversal index. That left userland or library shallow history entries without the metadata App Router traversal uses to classify and anchor back/forward state.
createExternalHistoryStatePreservingMetadata now preserves vinext navigation metadata as one unit from the current history entry. The shim regression covers object, null, undefined, and index-only external state writes.
* test(app-router): seed stale history context through unpatched state
Public History API calls now sanitize vinext metadata before delegating. The refresh regression test was seeding stale intercepted context through that public wrapper, so it tested the wrapper boundary instead of the stale internal-state condition.
Use the saved original replaceState handle to create the same framework-owned stale state the test is meant to refresh away.
* fix(draft-mode): keep draft secret out of client defines
The draft-mode bypass token was emitted through Vite define, so Vite's client env payload and client chunks could contain the server-only cookie secret when app code referenced that env key.
The violated boundary was treating a server runtime credential like a shared compile-time define. Generate one plugin-scoped secret and initialize server entries with it, while next/headers reads only internal server state.
Adds regression coverage for dev env exposure, production client bundle leakage, and the built server draft cookie round trip.
* test(draft-mode): initialize secret after module reset
* fix(draft-mode): scope draft secret to request context
Draft mode validation briefly lived in a package-global singleton after removing the client-visible define. That made multiple app instances in the same process able to overwrite each other's validator.
Thread the generated app secret through the App Router request and dispatch boundaries, store it on HeadersContext, and make cache bypass validation a pure request-plus-secret check. Add a regression that proves two contexts keep independent draft validators.
* fix(draft-mode): guard stale draft controls
* fix(font): preserve local font binding family
Local next/font/local calls currently rewrite font asset paths but drop the caller binding name that Next.js passes to its local font loader. App Router layouts still receive a generated font class and preload the font file, but computed font-family resolves to __local_font_N instead of the binding name observed by Next.js.
The root cause was that vinext's local font transform only handled asset URLs while the runtime shim always invented a private family name. Thread a private _vinext.font.family payload from const-bound localFont calls into the shim, validate it at the boundary, and fall back to the generated family for dynamic or invalid input.
Regression coverage ports the mdx-font-preload behavior at the transform and runtime boundaries so the layout class resolves to myFont while existing preload/path rewriting stays covered.
* fix(font): separate local font injection identity
Review found that the transform-provided local font family can repeat across modules when different files use the same binding name, such as const myFont = localFont(...).
Keep that binding name as the visible CSS font-family, but key @font-face injection by the generated class name so distinct localFont calls cannot suppress each other's source rules. Add a focused regression for repeated transformed binding names.
* test(font): cover invalid local family payloads
Address review feedback on the local font transform follow-up.
Simplify the balanced call guard, pin the MDX provenance links to the Next.js v16.2.6 source used for this port, and add coverage proving invalid internal family payloads fall back to the generated __local_font_N family without leaking CSS injection text.
* fix(font): harden internal family payloads
Address the latest review pass on local font family handling.
Reject non-string private family payloads before regex validation, cover the stateful toString case, and make binding-family injection tolerant of same-line block formatting so it stays aligned with the existing path rewrite behavior.
Repeated vinext builds currently leave stale hashed server chunks in dist/server. Workers deployments with no_bundle upload every matched JS module, so stale chunks count against Worker size limits.
The CLI assumed Vite's per-environment emptyOutDir would remove old output, but the multi-environment build and hybrid Pages Router pass preserve dist/server artifacts.
Clean the vinext dist output once immediately before the Vite build starts, while preserving build.emptyOutDir: false as the escape hatch. Add focused tests for default cleaning, explicit preservation, and outside-root safety.
* 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.
* fix: resolve relative server action redirects
Server action redirects could carry dot-relative locations from the action response. Treating those locations as raw browser redirects resolved them from the current document URL and forced a reload, so nested App Router actions could land on the wrong route or lose client state.
Resolve server action redirect locations against the action initiation pathname using Next.js directory-style semantics, keep same-origin redirects on the App Router navigation path, and hard-navigate only for external destinations or parse fallback.
Adds browser-level coverage ported from the Next.js relative server action redirect test plus focused helper assertions for the URL resolution contract.
* chore: rerun ci
* chore: rerun ci again
Extends #1565: the merged change gated the marker on the resolved segment
config, but only covered the two `buildAppPage*Response` builders. Five
other response construction paths bypassed those helpers and skipped the
header for edge-runtime routes:
- `app-page-cache.ts` — ISR HIT/STALE responses
- `app-server-action-execution.ts` — server action RSC responses
(redirect, no-revalidate, re-render)
- `app-page-boundary.ts` — boundary RSC responses
- `app-page-execution.ts` — 200 + flight-payload redirect responses
- `app-page-stream.ts` — HTML stream responses used by the boundary path
Centralizes the gating in a single `applyEdgeRuntimeHeader` helper so
every site shares one source of truth for header name + condition, and
threads `isEdgeRuntime` through the fallback renderer + dispatch options
so each construction site receives it.
Adds positive + negative coverage for each newly-covered path.
* fix(pages-router): pass revalidateReason to getStaticProps context
Pages Router `getStaticProps` now receives `context.revalidateReason`
matching Next.js: `'build'` during build-time prerendering,
`'on-demand'` when the dispatch is signalled as an explicit revalidation,
and `'stale'` for stale-while-revalidate background regeneration and
runtime cache-miss requests.
Closes#1462
* fix(pages-router): default gSSP Cache-Control in prod + JSON data paths
Mirrors the dev-server branch added earlier in this PR. Production HTML
gSSP responses (renderPagesPageResponse) and the _next/data JSON envelope
(pages-server-entry isDataReq) now default to
"private, no-cache, no-store, max-age=0, must-revalidate" when the user
did not set Cache-Control via res.setHeader, matching Next.js's
pages-handler.ts (revalidate: 0 → getCacheControlHeader). Closes the
dev/prod parity gap flagged by bigbonk.
Also fixes the broken `gsspRes.headers` access in the isDataReq path
(should be `getHeaders()`) so user-set headers actually propagate, and
notes that isOnDemandRevalidate is forward-looking plumbing (no
caller sets it yet — res.revalidate() isn't implemented).
* test(pages-router): fix misaligned continuation comment
Drop the extra leading space on the JSDoc continuation lines flagged by
bigbonk so the block aligns with the surrounding tests.
* fix(404): align default not-found copy with Next.js
The Pages Router previously emitted `<h1>404 - Page not found</h1>` and the
App Router emitted plain-text `This page could not be found` (no period) when
no user-defined 404 boundary was present. Next.js renders
`This page could not be found.` (with trailing period) for both routers.
Closes#1454.
* fix(404): address bot review — charset attribute and h1 line-height
- Use HTML attribute name 'charset' (not the JSX 'charSet') in the raw HTML template.
- Drop stray 'line-height:49px' on the Pages Router h1 — Next.js's _error.tsx inherits 48px from the parent div.
* fix(app-router): emit x-edge-runtime marker on edge RSC route response
Closes#1531
The Next.js edge-runtime app template (edge-ssr-app.ts) sets
`x-edge-runtime: 1` on every App Router response. vinext serves App
Router responses from a Cloudflare Worker (an edge-equivalent runtime)
but was not emitting this marker, so Next.js' upstream test suite
(`test/e2e/app-dir/app/index.test.ts`) saw `null` for the header.
Add the header to both the RSC and HTML response builders so the
assertion passes for any App Router page, regardless of which payload
shape the client requests.
* fix(app-router): gate x-edge-runtime marker on configured runtime
Only emit `x-edge-runtime: 1` when the route's resolved segment config
opts into the edge runtime (`export const runtime = "edge"` or
`"experimental-edge"`). Plumb the resolved runtime from the segment
config through dispatch and the render lifecycle so both RSC and HTML
response builders see it.
Without the gate, nodejs-runtime routes also advertise the marker, which
hides the configured runtime from downstream consumers.
* fix(image): emit /_next/image URLs to match Next.js
Closes#1513
The default image loader and optimization endpoint switched from the
vinext-specific /_vinext/image path to Next.js's canonical /_next/image.
This unblocks the deploy suite tests that import Next.js's expected
URL shape (/_next/image?url=...&w=...&q=...).
* refactor(image): use IMAGE_OPTIMIZATION_PATH constant at remaining call sites
Replace hardcoded "/_next/image" strings in index.ts, app-rsc-handler.ts,
and the generated worker entry templates in deploy.ts with the
IMAGE_OPTIMIZATION_PATH constant from server/image-optimization, matching
the pattern already used in prod-server.ts. Prevents future drift if the
path ever changes again.
* feat(image): accept both /_next/image and /_vinext/image at the optimizer
Add a VINEXT_IMAGE_OPTIMIZATION_PATH constant and an
isImageOptimizationPath() helper, then route through every match site
(prod-server, dev server passthrough, app RSC handler, generated worker
templates, and shipped example workers). Apps that wire image URLs to
either prefix now hit the same handler; new URLs are still emitted via
IMAGE_OPTIMIZATION_PATH.
* fix(pages-router): await Promise-shaped gssp props
Next.js explicitly supports a Promise value for `getServerSideProps`
`props`. Pages returning `{ props: Promise.resolve(...) }` previously
rendered empty pageProps because vinext passed the unresolved Promise
straight through to the serializer. Match Next.js' `render.tsx` by
detecting a thenable `props` and awaiting it before continuing.
The fix is applied symmetrically to the dev server gssp dispatch
(`dev-server.ts`) and the prod page-data dispatch (`pages-page-data.ts`)
so the two stay in sync per AGENTS.md. Other return shapes (`notFound`,
`redirect`, `revalidate`, fallback) are untouched.
Closes#1459
* refactor: simplify Promise-shaped props handling
await Promise.resolve(x) already handles both thenables and plain
values, so the .then duck-typing check is unnecessary.
Hoist the `javascript:` / `data:` / `vbscript:` block from inside the
async `performNavigation` body up into `Router.push` / `Router.replace`
themselves so the throw is fully synchronous. Mirrors Next.js's Pages
Router `push`/`replace` at
packages/next/src/shared/lib/router/router.ts:1025-1033,1057-1065.
Why: `performNavigation` is an `async` function, so throwing from inside
it wraps the error in a rejected Promise. `<button onClick={() =>
router.push("javascript:...")}>` does not await that Promise, so React's
event-handler error reporter never observes the throw and the matching
`console.error` is not surfaced through page logs. That is the failure
shape asserted by Next.js's
`test/e2e/app-dir/javascript-urls/javascript-urls.test.ts:341,376`.
Behaviour preserved: navigation is still blocked, and the matching
`console.error("Next.js has blocked a javascript: URL as a security
precaution.")` is still emitted by `assertSafeNavigationUrl`. The inner
guard inside `performNavigation` is kept as defence-in-depth.
Refs #1349
The App Router prerender harness forwarded requests through `fetch` with
fetch's default `redirect: "follow"` behavior. When a page called
`redirect()` the prod server emitted a 307, fetch silently followed the
Location header to the destination page, and the harness wrote the
destination's HTML under the redirecting route's filename. The seeded
cache then served that body with status 200 for every document request,
breaking Next.js parity (test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts
expects 307).
Set `redirect: "manual"` on the App Router prerender `rscHandler` so the
original 3xx surfaces back. `htmlRender.ok` is then false, the route is
marked skipped, and at runtime the document request hits the live
render path that already builds a proper 307 + Location response via
`buildAppPageSpecialErrorResponse`. RSC flight-payload handling (#1347)
is untouched.
The pages-prerender `renderPage` helper already used `redirect: "manual"`,
so this brings the App Router phase in line.
Closes#1530
`parseQueryString` and the parseQuery template embedded in the Pages
Router worker entry both used `url.split("?")[1]`, which silently drops
everything after the second `?`. URLs like `/linker?href=/about?hello=world`
have query `href=/about?hello=world` per RFC 3986, so the previous
implementation truncated `query.href` to `/about` and `<Link>` rendered
without the embedded query string. Switching to an `indexOf`-based slice
keeps the full query value intact.
Closes#1471
getServerSideProps responses now ship the Next.js default
`private, no-cache, no-store, max-age=0, must-revalidate` Cache-Control
header in both dev and prod, and forward `res.setHeader('Cache-Control', ...)`
overrides set inside gssp through to the final response.
Mirrors next/src/server/route-modules/pages/pages-handler.ts where a
gssp page resolves to `revalidate: 0` and getCacheControlHeader emits
the same string, gated by `if (!res.getHeader('Cache-Control'))` to
preserve user overrides.
Closes#1461
Pages Router `window.next.router.push`/`replace` already resolved to
`true` on a successful navigation (and `false` on a hard-navigation
fallback) via `performNavigation`, but the public `window.next.router`
surface typed both as `(...args: unknown[]) => unknown`, leaving
consumers (and the Next.js deploy test suite that `awaits` the result)
without a typed boolean to assert on.
- Narrow `PagesRouterPublicInstance.push`/`replace` to
`(...args: unknown[]) => Promise<boolean>` to match Next.js's
`Router.change` contract (router.ts:1206 — `Promise<boolean>`).
- Add unit-test regression coverage asserting
`window.next.router.push`/`replace` resolve to `true` on success.
- Void the `router.push(...)` calls in `tests/e2e/app-router/build-id-navigation.spec.ts`
to satisfy the tightened union (App Router push is `void`).
Closes#1467
* fix(link): warn on repeated forward slashes in href
Closes#1554
When a `<Link href>` contains repeated forward-slashes (e.g. `/foo//bar`)
or a backslash, Next.js's `resolveHref` emits a `console.error` so the
developer notices the invalid URL during dev/CI. vinext's Link shim did
not surface that warning, which caused the Next.js Deploy Suite test
`test/e2e/repeated-forward-slashes-error` to fail waiting for the
message in the CLI output.
Add a small `warnOnRepeatedSlashesInHref` helper that mirrors the regex
and message used by Next.js, and call it from the Link body after the
href is resolved. The href continues to render and navigate unchanged —
Next.js does not block, it only warns.
Ported from Next.js: packages/next/src/client/resolve-href.ts
* fix(link): normalize repeated slashes and drop dedup for Next.js parity
Address review feedback on #1563:
- Remove the module-level `warnedRepeatedSlashHrefs` Set. Next.js fires
this warning on every `resolveHref` call, so dedup was an unintended
divergence and a latent test-ordering hazard.
- Add `normalizeRepeatedSlashes` (port of
packages/next/src/shared/lib/utils/normalize-repeated-slashes.ts) and
apply it after warning, so `<Link href="/a//b">` renders `href="/a/b"`
exactly as Next.js does.
- Preserve protocol-relative URLs (`//example.com/...`) unchanged —
vinext treats those as external via `isAbsoluteOrProtocolRelativeUrl`,
so we skip both the warning and the normalization for them to avoid
regressing the existing "locale does not mangle protocol-relative
URLs" test.
- Document the `window.location.pathname` fallback (Next.js uses
`router.pathname`; the difference is cosmetic and not asserted by the
compat test).
- Add tests covering no-dedup behaviour, href normalization (including
backslashes, query-string preservation, and protocol-prefixed URLs).
* fix(app-router): router.prefetch throws on invalid URL
Match Next.js's createPrefetchURL behaviour: when the href cannot be
parsed via `new URL(href, window.location.href)`, throw the canonical
message `Cannot prefetch '<href>' because it cannot be converted to a
URL.` so the surrounding error boundary in the App Router can surface
the failure instead of silently swallowing it inside the async prefetch
IIFE.
Closes#1553
* fix(app-router): apply basePath before prefetch URL validation
Mirror Next.js's createPrefetchURL exactly by passing the href through
withBasePath before parsing. With a non-empty basePath, an otherwise
broken-looking href can be parseable (e.g. `new URL("/app///", origin)`
succeeds while `new URL("///", origin)` throws), so applying basePath
first matches upstream behaviour for basePath-configured apps.
* ci: address zizmor security audit findings
Run `zizmor .` and address all findings:
- Pin actions to commit SHAs with version comments (unpinned-uses).
Dependabot picks up the version comment for future bumps.
- Move `contents: write` / `id-token: write` to the publish job in
publish.yml; set `permissions: {}` at the workflow level
(excessive-permissions).
- Add `permissions: {}` to deploy-examples.yml workflow + scope
smoke-test job to `contents: read` (excessive-permissions).
- Move `${{ ... }}` expansions inside run blocks into `env:` vars
in publish.yml / benchmarks.yml / nextjs-deploy-suite.yml
(template-injection).
- Set `persist-credentials: false` on all checkouts that don't push;
publish.yml needs them for `git push origin v$VERSION`, so it
carries an inline `zizmor: ignore[artipacked]` instead.
- Keep dependabot's 1-day cooldown but annotate with `zizmor: ignore`
(intentional fast cooldown).
`zizmor .` is now clean.
* ci: add zizmor GitHub Actions workflow
Run zizmor on every push to main and every PR via the upstream
`zizmorcore/zizmor-action`. Findings surface in the GitHub Code
Scanning UI (Security → Code scanning alerts).
* ci(publish): explain zizmor artipacked ignore
* fix(app-router): hoist inline beforeInteractive scripts above resource hints (#1557)
React Fizz puts every stylesheet, modulepreload, and preload link emitted via React Float above user-rendered <script> children of <head>, so an inline `<Script strategy="beforeInteractive">` placed first in the root layout lands near the bottom of head instead — well after styles begin to parse. This breaks the standard no-flash dark-mode pattern.
Capture inline beforeInteractive content via a new BeforeInteractiveContext that the App Router SSR pipeline wraps the rendered tree with. The Script shim registers the escaped innerHTML + nonce + attributes and returns null from render. The stream transform splices the captured tag immediately after `<head ...>` opens — before any chunk that contains React-emitted resource hints. On the client the shim suppresses the inline render when the App Router runtime is present (detected via the new hasAppNavigationRuntimeBootstrap helper) so hydration matches.
External-src and non-App-Router rendering paths are unchanged: external scripts still preload + render in source order, and Pages Router falls through to the existing inline render.
* fix: address PR feedback
- e2e spec now reads `self.__vinextThemeInitRan` (matches the fixture's actual marker; the prior `documentElement.dataset` assertion would always have been undefined)
- gate `renderBeforeInteractiveInlineScripts` on a `/^[a-zA-Z][\w.-]*$/` attribute-name regex so unknown developer-supplied prop keys can't break out of the unquoted name position
- drop unused `BeforeInteractiveProvider` export — the SSR entry uses `BeforeInteractiveContext.Provider` directly
- correct the fixture layout docstring to describe the actual `self.__vinextThemeInitRan` marker and explain why we avoid `<html>` dataset attributes here
- consolidate the duplicate CSP-nonce middleware blocks for `/revalidate-test` and `/beforeinteractive-head-ordering`
* docs/test: address PR nit feedback
- correct the createTickBufferedTransform jsdoc: only `injectHTML` falls back to end-of-stream; `injectAfterHeadOpenHTML` is silently dropped when `<head ...>` is never found in a discoverable chunk (covered by the existing unit test)
- drop the dead pre-request scaffolding from the CSP-nonce test; the meaningful assertions live in the second request, so the first one was just `void html` noise
* fix(next-request): throw canonical relative URL error
Calling `new NextRequest('/foo')` previously bubbled up `Failed to parse
URL from /foo` because the constructor's fallback `new URL(input,
"http://localhost")` masked the real problem. Next.js's NextRequest
runs `validateURL(url)` at the top of its constructor and throws an
error containing `Please use only absolute URLs`. Middleware tests
that assert on this message text now match Next.js.
Closes#1521
* fix(next-request): collapse inline URL validation to validateURL helper
Address bot review nit: the 13-line inline URL validation duplicated the
existing `validateURL` helper at line 215. Replace it with a call to the
helper. Bonus: relative URL rejection now also picks up the
`assertSafeNavigationUrl` guard against `javascript:`/`data:`/`vbscript:`
URIs (defense-in-depth).
Pages Router <Head> now emits the canonical `data-next-head=""`
attribute matching Next.js, rather than the vinext-specific
`data-vinext-head="true"`. This unblocks `_document` head lookups
in the deploy suite.
Next.js 16 renamed `middleware.{js,ts}` to `proxy.{js,ts}` and emits a
canonical deprecation warning when the legacy filename is detected. vinext
was emitting a custom message that didn't match the wording the Next.js
deprecation-warnings and app-middleware e2e suites assert on.
Update `findMiddlewareFile` to log the exact text Next.js uses (from
`packages/next/src/build/index.ts` adjacent to `MIDDLEWARE_FILENAME`):
The "middleware" file convention is deprecated. Please use "proxy" instead.
...migration codemod and docs link...
The warning still fires only when `middleware.{js,ts}` is found (after
checking `proxy.{js,ts}` first) so behavior is unchanged for projects on
the new convention. Existing test in tests/shims.test.ts is tightened to
assert the canonical wording.
Closes#1455
* feat(pages-router): consume _next/data JSON endpoint from the client
The server-side `/_next/data/<buildId>/<page>.json` endpoint landed in
#1384, but the vinext client was still fetching the full HTML page on
every navigation and regex-extracting `__NEXT_DATA__` from it. This
wires the client to actually use the JSON endpoint, matching what
Next.js does and avoiding the HTML round trip.
## Client navigation (shims/router.ts)
`navigateClient` is split into two paths:
- `navigateClientData` (new): when `window.__VINEXT_PAGE_LOADERS__` has
a registered code-split loader for the target route AND we have a
buildId, fetch `/_next/data/<buildId>/<page>.json` with
`Accept: application/json` + `x-nextjs-data: 1`. On 200 parse the
envelope, look up the page module via the in-memory loader map (Vite
has already split each page into its own chunk), and re-render.
- `navigateClientHtml`: the previous HTML-fetch + regex-extract logic,
unchanged. Used in dev (where the inline hydration script does not
populate the loader map) and as a fallback for any route not in the
client loader map.
Failure modes — 404, 5xx, network error, malformed JSON, soft
redirect via `x-nextjs-redirect`, missing loader — all queue a hard
navigation (`window.location.href = url`). This is the deploy-skew
safety net: when the server's buildId has rotated, the data endpoint
returns 404 and the client lands on the new build via a full document
load.
## Code-split loader manifest (entries/pages-client-entry.ts)
Vite already code-splits each page into its own chunk via the dynamic
imports in `pageLoaders`. The generated client entry now exposes that
map on `window.__VINEXT_PAGE_LOADERS__` (route pattern → loader thunk),
plus `window.__VINEXT_PAGE_PATTERNS__` (ordered list) and
`window.__VINEXT_APP_LOADER__` (the `_app` thunk, when present). This
replaces Next.js' `_buildManifest.js` indirection — vinext already
generates the equivalent at build time, we just had to expose it.
## Data URL construction (shims/internal/pages-data-url.ts)
New pure helper module mirroring `getAssetPathFromRoute` +
`getDataHref` from Next.js. Handles:
- `/` → `/_next/data/<id>/index.json`
- `/about` → `/_next/data/<id>/about.json`
- `/index` → `/_next/data/<id>/index/index.json` (round-trip)
- `/blog/foo` → `/_next/data/<id>/blog/foo.json`
Also exports `matchPagesPattern` which uses the existing
`matchRoutePattern` to map a URL pathname to a registered route
pattern.
Round-trip tests verify the server's `parseNextDataPathname` agrees
with the client's `buildPagesDataPath` for every shape, and surfaced a
latent server bug: `/_next/data/<id>/index/foo.json` was parsing back
to `/index/foo` instead of `/foo`. Fixed in `pages-data-route.ts` so
the round-trip is now lossless.
## Prefetch (shims/link.tsx + shims/router.ts)
`<Link>` prefetch and `Router.prefetch()` now prefer the data path:
- Inject `<link rel="prefetch" as="fetch" crossorigin href={dataUrl}>`
for the JSON, matching the actual navigation's request shape.
- Invoke the loader thunk to warm the chunk in parallel. Vite's
`import()` caches the result, so the eventual click is a double
cache hit.
When no loader matches (dev, unmapped route), fall back to the legacy
`<link rel="prefetch" as="document">` so the browser at least preloads
the HTML.
Notably this also fixes a long-standing prefetch bug: the previous
Pages branch of `link.tsx`'s prefetch check
(`__NEXT_DATA__.__vinext?.pageModuleUrl`) was only populated by the
dev server's inline hydration script, so prefetch in production was a
no-op.
## Tests
- `tests/pages-data-url.test.ts` (new): 27 tests covering URL
construction and the client/server round trip.
- `tests/shims.test.ts` (+8): JSON-path navigation success, 404
hard-reload (build-skew), `x-nextjs-redirect` soft-redirect handling,
HTML fallback when no loader matches, dynamic param extraction,
malformed JSON hard-reload, prefetch JSON path, prefetch fallback.
- Ports from Next.js: `getDataHref` in
`.nextjs-ref/packages/next/src/client/page-loader.ts:154`,
`fetchNextData` in
`.nextjs-ref/packages/next/src/shared/lib/router/router.ts:498`,
`getAssetPathFromRoute` in
`.nextjs-ref/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts`.
Verified: 1393 vitest tests pass across shims, pages-router,
link/navigation, prefetch, data-route, data-url, entry-templates, and
routing.
Closes#1330 follow-up.
* fix(pages-router): drop unused export on PagesPatternMatch
Knip flagged the exported `PagesPatternMatch` type as unused. It is only
referenced as the return type of `matchPagesPattern` in the same file, so
narrow it to a local type.
* fix(pages-router): merge search params into __NEXT_DATA__.query on data-path nav
bigbonk review found that navigateClientData was synthesising
__NEXT_DATA__.query from dynamic route params only, missing the URL search
params. Code reading window.__NEXT_DATA__.query directly (a public Next.js
surface) would see incomplete data after a JSON navigation, even though
useRouter().query happened to still work because getPathnameAndQuery()
re-reads window.location.search.
Parse target.search and merge it into query alongside the route params,
with route params winning on key collision — same merge order as
getPathnameAndQuery() and Next.js itself.
Also fix a misleading comment in pages-client-entry.ts: the patterns list
is already sorted by pagesRouter()/compareRoutes (static → dynamic →
catch-all), not by the router at runtime.
* test(pages-router): e2e regression guard for JSON nav path
Adds a Playwright assertion that a Link click on a hydrated Pages Router
app on Cloudflare Workers fires a request to /_next/data/<buildId>/<page>.json
and does NOT trigger a full HTML reload. If the build pipeline ever stops
exposing __VINEXT_PAGE_LOADERS__ (or the loader map mis-keys), every other
navigation test still passes — they all assert the URL changed and the new
page rendered, neither of which fails when navigateClient silently falls
back to the HTML path. This test fails loudly in that scenario.
* test(pages-router): use hydration timestamp instead of resource-type sniffing
The original assertion 'no document-resource request to /ssr' caught false
positives: <link rel="prefetch" as="document"> can fire on initial paint
for unrelated reasons, and Chromium does not always classify prefetch hints
in a stable way. Swap to checking that __VINEXT_HYDRATED_AT survives the
click — a document reload would reset it, an in-place re-render preserves it.
Combined with the data-URL network assertion, this remains a tight guard on
the JSON nav path without the flake.
* test(pages-router): use window sentinel instead of timestamp comparison
Timestamps are timing-related signals; swap to a window-scoped sentinel
that a document reload would wipe and an in-place re-render preserves.
Same guarantee, no flake.
* fix(pages-router): handle _next/data requests in Cloudflare Workers example
The Node prod-server normalizes /_next/data/<buildId>/<page>.json before
middleware and flags it as a data request so renderPage emits the JSON
envelope. The Cloudflare worker example never did — data requests hit
matchPageRoute which can't match /_next/data/... and returned 404, so
navigateClientData saw the 404 and triggered a hard navigation. The
client-side JSON nav path was effectively dead on Workers.
Mirror the prod-server normalization in the worker: detect the data path
before middleware, validate the buildId, strip it to the page path, and
pass { isDataReq: true } through to renderPage.
Export ./server/pages-data-route from the vinext package so user-written
workers can import the helpers.
* Revert "fix(pages-router): handle _next/data requests in Cloudflare Workers example"
This reverts commit 719575b9ae.
* fix(pages-router): normalize _next/data requests inside the server entry
The Node prod-server normalized /_next/data/<buildId>/<page>.json before
middleware so renderPage could emit the JSON envelope. Cloudflare Workers
go through a user-written worker entry that doesn't (and shouldn't) know
about the data endpoint protocol, so data requests were reaching
matchPageRoute as /_next/data/... URLs, returning 404, and the client
fell back to a hard navigation — defeating the whole JSON-path
optimisation on Workers.
Move the detection into the generated server entry so it works for any
caller. runMiddleware now normalizes the URL for middleware matcher and
signals via result.rewriteUrl, and renderPage auto-detects data requests
from request.url so the JSON envelope fires whether or not middleware
rewrote the URL. Mismatched buildId short-circuits to a JSON 404 in
both paths so stale clients hard-navigate onto the new build.
* chore: re-trigger CI
* refactor(pages-router): share data-nav target resolver between Link and Router
Extract `resolvePagesDataNavigationTarget` + `prefetchPagesData` into
`shims/internal/pages-data-target.ts` so the Link prefetch path and the
Router (`navigateClient` + `Router.prefetch`) share one decision helper
instead of maintaining two structurally identical copies.
Also strip the locale prefix before matching `__VINEXT_PAGE_PATTERNS__`
so locale transitions (e.g. `/fr/about`) hit the JSON data fast path
instead of falling through to the slower HTML round-trip — route
patterns are locale-unaware (`/about`), but the browser URL for a
locale-prefixed page includes the locale. The data URL itself keeps
the locale prefix because the server uses it to pick locale-specific
gSSP data.
When the data path is used and the URL implies a different locale than
the current `__NEXT_DATA__.locale`, refresh the locale fields on the
synthesised `__NEXT_DATA__` (the JSON envelope itself only carries
`pageProps`, so the locale must be derived from the URL). Without this,
`useRouter().locale` stays stale after a client-side locale transition.
* chore: re-trigger CI
Hybrid App Router plus Pages Router production builds entered through the App Router server and only delegated route misses to renderPage. That meant pages/api routes were outside the fallback path and returned the App Router 404 in mixed apps.
The generated RSC entry now dispatches /api misses to the Pages server entry handleApiRoute, applies middleware response effects to that API response, and the hybrid SSR bridge re-exports the API dispatcher. Regression coverage ports the Next.js og-api Pages API ImageResponse case at the production boundary.
* perf(e2e): preinstall compatible React in deploy harness
App Router deploy-suite fixtures can start with React versions below the react-server-dom-webpack peer range. The first harness install then resolves that older React pair, and vinext build immediately runs its RSC compatibility upgrade, causing a second package-manager install inside each throwaway app.
Normalize App Router fixture manifests before the initial install by replacing older semver React and React DOM specs with the workspace-compatible specs. Pages-only fixtures and non-semver specs are left untouched.
* chore: rerun ci
* fix(e2e): derive React floor from harness spec
The deploy harness normalized App Router fixture React versions against a hardcoded 19.2.6 floor. That duplicated the compatibility threshold already expressed by vinext's install specs and could go stale when the workspace moves to a newer compatible React pair.
Compare each fixture dependency against the replacement spec that the harness would write. This keeps React and React DOM normalization tied to the package metadata instead of a separate constant.
Replaces 16 ad-hoc `globalThis.fetch = async () => new Response(...)`
swaps with a small `mockGoogleFontsCSS(body, opts?)` helper backed by
MSW's `server.use(http.get(...))`. The helper records every URL it
sees so the two tests that previously asserted on `fetchedUrls.find()`
just read `mock.urls` instead, and the one test that asserted on a
hit-count read `mock.count()`.
Two sites need bespoke handling:
- The Inter/Roboto URL-branching test uses an inline
`server.use(http.get("https://fonts.googleapis.com/*", ({ request }) => ...))`
that returns different CSS based on the URL.
- The "network error" test uses `HttpResponse.error()` to make the
fetch reject (replacing the previous `throw new TypeError(...)` in
the swapped fetch).
Handlers are reset between tests by the global `afterEach` in
`tests/_msw/setup.ts`, so the per-test `try { ... } finally
{ globalThis.fetch = originalFetch }` boilerplate disappears (the
`finally` blocks now only call `fs.rmSync` on the temp font root).
This is PR 2 of the MSW migration. PR 1 (#1444) introduced the
infrastructure and migrated `tests/font-google-build.test.ts`. A
follow-up will migrate `tests/fetch-cache.test.ts` and the in-test
fetch stub inside the font block of `tests/app-router.test.ts`.
App Router RSC deployment mismatches and related fallback paths could still write directly to window.location outside the navigation controller. That bypassed the persisted same-target hard-navigation guard, so a canary or rollback mismatch could repeatedly reload the current document instead of stopping after the first hard navigation.
Expose the controller's guarded hard-navigation executor and route RSC compatibility fallbacks, server-action deployment fallbacks, and RSC navigation error fallbacks through it. Add skip/cache canary rollback compatibility coverage plus a direct fallback guard regression test.
* test(msw): introduce MSW infrastructure and migrate font-google-build
Replaces ad-hoc `globalThis.fetch` hijacking in tests with Mock Service
Worker. MSW intercepts both `globalThis.fetch` and `node:http`/`https`,
so it covers fetches issued by in-process Vite servers and fixture pages
without each test having to manage its own stub.
The setup file enables `onUnhandledRequest: 'error'` so any future test
that issues an unmocked external request fails loudly instead of
silently hitting the network. Loopback hostnames (localhost, 127.0.0.1,
::1) are bypassed because the integration project deliberately hits
in-process test servers via fetch.
What this PR contains:
- `tests/_msw/{handlers,server,setup}.ts` — shared infra
- `setupFiles` wired into both vitest projects (unit + integration)
- `msw` + `@mswjs/interceptors` added to the workspace catalog
- `msw` postinstall skipped via `allowBuilds` (it only copies a browser
service worker; we exclusively use `msw/node`)
- `tests/font-google-build.test.ts` migrated as the proof of pattern
Temporary handlers for `httpbin.org/uuid` and
`example.com/not-cacheable` keep the existing fixture pages
(`revalidate-tag-test`, `dynamic-error-fetch`) working under the new
guard. These are placeholders flagged with `TODO(msw-migration)` — a
follow-up will replace those live URLs with test-local endpoints.
This is PR 1 of 3:
- PR 2 will migrate `tests/fetch-cache.test.ts`
- PR 3 will migrate `tests/font-google.test.ts` and the font block in
`tests/app-router.test.ts`
* test(msw): restrict MSW to the unit project + use passthrough for loopback
Two fixes to the initial MSW wiring that surfaced when CI ran the full
suite:
1. The integration project is now excluded from MSW. Integration tests
spin up in-process HTTP servers and fixture dev servers and exercise
them via `fetch("http://127.0.0.1:<port>/...")`. Even with a loopback
`passthrough()` handler installed, the @mswjs/interceptors layer
interferes with that traffic in subtle ways:
- `tests/prerender.test.ts > errors without writing .rsc when the
middleware short-circuit fallback RSC request fails` timed out at
30s — the 5xx response body never reached the caller.
- `tests/ecosystem.test.ts > nuqs` hung in `beforeAll` waiting for
its fixture process to come up, because the readiness fetch
stalled inside the interceptor.
Integration tests already talk to real local servers, not the
internet, so the unhandled-request guard buys little there. The
value of MSW for this repo is mocking external HTTP for unit tests
of fetch wrappers (font-google, font-google-build, fetch-cache),
which is preserved.
2. The `onUnhandledRequest` callback in `setup.ts` that tried to
silently let loopback through wasn't sufficient — MSW's interceptor
still ran for those requests and could stall them. Replaced with a
default `http.all(LOOPBACK_URL_PATTERN, () => passthrough())`
handler in `handlers.ts` and a plain `onUnhandledRequest: "error"`
in `setup.ts`. Unit tests like the middleware-rewrite proxy in
`tests/shims.test.ts` and the JSX-in-JS suite need this to keep
their in-process HTTP servers reachable.
* test(msw): address BigBonk review notes
- Update PR description in the commit body to drop the stale mention of
TODO(msw-migration) handlers — they were removed when the integration
project was excluded from MSW; the fixture pages run there.
- Tighten the loopback IPv4 octet regex from `\d+` to `\d{1,3}` so it
doesn't match absurdly long octet strings. `URL.hostname` normalises
real addresses, so this is purely a readability fix.
* test(msw): address remaining BigBonk notes from the initial review
- Add `0.0.0.0` to the loopback passthrough pattern so test servers that
bind to the unspecified address keep working without per-test mocks.
- Add a `TODO(msw-3)` comment in `server.ts` flagging the
`SetupServerApi` deprecation in MSW 2.14.6 — the `FetchInterceptor`-only
construction will need to migrate to `defineNetwork`'s interceptor
config when we bump to MSW 3.
Both raised by BigBonk on the first review of this PR (the second review
focused on the stale description and the loose `\d+` octet bound, which
were addressed in the previous commit).
* test(msw): scope server.use() inside the try block in font-google-build
Minor structural fix from BigBonk's review: moves the `server.use()`
handler registration inside the `try` block so all setup and the
matching `fs.unlink` cleanup share the same try/finally scope.
No behaviour change — `server.use()` is synchronous and infallible, so
this is purely a structural tidy-up.
* test(msw): correct comment in font-google-build to match interceptor config
BigBonk caught that the comment described the default `setupServer()`
behaviour (which uses both `FetchInterceptor` and `ClientRequestInterceptor`),
but this project's `server.ts` deliberately constructs `SetupServerApi`
with only `FetchInterceptor`. Update the comment to reflect that — it
intercepts `globalThis.fetch`, not `node:http`/`node:https`.
No code change, comment only.
* fix(router): preserve default locale for unprefixed root links
Unprefixed Pages Router URLs represent the default locale, even when the browser's preferred locale has previously been detected as a non-default locale. Treating the ambient runtime locale as authoritative makes a Link from /new to / request /id and returns the wrong page locale.
Derive implicit client navigation locale from the current URL first, then domain/default locale state, and render a locale-qualified default root fallback href so non-intercepted root links do not re-enter preferred-locale detection. The regression tests port the relevant Next.js i18n preferred-locale cases at the Link and router boundaries.
* refactor(router): share client locale inference
The review called out that Link and Router had identical browser locale fallback logic. Keeping that chain duplicated makes future i18n changes easy to apply in one place but miss in the other.
Move the URL-first browser locale lookup into a shared shim helper and add SSR coverage for the intentional default-locale root fallback href.
Mirrors Next.js's server-side default-locale path normalisation in
`packages/next/src/server/lib/router-utils/resolve-routes.ts` (lines
~250-263): every request that arrives without a locale prefix is
spliced with `/${defaultLocale}` before any `next.config.js`
redirect / rewrite / header rule (or filesystem route) match runs.
Without this, rules like `source: '/:locale/to-sv'` with
`locale: false` failed to match requests for `/to-sv` because there
was no segment for the `:locale` placeholder. After normalisation the
matcher sees `/en/to-sv` and the rule matches with `:locale=en`.
Per the merged PR #1382's description, the issue text's call for a 308
redirect from `/en/page` to `/page` is incorrect: Next.js serves
the same content under both URLs and normalises the path internally.
This change implements that internal normalisation; no redirect.
The new helper lives next to the existing pages i18n helpers so both
routers can use it. Wired into:
- `prod-server.ts` (Pages Router prod)
- `app-rsc-handler.ts` (App Router dev + prod)
- `index.ts` (Pages Router dev plugin)
- `deploy.ts` (Cloudflare Workers entry template)
The domain-mapped default locale (`i18n.domains[].defaultLocale`) is
preferred over the global default when the inbound host matches,
matching Next.js's `domainLocale.defaultLocale` branch in
resolve-routes.ts. `/_next/*` and `/__vinext/*` paths are excluded
to mirror Next.js's exception for build assets and prerender manifests.
The fallback "retain the original unprefixed source" branch in
`applyLocaleToRoutes` is intentionally kept as belt-and-braces: the
new normalisation pass renders it dead-code for ordinary requests, but
it harmlessly preserves backwards compatibility with any matcher
caller that still passes raw, unnormalised pathnames.
Refs #1336 (item 4)
Two workflows now share an identical playwright-version + cache-restore
pattern: ci.yml (which owns the cache, used by E2E jobs) and
nextjs-deploy-suite.yml (which restores it read-only, added in #1435).
Pull the duplication into .github/actions/playwright-cache so the cache
key format can't drift between the two callers — if it ever does, the
deploy-suite stops hitting ci.yml's cache and the upstream Playwright
extraction hang from 2026-05-22 (runs 26265986264 .. 26281758803)
reappears.
The composite action:
- Reads the playwright version from a configurable lockfile path
(`lockfile` input, defaults to pnpm-lock.yaml).
- Restores ~/.cache/ms-playwright keyed
`playwright-${browser}-${runner.os}-v${version}`.
- Supports two modes: `cache` (restore + save, default — for the workflow
that owns the cache) and `restore` (read-only — for downstream
consumers that don't want to fight over the save).
- Exposes `cache-hit`, `version`, and `key` outputs so callers can gate
install steps or use the version for sibling cache keys (e.g. ci.yml's
apt-archives cache for WebKit deps).
No behavioural change — same cache keys, same restore-keys, same
~/.cache/ms-playwright path.
Follow-up to #1428 and #1430. The previous two PRs added log markers
and skipped the wedged transitive postinstall, but the underlying
problem remained: the explicit `pnpm playwright install chromium
chromium-headless-shell` in the prepare script hits the same upstream
extraction hang (download to 100%, then no progress until step
timeout).
What's actually different between the runs that worked (May 19-21)
and the runs that hang (May 22): nothing on our side. Same Chrome
145.0.7632.6, same Playwright 1.58.2, same Node 24.16.0, same runner
image fleet. On the May 21 success the extraction took 2.7 seconds
after 100% download; on May 22 it never completes (>9 min, killed by
the 15-min step timeout). Some upstream change — bad bytes from
cdn.playwright.dev, an Azure storage hiccup affecting fsync, a
Playwright installer interaction we can't see from here — is wedging
extraction. We can't fix it from this repo.
What we *can* do: stop downloading. vinext's own ci.yml already keeps
a `playwright-chromium-Linux-v1.58.2` cache hot (261 MB, last accessed
within minutes on every CI run today). The deploy-suite has never used
this cache — its only cache (the post-build `Save build artifacts`
step) is keyed per-run, so every nightly starts with an empty
~/.cache/ms-playwright and re-runs the install that's now hanging.
Add a restore step before the prepare that uses ci.yml's exact key
format. On hit, `pnpm playwright install` sees the browsers already
present and is a fast no-op. On miss (e.g. Next.js bumps Playwright
before vinext catches up), we fall through to the existing behavior.
Restore-only: ci.yml owns the save side.
* feat(cache): add skip cache proof cross-checks
Skip manifest entries remain untrusted hints, but future skip transport needs a cheap server-side proof boundary before it can omit any payload. Cache presence, client-declared artifact metadata, and stored artifact metadata are not individually enough to prove correctness.
Add a pure skip/cache cross-check helper that requires an accepted static layout cache proof, compatible artifact metadata, matching cache variant, matching payload hash, and valid invalidation proof. The helper always returns the disabled render-and-send skip disposition in this slice.
Cover graph, deployment, render epoch, variant, payload hash, invalidation, missing proof, and rejected proof cases at the helper boundary.
* fix(cache): bind skip cache artifact proof metadata
Accepted static layout artifact reuse decisions proved candidate artifact compatibility but did not carry that compatibility envelope forward. That let skip/cache cross-checks accept independently supplied artifact metadata as long as the client entry matched it, even when the cache proof had authorized a different artifact.
Store a snapshot of the candidate artifact compatibility on accepted artifact reuse proofs and require skip/cache cross-checks to match it exactly before checking client compatibility, variants, payload hashes, or invalidation state.
The skip/cache test matrix now covers the rejected proof-binding case, root-boundary compatibility mismatches, and layout entry id mismatches.
Follow-up to #1428. The previous PR added log markers, which revealed
the actual hang: `playwright-chromium`'s postinstall (`node install.js`,
triggered transitively by the Next.js workspace install) reaches 100%
on the 167 MiB Chrome zip download from cdn.playwright.dev, then never
prints "Done" — extraction/verification silently wedges. Every other
postinstall in the same install (cpu-features, sharp, esbuild, etc.)
finishes cleanly. Confirmed on runs 26265986264, 26279790373, and the
post-#1428 retry 26281061094 — same fingerprint each time.
Set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` on the Prepare step so the
transitive `playwright-chromium` postinstall becomes a no-op. The
explicit `pnpm playwright install chromium chromium-headless-shell`
that runs later in `scripts/run-nextjs-deploy-suite.sh` provisions the
browsers we actually use at test time, populating ~/.cache/ms-playwright
which is cached and restored by the test shards.
* ci(deps): add dependabot config with 1-day cooldown
Mirrors pnpm-workspace.yaml's `minimumReleaseAge: 1440` (24h) so
Dependabot won't open PRs for versions younger than the cooldown
window we already apply to local installs.
* ci(deps): run dependabot daily instead of weekly
The nightly deploy suite hung for 29m+ in "Prepare Next.js checkout"
on two consecutive runs (26265986264, 26279790373), eating the 30m job
timeout. Same v16.2.6 SHA (ee6e79b1) built fine the three previous
nights, so it's environmental, not source drift.
The first `pnpm install` completes around the 30s mark and then logs go
silent until cancellation. With no per-command markers, we couldn't tell
which of the remaining commands actually hangs.
This patch:
- Drops `--with-deps` from the build-job playwright install. It runs
`apt-get install` for browser system libraries, which is by far the
most plausible silent-hang culprit (stuck dpkg lock / slow apt repo).
The 16 test-shard runners each spin up a fresh ubuntu-latest and
already run `playwright install` without `--with-deps` — so the apt
state never transferred anyway. Doing it on the build runner is wasted
work even when it doesn't hang.
- Removes the redundant second `pnpm install`. The workspace symlinks
survive the build step; nothing between the build and re-install adds
new packages.
- Adds timestamped `>>> ...` markers around each prepare command so a
future hang is diagnosable from the logs.
- Adds `timeout-minutes: 15` on the Prepare step so a future hang fails
fast (well under the 30m job cap) instead of starving the whole job.
* feat(config): add experimental.appShells plumbing
Accepts experimental.appShells in next.config and defines
process.env.__NEXT_APP_SHELLS=false at build time, matching
Next.js PR #93997 plumbing scope. Behavorial implementation
is gated on upstream co-flags that vinext does not yet support.
- Adds experimental.appShells to CONFIG_SUPPORT as unsupported
- Sets __NEXT_APP_SHELLS define to false regardless of config value
- Tests for vinext check detection and Vite define injection
Fixes#1405
* review: address PR nits
- Expand experimental.appShells detail to explain why it's unsupported
- Move build-time define tests into their own describe block, so the
__NEXT_APP_SHELLS define test isn't nested under basePath
* review: drop orphaned server lifecycle from basePath describe
The two remaining resolveNextConfig tests import from source directly
and don't use the dev server.
---------
Co-authored-by: James <james@eli.cx>