mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
codex/cacheability-platform-io
1925 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7075d210e7 |
feat(cli): native App Router route type generation (#1144)
* Add native App Router route typegen * Address route typegen review feedback * Respect typegen target directory * Require explicit LayoutProps route * Serialize route type regeneration * Derive layout params from route graph * Scope layout slot typegen by route group * Map typegen slots to owner layouts * Preserve slot-local layout type keys * Generate next-env.d.ts during typegen * Address review feedback for route typegen * Defer route sort and tighten typegen helpers * Stabilize Windows create-next-app smoke check * Address route typegen review fixes * Match Next route type import * Stabilize Windows create-next-app smoke * Skip redundant route param shape writes * Defer dev route type generation |
||
|
|
fa1392616d |
fix(pages-router): preserve i18n client root navigation (#1323)
* fix(pages-router): preserve i18n client root navigation Pages Router client navigation fetched the unprefixed root URL even when the transition resolved to the default locale. That let root Accept-Language detection redirect a link click from /new to the preferred locale path, diverging from Next.js. Use the active locale for implicit Link and Router transitions, fetch default-locale root HTML through a locale-qualified internal URL while preserving the visible unprefixed URL, and parse i18n __NEXT_DATA__ scripts that append locale globals. * fix(pages-router): preserve i18n client root navigation Pages Router client navigation fetched the unprefixed root URL even when the transition resolved to the default locale. That let root Accept-Language detection redirect a link click from /new to the preferred locale path, diverging from Next.js. Use the active locale for implicit Link and Router transitions, fetch default-locale root HTML through a locale-qualified internal URL while preserving the visible unprefixed URL, and parse i18n __NEXT_DATA__ scripts that append locale globals. * fix(pages-router): validate next data parsing * fix(pages-router): preserve link locale transitions * fix(pages-router): preserve popstate locale fetch * chore(pages-router): address locale review nits |
||
|
|
705da9a52f |
feat(cache): introduce static layout artifact reuse proof primitive (#1307)
* Enable static layout artifact reuse proof * fix(cache): reject unproven static layout variants Static layout reuse could previously accept public or internal candidate variant dimensions without proving that the current request had the same canonical dimensions. Public visibility only means the values are safe to expose, not that the layout artifact is invariant across routes, params, search state, or other dimensions. Reject dimensioned static layout candidates until the API can compare candidate and current variants by canonical dimension source, name, privacy, and value hashes. Make cache outcome recording best-effort so telemetry failures cannot turn a cache hit into a miss or escape the read path. * fix(cache): require route-budgeted layout reuse candidates Static layout artifact reuse could still accept a raw buildCacheVariant result at the authority boundary. That allowed callers to bypass route-budget admission before receiving a canReuse true decision. Require BuildCacheVariantWithRouteBudgetResult for createStaticLayoutArtifactReuseDecision and add a type-level regression that raw variants do not satisfy the authority input. * chore(cache): clarify static layout proof traces The approved review called out non-blocking ambiguity in trace fields and telemetry documentation. The proof behavior is unchanged, but the trace output and type regression are easier to reason about. Sort unproven dimension sources defensively, rename artifact compatibility trace fallback fields, document raw app-page cache keys as internal-only, and make the raw-variant type regression explicit without a dead branch. * chore(cache): dedupe static layout dimension sources The unproven-dimension trace already sorted sources, but duplicate source labels could still make traces noisier than necessary. Use the existing sortedUnique helper so the source summary is deterministic and compact while dimensionCount remains exact. |
||
|
|
0d8d3e3c60 |
refactor(router): quarantine navigation runtime topology (#1322)
* refactor(router): quarantine navigation runtime topology * fix(router): restore manifest-backed interception runtime App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload. The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data. Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice. * fix(router): narrow prerender RSC script parsing The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun. Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix. * fix(router): address navigation runtime review * fix(router): resolve navigation runtime review threads |
||
|
|
3262443f0b |
fix(pages-api): execute edge API routes with Fetch Request (#1320)
* fix(pages-api): execute edge API routes with Fetch Request Pages API routes with config.runtime set to edge were still executed through the Node-style req/res adapter. That gives user handlers a plain headers object and turns valid Edge API code that returns a Response into a 500. Detect edge and experimental-edge API route modules before Node body parsing, pass the Fetch Request through, and forward the returned Response. The dev Node bridge mirrors the generated Web Request path and preserves Set-Cookie response headers. Regression coverage ports the relevant Next.js edge-async-local-storage contract for Fetch Request execution, concurrent AsyncLocalStorage isolation, and the dev bridge. * fix(pages-api): stream edge API responses in dev * fix(pages-api): handle closed dev edge streams * fix(pages-api): stream dev edge request bodies * fix(pages-api): validate dev edge forwarded protocol |
||
|
|
afc549d624 |
fix(router): normalize trailing slash parity (#1316)
Trailing slash handling treated every non-api path the same on server redirects, so trailingSlash:true added slashes to file-looking catch-all routes and Pages Router imperative navigation skipped the client canonicalization path used by Link. Next.js splits the invariant between route-manifest redirects and client resolveHref normalization: file-looking paths lose a trailing slash, non-file paths gain one, and queries stay attached to the canonical pathname. Share the server redirect decision across Pages Router runtimes and apply the same client helper in Router.push and Router.replace. Adds focused regressions for catch-all dot segments, query preservation, .well-known exclusion, and Pages Router history writes. |
||
|
|
5b6b764bbd |
fix(app-router): prerender layout static params (#1318)
* fix(app-router): prerender layout static params * address review: uniform resolver contract, sentinel comment, drop defensive clamp --------- Co-authored-by: James <james@eli.cx> |
||
|
|
1551f91cbd |
fix(metadata): match Next dynamic route semantics (#1317)
* fix(metadata): match Next dynamic route semantics Metadata routes diverged from Next.js in several observable cases: robots.txt emitted Sitemap before Host, dynamic metadata Response results could miss framework cache headers, TSX sitemap files were not discovered, and rendered metadata URLs treated canonical, manifest, and file-based social image routes as the same URL class. The implementation now follows Next's metadata route extension, cache, robots, and URL resolution rules while keeping file-based social image route markers non-enumerable so resolved metadata shape does not leak internal state. * fix(metadata): gate social image preview fallback File-based social image routes used the preview deployment URL whenever VERCEL_URL was present outside development. That let deployment URLs override an explicit metadataBase in non-preview production environments. Gate preview URL fallback on VERCEL_ENV=preview, add coverage for metadataBase precedence and production fallback URLs, and keep static metadata image cache headers stable in development.v0.0.51 |
||
|
|
e0eabc122b |
feat(router): add manifest-backed interception topology (#1314)
* feat(router): add manifest-backed interception topology * test(router): cover concrete dynamic interception proof ids * feat(router): index interception facts by slot |
||
|
|
9415a238f4 |
feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time Wire `assetPrefix` from next.config through to the Vite build so every emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors Next.js semantics: `assetPrefix` controls asset URLs only, independent of `basePath` (which still controls route URLs). * Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via a new `normalizeAssetPrefix` helper that trims trailing slashes, accepts absolute URLs (`https://cdn.example.com[/sub]`) and path prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable values. * New `packages/vinext/src/utils/asset-prefix.ts` module collects the shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix` (emitted URL prefix), `assetPrefixPathname` (path portion for runtime matching), and the canonical `_next/static` directory constant. * In the Vite plugin, set `build.assetsDir` so on-disk layout matches the URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's default `base + url` URL emission so `assetPrefix` and `basePath` stay independent — routes continue to anchor at `base = basePath + '/'`. * Persist `assetPrefix` in the embedded `vinextConfig` for the Pages Router server and in a new `vinext-runtime-config.json` sibling for the App Router prod server (the RSC entry only embeds `__basePath`). @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix * feat(server): serve assetPrefix-prefixed assets at runtime Update the production server to resolve incoming asset requests under `<assetPrefix>/_next/static/<filename>` (path-prefix form) and `/_next/static/<filename>` (absolute-URL form) in addition to the historical `/assets/` URLs. Both routers (Pages and App) share a new `resolveAppRouterAssetPath` helper that returns the lookup path on disk or `null` to fall through. * App Router prod server loads `assetPrefix` from the new `vinext-runtime-config.json` sibling written by the Vite plugin. * Pages Router prod server reads `assetPrefix` from the embedded `vinextConfig` and threads it through the existing `stripBasePath` lookup. * StaticFileCache marks any path under `_next/static/` as content-hashed (alongside the existing `assets/` rule) so emitted bundles get the long-lived `immutable` cache headers. Same change in the slow-path branch of `tryServeStatic`. Cloudflare Workers needs no code change: the ASSETS binding serves `<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...` because the on-disk layout already mirrors the URL. Absolute-URL prefixes are expected to be served by the CDN, so they don't reach the worker. * test(asset-prefix): add unit + e2e build tests for next.config assetPrefix Covers the surface area added in the previous two commits: * Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL, trailing-slash trimming, validation), `resolveAssetsDir` / `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout), `assetPrefixPathname` (runtime matching), and `resolveAppRouterAssetPath` (request → on-disk lookup translation). * End-to-end build tests that exercise `vinext build` against the `tests/fixtures/app-basic` app patched with each `assetPrefix` shape: - Path prefix (`/custom-asset-prefix`): SSR HTML references `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches, and fetching the bootstrap URL from the prod server returns 200. - Absolute URL (`https://cdn.example.com`): emitted asset URLs are fully qualified; no `/assets/` leaks. - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes stay under `/app/` while asset URLs live under `/cdn-prefix/` — proving the two configs stay independent. - Unset (default): historical `/assets/` layout is preserved. Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts * test(next-config): add assetPrefix field to makeResolved helper ResolvedNextConfig requires assetPrefix; the test helper was missing it which broke the type check. * fix(config): replace polynomial regex with loop (CodeQL) CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "") as polynomial-time backtracking on uncontrolled input. Swap both for explicit while loops with linear-time semantics. Same fix applied to the URL pathname trim in assetPrefixPathname(). * refactor(prod-server): drop dead null fallback; tighten hashed-asset detection - Remove the `?? null` after resolveAppRouterAssetPath; the helper already returns null on miss, so the fallback (and its obsolete comment) were no-ops. - Add explicit startsWith("_next/static/") / startsWith("/_next/static/") prefix checks before the substring fallback in both static-file-cache and prod-server. The substring check stays so assetPrefix-prefixed layouts (e.g. "cdn/_next/static/") are still recognised. * fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix` is configured the build writes hashed assets to a different subdirectory (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs) and the precompression step walked an empty directory, silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds with assetPrefix would then serve uncompressed assets. Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix) at the plugin call site, default to `"assets"` for backward compat, and add focused tests for both assetPrefix layouts. * refactor: drop redundant isHashed startsWith; harden test cleanup against build failures - prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")` check; `pathname` always has a leading slash so the existing `includes("/_next/static/")` already covers it. - static-file-cache.ts: keep both `startsWith("_next/static/")` and `includes("/_next/static/")` because `relativePath` has no leading slash — the prefix and substring checks cover different layouts (absolute-URL prefix vs path-prefix). Clarify the comment. - tests/asset-prefix.test.ts: register the tmp-dir cleanup with the shared `cleanups` array synchronously inside `buildFixtureWithConfig`, right after `mkdtempSync`. Previously the cleanup was only pushed after the build returned, so a thrown `createBuilder`/`buildApp` would leak the tmp dir. * refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts) and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry. The runtime-config.json sidecar was unnecessary — the App Router entry template already inlines config values as compile-time constants, so assetPrefix should be inlined the same way. Changes: - Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts. - Emit `export const __assetPrefix = <json>` from the generated RSC entry alongside the existing `__basePath` / `__configHeaders` / `__expireTime` constants. Single-line declaration to stay within the "thin generated entry" guideline. - Thread `nextConfig.assetPrefix` through the generateRscEntry call site in index.ts. - App Router prod-server reads `rscModule.__assetPrefix` (with a string fallback for older builds) instead of `vinext-runtime-config.json`. - Delete the vinext-runtime-config.json write in index.ts. Nothing else in the repo reads this file (verified via rg on packages/ tests/ examples/). Pages Router behaviour is unchanged: it already gets assetPrefix from the embedded `vinextConfig` exported by pages-server-entry.ts. Addresses james-elicx review feedback on #1311. * fix(ssr): emit bootstrap as <script src> via bootstrapModules The App Router SSR pipeline previously passed only `bootstrapScriptContent` to React's renderToReadableStream, producing inline `<script>import("/path/to/entry.js")</script>` tags. The URL was therefore inaccessible to anything that reads `script.attribs.src` — including the Next.js asset-prefix fixture test "bundles should return 200 on served assetPrefix" which iterates script tags looking for a `src` attribute prefixed by the configured assetPrefix. Extract the bootstrap URL from the plugin-rsc-generated content and pass it as `bootstrapModules: [url]` so React emits a real `<script type="module" src="url">` tag. The existing `extractModulePreloadHtml` helper already did the same extraction for the modulepreload <link>, so the URL was already exposed (just renamed `extractBootstrapModuleUrl` and lifted out of the head-injection helper to share between the bootstrap and the preload tags). The inline `bootstrapScriptContent` is no longer needed — it was only carrying the dynamic-import call that bootstrapModules now performs natively. Passing both would load the bootstrap module twice. `bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`) that the browser fetches as a module from the dev server; in prod it's the hashed bundle URL, optionally prefixed by `assetPrefix`. Mirrors Next.js's app-render path which combines both `bootstrapScriptContent` (for session state) and `bootstrapScripts` (for the actual chunk URL). See .nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx. We use `bootstrapModules` instead of `bootstrapScripts` because vinext's chunks are native ES modules (Vite output) — a `type="module"` tag is the correct loader. Tests updated where they asserted on the old `import("URL")` shape: tests/asset-prefix.test.ts, tests/app-router.test.ts, tests/e2e/cloudflare-workers/navigation.spec.ts. Verified all related suites pass locally (asset-prefix, app-router, script-nonce, script-preload, use-client-page-pathname, document, prerender, ISR, fetch-cache, entry-templates, precompress). * fix(build): use resolveAssetsDir for Pages Router client entry fallback scan The Pages Router Cloudflare Workers post-build step scans the on-disk assets directory for the client entry chunk when the SSR manifest lookup doesn't surface one. The directory path was hardcoded to `<clientDir>/assets/`, which is the Vite default — but when `assetPrefix` is configured, the chunk lives under `<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL prefix). The fallback would silently miss the entry and the worker would ship without its client bootstrap reference. Use the shared `resolveAssetsDir(assetPrefix)` helper that already drives `build.assetsDir` and the prod-server lookup, so this scan works for every supported asset layout. Addresses round-4 reviewer feedback on #1311 (low-priority follow-up item — only fires when the SSR manifest lookup fails). * feat(config): mirror Next.js basePath → assetPrefix fallback Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531). Without this, an app deployed under a basePath serves its routes correctly but emits its assets from `<basePath>/assets/...` (Vite's default `base + assetsDir` composition) rather than from the Next.js- canonical `<basePath>/_next/static/...`. Add the same fallback in resolveNextConfig, gated by Next.js's own conditions: - basePath !== "" (skip when basePath is unset) - basePath !== "/" (Next.js rejects this earlier; we mirror the guard so we never produce assetPrefix === "/") - assetPrefix === "" (user did not opt out) The fallback feeds the rest of the asset-prefix pipeline (Vite's `build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server URL→file resolver) so no other code paths need to change. Tests: - tests/next-config.test.ts: 5 unit tests for the four edge cases of the fallback condition. - tests/asset-prefix.test.ts: new end-to-end test that builds with `basePath: "/app"` (no assetPrefix), starts the prod-server, and asserts the bootstrap <script src> lands at /app/_next/static/... (and that the legacy /app/assets/... layout is gone). - tests/pages-router.test.ts: updated the "preserves basePath on backfilled SSR manifest entries" test to expect the new /docs/_next/static/ layout (was /docs/assets/). The manifest assertion is relaxed to startsWith("docs/") because Vite's raw SSR manifest now emits some duplicate-prefixed entries ("docs/docs/_next/static/...") alongside the backfilled ones; both forms anchor under basePath so the prod-server's URL→file lookup remains correct — and the user-visible HTML assertions in the same test are the source of truth. Addresses round-4 reviewer feedback on #1311. * fix(server): Pages Router must not strip basePath before asset lookup The Pages Router prod-server stripped `basePath` from the request pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke the asset lookup whenever the assetPrefix path-prefix branch was active with a path that happened to equal basePath. Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's `basePath → assetPrefix` fallback, an app configured with `basePath: "/docs"` (and no explicit `assetPrefix`) now resolves `assetPrefix = "/docs"`. Emitted asset URLs are `/docs/_next/static/…` (correct). But the Pages Router branch did: const staticLookupPath = stripBasePath(pathname, basePath); // "/docs/_next/static/x.js" → "/_next/static/x.js" resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs"); // path-prefix branch needs pathname to start with "/docs/" → null → 404 The App Router branch already passes the un-stripped `pathname` to the same helper (see line ~1140). Match that behaviour. `staticLookupPath` is still computed and used downstream by non-asset paths (image-optimization endpoint, page routing), where the basePath-stripped form is correct. Test coverage: - tests/pages-router.test.ts: extended the existing "preserves basePath on backfilled SSR manifest entries" test to FETCH every emitted asset URL and assert HTTP 200 (previously it only inspected HTML for the URL string). This is the missing assertion that would have caught the bug the first time. - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test that builds an inline pages/ fixture with `basePath: "/docs"`, starts the prod-server, collects every script/link URL from the SSR HTML, and asserts every URL serves a 200 + correct content-type. Confirmed to fail on `bac8425` before this fix and pass after. Addresses round-5 reviewer feedback on #1311. * refactor(ssr): accept either quote style in bootstrap URL regex plugin-rsc currently emits the bootstrap as `import("<url>")` with double quotes, but the contract isn't documented and a future version could switch to single quotes. Tighten the URL extraction regex to `import(["'])([^"']+)\1` so either form works. Addresses non-blocking round-5 review feedback on #1311. * docs(ssr): note CSP implications of bootstrapModules switch The round-3 commit `3a2c886` switched the App Router SSR bootstrap from inline `bootstrapScriptContent` (`<script>import(…)</script>`) to React's `bootstrapModules` option (`<script type="module" src=…>`). That has user-visible CSP consequences worth documenting at the call site so app authors hit them in code search rather than at runtime: - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap (improvement — inline imports previously required it). - Apps that restrict script sources need `'self'` for same-origin, or the CDN origin when assetPrefix is an absolute URL. - React still threads nonce through to the emitted <script> tag, so nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps working unchanged. Addresses non-blocking round-5 review feedback on #1311. |
||
|
|
1dbcbfbcc8 |
feat(app-router): plan navigation from RouteManifest topology (#1312)
* feat(app-router): plan navigation from RouteManifest topology Navigation commit approval still trusted flight payload snapshots for root-boundary and target-slot topology even when the static RouteManifest existed. That made stale or missing snapshot metadata authoritative during App Router commits, which could preserve layouts across the wrong root or fall back to soft commits when the graph already proved a same-root transition. The planner now resolves current and target topology through RouteManifest when available, uses snapshot slot bindings only as visible content proof, and keeps explicit interception proof as the authority for intercepted slot commits. The generated browser entry embeds the manifest and approval paths pass it through to the planner. Targeted coverage exercises stale snapshot roots, missing snapshot roots, dynamic URL pattern resolution, manifest target slot bindings, visible-slot proof, intercepted source topology, and browser-entry manifest serialization. * fix(app-router): read route manifest lazily during navigation approval The browser entry assigned the generated RouteManifest global before a static import, but ESM evaluates imported modules before the importer body. That let the browser navigation controller capture null at module evaluation, leaving manifest-aware planning disabled in real browser bootstraps. Make the controller accept a lazy RouteManifest getter and read it at commit-planning time. Also require direct manifest route-id hits to match the snapshot matchedUrl before trusting them, so stale-but-valid route ids fall back to concrete URL pattern resolution. Covered by app browser controller and navigation planner regressions, plus targeted entry template checks. * fix(app-router): clarify manifest route topology resolution Manifest route lookup now shares one matched-url normalization helper across direct route checks and pattern fallback. That keeps concrete dynamic URL fallback from drifting across call sites. Document that manifest pattern iteration follows buildAppRouteGraph route precedence, and record why intercepted targets cannot use direct-target manifest slot bindings. |
||
|
|
a85c2c1dee |
fix: add x-action-forwarded guard and E2E tests for server action forwarding loop (#1292)
* feat(server-action): add x-action-forwarded guard to prevent forwarding loops Defensively implements the Next.js server action forwarding loop fix. - Adds ACTION_FORWARDED_HEADER constant to headers.ts - Adds x-action-forwarded to INTERNAL_HEADERS blocklist - Adds guard to handleServerActionRscRequest and handleProgressiveServerActionRequest - Returns 404 + x-nextjs-action-not-found when header is present Ported from Next.js: vercel/next.js@20892dd * test(e2e): add action-forward-loop fixtures and tests - Adds /nextjs-compat/action-forward-loop fixture with middleware rewrite - Adds /nextjs-compat/action-forward-loop-rewrite as rewrite target - Adds error boundary component using unstable_isUnrecognizedActionError - Adds E2E tests: middleware rewrite + action POST, not-found header contract * test(e2e): fix action-forward-loop tests per review feedback - page.tsx: use useActionState so action result renders into #action-result - actions.ts: update runAction signature for useActionState compat - Remove dead error-boundary.tsx (never exercised in single-worker model) - Rename second test to match actual behavior (stale action ID -> 404) * test(request-pipeline): add x-action-forwarded to INTERNAL_HEADERS snapshot |
||
|
|
4aaae3d23e |
fix(build): pin cssTarget so esbuild preserves max-width media-query syntax (#1310)
* fix(build): pin cssTarget so esbuild preserves `max-width` media-query syntax Vite's default `build.cssTarget` follows `build.target` (modern evergreens), which lets esbuild's CSS minifier rewrite `@media (max-width: 768px)` to the Media Queries Level 4 range syntax `@media (width <= 768px)`. Both forms are semantically equivalent in modern browsers, but the rewrite is observable to user code that inspects `cssText` on `CSSMediaRule`s and breaks tools that pattern-match the raw query string. Next.js's CSS pipeline (webpack + cssnano, or lightningcss only when opted in via `experimental.useLightningcss`) preserves the original syntax by default, so user code carried over from Next.js can break when migrating to vinext. Pin `build.cssTarget` to ["chrome87", "edge88", "firefox78", "safari15"], which predate Safari 16.4 — the first browser to support MQ Level 4 range syntax. esbuild checks the configured target and skips lowering when any target is too old. Only literal media-query syntax is affected; matching behavior at runtime is unchanged. Mirrors the Next.js fixture test/e2e/app-dir/css-media-query/css-media-query.test.ts which asserts `cssText` preserves `max-width: 768px`. Tests: - New tests/css-media-query-target.test.ts builds a minimal fixture and asserts the produced CSS asset contains `max-width: 768px` and does not contain `width<=768px`. Fails without this change; passes with it. - All existing CSS / SCSS / build-optimization / nextjs-compat app-css tests continue to pass. * refactor(build): narrow cssTarget to only pin Safari low Address review feedback on #1310. Only Safari needs to be pinned to a version that predates MQ Level 4 range syntax (Safari 16.4) so esbuild skips the @media (max-width: …) → @media (width <= …) rewrite. The other targets were pinned far older than necessary, which caused esbuild to downlevel unrelated modern CSS features (`:is()`, CSS Nesting, `lab()`, `oklch()`, gradient interpolation, etc.). Bumps chrome/edge/firefox targets to ~2023 baselines while keeping safari15 to retain the MQ rewrite fix. |
||
|
|
54e271ddc9 |
fix(cache-runtime): attach inner "use cache" call site as cause of nested-dynamic error (#1289)
* fix(cache-runtime): attach inner "use cache" call site as cause of nested-dynamic error Ports Next.js PR #93707 to vinext. When a nested "use cache" propagates a dynamic cache life (revalidate: 0 or expire < 5 minutes) to a parent without an explicit cacheLife(), the error now attaches the inner invocation as . Changes: - Add NestedDynamicUseCacheError class (captured eagerly at inner call site) - Track hasExplicitRevalidate / hasExplicitExpire on CacheContext - Propagate dynamicNestedCacheError via ??= (first child wins) - Throw with when outer lacks explicit cacheLife for the relevant field - Update error messages to consistently backtick "use cache" - Add unit tests covering revalidate:0, short expire, explicit outer, first-child-wins Refs: #1194 Ported from: https://github.com/vercel/next.js/pull/93707 * docs(cache-runtime): document nested-dynamic error propagation invariants Addresses bigbonk review feedback on #1289: - Guard `Error.captureStackTrace` with a typeof check for non-V8 runtime robustness. The constructor already captures a stack; this call only trims the constructor frame. - Document that propagating the inner's resolved life into the parent's `lifeConfigs` is load-bearing: minimum-wins must reflect the inner's dynamic values for the outer's nested-dynamic detection to fire, and the outer's explicit `cacheLife()` correctly suppresses the throw by pushing the resolved values above the dynamic thresholds. Matches Next.js `propagateCacheLifeAndTagsToRevalidateStore`. - Clarify that `hasExplicitRevalidate` / `hasExplicitExpire` flags really mean "cacheLife() was called and the resolved config includes this field", not "the user explicitly passed this field" — matches upstream which tracks the flag at the work unit store level. - Strengthen the first-child-wins test to assert the outer error message contains "revalidate" (not "expire"), rigorously verifying that `dynamicNestedCacheError ??=` preserves the first dynamic child's eager error as the cause. * fix(cache-runtime): gate nested-dynamic throw on prerender/dev to match Next.js Addresses bigbonk review feedback on #1289 (round 2): - Previously the nested-dynamic-cache error threw unconditionally, even during normal production dynamic SSR. The error messages say "not allowed during prerendering", which is misleading outside the prerender phase. Next.js gates the equivalent check on the work unit type being `prerender` (or `request` in development) — see use-cache-wrapper.ts cases 'prerender'/'request' at the read site. - Gate the throw on `process.env.VINEXT_PRERENDER === "1"` (set by build/prerender.ts during the prerender phase) or NODE_ENV === "development". Production runtime SSR no longer throws; the outer function still executes and its result simply isn't cached (minimum-wins resolves its effective revalidate to 0). - Add a regression test verifying no-throw behavior under production runtime conditions (no VINEXT_PRERENDER, NODE_ENV=production). - Document caller paths (shared MISS, private, dev) on `runCachedFunctionWithContext` — all three reach this helper and propagate lifeConfigs / dynamicNestedCacheError up to the parent, matching Next.js's `propagateCacheEntryMetadata`. Request-scoped recording uses minimum-wins so inner-vs-outer order doesn't matter. - Add a test documenting the potentially surprising minimum-wins semantics: with `cacheLife({ revalidate: 60 })` on the outer and `cacheLife({ revalidate: 0 })` on the inner, the throw is suppressed but the outer's effective revalidate resolves to 0, so the outer's result isn't cached and the function executes on every call. Matches Next.js `propagateCacheLifeAndTagsToRevalidateStore`. - Wrap prerender-gated tests in a `withPrerenderFlag` helper that sets/restores VINEXT_PRERENDER around each test. * fix(cache-runtime): make nested-dynamic error message context-aware; pin dev/prod gate Addresses bigbonk review feedback on #1289 (round 3): - Make the nested-dynamic error messages context-aware: emit "in development" when NODE_ENV=development, "during prerendering" otherwise. The previous unconditional "during prerendering" wording was misleading when the gate fired in dev SSR. Done via two small message-builder functions instead of constants. - Add a dev-mode test ("does throw in development mode even without VINEXT_PRERENDER set") that pins down the other side of the gate. Without this, Vitest's default NODE_ENV=test would mask a regression that broke the gate to e.g. `!== "production"` instead of `=== "development"`. Together with the existing production test, both branches of the gate are now covered. - Document the sequencing invariant between the inner's `parentCtx.lifeConfigs.push(effectiveLife)` and the outer's `resolveCacheLife(ctx.lifeConfigs)`. The correctness depends on the inner's await completing (including its post-execution propagation) before the outer's resolveCacheLife runs. A future refactor away from `await` (e.g. Promise.all with short-circuit) would silently break this and produce stale minimum-wins results. - Document the eager-Error allocation overhead trade-off: matches Next.js (captured eagerly so `cause` points at the original call site), with the lazy-capture optimization noted for future profiling. Verified: tests/shims.test.ts (918 tests), cache + ISR + fetch-cache suites, and full `vp check` all clean. * docs(cache-runtime): clarify gate priority, effectiveLife semantics, and per-field flag note Addresses bigbonk review feedback on #1289 (round 4): - Prioritize VINEXT_PRERENDER over NODE_ENV=development in nestedCacheContextPhrase. If the prerender flag is set, the user is prerendering regardless of NODE_ENV (e.g. a dev-config prerender), so "during prerendering" is the accurate phrase. - Document the semantic difference between checking the outer's merged effectiveLife.revalidate vs the inner's entry metadata (as Next.js does). Behavior is observably equivalent in all cases due to the hasExplicit* guards; we use effectiveLife because vinext doesn't model a CacheResultMetadata type. - Extend the hasExplicit* comment in cache.ts to clarify that the `!== undefined` checks are effectively unconditional (default profile always provides both fields). They remain as defensive guards; per- field suppression would require inspecting the raw user input and would diverge from Next.js semantics, so it's noted as a deliberate design change rather than an incidental one. Bonk's remaining comments are informational: - Comment 2 ("lifeConfigs.push is a behavioral change"): the PR description already calls this out; no code change. - Comment 4 ("NODE_ENV=test gate coverage gap"): explicitly flagged as not blocking; the dev + prod test pair already pins both branches. Verified: tests/shims.test.ts (918 tests) and full `vp check` clean. * docs(cache-runtime): correct hasExplicit guard explanation; note parallel-call invariant Addresses bigbonk review feedback on #1289 (round 5): - Fix incorrect minimum-wins explanation in the throw block comment. The previous wording claimed minimum-wins "drops" the inner's values when the outer has an explicit cacheLife() — that's wrong. Minimum- wins takes Math.min(60, 0) = 0, so the threshold check still evaluates true. What actually suppresses the throw is the `!ctx.hasExplicitRevalidate` / `!ctx.hasExplicitExpire` guard. Future maintainers reading the old comment might have concluded the hasExplicit* guards were redundant; the corrected wording makes clear they are load-bearing. - Also corrected the same misconception in the comment above the `parentCtx.lifeConfigs.push` block ("the outer only suppresses the throw when its own explicit cacheLife() pushed the effective values above the dynamic thresholds"). Replaced with the accurate two-step description: lifeConfigs.push makes the threshold checks possible; hasExplicit* guards independently decide suppression. - Add a sentence to the sequencing-invariant comment noting it holds for both sequential (`await innerA(); await innerB()`) and parallel (`await Promise.all([innerA(), innerB()])`) inner call patterns, since the outer's `await cacheContextStorage.run(...)` only resolves after fn's returned promise settles. - Updated PR description to call out `parentCtx.lifeConfigs.push` as a standalone behavioral change in production (not just during prerender/dev), separate from the error-cause feature. Linked to the minimum-wins documenting test that pins this behavior. Verified: tests/shims.test.ts (918 tests) and `vp check` clean. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
558cec0328 |
fix(shims/script): preload <script> sources during SSR via ReactDOM.preload (#1309)
Mirror Next.js's App Router behavior at
.nextjs-ref/packages/next/src/client/script.tsx:298-376: when SSR-rendering
`<Script src={...} />` with `strategy="afterInteractive"` (the default)
or `strategy="beforeInteractive"`, call `ReactDOM.preload(src, { as: 'script', ... })`
so React Float hoists `<link rel="preload" as="script" href={src} />` into
`<head>`. The script is then fetched in parallel with the streamed HTML.
Before this change, the SSR branch of the Script shim only emitted a
`<script>` tag for `beforeInteractive` and returned `null` for every other
strategy, with no preload. The matching Next.js fixture test
test/e2e/app-dir/app-esm-js/index.test.ts ("should be able to render
nextjs api in app router") asserts:
expect($('head link[href="/test-ext.js"]').length).toBe(1)
expect($('head link[href="/test.js"]').length).toBe(1)
…which fails today because no preload links are emitted.
The existing Image shim already uses this exact React Float pattern
(packages/vinext/src/shims/image.tsx:285-286), so the SSR pipeline
already supports Float hoisting.
Tests:
- Updated existing Script SSR tests in tests/script.test.ts and
tests/shims.test.ts that asserted afterInteractive/default rendered
empty — they now assert the preload <link> is emitted (no <script>
tag), matching Next.js.
- Added tests/nextjs-compat/script-preload.test.ts that ports the
app-esm-js fixture's preload assertions against the app-basic
/script-nonce page (which already exercises afterInteractive and
beforeInteractive strategies via the existing fixture).
- crossOrigin handling matches React's type contract
("anonymous" | "use-credentials" | undefined). React normalises
"anonymous" to `crossorigin=""` in HTML; tests accept both forms.
All shim / script / page / document / nonce tests pass (1151 tests).
|
||
|
|
14ec0a1e62 |
fix(shims): treat draftMode() as non-dynamic; mark dynamic only on enable/disable (#1308)
Calling `draftMode()` and reading `isEnabled` are plain reads and must not opt the route out of static prerendering. Only `enable()` / `disable()` mutate state and need to be tracked as dynamic, matching Next.js's `trackDynamicDraftMode` (see .nextjs-ref/packages/next/src/server/request/draft-mode.ts:152-165). Before this change, any App Router page that called `draftMode()` — even just to check `isEnabled` — set `dynamicUsageDetected = true`, which: - caused build-time speculative prerender to skip the route - caused the request-time ISR HTML write to bail out Tests for the matching Next.js fixture (test/e2e/app-dir/draft-mode/draft-mode.test.ts, "should not generate rand when draft mode disabled during next start") fail today because vinext returns a fresh random value on every request instead of the cached prerender. After this change, the page is prerendered/cached correctly, and `enable()`/`disable()` still bail out of caching as expected. The existing test that enforced the old (wrong) behavior is updated to match Next.js, and dedicated assertions are added that `enable()` and `disable()` each mark dynamic usage. |
||
|
|
c483253b58 |
fix(rsc): allow JSX in .js client modules inside node_modules (#1295)
Many third-party Next.js client libraries ship plain `.js` files containing
`'use client'` (or `'use server'`) plus JSX. Next.js's SWC pipeline compiles
JSX in `.js` transparently, but vinext's `vinext:jsx-in-js` plugin
previously skipped node_modules entirely, so `@vitejs/plugin-rsc`'s
`rsc:use-client` analysis pass would feed the raw source to rolldown/oxc
with `lang: 'js'` and fail:
[plugin rsc:use-client] node_modules/.../client/index.js
RolldownError: Parse failed with 1 error:
Unexpected JSX expression
4: return <p>hello world</p>
This affected the Next.js deploy-suite test fixtures:
- test/e2e/app-dir/next-dist-client-esm-import
- test/e2e/app-dir/client-module-with-package-type
Fix: extend `vinext:jsx-in-js` to also handle `.js`/`.mjs` files inside
`node_modules` *when* they begin with a React directive. We mirror
`@vitejs/plugin-rsc`'s detection by parsing the directive prologue
(skipping BOM, hashbang, comments, and earlier directives like
`'use strict'`), which keeps the transform cost essentially zero for the
vast majority of node_modules files.
Validation:
- vp check (format / lint / type) — clean
- vp test run tests/jsx-in-js.test.ts tests/jsx-in-js-node-modules.test.ts
tests/intercepting-routes-build.test.ts tests/build-optimization.test.ts
- vp test run tests/nextjs-compat/ tests/font-google-build.test.ts
tests/build-time-classification-integration.test.ts
tests/tsconfig-path-alias-build.test.ts (281 passing, 0 regressions)
- vp run vinext#build (clean)
- New tests/jsx-in-js-node-modules.test.ts reproduces the reported error
on main and passes with the fix.
|
||
|
|
3e0ae047fc |
fix(build-report): parse route exports with OXC AST (#1260)
* fix(build-report): parse route exports with OXC AST Route report classification relied on regexes and a hand-written return-object scanner. That made comments look like exports and missed valid static syntax such as no-substitution template literals and numeric separators. The report now parses route modules with oxc-parser, reads top-level static exports from the AST, and keeps getStaticProps revalidate extraction scoped to the exported function body. Tests cover the prior false positives, supported literal forms, TypeScript generic syntax fallback, and Next.js-style export specifier handling. * fix(cli): lazy-load build report generation The CLI imported the route report module at startup, which also loaded the native parser dependency added for build report classification. That widened the parser and native binding path to non-build commands. Load the build report module inside buildApp() immediately before printing the report so dev, start, check, lint, deploy, and help paths do not touch the parser-backed report code. * perf(build-report): reuse parsed route modules Route classification reparsed the same source for each static export query. With the TSX-to-TS fallback, a single route could invoke the parser several times before producing one report row. Thread the parsed OXC Program through internal route-report helpers so classifier paths parse each source once while preserving the public string-based helper signatures. Also clarifies the synthetic getStaticProps wrapper path and the unsupported unary-operator branch. * fix(build-report): use Vite parser export Route report parsing now uses Vite's OXC-backed parser export instead of depending on oxc-parser directly. Adding the parser as a package dependency duplicated the toolchain parser and exposed native optional bindings through vinext itself. Import parseSync and ESTree types from Vite, keep the existing parser options and AST analysis unchanged, and remove the direct package/catalog entries so the lockfile only retains transitive parser usage from dev tooling. |
||
|
|
69c72b02e2 |
fix(build): emit SSR CSS assets referenced by SSR entry (#1304)
Set `build.ssrEmitAssets: true` so the SSR environment writes asset files (CSS, etc.) that JS chunks reference. Vite defaults `emitAssets` to `false` for environments with `consumer: "server"`, so when the CSS plugin rewrites a server-component CSS import into `import "<hash>.css"` and emits the CSS asset, the asset file is subsequently stripped from the bundle by Vite's asset-cleanup hook. The dangling import then crashes `vinext start` with ERR_MODULE_NOT_FOUND on `dist/server/ssr/style.css`. We set the top-level `build.ssrEmitAssets` rather than `environments.ssr.build.emitAssets` because Vite re-applies the top-level value during `resolveConfig` and would otherwise overwrite any per-environment value. Mirrors `@vitejs/plugin-rsc`'s existing `emitAssets: true` on the `rsc` environment. Adds tests asserting both the App Router and Pages Router SSR environments resolve to `emitAssets: true` and that every CSS import in the SSR build output resolves to a real file on disk. |
||
|
|
1d57274ba4 |
test(config): regression coverage for next.config runtime value shapes (#1297)
Adds 12 ported upstream fixtures as unit tests for loadNextConfig,
covering shapes not exercised by the four extension/import tests added
in #1296 (which validates the runner's resolve.extensions + commonjs()
configuration). These complement that coverage by asserting the
returned config value is actually surfaced for shapes that historically
build successfully but resolve to empty/undefined when normalisation
has gaps:
- async default-function export (next-config-ts/async-function)
- plain default object export (next-config-ts/export-default)
- aliased default export, 'export { x as default }' (export-as-default)
- mixed sibling extensions under both type:commonjs and type:module
(next-config-ts/import-js-extensions-{cjs,esm})
- nested-import propagation under both module-type flavours
(next-config-ts/nested-imports-{cjs,esm})
- Node API at module scope under type:module (next-config-ts/node-api-cjs)
- bare-specifier CJS packages (next-config-ts/import-from-node-modules)
Originally this PR also introduced a hand-rolled cjsImportsPlugin that
used createRequire + globalThis stashing to interop CJS siblings. After
#1296 (
|
||
|
|
8de3afdf25 |
fix(config): recognize next.config.mts and interop CJS imports in config loader (#1296)
* fix(config): recognize next.config.mts and interop CJS imports in config
The next.config.ts loader did not recognize next.config.mts and did not
interop CJS imports (.cjs/.cts/.js/.ts files that assign to module.exports)
brought in by the config file. As a result, fixtures from the Next.js
test/e2e/app-dir/next-config-ts-native-mts/ family failed with either
"Cannot find next.config.*" (silent null return) or "module is not
defined" inside Vite's ESM-only module runner.
Two changes:
1) The loader's local CONFIG_FILES list is extended to include
next.config.mts (matching the canonical list in shims/constants.ts,
which already mirrors Next.js packages/next/src/shared/lib/constants.ts)
while keeping .cjs for backwards-compat with existing vinext users.
deploy.ts and check.ts pick up the same extended list so their config
scanners stay in sync.
2) A new Vite plugin (cjsImportsInteropPlugin) is installed on the inline
runnerImport instance. It wraps .cjs/.cts files (and .js/.ts files that
statically assign to module.exports) so that module.exports becomes the
ESM default export, mirroring how Next.js's SWC pipeline auto-converts
CJS imports from inside next.config.ts. The transform is scoped to the
config-load instance only, so it never affects the user's application
module graph.
Tests ported from .nextjs-ref/test/e2e/app-dir/next-config-ts-native-mts/
{nested-imports,import-js-extensions-cjs,import-js-extensions-esm,
import-from-node-modules}/.
* docs(config): clarify CJS exports/module.exports semantics in comment
The previous comment said "Named exports off module.exports are also
reflected", which glosses over the asymmetric behaviour that Node CJS
modules exhibit when user code does `module.exports = X` followed by
`exports.foo = Y`: the latter writes to the discarded original object
and does not show up on the final default export. Rewrite the comment to
spell out the aliasing, the reassignment break, and the
`module.exports.foo` workaround.
* refactor(config): reuse vite-plugin-commonjs for next.config sibling imports
The hand-rolled cjsImportsInteropPlugin duplicated logic that vinext
already pulls in via vite-plugin-commonjs (used globally for application
code in packages/vinext/src/index.ts:795). Drop the bespoke plugin and
install commonjs() on the inline runnerImport instance instead.
Two pieces of configuration are needed for parity with the previous
behaviour:
1. resolve.extensions explicitly includes .cjs and .cts. vite-plugin-commonjs
keys its 'should I transform this id?' check off config.resolve.extensions,
and Vite's default list (.mjs/.js/.mts/.ts/.jsx/.tsx/.json) omits both
CJS-only extensions. Without this, .cjs/.cts imports flowed through
the runner untransformed and still crashed with 'module is not defined'.
2. A filter that:
- Returns false for the config file itself, so cjsGlobalsInjectorPlugin
remains the sole producer of the file's module wrapper (otherwise
both plugins inject 'module = …' and the runner trips on
'Identifier module has already been declared').
- Returns true for ids under node_modules, so bare-import packages
imported by next.config.* (CJS plugin wrappers, etc.) get transformed.
vite-plugin-commonjs defaults to skipping node_modules.
Net diff: -59 lines, all four next-config-extensions tests still pass,
no regression on tests/next-config.test.ts (108 passing) or the broader
deploy/features/shims/check suites (1637 passing in aggregate).
|
||
|
|
aed79d46b0 |
test: regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) (#1291)
* test: add regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) Adds integration and unit tests to verify that catch-all route params are preserved when basePath, rewrites, and middleware are combined. - Integration test in pages-router.test.ts: checks server-rendered __NEXT_DATA__ contains correct page and query for a catch-all route. - Unit test in shims.test.ts: verifies client-side router.query stays intact for a catch-all page under basePath. Both tests are ported from the Next.js test suite and target the regression described in GitHub issue #1196. * test: address review feedback on #1196 regression tests - Use {.*?} regex for consistency with other __NEXT_DATA__ extractions - Drop unused { appRouter: false } option (vinext auto-detects) - Rename shims test to describe what it actually verifies (initial state) - Move vi.resetModules() after window restore for clean teardown ordering * test: add real client-nav test for #1196 catch-all router.query Adds a Router.push() test that mocks fetch and exercises the actual client-side navigation code path: performNavigation → pushState → navigateClient → fetch. The mocked fetch handler captures router state mid-navigation via a Probe component, then asserts router.query.path reflects the destination URL segments after basePath is stripped. This addresses the bonk review feedback that the previous tests only verified initial hydration state and didn't exercise client navigation. The new test also uses the previously-unused buildNavHtml `query` parameter, addressing the dead-signature concern. vinext's navigateClient doesn't construct _next/data/... URLs the way Next.js does, so it doesn't have the architectural pattern that causes #1196 in Next.js. But the route-param extraction path that ultimately produces router.query must still strip basePath from the resolved pathname before matching the catch-all pattern — and that's what this test verifies. --------- Co-authored-by: opencode <noreply@opencode.ai> |
||
|
|
0a77ebd46f |
fix(router): gate intercept matching on Next-URL source pattern (#1302)
Vinext's app-rsc route matcher previously fired interception on any RSC request whose URL matched an intercept target, regardless of the source pathname. Next.js implements interception as a rewrite that is only activated when the Next-URL header matches the *intercepting route* regex (`^<interceptingRoute>(?:/.*)?$`). Without that gate, a direct `.rsc` fetch to an intercept target would incorrectly render the modal page instead of the underlying route, and unrelated sources could pull in modals they have no slot for. This change: 1. Captures the *intercepting route* URL pattern (`sourceMatchPattern`) alongside each `InterceptingRoute` at scan time. The pattern is the slot owner's path with route groups and `@slot` segments stripped, matching Next.js' `extractInterceptionRouteInformation`. The value is propagated through the App RSC manifest. 2. Adds source-pathname filtering to `createAppRscRouteMatcher`'s `findIntercept`: an entry only matches when the provided source pathname satisfies its `sourceMatchPattern` (with descendants allowed). When no source pathname is provided (no Next-URL / X-Vinext-Interception-Context header), no intercept fires. 3. Adds unit coverage in `app-rsc-route-matching.test.ts` and `app-route-graph.test.ts` for the new behavior, including the patterns used by the failing Next.js deploy suite cases (`parallel-routes-and-interception-basepath`, `parallel-routes-and-interception-from-root`, multi-catchall, and two-levels-up). 4. Updates `app-router.test.ts` integration tests that previously relied on the buggy behavior; intercept fires only when the test sends `X-Vinext-Interception-Context` and that context matches the slot's intercepting-route prefix. Cross-references: - https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/interception-routes.ts - https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/generate-interception-routes-rewrites.ts PR #1249 is orthogonal: it carries proof of interception through the planner/lifecycle when the matcher has already fired. This change restricts when the matcher fires in the first place. |
||
|
|
c6e8bda55e |
feat(app-router): support app/global-not-found.tsx (Next.js 16 parity) (#1301)
When users define app/global-not-found.tsx, route-miss 404s should render that module standalone — it provides its own <html>/<body> and replaces the root layout, matching the behavior of Next.js 16's experimental.globalNotFound feature. https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/global-not-found https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/app-render.tsx#L495-L520 Implementation: - Discover app/global-not-found.{tsx,ts,jsx,js} alongside the existing global-error scan (src/index.ts). - Thread the path through generateRscEntry via AppRouterConfig and buildAppRscManifestCode so the generated entry imports it and binds it as globalNotFoundModule. - Pass the module into createAppFallbackRenderer; renderHttpAccessFallback switches to the global-not-found document when statusCode === 404, the route is null (route-miss), and no boundary override was supplied. - Add a skipLayoutWrapping flag to renderAppPageHttpAccessFallback so the standalone document isn't wrapped in the root layout, and so the RSC payload omits layout-id entries that would otherwise refer to a layout tree the markup doesn't contain. Page-triggered notFound() calls keep the existing path: they still resolve through the regular not-found.tsx boundary inside layouts — global-not-found.tsx only takes over for route-miss 404s. Tests: - tests/app-fallback-renderer.test.ts adds four unit tests covering the new branch (route-miss with global-not-found, page-call with global-not-found, absent global-not-found, 403/401 unaffected). - tests/entry-templates.test.ts adds four codegen assertions covering the manifest import and the generated-entry wiring in both directions (present and absent). - tests/nextjs-compat/global-not-found.test.ts ports the basic Next.js fixture as an integration test against a dev server, asserting that the route-miss 404 renders global-not-found's <html> standalone with exactly one <html>/<body>, and that page-call notFound() does not use the global-not-found document. |
||
|
|
f526d59dff |
fix(basepath): thread basePath through metadata, manifest, and server-action redirects (#1300)
App Router file-based metadata URLs (icon, opengraph-image, twitter-image,
apple-icon, favicon, manifest) and server-action redirect targets did not
include the configured next.config basePath. Next.js prefixes basePath into
both surfaces — bakes it into static metadata route URLs at webpack build
time and threads it through the dynamic image loader's pathnamePrefix, and
applies addPathPrefix() to redirect URLs in app-render and action-handler.
Fixes for two of the five Next.js parity gaps surfaced by
test/e2e/app-dir/app-basepath/index.test.ts:
- Metadata image URLs: opengraph-image, icon, apple-icon, twitter-image,
favicon now emit '/<basePath>/<route>/<file>?<hash>' in the rendered
<head>. Applies to both static (build-time resolved) and dynamic
(per-request) metadata file routes via a shared post-resolution helper
in applyFileBasedMetadata. Idempotent: hrefs that already start with the
configured basePath are left untouched. External URLs (http://, https://,
//, data:) are never prefixed.
- Manifest URL: the <link rel="manifest"> href is prefixed with basePath
via the same helper, so an app with basePath='/base' emits
'/base/manifest.webmanifest' instead of '/manifest.webmanifest'.
- Server-action redirect (progressive form POST): the Location header
is computed from the basePath-prefixed redirect target so
redirect('/foo') from a server action mounted at /base/... lands the
browser at /base/foo.
- Server-action redirect (RSC): the ACTION_REDIRECT_HEADER value is
prefixed with basePath. The client-side handler reads this header and
calls window.location.assign/replace verbatim, so the value must
already be a fully basePath-prefixed pathname.
Adds a shared applyActionRedirectBasePath() helper exported from
app-server-action-execution and a prefixMetadataHrefWithBasePath() helper
in file-based-metadata. Both are idempotent, external-aware, and no-op
when basePath is unset.
Surfaces still pending follow-up (tracked separately):
- Server-side redirect() from page renders ALREADY works via
applyAppPageRedirectBasePath in app-page-execution. No change needed.
- RSC fetch URLs ALREADY work because Link, router.push, and
useRouter all funnel through toBrowserNavigationHref() which
prepends basePath before createRscRequestUrl().
- Pages Router redirect basePath (separate code path in
pages-server-entry) — not covered here.
Ported test cases live in tests/nextjs-compat/basepath.test.ts.
See:
https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/loaders/metadata/discover.ts
https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts
https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-basepath/index.test.ts
|
||
|
|
bafbcd4d75 |
fix(server): clear browser globals before SSR user modules (#1285)
Server-rendered user modules can currently observe a partially populated browser global when the runtime provides one. That violates Next.js SSR semantics, where server code must see browser globals as absent, and it breaks common typeof window guards during module evaluation. Add a shared server globals bootstrap that removes window and document before user modules load, preserves the existing AsyncLocalStorage edge-runtime global, and import it ahead of generated App and Pages Router user imports. Cover the runtime behavior and generated import order with focused regression tests. |
||
|
|
329b89e3c3 |
fix(navigation): populate useParams/useSearchParams under Pages Router (#1303)
Under the Pages Router, hooks from `next/navigation` returned empty
objects:
expect(pagesParamsJSON).toEqual({ slug: 'foobar' }) // got {}
expect(pagesSearchparamsJSON).toEqual({ q: 'pages' }) // got {}
Next.js treats `next/navigation` as a cross-router compat shim. The
Pages Router server (render.tsx) and client (client/index.tsx) wrap
each page with SearchParamsContext / PathParamsContext / PathnameContext
providers populated from the router via `adaptForSearchParams` and
`adaptForPathParams` in
.nextjs-ref/packages/next/src/shared/lib/router/adapters.tsx.
vinext drives those hooks from a module-level navigation context that is
only set for App Router renders. For Pages Router renders the context is
null, so useParams / useSearchParams / usePathname fell through to empty
fallbacks.
This change adds a Pages Router compat source published by next/router
through a Symbol.for global handle. next/navigation reads that handle
when the App Router context is null and derives:
- pathname from window.location / SSR asPath
- searchParams from the URL query string only (not merged route params)
- params from the route pattern matched against the resolved path
The publisher is exposed via globalThis[Symbol.for(...)] rather than a
direct module import so navigation.ts does not become a transitive
dependency of every next/router consumer (which would pull in the
window.history.pushState patch at module-load time and break unit tests
that only need next/router).
Ports the behaviour covered by these Next.js tests:
- test/e2e/app-dir/params-hooks-compat/index.test.ts
- test/e2e/app-dir/use-params/use-params.test.ts ("should work on pages
router")
Adds vitest coverage at tests/pages-router.test.ts ("next/navigation
useParams returns only dynamic route params under Pages Router" and
"next/navigation useSearchParams returns only query string under
Pages Router") via a new fixture page at
tests/fixtures/pages-basic/pages/nav-compat/[slug].tsx that mirrors
.nextjs-ref/test/e2e/app-dir/params-hooks-compat/shared/params-component.js.
|
||
|
|
f4a37b4ade |
fix(app-router): track hash-only traversal metadata (#1306)
* fix(app-router): track hash-only traversal metadata Hash-only App Router navigations wrote null history state even though they are app-owned browser-visible entries. That kept traversal direction unknown, but it broke the metadata invariant introduced by the App Router navigation lifecycle work. Route hash-only push and replace through a browser-entry-owned commit hook so the same traversal index allocator and metadata shape are used without fetching RSC. Also update the local traversal index during same-route hash popstate so replace-after-back uses the current entry index. Closes #1252 * fix(app-router): avoid stale hash replace scroll state Hash-only App Router replace now passes the effective scroll option into the browser-entry commit hook. Default-scroll replaces strip vinext scroll restoration keys before adding traversal metadata, avoiding contradictory history entries whose URL points at a hash target but whose state restores the old scroll position. Explicit scroll:false keeps the previous scroll restoration state. Add browser coverage for replace-after-back to #content, including the traversal back to the replaced entry. |
||
|
|
7397d676c6 |
refactor(router): share interception matched-url validation (#1298)
Interception proof construction and AppElements wire parsing enforce the same matched-url path shape. Keeping the predicates separate makes future proof validation changes easy to drift while preserving the same runtime contract today. Extract the shared path validator and use it from both callers. Proof construction still returns null for invalid input, and wire parsing still throws for invalid serialized metadata. |
||
|
|
69165d4557 |
fix(app-page-render): surface invalid dynamic usage errors via flight in dev (#1290)
Wraps the RSC response body in dev mode to check for invalid dynamic usage errors (e.g. cookies()/headers() inside "use cache") after the stream is fully consumed. This mirrors Next.js behavior where workStore.invalidDynamicUsageError is surfaced via the Flight pipeline so errors caught by user try/catch don't silently disappear. - Dedups against React's Flight error chunk using digest presence - Only applies to RSC responses (client-side navigations) in dev - HTML path intentionally deferred; error still flows through rscErrorTracker - Ported from Next.js: https://github.com/vercel/next.js/commit/f5e54c06726b571a042fce67417e40a29f6b8689 Closes #1195 |
||
|
|
6790e3431f |
feat(cache): add static layout proof and variant budget guardrails (#1288)
* feat(cache): add disabled static layout reuse proof Static layout cache artifacts can already carry render observations, but the cache proof model could not prove or reject static layout reuse independently of cache serving. That left private, dynamic, and incomplete observations covered only by downgrade classification, not by the reusable artifact boundary. The missing invariant was that a reusable layout needs positive layout/root/boundary proof and negative request API proof before any future cache authority can consume it. Add a disabled static layout proof evaluator with rejection trace codes for mismatched layout output, unknown root boundaries, private variant dimensions, private/dynamic observations, and missing or observed request APIs. Keep runtime cache reuse disabled by carrying the proof only on the disabled decision. * feat(cache): enforce route variant budget fallback Cache variant construction previously had a lossy scalar route count, so callers could not distinguish a duplicate admissible variant from a genuinely new variant that would exceed the route ceiling. That violated the #726 cache proof invariant that variant cardinality must have an owned breaker path. Route budget admission now tracks canonical variant keys per route, allows duplicate variants without consuming budget, and returns structured breaker fallbacks for route mismatches or over-ceiling variants. Tests cover duplicate admission, second distinct admission, and the over-budget breaker fallback for #726-CACHE-10. * fix(cache): derive static layout proof authority * docs(cache): clarify static proof evidence copy * perf(cache): binary-search route variant keys * refactor(cache): move sorted key search to utils * test(cache): cover static proof boundary mismatch |
||
|
|
8dfd6c8ede |
fix(image): preserve inline styles on remote images (#1287)
* fix(image): preserve inline styles on remote images Remote next/image renders with explicit width and height delegate through UnpicImage, but that branch dropped the user style prop. This made remote images ignore inline styling such as borderRadius, objectPosition, and transform while local images and fill images preserved it. The shim now passes style through the remote constrained Unpic boundary and isolates the Unpic type gap where the runtime supports style but the public type omits it. A focused SSR regression test covers the rendered remote image style contract. * refactor(image): avoid remote style type assertion The previous fix isolated an assertion around @unpic/react because the runtime forwards style but the public component type omits it. Keep the existing dependency shape and pass the runtime-supported style prop through a typed spread object instead. The remote image style regression coverage remains unchanged. |
||
|
|
e8cf0dad13 |
fix(script): honor async false for client scripts (#1286)
Client-side next/script insertion left dynamically created script elements in the browser's forced async state when callers passed async={false}. That breaks ordered third-party script loading because the shim skipped false boolean props instead of clearing the DOM script async flag.
Set script boolean attributes through their DOM properties and explicitly set/remove disabled boolean attributes to clear force async. Cover the regression with a focused handleClientScriptLoad test.
|
||
|
|
16f9ea393f |
fix(app-router): model RSC redirect and traversal lifecycle (#1248)
RSC redirects were followed by mutating browser history before the redirected payload had passed the visible commit lifecycle. That could collapse the initiating history entry and leave back/forward traversal without an explicit intent model. Keep redirect hops inside the initiating navigation until an approved commit publishes history, add explicit terminal redirect decisions, and record per-entry traversal metadata so unknown browser history state stays unknown instead of being guessed. Add focused unit coverage for redirect decisions and traversal metadata plus a browser regression for router.push redirect back navigation. |
||
|
|
8dc6810fb6 |
fix(rsc): preserve exported client reference subpaths (#1257)
Client reference dedupe currently rewrites every package file imported through the RSC package proxy back to the package root. That loses subpath identity when a package exposes client modules through an exported subpath instead of its root barrel. The plugin now derives a bare import specifier from package metadata, preserving exact and pattern exports while keeping private internals on the package root so existing context dedupe behavior stays intact. Regression coverage exercises exported subpaths, scoped packages, export patterns, private internals, and legacy deep imports. |
||
|
|
656d954abf |
feat(router): promote intercepted preservation through planner (#1249)
* feat(router): promote intercepted preservation through planner Intercepted navigation could still preserve source UI through context-bearing payload shape and same-URL history state. That made modal preservation depend on transport metadata instead of the visible route world that had actually been committed. The planner now requires explicit interception proof before preserving source layouts or unrelated slots, and the browser lifecycle clears stale previous-next history state when normal target payloads commit. Wire metadata carries proof, while missing, stale, or incompatible proof falls back to hard navigation. Tests cover approved source-slot preservation, missing, stale, and malformed proof rejection, current-context refresh and action handling, traverse restoration, and direct-target refresh clearing stale interception state. * fix(router): normalize intercepted proof paths Interception proof could be built from browser-derived percent-encoded paths while committed route snapshots used the server-normalized route-state form. Non-ASCII intercepted source or target paths could then fail proof validation and fall back to a hard navigation. Normalize proof matched URLs before encoding source and target route IDs, and normalize client planner snapshot matched URLs to the same route-state representation without changing the user-facing navigation snapshot. Adds regressions for encoded non-ASCII proof generation and planner validation. * review: address ask-bonk PR review comments - Add interceptedRejectedTargetMismatch reason code for target URL mismatch in planner traces (was misusing UnknownSource) - Document double-attempt replaceState fallback for browser quirks - Clarify navigate branches: proven interception vs legacy fallback - Document cache migration safety for legacy interceptionContext - Tighten isInterceptionMetadataValue type guard with string checks - Extract interception attachment into shared step in wire metadata entries to remove asymmetric branching * fix(router): ignore context-only interception metadata Context-only AppElements payloads could enter intercepted preservation planning or leave context-suffixed route identity as later proof authority. That made transport metadata influence visible-world preservation when a normal response was rendered from an intercepted request context. Only explicit interception proof now enters the preservation branch. Planner snapshots also canonicalize context-suffixed route IDs when no proof is present, keeping render keys partitioned while preventing stale context from poisoning future explicit interception proof. Tests cover the direct planner decision and the two-step browser lifecycle regression. * test(router): cover intercepted proof rejection matrix Planner traces now expose distinct rejection reasons for mismatched intercepted targets and incompatible roots. Without direct tests, those branches could regress while the broader intercepted route scenarios still passed. Add focused planner tests for target mismatch and incompatible root rejection, use the local target snapshot consistently, document two lifecycle ordering contracts, and avoid the no-op interception metadata spread on non-intercepted payloads. |
||
|
|
7283e44beb |
fix(router): keep next/router import free of popstate side effects (#1294)
* fix(router): keep next/router import free of popstate side effects Importing next/router currently installs the Pages Router popstate listener as module-level browser state. That listener can be pulled into App Router client bundles by compatibility imports, where it races the App Router history runtime and can turn back/forward traversal into a document navigation. Move Pages Router popstate installation behind an internal vinext runtime module that only Pages Router entrypoints call after hydration setup. Keep the existing popstate decision logic in the router shim, but make global listener ownership explicit at the Pages boundary. Add unit coverage for side-effect-free next/router imports and an App Router e2e regression that imports next/router, traverses history, and asserts no document request occurs. * refactor(router): remove redundant beforePopState assertion The Pages Router popstate handler already narrows the beforePopState callback before invoking it. Call the narrowed function directly so the production router code does not carry an unnecessary type assertion. |
||
|
|
85f2c10fab | chore: update @vitejs/plugin-rsc to 0.5.26 (#1293) | ||
|
|
d558923fde |
fix(script): dedupe concurrent same-src loads (#1263)
* fix(script): dedupe concurrent same-src loads Multiple next/script components with the same src could append duplicate DOM scripts when they mounted before the first script load event fired. That double-executed third-party code and diverged from Next.js' in-flight script cache behavior. The shim now tracks remote scripts that are currently loading separately from scripts that have completed loading, and fans out load/error callbacks without adding another script element. Adds a Pages Router browser regression that verifies simultaneous same-src scripts produce one DOM script and one execution. * fix: address review feedback for script load dedupe - Defer loadedScripts.add(key) to after in-flight promise resolves - Add onReady() callback for duplicate callers joining in-flight load - Clean up loadingScripts entry on promise settlement to allow retry |
||
|
|
ff46514563 |
fix(link): preserve native URI scheme navigation (#1268)
* fix(link): preserve native URI scheme navigation Link treated only http, https, and protocol-relative hrefs as browser-owned. Native schemes such as mailto:, tel:, and sms: were therefore locale-prefixed, eligible for prefetch normalization, and intercepted by client navigation. The URL boundary now mirrors Next.js absolute URL scheme classification while preserving vinext's protocol-relative handling. Link rendering, click interception, relative resolution, and prefetch normalization all share that classification so non-local schemes stay with the browser while same-origin absolute URLs remain routable. Adds focused regression coverage for native scheme rendering, click handling, and prefetch decisions. * refactor(router): reuse shared URL scheme detection Router and navigation shims already had local external URL predicates with the same browser-owned URL semantics now used by Link. Reuse the shared url-utils predicate for Pages Router locale handling, Pages Router external detection, App Router external detection, and App Router prefetch normalization. Add focused shim coverage for native URI schemes through the Pages Router helpers. * test(link): add hasAttribute stub to native URI scheme click event The merge of main introduced a download-attribute check in handleClick that runs before the native-URI early return, so test click events must provide currentTarget.hasAttribute. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
1cd7255881 |
fix(image): emit preload hints for priority images (#1266)
Priority images currently only render eager loading and high fetch priority. That misses the resource hint Next.js emits, so the browser cannot discover the optimized image until it reaches the img element. The image shim now calls ReactDOM.preload with the final optimized image URL, srcset, sizes, and fetch priority metadata for priority and preload images. Focused image component coverage asserts the SSR preload hint and the modern preload prop behaviour. Co-authored-by: James <james@eli.cx> |
||
|
|
e69721f392 |
fix(config): provide CJS globals when loading next.config.ts (#1278)
* fix(config): provide CJS globals when loading next.config.ts
Next.js's `next.config.ts` loader transpiles the file to CommonJS via SWC
and evaluates it through Node's `Module._compile`, which exposes the CJS
globals (`__filename`, `__dirname`, `module`, `exports`, `require`) even
when the source uses ESM syntax. vinext previously loaded the config
through Vite's ESM-only module runner, so configs that read `__dirname`,
called `require()`, or used `module.exports` threw `ReferenceError`s
during build.
Inject the same globals via a small Vite `transform` plugin that runs
only on the `.ts`/`.mts`/`.cts` config entry point. Reassigning
`module.exports` is preserved by exposing the wrapper object as a named
export and reading it back in `unwrapConfig`.
Reference: packages/next/src/build/next-config-ts/{transpile-config,require-hook}.ts
* fix(config): guard CJS-globals injection on static reference check
Skip cjsGlobalsInjectorPlugin's transform entirely when the next.config
source contains no bareword reference to __filename, __dirname, require,
module, or exports. The vast majority of next.config.ts files are pure
ESM (every upstream `test/e2e/app-dir/next-config-ts` fixture except
node-api-cjs is) and pay no cost from the transform.
Substring matching is intentional: a false positive (e.g. the word
`module` inside a comment or `node:module` specifier) costs at most one
wasted, idempotent transform, while a false negative would be a
correctness regression. Bareword `\b` boundaries already exclude
`requireSomething` and `myModule` style identifiers, and `\bexports\b`
does not match `export default`.
* fix(config): skip module.exports wrapper when source uses ESM-only
Statically detect whether a `next.config.ts` source assigns to
`module.exports` (via `module.exports =`, `module.exports.foo =`, or
`module.exports[...] =`) with a new `reassignsModuleExports` helper.
Only emit the `__vinext_cjs_exports` named export and its companion
initial-reference export when that check matches. Pure-ESM configs —
the overwhelming majority — get a slightly smaller injected preamble
and a simpler unwrap path that just reads the ESM `default` export.
The previous sentinel-on-module.exports approach tagged the initial
exports object with a `Symbol.for("vinext:cjs-initial")` property on
every config load and checked for it during unwrap. That worked but
ran unconditionally and left a stray symbol-keyed property on the
final exports object whenever the user reassigned. The new shape only
pays for the wrapper when the source plausibly needs it, and keeps a
referential-identity check (`module.exports !== __vinext_cjs_initial_exports`)
as belt-and-braces so that a `reassignsModuleExports` false positive
inside a string or comment still falls back to the ESM default
instead of returning an empty `{}`.
|
||
|
|
ba1829a804 |
fix(image): clear blur placeholder after image load (#1267)
* fix(image): clear blur placeholder after image load Blur placeholders currently stay on the img background after the high resolution image finishes loading. That is wrong for transparent image formats because the low quality placeholder remains visible through the rendered pixels. The shim treated placeholder styling as static props instead of load lifecycle state. Track blur completion per src, clear local and remote placeholder backgrounds after successful load or error, and cover the transparent-image regression in an app-router browser test. * chore: rerun ci --------- Co-authored-by: James <james@eli.cx> |
||
|
|
da9998a00f |
fix(link): normalise href for trailingSlash config (#1282)
* fix(link): normalise href for trailingSlash config Port `normalizePathTrailingSlash` from Next.js (client/normalize-trailing-slash.ts) into a small util under `shims/url-utils.ts`, wire the resolved `trailingSlash` config through a new `process.env.__VINEXT_TRAILING_SLASH` build-time define, and apply it inside `<Link>` so the rendered `href` matches the canonical URL form instead of relying on a 308 redirect bounce. This unblocks the upstream `e2e/trailing-slashes/*` suite, which asserts on rendered `<Link>` hrefs (not just redirect Location headers). * fix(link): normalise href again after applying basePath Matches Next.js's `addBasePath`, which runs `normalizePathTrailingSlash` after prefixing. Without this second pass, joining a non-empty basePath with the bare root (`/`) under `trailingSlash: false` would render `/foo/` instead of the canonical `/foo`. * Update packages/vinext/src/shims/url-utils.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
fdf3029ea4 |
fix(router): share pages useRouter state through context (#1271)
Pages Router useRouter previously owned a hook-local router snapshot and subscribed each hook call to vinext:navigate. That diverged from Next.js RouterContext semantics and made mounted hook count scale global listeners. Move the reactive snapshot into the Pages Router provider, make next/router consume RouterContext like Next.js, and keep singleton navigation methods as the shared command surface. Add regression coverage for context reads, missing context errors, and per-hook listener scaling. Co-authored-by: James <james@eli.cx> |
||
|
|
d280e3620b |
fix(app-router): skip RSC navigation for hash-only history traversal (#1270)
App Router popstate currently sends every history traversal through __VINEXT_RSC_NAVIGATE__, even when the target pathname and search already match the committed route and only the hash entry changed. That violates the hash-only traversal invariant and replays cached RSC payloads unnecessarily.\n\nCompare popstate targets against the committed App Router pathname and search before dispatching an RSC traversal. Same-route entries now restore scroll directly, while real route traversals keep the existing pending-navigation coordination.\n\nAdd an App Router E2E regression that wraps __VINEXT_RSC_NAVIGATE__ and verifies hash back/forward traversal does not call it. Co-authored-by: James <james@eli.cx> |
||
|
|
be9f338306 |
fix(router): derive shallow dynamic params from the URL (#1272)
* fix(router): derive shallow dynamic params from the URL Pages Router shallow navigation across one dynamic route template updated the browser URL but kept router.query params from the original __NEXT_DATA__ payload. That left client state stale after transitions such as /posts/42 to /posts/43. The router now matches the current pathname against the hydrated route pattern before falling back to __NEXT_DATA__.query. This preserves shallow data-fetch skipping while keeping dynamic params aligned with the visible URL. Tests cover the router shim boundary and the browser-visible Pages Router shallow transition. * refactor(router): reuse route-pattern matching for shallow params The shallow dynamic params fix originally duplicated route-pattern parsing inside the router shim. Reuse the existing routePatternParts and matchRoutePattern helpers instead, keeping the __NEXT_DATA__ fallback behavior unchanged. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
9252ca2966 |
fix(css): pass sassOptions from next.config to Vite preprocessor (#1277)
* fix(css): pass sassOptions from next.config to Vite preprocessor
Next.js threads `sassOptions` from `next.config.js` into sass-loader,
destructuring `prependData` / `additionalData` / `implementation` and
forwarding the rest (e.g. `includePaths`, `loadPaths`) as Sass options.
vinext was dropping this entirely, so SCSS fixtures that rely on
`additionalData` to define variables, `includePaths`/`loadPaths` to
resolve partials, or a custom `implementation` all failed to build
with `Undefined variable` / `Can't find stylesheet to import`.
Resolve `sassOptions` from next.config into `ResolvedNextConfig`, then
translate it onto Vite's `css.preprocessorOptions.scss` (and `.sass`)
shape:
- prependData precedence matches Next.js (prependData || additionalData)
- legacy includePaths is merged into modern loadPaths (Vite uses
modern Sass, which dropped includePaths)
- implementation and any remaining Sass options are passed through
Affected upstream fixtures:
- test/e2e/app-dir/scss/basic-module-include-paths
- test/e2e/app-dir/scss/basic-module-additional-data
- test/e2e/app-dir/scss/basic-module-prepend-data
Refs: cloudflare/vinext CI run 25951111875
Source: packages/next/src/build/webpack/config/blocks/css/index.ts
* fix(css): address sass passthrough review feedback
- Drop `export` on `VitePreprocessorOptions` so Knip stops flagging it
as unused (CI Check blocker).
- Use truthy-OR (`prependData || additionalData`) to match Next.js's
`sassPrependData || sassAdditionalData` exactly — empty-string
`prependData` now falls through to `additionalData` instead of
overriding it.
- Widen `additionalData` type to accept Vite's function form
(`(source, filename) => string | Promise<string>`) and drop the
inaccurate `as string` cast.
- Add tests covering the empty-`prependData` fallthrough and the
function-form `additionalData` passthrough.
|
||
|
|
deef64470b |
fix(app-router): keep refresh transitions pending (#1269)
router.refresh() currently skips the pending browser router state even when it is dispatched from a React transition. That lets useTransition return to idle while the refresh RSC request is still in flight. The refresh path should follow the same App Router invariant as other programmatic navigations: publish a deferred router-state promise before the request and resolve it when the refreshed tree commits. Allow programmatic refresh navigations to create pending router state, and cover the behaviour with a browser regression that observes isPending during a delayed same-route refresh. |
||
|
|
1d66c09e15 |
refactor(edge-globals): consolidate into installEdgeGlobals() helper (#1284)
Replaces the bare-side-effect install in edge-globals.ts with an exported installEdgeGlobals() function, called explicitly from each runtime entry that may evaluate user edge code (App Router worker entry, middleware runtime, Pages API route). Also documents that URLPattern is intentionally NOT installed: Cloudflare Workers expose it natively, and CI now pins Node 24+ (#1283), which has the global URLPattern. The TODO(edge-globals) marker is dropped. |