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 | |
|---|---|---|---|---|
|
|
fe700996d9 |
ci: extract Playwright cache plumbing into a composite action (#1438)
Two workflows now share an identical playwright-version + cache-restore pattern: ci.yml (which owns the cache, used by E2E jobs) and nextjs-deploy-suite.yml (which restores it read-only, added in #1435). Pull the duplication into .github/actions/playwright-cache so the cache key format can't drift between the two callers — if it ever does, the deploy-suite stops hitting ci.yml's cache and the upstream Playwright extraction hang from 2026-05-22 (runs 26265986264 .. 26281758803) reappears. The composite action: - Reads the playwright version from a configurable lockfile path (`lockfile` input, defaults to pnpm-lock.yaml). - Restores ~/.cache/ms-playwright keyed `playwright-${browser}-${runner.os}-v${version}`. - Supports two modes: `cache` (restore + save, default — for the workflow that owns the cache) and `restore` (read-only — for downstream consumers that don't want to fight over the save). - Exposes `cache-hit`, `version`, and `key` outputs so callers can gate install steps or use the version for sibling cache keys (e.g. ci.yml's apt-archives cache for WebKit deps). No behavioural change — same cache keys, same restore-keys, same ~/.cache/ms-playwright path. |
||
|
|
8df248863d | fix: match Next metadata image route behavior (#1436) | ||
|
|
68ae68a57c |
ci(deploy-suite): restore Playwright browsers from ci.yml's cache (#1435)
Follow-up to #1428 and #1430. The previous two PRs added log markers and skipped the wedged transitive postinstall, but the underlying problem remained: the explicit `pnpm playwright install chromium chromium-headless-shell` in the prepare script hits the same upstream extraction hang (download to 100%, then no progress until step timeout). What's actually different between the runs that worked (May 19-21) and the runs that hang (May 22): nothing on our side. Same Chrome 145.0.7632.6, same Playwright 1.58.2, same Node 24.16.0, same runner image fleet. On the May 21 success the extraction took 2.7 seconds after 100% download; on May 22 it never completes (>9 min, killed by the 15-min step timeout). Some upstream change — bad bytes from cdn.playwright.dev, an Azure storage hiccup affecting fsync, a Playwright installer interaction we can't see from here — is wedging extraction. We can't fix it from this repo. What we *can* do: stop downloading. vinext's own ci.yml already keeps a `playwright-chromium-Linux-v1.58.2` cache hot (261 MB, last accessed within minutes on every CI run today). The deploy-suite has never used this cache — its only cache (the post-build `Save build artifacts` step) is keyed per-run, so every nightly starts with an empty ~/.cache/ms-playwright and re-runs the install that's now hanging. Add a restore step before the prepare that uses ci.yml's exact key format. On hit, `pnpm playwright install` sees the browsers already present and is a fast no-op. On miss (e.g. Next.js bumps Playwright before vinext catches up), we fall through to the existing behavior. Restore-only: ci.yml owns the save side. |
||
|
|
b7decf63cf |
feat(cache): add skip cache proof cross-checks (#1434)
* feat(cache): add skip cache proof cross-checks Skip manifest entries remain untrusted hints, but future skip transport needs a cheap server-side proof boundary before it can omit any payload. Cache presence, client-declared artifact metadata, and stored artifact metadata are not individually enough to prove correctness. Add a pure skip/cache cross-check helper that requires an accepted static layout cache proof, compatible artifact metadata, matching cache variant, matching payload hash, and valid invalidation proof. The helper always returns the disabled render-and-send skip disposition in this slice. Cover graph, deployment, render epoch, variant, payload hash, invalidation, missing proof, and rejected proof cases at the helper boundary. * fix(cache): bind skip cache artifact proof metadata Accepted static layout artifact reuse decisions proved candidate artifact compatibility but did not carry that compatibility envelope forward. That let skip/cache cross-checks accept independently supplied artifact metadata as long as the client entry matched it, even when the cache proof had authorized a different artifact. Store a snapshot of the candidate artifact compatibility on accepted artifact reuse proofs and require skip/cache cross-checks to match it exactly before checking client compatibility, variants, payload hashes, or invalidation state. The skip/cache test matrix now covers the rejected proof-binding case, root-boundary compatibility mismatches, and layout entry id mismatches. |
||
|
|
0a8c3819ab |
chore(deps): bump cloudflare/wrangler-action from 3 to 4 (#1433)
Bumps [cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action) from 3 to 4. - [Release notes](https://github.com/cloudflare/wrangler-action/releases) - [Changelog](https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/cloudflare/wrangler-action/compare/v3...v4) --- updated-dependencies: - dependency-name: cloudflare/wrangler-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
aa7730ca6d |
chore(deps): bump actions/download-artifact from 7 to 8 (#1432)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
47f697c98e |
chore(deps): bump actions/github-script from 7 to 9 (#1431)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v9) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
4fba647a2c |
ci(deploy-suite): skip wedged playwright-chromium postinstall (#1430)
Follow-up to #1428. The previous PR added log markers, which revealed the actual hang: `playwright-chromium`'s postinstall (`node install.js`, triggered transitively by the Next.js workspace install) reaches 100% on the 167 MiB Chrome zip download from cdn.playwright.dev, then never prints "Done" — extraction/verification silently wedges. Every other postinstall in the same install (cpu-features, sharp, esbuild, etc.) finishes cleanly. Confirmed on runs 26265986264, 26279790373, and the post-#1428 retry 26281061094 — same fingerprint each time. Set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` on the Prepare step so the transitive `playwright-chromium` postinstall becomes a no-op. The explicit `pnpm playwright install chromium chromium-headless-shell` that runs later in `scripts/run-nextjs-deploy-suite.sh` provisions the browsers we actually use at test time, populating ~/.cache/ms-playwright which is cached and restored by the test shards. |
||
|
|
37cfeb11ce |
ci(deps): add dependabot config with 1-day cooldown (#1429)
* ci(deps): add dependabot config with 1-day cooldown Mirrors pnpm-workspace.yaml's `minimumReleaseAge: 1440` (24h) so Dependabot won't open PRs for versions younger than the cooldown window we already apply to local installs. * ci(deps): run dependabot daily instead of weekly |
||
|
|
b203fa7fd6 |
ci(deploy-suite): unstick Next.js prepare step (#1428)
The nightly deploy suite hung for 29m+ in "Prepare Next.js checkout" on two consecutive runs (26265986264, 26279790373), eating the 30m job timeout. Same v16.2.6 SHA (ee6e79b1) built fine the three previous nights, so it's environmental, not source drift. The first `pnpm install` completes around the 30s mark and then logs go silent until cancellation. With no per-command markers, we couldn't tell which of the remaining commands actually hangs. This patch: - Drops `--with-deps` from the build-job playwright install. It runs `apt-get install` for browser system libraries, which is by far the most plausible silent-hang culprit (stuck dpkg lock / slow apt repo). The 16 test-shard runners each spin up a fresh ubuntu-latest and already run `playwright install` without `--with-deps` — so the apt state never transferred anyway. Doing it on the build runner is wasted work even when it doesn't hang. - Removes the redundant second `pnpm install`. The workspace symlinks survive the build step; nothing between the build and re-install adds new packages. - Adds timestamped `>>> ...` markers around each prepare command so a future hang is diagnosable from the logs. - Adds `timeout-minutes: 15` on the Prepare step so a future hang fails fast (well under the 30m job cap) instead of starving the whole job. |
||
|
|
3e0af061b8 |
feat: add disabled ClientReuseManifest protocol (#1418)
* add disabled client reuse manifest protocol * fix: reject unsafe client reuse commit versions * refactor: reuse shared record guard * fix: address client reuse manifest review * fix: canonicalize client reuse manifest entries * docs: clarify manifest ordering invariant * fix: make client reuse protocol invariants explicit |
||
|
|
cd1013ea33 |
feat(config): add experimental.appShells plumbing (#1415)
* feat(config): add experimental.appShells plumbing Accepts experimental.appShells in next.config and defines process.env.__NEXT_APP_SHELLS=false at build time, matching Next.js PR #93997 plumbing scope. Behavorial implementation is gated on upstream co-flags that vinext does not yet support. - Adds experimental.appShells to CONFIG_SUPPORT as unsupported - Sets __NEXT_APP_SHELLS define to false regardless of config value - Tests for vinext check detection and Vite define injection Fixes #1405 * review: address PR nits - Expand experimental.appShells detail to explain why it's unsupported - Move build-time define tests into their own describe block, so the __NEXT_APP_SHELLS define test isn't nested under basePath * review: drop orphaned server lifecycle from basePath describe The two remaining resolveNextConfig tests import from source directly and don't use the dev server. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
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. |