mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.1
1163 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ebc28f0e1 |
fix: preserve Next.js cache headers for prerendered app pages (#1424)
* fix: preserve Next.js cache headers for prerendered app pages * fix: address cache header review feedback |
||
|
|
82b6388ece |
fix(app-router): render optimistic loading shells for dynamic navigation (#1421)
* fix(app-router): render optimistic loading shells for dynamic navigation Dynamic App Router links currently skip automatic prefetch for route patterns with params, so a navigation to a sibling dynamic URL waits for fresh RSC data before committing anything. That diverges from Next.js, where automatic prefetch learns the loading shell for dynamic routes and the router can commit the shell immediately while the full payload is fetched. The client prefetch path now requests a loading-shell render for dynamic app routes, keeps that entry out of the navigation cache, learns reusable optimistic route templates from settled shell prefetches, and commits a detached optimistic payload for matching dynamic navigations. The server emits shell-only RSC payloads under a separate cache variant so normal navigation data is not polluted. Tests cover matching semantics, shell-only page wiring, cache key isolation, prefetch cache consumption, Link prefetch behavior, and the upstream Next.js optimistic-routing deploy-suite case. * chore(app-router): address optimistic routing review |
||
|
|
c1c51fdbed |
fix(config): warn on deprecated experimental.rootParams (#1417)
has been stable since Next.js stabilized the feature upstream. - Emit a deprecation warning when users still set - Matches Next.js upstream wording for parity Closes #1325 |
||
|
|
3c77b2bb32 | ci: avoid killing deploy-suite test runners during cleanup (#1423) | ||
|
|
81bdd6acd3 |
fix(pages-router): render loading shell for unlisted fallback: true paths (#1414)
* fix(pages-router): render loading shell for unlisted `fallback: true` paths PR #1394 fixed the post-rewrite static-vs-dynamic priority sub-problem from #1331 and explicitly tracked `fallback: true` rewrite behaviour separately. When `getStaticPaths` returns `fallback: true` and a request hits a path not in the pre-rendered list, vinext was SSRing the page fully — running `getStaticProps` and emitting `isFallback: false` in `__NEXT_DATA__`. Next.js instead ships a loading shell with empty props and `useRouter().isFallback === true`, then the client hydrates by fetching the data URL. The upstream regression test `should rewrite to fallback: true page successfully` in `test/e2e/middleware-rewrites/test/index.test.ts` asserts "Loading..." in the SSR HTML, which vinext could not produce. Thread an `isFallback` signal through `resolvePagesPageData`, `buildPagesNextDataScript`, the Pages Router SSR context, and both the prod (`pages-server-entry.ts`) and dev (`dev-server.ts`) renderers: - `getStaticProps` and `getServerSideProps` are skipped on the fallback render (matches Next.js `render.tsx`'s `if (isSSG && !isFallback)` gate around `getStaticProps`). - Data requests (`/_next/data/<buildId>/<page>.json`) still resolve real props so the client can swap in after the shell ships. - `useRouter().isFallback` reads from the SSR context server-side and from `__NEXT_DATA__.isFallback` client-side — both flip to true on the shell render. The dev-server comment that claimed Next.js dev "also renders fully" was incorrect; Next.js's `pages-handler.ts` invokes `doRender` with `isFallback: true` for dev fallback responses, so dev now serves the shell too. Tests: - Updated `tests/pages-router.test.ts` "SSR renders unlisted path with getStaticPaths fallback: true" — previously documented the deviation; now asserts the loading shell, empty props, and `isFallback: true`. - Added a counterpart test that the data URL for the same unlisted path still returns real props from `getStaticProps`. - Added a production-side regression test mirroring the upstream "should rewrite to fallback: true page successfully" expectation. Refs #1331 * test(pages-page-data): include isFallback: false in normalized render assertion The render branch of `resolvePagesPageData` now returns `isFallback` alongside the other fields (added in the parent commit for the `fallback: true` loading shell). Update the strict-shape `toEqual` in the cache-miss unit test so it matches the new return shape. |
||
|
|
512be7f25c |
fix(middleware): preserve route priority on rewrite target resolution (#1394)
* fix(middleware): preserve route priority on rewrite target resolution When `trailingSlash: true` is set in next.config.js, a request like `/rewrite-1/` was matched against the next.config.js rewrite source `/rewrite-1` literally, including the trailing slash. The match failed, so the request fell through to the dynamic `[id]` route and rendered the dynamic page with `id="rewrite-1"` instead of being rewritten to the static target. Strip a single trailing slash from the incoming pathname before matching against config rewrite/redirect/header sources, matching Next.js's `source + '(/)?'` behavior under `trailingSlash: true`. The source pattern itself is preserved so catch-all (`:param*`, `:param+`) and root (`/`) semantics are unaffected. Refs #1331 * address bonk feedback: locale-static path, comment wording, tighter tests - Extract `stripTrailingSlashForConfigMatch` and call it from `matchRedirect` so the locale-static fast path (Map.get on the pathname) also honors trailing slashes — previously a pure locale-static redirect rule could miss when `trailingSlash: true`. - Clarify the comment to reflect that the strip is unconditional defense-in-depth rather than a faithful port of Next.js's conditional `(/)?` suffix. - Disambiguate the pages-router integration tests by asserting on `id="home"` / `id="ssr"` so the assertion can't pass on the wrong page (both pages render "Hello World"). - Add direct unit tests for `matchConfigPattern` trailing-slash behavior covering exact, :param, regex-group, root, and catch-all cases. * extend trailing-slash normalization to matchHeaders Bonk flagged that matchHeaders runs its own compiled regex path that doesn't go through matchConfigPattern, so a header source like `/api/:path*` would miss `/api/foo/` under `trailingSlash: true`. Normalize once at the top of matchHeaders the same way matchRedirect already does, and add a regression test. |
||
|
|
ddfdd18a1a |
fix(pages-router): honour shallow prop on next/link (#1413)
Closes the last sub-problem of #1332. The Pages Router `Router.push` / `Router.replace` already short-circuit the `_next/data` fetch when `options.shallow === true`, but `<Link shallow>` never made it to the router because `shallow` was missing from `LinkProps`. The prop silently fell into `...rest`, was spread onto the underlying `<a>`, and the click handler called `Router.push(href, undefined, { scroll, locale })` with no shallow flag — so every `<Link shallow>` did a full server roundtrip and GSSP re-ran. This matches Next.js's deploy-suite fixture `test/e2e/middleware-trailing-slash/app/pages/shallow.js`, which clicks a `<Link href="/sha?hello=world" shallow>` and asserts the GSSP-generated random message stays unchanged afterwards. The fix is mechanical: add `shallow?: boolean` to `LinkProps`, destructure it in the Link component, forward it through `navigatePagesRouterLink` into the router transition options. `shallow` is only included in the router options when explicitly set so existing callers that omit it keep the same options shape. |
||
|
|
f0f3e35947 |
fix(trailing-slash): enforce config in routing and middleware redirects (#1395)
Plumb `trailingSlash` from the resolved next.config into NextURL via NextRequest, so `NextResponse.redirect(request.nextUrl)` (and any other stringification of a NextURL) emits a Location header that matches the user's slash policy. Mirrors Next.js's `formatNextPathnameInfo`. Pre-route trailing-slash normalization (e.g. `/foo` -> `/foo/` when trailingSlash:true) already runs at every entry point via `normalizeTrailingSlash` in request-pipeline. This commit closes the remaining gap by making redirect Locations produced from inside middleware (the common one-hop `request.nextUrl` redirect pattern in the Next.js middleware-trailing-slash fixture) carry the slash too. Refs #1332. Sub-problem 3 (shallow navigation triggering a server roundtrip) is a separate Pages Router client-side concern and deferred as follow-up. |
||
|
|
d504f022b0 |
fix(assets): default assetsDir to _next/static (Next.js parity) (#1411)
* fix(assets): default assetsDir to _next/static (Next.js parity) Closes #1337. Supersedes #1383. vinext's default `assetsDir` was Vite's historical `assets/`, so URL emission (`/_next/static/...` via `resolveAssetUrlPrefix("")`) and on-disk layout (`dist/client/assets/...`) disagreed in the empty-`assetPrefix` case. The `build.assetsDir` and `experimental.renderBuiltUrl` Vite overrides only applied when `assetPrefix` was configured, leaving the no-prefix branch serving from a different path than the URL contract Next.js's client runtime and test harness assert against. This commit flips the default to Next.js's canonical layout: - `resolveAssetsDir("")` returns `_next/static` (was `assets`) - `build.assetsDir` is now set unconditionally from `resolveAssetsDir` - Vite's default `base + assetsDir` composition produces correct URLs in the no-prefix case; `renderBuiltUrl` stays gated on `assetPrefix` because it's only needed for the configured cases - Drops the legacy `/assets/` branches in prod-server (3 sites) and static-file-cache (hard cutover — see PR description for rationale) - Updates `_headers` generation, precompress default, fonts plugin `DEFAULT_ASSETS_DIR` to match the new default With the layout aligned, invalid `_next/static/*` requests naturally return plain-text `404 + "Not Found"` from the static-file layer instead of falling through to the page renderer (which would produce an HTML 404 with bootstrap scripts and CSS). This replaces #1383's parity short-circuit with the natural code path: - Node prod-server (App + Pages branches): missing asset under `resolveAppRouterAssetPath` returns `text/plain; charset=utf-8` 404 - Cloudflare worker entry: `isNextStaticPath` recognises asset-shape after ASSETS-binding misses; returns `notFoundStaticAssetResponse` Mirrors Next.js: packages/next/src/server/lib/router-server.ts. Adds `tests/invalid-static-asset-404.test.ts` (3 App Router + 3 Pages Router cases — no prefix / basePath / assetPrefix) ported from Next.js e2e suites. Updates 11 source files + 12 test files; 914+ tests pass in the touched suites (asset-prefix, app-router, pages-router, deploy, font-google, static-file-cache, serve-static, precompress, features, routing, isr-cache, build-optimization, app-rsc/ssr, standalone, middleware, shims). Ported from Next.js: - test/e2e/invalid-static-asset-404-app/*.test.ts - test/e2e/invalid-static-asset-404-pages/*.test.ts * fix(e2e): update cloudflare-pages-router hydration spec for _next/static * PR #1411 approved. Clean fix, 2 nits. Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> * refactor(assets): derive isHashed checks from ASSET_PREFIX_URL_DIR Replace hardcoded '_next/static' string literals in runtime code paths with the ASSET_PREFIX_URL_DIR constant so the asset URL convention has a single source of truth. - server/prod-server.ts isHashed: pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`) - server/prod-server.ts public-dir guard: startsWith(`/${ASSET_PREFIX_URL_DIR}/`) - server/static-file-cache.ts isHashed: both startsWith + includes derived - plugins/fonts.ts DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR Behavioural no-op — the constant is '_next/static' so the resolved strings are identical. Centralises the URL contract so future changes to ASSET_PREFIX_URL_DIR (or a refactor that derives it from build config) flow through to every consumer. Doc-comment references to '_next/static' left in place — they describe the canonical value for human readers and are not code paths. --------- Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com> Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> |
||
|
|
6e1669d76d |
fix: warn on blocked javascript: URL navigation (#1372)
Match Next.js's observable behaviour when `<Link>` clicks, `router.push`, `router.replace`, and `router.prefetch` block a `javascript:` (or `data:`/ `vbscript:`) URL. Vinext already prevented navigation, but the `console.error` that Next.js's E2E suite asserts on (`test/e2e/app-dir/javascript-urls`) was missing because: - App Router `router.push`/`replace`/`prefetch` threw synchronously inside `React.startTransition`, so the throw never reached React's event-handler error reporter. - `<Link>` rendered an inert anchor with no `href`, so clicking did nothing — no throw, no log. - Pages Router `next/router` had no `javascript:` guard at all and would fall through to `window.location.assign(...)`. Centralise the matching message in `url-safety.ts` via a new `reportBlockedDangerousNavigation()` helper. `assertSafeNavigationUrl` now logs before throwing, and Link's dangerous-click branch and the Pages Router `performNavigation` both invoke the guard so the throw + console.error pair mirrors Next.js exactly. Closes #1349 References: - packages/next/src/client/components/app-router-instance.ts:345,402,442,460 - packages/next/src/client/components/segment-cache/navigation.ts:537 - packages/next/src/shared/lib/router/router.ts:1025,1057 |
||
|
|
576c55d7db |
feat(pages-router): implement _next/data JSON endpoint (#1384)
* feat(pages-router): implement _next/data JSON endpoint
Recognize `/_next/data/<buildId>/<page>.json` in dev and prod request
pipelines. The URL is normalized to the page path BEFORE middleware
runs (matching Next.js' `handleNextDataRequest`), and the server emits
`{ pageProps }` as JSON with `Content-Type: application/json` instead
of the rendered HTML.
Closes #1330
* fix(pages-router): address bigbonk findings on _next/data endpoint
- Delegate JSON envelope construction from the generated entry to the
typed `buildNextDataJsonResponse` helper so the envelope shape lives
in one place (entries stay thin per AGENTS.md).
- Return a JSON 404 from `resolvePagesPageData` when `getStaticPaths`
with `fallback: false` rejects the path for a data request. Without
this, client-side navigations to unlisted slugs got an HTML 404 body
which fails to parse as JSON in the Next.js client router.
- Apply the same JSON-404 gating in the dev server for
`getStaticPaths fallback:false`, `getServerSideProps.notFound`, and
`getStaticProps.notFound` paths.
- Read the dev buildId from the plugin's resolved `nextConfig.buildId`
first so a user-supplied `generateBuildId` is honored, matching the
value embedded into the prod server entry.
- Add coverage for `getStaticProps` data requests and the
`fallback:false` JSON-404 path.
* chore: drop unused NextDataMatch export (knip)
* fix(pages-router): JSON 404 for dev stale buildId; HTML 404 for gSSP/gSP notFound on HTML nav
Follow-ups from bigbonk round 2:
- Dev request pipeline now returns a JSON 404 when `_next/data/<wrong>` is
hit, mirroring the prod-server path. Previously the dev fall-through
served Vite's HTML 404, breaking client routers that try to parse the
body as JSON.
- `resolvePagesPageData` now gates the gSSP/gSP `notFound: true` 404 body
on `isDataReq` so HTML navigations get the HTML 404 page and data
requests get the JSON envelope. Previously both shapes always returned
the JSON body, which surfaced text-content-type mismatches on HTML
navigations through gSSP/gSP `notFound`.
|
||
|
|
fb7a2f795b |
fix(rsc): redirect returns flight payload (200) for RSC navigations (#1390)
* fix(rsc): redirect returns flight payload (200) for RSC navigations When a server component calls `redirect()` and the client makes an RSC navigation request (`.rsc` URL), encode the redirect as a React error chunk inside the flight stream and return HTTP 200 instead of a raw 307. The client's `RedirectErrorBoundary` decodes the `NEXT_REDIRECT;...` digest and performs the navigation — matching Next.js's behavior in `app-render.tsx generateDynamicFlightRenderResult`. `generateMetadata()` redirects now always return 200 with a flight payload (both RSC and SSR document requests), because metadata is suspended/streamed in Next.js — its redirects ride inside the flight body, never the HTTP status line. Page-component redirects on SSR document requests still emit a 307, unchanged. Closes #1347 * fix(rsc): preserve soft-nav and useTransition pending across flight redirect The previous commit returned a 200 + flight payload for RSC `redirect()` responses but left vinext's client-side navigation loop without a way to detect the redirect — the 200 response fell through to React's render, threw at decode time, and hit the catch path's `window.location.href` hard navigation. That tore down the page mid-transition and clobbered `useTransition`'s isPending state. Add an out-of-band header (`X-Vinext-Rsc-Redirect`) that carries the redirect target alongside the flight body. The browser-entry loop now detects the header right after the existing `resolveRscRedirectLifecycleHop` (307-following) check, drains the body, and `continue`s the loop with the new href. The same `pendingRouterState` spans both hops, so the outer transition stays pending continuously — restoring pre-1347 behavior. Also stamp the build-time `X-Vinext-Rsc-Compatibility-Id` header on the flight-redirect response. Without it, `resolveRscCompatibilityNavigationDecision` treats the response as cross-build and hard-navigates before the redirect header is read. Export `MAX_RSC_REDIRECT_DEPTH` from `app-browser-rsc-redirect` so the new header path can reuse the same hop-limit ceiling. |
||
|
|
b06a1a1d04 |
fix(i18n): strip locale prefix for API routes (#1408)
* fix(i18n): strip locale prefix for API routes Mirror Next.js's behaviour where a locale-prefixed request path like `/fr/api/ok` is matched against `pages/api/ok` after stripping the locale segment. Previously the locale prefix was kept through the `/api/` startsWith check, so any locale-prefixed API URL 404'd. Add a small helper `stripI18nLocaleForApiRoute(url, i18nConfig)` in `server/pages-i18n.ts` that mirrors Next.js's `normalizeLocalePath().pathname` (see packages/next/src/shared/lib/i18n/normalize-locale-path.ts) and wire it into all three Pages Router request pipelines so dev/prod parity holds: - dev plugin: `packages/vinext/src/index.ts` - prod (Node): `packages/vinext/src/server/prod-server.ts` - prod (Cloudflare worker entry): `packages/vinext/src/deploy.ts` App Router is intentionally unchanged: Next.js does not support the `i18n` config field with App Router, so App Router route handlers under `app/*/route.ts` never see a locale prefix to strip. Refs #1336 (item 3). Items 2 (sticky locale on client navigations) and 4 (default-locale path normalisation) are being handled in parallel PRs. Tests: - Unit tests for `stripI18nLocaleForApiRoute` covering basic, regional (`nl-NL`), query-preserving, unconfigured-prefix, and null-config cases (`tests/pages-i18n.test.ts`). - Dev integration tests (`tests/features.test.ts`) and prod integration tests (`tests/pages-i18n-prod.test.ts`) that GET `/fr/api/ok` and assert a 200 "ok" response against the existing `pages-i18n-domains` fixture, ported from Next.js's `test/e2e/middleware-redirects/test/index.test.ts` ("should redirect to api route with locale"). - Updates to `tests/deploy.test.ts` assertions for the generated worker-entry template strings. * test(after-deploy): update worker-entry assertion after locale strip The Pages Router worker entry now calls handleApiRoute(request, apiLookupUrl, ctx) instead of using resolvedUrl directly, so this generated-code assertion needs the new variable name. Refs #1336 (item 3). |
||
|
|
b7f7ad649d |
fix(basepath): enforce scoping on rewrites/redirects/routes (#1397)
* fix(basepath): enforce scoping on rewrites/redirects/routes Sub-issues addressed under #1333: 1. Rewrites/redirects/headers were firing on requests outside the configured `basePath`. Threaded a `BasePathMatchState` through the matchers and prod-server / deploy worker / dev plugin so default rules (no `basePath: false` opt-out) only evaluate when the request was under basePath, and opt-out rules only evaluate outside it. 5. Pages Router requests that landed outside basePath fell through to internal route matching. After redirects and beforeFiles rewrites run, the request now 404s when it was outside basePath and no `basePath: false` rule rewrote it — matching Next.js's `resolve-routes.ts:304-309`. 4. With rewrites no longer firing on out-of-basepath requests, the downstream code that prepends basePath to destinations no longer sees a stripped-but-already-internal pathname, eliminating the `/docs/docs/...` doubling for the rewrite-driven cases. App Router uses the same gating; `basePath: false` rules don't yet fire there because `normalizeRscRequest` 404s out-of-basepath requests before the matchers see them — flagged as follow-up in the rule gating site. External rewrite proxying (sub-issue #2) and static asset doubling (sub-issue #3, partly addressed by #1337/#1383) are deferred. Refs #1333 * test: update deploy.test signature assertions and format basepath tests * review: address bonk nits — add inverse basePath:false tests and TODO comment |
||
|
|
371817a07b | chore: upgrade vite plus (#1410) | ||
|
|
f5e2105b94 |
fix(error): match Next.js default error page UI (#1402)
When no `app/global-error.tsx` (or route-level `error.tsx`) is defined, vinext's runtime returned null from the error boundary fallback, which caused the request to bubble up as a generic 500 instead of rendering the styled default error page Next.js ships out of the box. Port Next.js's built-in default global error component (`packages/next/src/client/components/builtin/global-error.tsx`) to `vinext/shims/default-global-error.tsx`, including the 32x32 warning icon, the "This page couldn't load" heading, theme CSS, the Reload form button, the Back button (client errors only), and the `ERROR <digest>` footer (server errors only). Wire this default in via a small module wrapper (`server/default-global-error-module.ts`) so both the runtime error boundary fallback (`app-fallback-renderer.ts`) and the route-level client `<ErrorBoundary>` wrapper (via `app-page-element-builder.ts`) fall back to it whenever `globalErrorModule` is null. User-defined global error modules continue to take precedence. Closes #1362 |
||
|
|
818093b7d9 |
fix(i18n): honor locale: false on rewrites/redirects + default-locale redirect (#1382)
* fix(i18n): honor locale: false on rewrites/redirects
When a rewrite or redirect carries `locale: false`, Next.js leaves the
source pattern untouched so user-supplied `:locale` segments can capture
the locale prefix themselves. Without `locale: false`, Next.js prepends
an internal locale alternation so the rule matches every locale-prefixed
URL automatically.
Vinext previously ignored the flag entirely and never injected the
locale alternation, so:
- rewrites with `locale: false` on a source like
`/:locale/rewrite-files/:path*` failed because the underlying
matcher could not handle a non-trailing named param combined with a
trailing catch-all;
- plain rewrites/redirects with i18n configured silently failed to
match locale-prefixed paths.
This change:
1. Adds `locale?: false` (and `basePath?: false` for forward
compatibility) to NextRedirect/NextRewrite.
2. Fixes matchConfigPattern to use the regex branch whenever a pattern
has multiple named params and a catch-all.
3. Adds applyLocaleToRoutes mirroring Next.js's processRoutes — emits
`/:nextInternalLocale(en|sv|nl)/...` and (for redirects) per
default-locale literal variants while skipping rules flagged with
`locale: false`. Retains the original unprefixed source as a
fallback so default-locale requests that arrive without a prefix
still match (Next.js handles this upstream via request path
normalisation; doing it here avoids a server-wide change).
Ported tests from
test/e2e/i18n-ignore-rewrite-source-locale/rewrites.test.ts
Refs #1336 — items 2, 3, 4 remain as follow-ups.
* fix(i18n): address Bonk review feedback
- Keep the locale-capture variants emitted by applyLocaleToRoutes on the
redirect index fast path. `_LOCALE_STATIC_RE` now matches both the
optional `/:locale(en|fr)?/foo` user-written form and the mandatory
`/:nextInternalLocale(en|fr)/foo` form. Each entry records whether the
locale segment is optional; the no-locale-prefix branch only fires for
optional entries so mandatory variants still require a prefix.
- Thread `trailingSlash` through `applyLocaleToRoutes` so the root source
is preserved (`/:nextInternalLocale(en|fr)/`) when trailingSlash is
configured, matching Next.js processRoutes.
- Promote the dynamic import in next-config.ts to a static import — the
module is already loaded for isExternalUrl.
- Drop the unnecessary `new Set([...])` dedup and document that the
defaultLocales list will grow once domain-locale support lands.
|
||
|
|
2d035d47bd |
fix: await top-level await module imports (#1381)
* fix: make next/document default export a class Next.js's `next/document` default export is `class Document extends React.Component`. vinext's shim exported a function, so user `_document.tsx` files that use the class-based form (the common Next.js idiom: `class MyDocument extends Document`) ended up extending a function. React refuses to call class constructors without `new`, which caused SSR to throw "Class constructor MyDocument cannot be invoked without 'new'" — surfacing in e2e as 500 / empty responses. This is the actual root cause of the "async modules render empty pages" report (issue #1361): the Next.js `test/e2e/async-modules` fixture's `pages/_document.jsx` uses `class extends Document`, and once it crashes the whole page renders empty. Top-level await itself propagates correctly through Rolldown's static-import graph already. Adds a class-based Document shim that mirrors Next.js's signature (static `getInitialProps`, instance `render()`), plus production-build e2e coverage for pages, `_app.tsx`, `getStaticProps`, `getServerSideProps`, API routes, and class-based `_document.tsx`, all using top-level `await`. Closes #1361 Ports from Next.js: test/e2e/async-modules/index.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts * fix(next/document): align Document base class with Next.js's typed contract Addresses Bonk review feedback on PR #1381: - Export DocumentContext / DocumentInitialProps types matching Next.js, so subclasses that delegate via `await Document.getInitialProps(ctx)` receive typed objects. The runtime path is still a stub (the Pages Router renderPage/defaultGetInitialProps chain is not wired up), but the signatures pin the contract. - Declare the default Document class export in next-shims.d.ts so strict-mode consumers don't see `any` for `import Document from "next/document"`. - Add a class-extends regression test in tests/document.test.ts — this is the contract that broke in issue #1361 (the original Next.js `pages/_document.jsx` uses `class extends Document`). - Switch the class generic default from `unknown` to `{}` to match Next.js's `class Document<P = {}>`. - Clarify App Router fixture comment: the upstream Next.js async-modules suite is Pages-Router-only, so this is adapted, not ported verbatim. |
||
|
|
d92d4ff2ab |
fix(css): support data URL composes in CSS modules (#1392)
Turbopack supports importing inline CSS via `data:text/css,...` and `data:text/css+module,...` data URLs. Vite/Rolldown treats `data:` specifiers as external, so the literal URL was reaching the bundled output and Node/workerd refused to load it with `ERR_UNKNOWN_MODULE_FORMAT: Unknown module format: text/css+module`, breaking the entire build for any app that adopted the Turbopack-only syntax (e.g. test/e2e/app-dir/css-modules-data-urls). Add a pre-transform that rewrites these imports into synthetic `\0vinext-data-css/<hash>.module.css` (or `.css`) specifiers. Vite's existing CSS pipeline (LightningCSS, CSS modules) then handles class-name hashing, JS export map generation, and stylesheet asset emission. The decoded payload is cached by synthetic id so the same data URL imported from RSC and client environments deduplicates the way real CSS imports do. Closes #1363 |
||
|
|
ab37cc661e |
fix(middleware): allow server-only imports (#1376)
Middleware runs server-side, so importing `server-only` is semantically
correct. However, vinext bundles middleware into the SSR environment
via `virtual:vinext-server-entry`, and @vitejs/plugin-rsc's
`rsc:validate-imports` treats any non-RSC environment as a "client"
build, rejecting the bare specifier with:
'server-only' cannot be imported in client build ('ssr' environment):
imported by middleware.js
imported by virtual:vinext-server-entry
imported by virtual:vinext-app-ssr-entry
Next.js solves this with webpack `issuerLayer` rules: middleware sits
in the `neutralTarget` layer where `server-only` aliases to a no-op
while `client-only` still errors
(packages/next/src/build/webpack-config.ts).
Vite has no per-layer aliasing within an environment, so we mirror the
behavior with import-chain taint tracking: seed a Set with the
middleware entry path, propagate the taint along the resolveId graph,
and short-circuit `server-only` to the existing no-op shim when the
importer is tainted. The plugin uses `enforce: "pre"` so its resolveId
beats plugin-rsc's `order: "pre"` validate-imports filter on the bare
specifier.
Closes #1344
|
||
|
|
bc586788a2 |
fix(middleware): preserve query params on rewrite (#1375)
* fix(middleware): preserve query params on rewrite When middleware (or a `next.config.js` rewrite) sends a request to a new target, vinext was throwing away the original request's query string, breaking `getServerSideProps`/route-handler `query` for any URL of the form `/rewrite-me?key=value`. Match Next.js's behaviour: `Object.assign(parsedUrl.query, rewrittenParsedUrl.query)` — keep the original query first, let the rewrite-target's query win on key conflicts. Applied to: - middleware rewrites (middleware-runtime.ts) - beforeFiles / afterFiles / fallback config rewrites (prod-server.ts, index.ts, deploy.ts) Closes #1342 References: - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/route-module.ts (Object.assign merge) - https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts (rewrite query expectations) * test(query): document URL-encoded space normalization on rewrite merge Addresses Bonk review observations on #1375: - Add test confirming `%20` → `+` normalization through URLSearchParams is intentional and lossless (downstream consumers re-parse). - Drop unreachable `?? ""` fallback in mergeRewriteQuery; `String#split` always yields a string. |
||
|
|
24a1bd4edc |
fix(middleware): redirect protocol — relative Location and x-nextjs-redirect (#1377)
* fix(middleware): redirect protocol — relative Location and x-nextjs-redirect Match Next.js's edge adapter behaviour for middleware redirects: 1. Relativize the `Location` header for same-host redirects. Next.js only emits absolute URLs when the redirect target is cross-origin; vinext was always emitting `http://host/path` for same-host targets, which broke deploy-suite parity tests. 2. Translate redirects to the `x-nextjs-redirect` soft-redirect protocol when the request carries `x-nextjs-data: 1`. The client router uses this header instead of a raw HTTP 3xx to avoid CORS issues on cross-origin data fetches. Both fixes live in `executeMiddleware`, so all four call sites (prod-server, dev plugin, deploy worker, app-middleware) inherit them. Refs #1334 * fix(middleware): thread isDataRequest through all callers Address Bonk review. The previous commit checked `x-nextjs-data` on the middleware request, but that header is in INTERNAL_HEADERS and is stripped by `filterInternalHeaders` before any caller constructs the middleware request -- so the soft-redirect branch was unreachable. Make `isDataRequest` an explicit option on `ExecuteMiddlewareOptions` and `ApplyAppMiddlewareOptions`, and capture it from the raw incoming headers in each of the four entry points (dev plugin, prod server, deploy worker, App Router RSC handler) before filtering. The generated Pages Router runMiddleware now accepts a third `options` argument carrying the flag through to the runtime helper. Also: drop redundant lowercase `headers.delete("location")` -- the Fetch spec makes Headers.delete case-insensitive. Add a regression test ensuring a forged `x-nextjs-data: 1` request header cannot trigger the soft-redirect path without the caller explicitly opting in. Refs #1334 |
||
|
|
7e060b98f7 |
feat(link): support onNavigate prop (#1371)
vinext already exposes `<Link onNavigate>` and the matching `NavigateEvent`
contract (`{ url, preventDefault(), defaultPrevented }`), but the Next.js
deploy suite still fails the `replace history state for external links
with replace prop` case: when `<Link>` points at a truly external URL and
has `replace` set, vinext falls through to the browser's default click
navigation, which pushes a new entry instead of replacing the current one.
This mirrors `linkClicked` in
`.nextjs-ref/packages/next/src/client/link.tsx` for the external + replace
branch: call `e.preventDefault()` and `window.location.replace(href)` so
the entry is replaced rather than pushed.
Adds focused unit tests around the onNavigate contract: onClick + onNavigate
fire for internal clicks, `preventDefault()` cancels the navigation, and
onNavigate is skipped for modifier-key clicks, target=_blank, download
links, and external URLs (with and without `replace`).
Closes #1357
|
||
|
|
0fbbe33a2e |
fix: pass params: null (not {}) for non-dynamic routes (#1374)
Next.js exposes `params` as `null` (effectively — via `params || null` or `params ? await params : null` user-side) to route handlers and `getServerSideProps`/`getStaticProps` on routes with no dynamic segments. vinext was passing an empty object, which is truthy and breaks user code that branches on `params`. The fix computes a user-facing params value at each dispatch boundary (`null` when `route.isDynamic === false`, otherwise the matched object) while leaving internal bookkeeping — navigation context, query merging, `useParams()` — on the empty-object shape that those consumers expect. References the Next.js test suite assertions: - test/e2e/app-dir/app-routes/app-custom-routes.test.ts "does not provide params to routes without dynamic parameters" - test/e2e/edge-pages-support/index.test.ts "should have correct query/params on index" Closes #1350 |
||
|
|
517c2bde19 |
feat(pages-router): expose window.next.router on hydration (#1400)
* feat(pages-router): expose window.next.router on hydration
Promote next/router from a lazy dynamic import inside hydrate() to a
static side-effect import at the top of the generated client entry, so
window.next = { version, router, ... } is published before any test
harness or third-party script reads it.
Without this, the Next.js deploy test suite — which drives client-side
navigations via `browser.eval('next.router.push(...)')` — crashes with
`TypeError: Cannot read properties of undefined (reading 'router')` (~80
test failures across middleware-general, middleware-rewrites,
trailing-slashes, basepath, with-router, and ignore-invalid-popstateevent).
Mirrors Next.js's bootstrap order in
.nextjs-ref/packages/next/src/client/next.ts (line 5–13), which
statically imports the router from './' and sets window.next as a
top-level side effect before initialize/hydrate.
Closes #1329
* feat(router): expose state fields as live getters on Router singleton
window.next.router must expose `pathname`, `route`, `query`, `asPath`,
`basePath`, `locale`, `locales`, `defaultLocale`, `domainLocales`,
`isReady`, `isPreview`, and `isFallback` as live properties — not just
the navigation methods. The Next.js deploy test suite drives navigations
via `browser.eval('next.router.push(...)')` and then asserts on
`browser.eval('next.router.pathname')` to verify the navigation
completed; without the field getters the assertion reads undefined.
Mirrors Next.js's `singletonRouter` shape in
.nextjs-ref/packages/next/src/client/router.ts (lines 32-47), which
uses Object.defineProperty for urlPropertyFields so reads forward to
the active router state.
Strengthens the existing shims.test.ts coverage so future regressions
of the field surface fail at unit-test time.
|
||
|
|
c66b86e8f5 |
fix(after): wire next/after to Workers ctx.waitUntil in deploy mode (#1403)
Pages Router API routes did not receive the per-request Workers `ExecutionContext`, so any `after()` (or other shim) call inside a `pages/api/*` handler had no `ctx.waitUntil()` to keep the isolate alive past the response. Thread `ctx` through `handleApiRoute` → `handlePagesApiRoute`, and wrap the user handler in `runWithExecutionContext(ctx, ...)` so the ALS surfaces the ctx for downstream `after()` calls. Also pass a Node-shaped execution context from `vinext start` so behavior is consistent across the dev / start / deploy server paths. Closes #1365 |
||
|
|
fb927fdb00 |
feat(form): apply basePath and route via Pages Router for soft nav (#1387)
The `next/form` shim was rendering raw `action` strings without applying basePath, and its Pages Router fallback bypassed the Router singleton — meaning route events, HTML fetch, and scroll handling didn't fire on form submission. Bring it in line with `next/link`: - Prefix the form-level `action` prop with `__NEXT_ROUTER_BASEPATH` (matches Next.js `addBasePath(actionProp)`). A submitter's `formAction` is intentionally left alone, matching upstream. - In Pages Router, delegate to `next/router` Router.push/replace so the navigation flows through `performNavigation` like Link does. Keep the pushState+popstate path as a recovery fallback only. Closes #1355 |
||
|
|
b01fe032fa |
fix(pages-router): avoid getServerSideProps identifier collision in build (#1370)
When a page declared `getServerSideProps` (or `getStaticProps` /
`getStaticPaths`) as a local binding and re-exported it via
`export { getServerSideProps }`, the client-bundle transform overwrote
the `export { ... }` statement with `export const getServerSideProps =
undefined;`. The original local `const getServerSideProps = ...` was
left in place, producing two declarations of the same identifier in
module scope. Rolldown/OXC rejects this with `Identifier
'getServerSideProps' has already been declared`, breaking the entire
production build (and cascading to 48 test failures in Next.js's
`test/e2e/getserversideprops` suite).
Drop the export specifier without emitting a stub, matching Next.js's
`next-ssg-transform` behaviour. The now-unused local declaration becomes
dead code and is tree-shaken later.
Closes #1354
|
||
|
|
b8617a4c40 |
fix(pages-router): wrap edge API request in NextRequest, support bare runtime export (#1391)
Two parity gaps with Next.js were causing edge runtime API routes to fail
with 500 errors:
1. Edge API handlers received a plain `Request` object, so accessing
`req.nextUrl.searchParams` (the idiomatic pattern in Next.js edge API
routes) threw at runtime. Next.js wraps the request in `NextRequest`
before invoking the handler (NextRequestHint in
next/src/server/web/adapter.ts).
2. Only `export const config = { runtime: 'edge' }` was recognised as an
edge runtime declaration. Next.js also accepts a bare
`export const runtime = 'edge'` and resolves the effective runtime as
`config.runtime ?? config.config?.runtime`
(next/src/build/analysis/get-page-static-info.ts).
Both fixes are applied in dev (`server/api-handler.ts`) and prod
(`server/pages-api-route.ts`) so the two paths stay in parity.
Refs #1338
|
||
|
|
7771099d1d |
fix(pages-router): guard navigation shim against SSR (#1378)
`performNavigation()` and the `Router` singleton's `back`/`reload`/ `prefetch`/`beforePopState` methods referenced browser globals (`window`, `document`) unconditionally. Invoking `router.push()` from a Pages Router component during SSR or prerender (`VINEXT_PRERENDER=1`) therefore crashed with `ReferenceError: window is not defined` and took down the request pipeline. Mirror Next.js's `ServerRouter` (packages/next/src/server/render.tsx), where every navigation method synchronously throws "No router instance found". The throw happens before any browser global is accessed, so the failure surfaces as a normal render error instead of a ReferenceError — and the documented message + docs link keeps userland error handling intact. Closes #1353 |
||
|
|
f61de5d39a |
fix(parallel-routes): resolve nested slot pages over default.tsx (#1389)
* fix(parallel-routes): resolve nested slot pages over default.tsx When two sibling route groups occupy the same URL pattern (e.g. `(group-a)/layout.tsx` next to `(group-b)/page.tsx`), the layout-only sibling was dropped entirely because its pattern collided with the page route, taking its parallel slots with it. Nested pages inside its `@slot` directories were never materialized, so URLs that only the catch-all slot could handle returned 404 / not-found instead of rendering the matched slot page. This change keeps the dropped layout-only routes as "ghost parents" that participate in `discoverSlotSubRoutes`. Their slot sub-pages still mint synthetic sub-routes (carrying the ghost's layout chain and slot metadata), but the ghost itself is not added to the route table to preserve the existing single-route-per-pattern invariant. Closes #1339 Ported from Next.js tests: - test/e2e/app-dir/parallel-routes-catchall-groups * test(parallel-routes): cover shared-root URL of sibling route groups Adds an e2e assertion that /parallel-group-catchall (the URL shared by both (group-a) and (group-b)) renders (group-b)/page.tsx, not the (group-a) ghost or its catch-all slot. Closes the coverage gap called out in code review for the existing /foo and /bar tests. |
||
|
|
086c6e8b52 |
fix(app-router): parse interception route markers in route scanner (#1398)
* fix(app-router): parse interception route markers in route scanner Refs #1364 * test(app-router): cover (.) and (...) markers, fix JSDoc typo Addresses Bonk review feedback on #1398: add explicit coverage for the remaining two interception marker shapes when used outside `@slot`, and remove the stray quote in `isInterceptionMarkerDir`'s JSDoc. |
||
|
|
5d397d5f4b |
fix(metadata): emit metadata from parallel route slots (#1399)
When the route has no children page providing a title (parallel layout
that doesn't render `{children}`, or a parent that only has
`default.tsx`), allow the active parallel slot page to contribute the
primary <title>. Previously every slot was forced with
`contributesTitle: false`, which suppressed the slot title even when no
primary page title existed — producing an empty title or stale layout
title for these routes.
Slots keep the existing suppression when the matched page module already
provides a title, preserving the modal/sidebar case where the slot
metadata must not clobber the page title.
Refs #1341
|
||
|
|
a4ec9afeeb |
fix(actions): return 404 + x-nextjs-action-not-found for missing actions (#1386)
Treat both build-time error shapes raised by `@vitejs/plugin-rsc` when a server-action reference is missing from the manifest — dev's `[vite-rsc] invalid server reference '<id>'` and prod's `server reference not found '<id>'` — as Next.js action-not-found. Previously only the dev-mode message was recognised, so production builds (and apps with no server actions defined at all) returned 500 instead of Next.js' 404 + `x-nextjs-action-not-found: 1` response. Both the fetch-action and progressive (no-JS) code paths benefit, since the latter calls `isServerActionNotFoundError(error, null)` before it has resolved an action id. Refs #1340 |
||
|
|
912369840c |
fix: usePathname returns canonical URL after middleware rewrite (#1379)
When middleware or a next.config.js rewrite redirects an external URL to an internal route target, `usePathname()` (and the navigation context that backs it) was returning the internal rewrite destination instead of the canonical/external path the user requested. Preserve the pre-rewrite pathname as `canonicalPathname` and use it when populating the App Router navigation context, while leaving `cleanPathname` to drive internal route matching. This matches Next.js behavior: test/e2e/app-dir/hooks/hooks.test.ts — "should have the canonical url pathname on rewrite". Closes #1348 |
||
|
|
5748be3ba5 |
feat(cache): gate planner commits on explicit reuse proof (#1327)
* feat(cache): gate planner commits on explicit reuse proof Runtime cache entries could enter browser commit planning as ordinary payloads even when the entry had no planner-visible reuse proof. That violated the #726 invariant that a cache hit is not semantic authority by itself. The planner now receives a compact cache-entry proof decision through AppElements metadata and rejects missing or incompatible cache entries before route-topology commit approval. Cache-restored browser payloads synthesize the missing-proof rejection when metadata is absent, while fresh payloads keep the existing path. Targeted tests cover proof projection, metadata transport, planner decisions, and the cached-payload lifecycle path. * fix(cache): skip unproofed visited RSC cache restores Visited App Router RSC snapshots were still eligible for replay even though production payloads do not yet carry cache-entry reuse proof. That let the new planner gate reject the cache path and turn soft restores into hard navigations. Require decoded proof metadata before a payload is stored as a visited-response restore candidate, and update the real app-router navigation test to prove unproofed responses refetch without reloading. Also make cache-proof rejection-code parsing exhaustive at the type boundary. * fix(cache): require explicit App Router payload origins App Router navigation commits still carried cache-restore semantics as an optional boolean. A fresh default could hide a future restore path that forgot to request reuse proof, reopening the cache authority bug for visited responses. The violated invariant is that cache origin is part of the payload contract, not a caller memory check. Replace the boolean with a discriminated AppNavigationPayloadOrigin and thread it through browser entry, navigation controller, visible commit classification, and tests. Fresh payloads remain proof-optional; visited-cache payloads synthesize a missing-proof rejection and fail closed. * docs(cache): document static layout proof parsing boundary Runtime cache proof parsing currently accepts one proven reuse class: static layout. Future proof classes must update the parser before visited-cache responses can safely restore as commit-capable payloads. The review also noted that the cache-restorable metadata predicate behaves as a narrowing boundary. Make that contract explicit as a TypeScript type guard without changing runtime behavior. * fix(cache): keep accepted proof in rejection traces Cache-proofed payloads that failed a later interception or root-boundary gate produced correct hard-navigation decisions, but the rejection trace lost the accepted proof fields. That made cache-proof diagnostics incomplete for payloads that passed proof validation and failed a later topology gate. The root cause was that later rejection branches still used the pre-cache-proof trace fields after commitTraceFields had been computed. Thread the enriched trace fields into those rejection decisions, document cache proof parser three-way semantics, narrow the cache-restorable metadata type guard precisely, and read the pending commit proof from the canonical top-level field. |
||
|
|
f27c40a77f |
chore: split e2e compatibility by App Router vs Pages Router (#1321)
* feat(compat): split e2e compatibility by App Router vs Pages Router
The /compatibility page previously showed one undifferentiated grid of
~1000 Next.js test files. This change classifies each test by which
router(s) its fixture exercises (App, Pages, both, or unknown) and
surfaces the breakdown in the UI.
How:
- A new `router` column on `compat_file_results` (enum: app | pages |
both | unknown). Defaults to 'unknown' so pre-classifier rows still
render — they just show up under 'Other'.
- A new `scripts/classify-nextjs-suites.mjs` walks each test's fixture
directory and looks for app/page.tsx, app/route.ts, app/layout.tsx,
pages/*.tsx (excluding _app / _document / _error specials). A fixture
with real routes in both folders is classified as 'both' — these are
the genuine parity tests and counting them only once would hide a
router-specific failure.
- Edge cases handled in the classifier:
* pageExtensions naming (layout.page.tsx → layout)
* Suites whose .test.ts lives in a test/ subdir alongside the fixture
* Inline-fixture suites under test/e2e/app-dir/ (no on-disk routes,
but path convention says App Router)
* APP_ROUTER_NON_APP_DIR_SUITES curated override
* Skips node_modules and .next when scanning fixtures
- The nightly workflow now runs the classifier in the build job once
per run (the cheap part — happens with Next.js still on disk),
uploads the suite → router map as an artifact, and the report job
joins it into the ingest payload before POSTing.
- The compatibility page gains a 'By router' card row showing per-router
pass rates and file counts (parity tests are counted toward both
router buckets — adding them exceeds the total, see the explainer
card). The contribution grid grows filter chips above it for
interactive narrowing.
Verified:
- vp check (format + type + lint) clean
- 14 new classifier unit tests pass (vp test run tests/classify-nextjs-suites.test.ts)
- apps/web build:vinext succeeds end-to-end
- Local classifier run against .nextjs-ref reports
app=576 / pages=325 / both=113 / unknown=22 out of 1036 suites, with
spot-checked classifications matching expectations.
Backward compatibility: the ingest endpoint treats `router` as optional;
existing workflow runs and historical rows continue to function and
render as 'Other' until the next nightly classifies them.
* refactor(compat): store router classification in its own table
Replaces the inline `router` column on `compat_file_results` (introduced
in the previous commit) with a dedicated `compat_suite_meta` table,
classification keyed by `suite`. The /compatibility UI LEFT JOINs the
two tables at query time.
Rationale (per design discussion in PR #1321):
- Classifications conceptually describe test files, not test runs. Storing
them per-row coupled their cadence to results ingestion, which is wrong
when (a) the Next.js ref bumps and re-classifies everything, (b) an
override fix lands without re-running tests, or (c) a partial test run
still wants fresh classifications.
- One row per suite (PK on `suite`) means re-classifying is an upsert,
not a backfill loop. Provenance (`next_ref`, `classified_at`) is
stored on the row for debugging.
- Decoupling lets the workflow POST classifications from the build job
immediately after running the classifier, without round-tripping through
the report job. Results ingestion stays focused on results.
Endpoints:
- `POST /api/compatibility` no longer accepts `router` per file
(reverted to its pre-PR shape).
- `POST /api/compatibility/classify` (new): accepts
`{ nextRef, classifiedAt?, suites: [{ suite, router }] }`,
upserts in chunks (25 rows per INSERT to fit SQLite's variable cap),
shares auth with the results endpoint via a new `_auth.ts` helper.
Workflow:
- The build job now POSTs the classification map directly to the new
endpoint after running the classifier. Same guardrails as the results
POST (only full-suite, only against main).
- The report job no longer needs to download / merge the classification
artifact — it's already in D1 by the time results land.
- The classification JSON is still uploaded as a workflow artifact for
manual re-submission / debugging.
Schema:
- New table `compat_suite_meta(suite PK, router, next_ref, classified_at)`
with an index on `router` for the per-router count queries.
- `compat_file_results` reverts to its pre-PR columns. No data migration
needed because the column was only ever populated on this branch.
Trade-off: classification changes are now retroactive — re-classifying a
suite updates how it appears in every historical run. This is usually
what you want (corrections heal the whole history) but means the trend
chart isn't a strict point-in-time record. The provenance fields on the
meta row let you tie a reclassification back to a specific Next.js ref.
Verified:
- vp check clean
- 14 classifier unit tests still pass
- apps/web build:vinext succeeds; /api/compatibility/classify shows up
in the route list
* refactor(compat): drop redundant next_ref from compat_suite_meta
The Next.js ref a classification was produced against is already
recoverable from the run history — compat_runs records next_ref per
run, and the most recent classification's ref is implicit (it's the
ref of the most recent classify POST, which the workflow always pairs
with a run).
The future-proofing case for per-ref classifications would want a
composite (suite, next_ref) PK rather than a single global row anyway,
so this column doesn't help with that scenario either. classified_at
is enough for the debug case ('when was this last computed?').
Changes:
- Drop next_ref column from compat_suite_meta (3 columns now:
suite PK, router, classified_at)
- /api/compatibility/classify no longer requires nextRef in the body
- Workflow no longer passes NEXT_REF when building the classify payload
- Migration regenerated as 0001_romantic_skullbuster.sql (one fewer
column on the CREATE TABLE)
- Chunk size bumped to 33 rows/INSERT (100-var cap / 3 columns)
Verified: vp check clean, classifier tests pass, apps/web build succeeds.
* feat(compat-ui): share router filter between grid and trend chart
Lifts the router-filter state out of ContributionGrid into a new
CompatibilityViews client wrapper that owns the Kumo segmented Tabs
control. The grid and the line chart both consume the active filter
as a prop, so changing the tab updates both visualisations in lockstep.
The line chart now plots per-router series. The trend query was
rewritten to aggregate via JOIN against compat_suite_meta — one row
per run with app/pages/both/unknown rollups in a single round-trip,
~90 runs × ~1000 file rows over an indexed join. The TrendPoint
carries all five series; the chart picks one based on the filter
without re-fetching.
Other UI tweaks in this commit:
- Replaced the hand-rolled pill row with Kumo Tabs (variant='segmented',
size='sm'). Fixes a weird active state (bg-kumo-default + text-kumo-base
was using the text color for backgrounds, producing a saturated
inversion).
- 'Parity' → 'Mixed' in user-visible labels. Internal identifiers
(DB enum 'both', bucket variable 'parity') unchanged.
- Removed the standalone 'Other' stat card; the segmented Tabs still
expose the 'Other' filter so unclassified suites are reachable.
- Reorganised the page into one 'Test files and trend' section
containing both visualisations, gated by the shared Tabs.
- .gitignore: ignore .dev.vars (Wrangler's local-secrets convention)
so the COMPAT_INGEST_SECRET we use for local testing never gets
committed by accident.
Verified:
- vp check clean (format + type + lint)
- 14 classifier unit tests pass
- apps/web build:vinext succeeds; both API endpoints + page render
* fix(compat): address PR review comments
Addresses actionable items from the two /bigbonk review passes:
1. scanFixture: skip recursion into already-checked app/ and pages/
(with a wrinkle around fixture wrappers).
The previous code unconditionally pushed every child onto the walk
stack, which (a) wasted work re-walking subtrees the route checker
already covered and (b) risked a false positive on App Router route
groups literally named 'pages' (e.g. app/pages/index.tsx would have
tripped the Pages Router detector).
Naive fix (skip descent into any app/ or pages/) regressed ~10 real
Next.js fixtures that use a wrapping directory literally named 'app'
as the test app's project root, with the real app/ and pages/ nested
inside (test/e2e/og-api, test/e2e/middleware-static-files, etc.).
Final fix: a directory named app or pages is treated as a fixture
wrapper (and recursed into) if it contains a top-level next.config.*
OR an inner app/ alongside an inner pages/. Otherwise it's handed to
the route checker, which either finds real routes or returns nothing
and the walk skips it. The wrapper detector intentionally does NOT
treat middleware.{js,ts} as a wrapper signal, because Next.js tests
put noop middleware.js files inside real App Router app/ to assert
the file is ignored at that level (test/e2e/app-dir/app-middleware).
Verified against the full test/e2e/ tree of the local Next.js
checkout: 1036 suites classify identically to the pre-fix output
(576 app / 325 pages / 113 both / 22 unknown), with the
app/pages/index.tsx false-positive now correctly handled.
2. list-nextjs-e2e-suites.mjs: add the same import.meta.url guard the
classifier already has, so importing the module programmatically
doesn't immediately parse process.argv and write a file.
3. _auth.ts: early-exit when the X-Compat-Secret header is missing
or empty, before paying two SHA-256 digests. The constant-time
guarantee we care about (don't leak the contents of the expected
secret via length or prefix matching) is preserved — we only
short-circuit on values we already know can't match a non-empty
secret.
4. Migration 0001_romantic_skullbuster.sql: add trailing newline.
Tests:
- 3 new regression tests added (18 total, all pass):
* fixture wrapper with next.config.js + inner app/ + pages/
* fixture wrapper with next.config.js + inner pages/ only
* App Router app/ containing a route group named 'pages'
* App Router app/ containing a noop middleware.js
- vp check clean
- Classifier output diff against current Next.js HEAD: empty
* fix(compat-ui): unexport cellMatchesFilter (knip)
CI's knip step flagged `cellMatchesFilter` as an unused export. It's only
used inside contribution-grid.tsx itself — the shared wrapper has its
own bucketing logic in compatibility-views.tsx. Drop the `export`.
* fix(compat): address third-pass review comments
1. sqlExcluded: narrow parameter type to a closed union
("router" | "classified_at") so the no-user-input invariant for
sql.raw is compiler-enforced rather than relying on a code comment.
2. ContributionGrid: gate the SVG render on visibleCells.length > 0,
and also clamp svgWidth/svgHeight to >= 0. Previously, when a
filter emptied the grid, the SVG rendered with width=-3/height=-3
alongside the placeholder div (browsers clamped silently but it
was invalid SVG).
3. compatibility/page.tsx 'How this works' card: add a short note
that per-router trend lines use the latest classification, so
reclassifying a suite updates how it appears in historical runs.
The aggregate "All" line is unaffected. Avoids a confusing
support question if users notice old per-router numbers shift
after a classifier improvement.
4. scanFixture readability: simplify the early-return checks from
'if (hasApp && hasPages)' to 'if (hasPages)' / 'if (hasApp)',
since the other flag was just set on the line above. No
behavioural change; classifier output against the full Next.js
test/e2e tree is byte-identical (1036 suites, 576/325/113/22).
5. /api/compatibility/classify body validator: bounds-check
classifiedAt. Reject NaN/Infinity and any timestamp earlier
than 2023-01-01 UTC. Catches the common seconds-vs-milliseconds
mistake and accidental 0/-1 values from buggy callers. The
only legitimate caller (the GH workflow) sends Date.now() so
this is purely defensive.
Verified:
- vp check clean (format + type + lint + knip)
- 18 classifier unit tests pass
- apps/web build:vinext succeeds
- Classifier output against current Next.js HEAD is unchanged
* refactor(compat-ui): extract shared router bucketing module
Addresses items 1, 3, and 4 from the fourth review pass.
1. Trend GROUP BY note (item 1): added an inline comment explaining
that selecting r.created_at while grouping by r.id only is a SQLite
functional-dependency affordance. Standard SQL (postgres etc.)
would reject it; the comment heads off a future 'fix' that breaks
the query if it's ever ported.
2. Unified router bucketing (item 3): the three places that sliced
cells by router filter all used slightly different naming
conventions ('parity' vs 'both', 'other' vs 'unknown'). Extracted
the canonical logic to a new ./router-buckets module with one
shared vocabulary (the same as RouterKind), and updated all three
call sites:
- contribution-grid.tsx: cellMatchesFilter from shared module
- compatibility-views.tsx: countByFilter for tab labels
- page.tsx: bucketByRouter + bucketPassRate for stat cards,
byRouter.parity/other renamed to byRouter.both/unknown
Doc comment at the top of router-buckets.ts pins the
'Mixed counts in both app and pages' rule in one place. No
behavioural change.
3. passRate function rename (item 4): renamed the line chart's local
ratio helper from passRate -> computePassRateRatio to avoid
shadowing the passRate variable in page.tsx (and to distinguish
it from bucketPassRate in router-buckets which returns a
percentage, not a ratio).
Not addressed (intentionally):
- 'Mixed (both)' vs 'Parity'/'Interop' label (item 2): the user
explicitly chose 'Mixed' over 'Parity' in an earlier message.
- trendRowsDesc rename (item 5): reviewer self-tagged 'very minor';
renaming would muddle the diff.
- deriveSuiteGroup inconsistent levels (item 5b): pre-existing
tooltip-only behaviour, not introduced by this PR.
Verified:
- vp check clean (format + type + lint + knip)
- 18 classifier unit tests pass
- apps/web build:vinext succeeds
|
||
|
|
76c5d448fc |
fix: enforce strict parameter isolation for root-params in prerendering and SSR (#1324)
* fix: support root-params dynamic getters during static prerendering and SSR * fix: enforce strict parameter isolation for root params in prerendering and SSR * fix: correct parameter mapping in multi-source resolver and clean up handleSsr fallback * fix: use dedicated standalone ALS for root-params outside of unified request context * fix: thread root params through generated app page dispatch Normal App Router page requests computed root params in the typed handler, but the generated RSC entry did not forward them into page dispatch. SSR then received an empty root params scope for requests that should expose root params. Destructure and forward rootParams in the generated entry, assert that template wiring, reuse the shared root-param picker, and align the scope helper API with the existing runWith overload pattern. |
||
|
|
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 |