mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.4
1286 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40d23de157 |
fix(next-request): throw canonical relative URL error (#1560)
* fix(next-request): throw canonical relative URL error
Calling `new NextRequest('/foo')` previously bubbled up `Failed to parse
URL from /foo` because the constructor's fallback `new URL(input,
"http://localhost")` masked the real problem. Next.js's NextRequest
runs `validateURL(url)` at the top of its constructor and throws an
error containing `Please use only absolute URLs`. Middleware tests
that assert on this message text now match Next.js.
Closes #1521
* fix(next-request): collapse inline URL validation to validateURL helper
Address bot review nit: the 13-line inline URL validation duplicated the
existing `validateURL` helper at line 215. Replace it with a call to the
helper. Bonus: relative URL rejection now also picks up the
`assertSafeNavigationUrl` guard against `javascript:`/`data:`/`vbscript:`
URIs (defense-in-depth).
|
||
|
|
97c11cdc37 |
fix(pages-router): rename data-vinext-head to data-next-head (#1559)
Pages Router <Head> now emits the canonical `data-next-head=""` attribute matching Next.js, rather than the vinext-specific `data-vinext-head="true"`. This unblocks `_document` head lookups in the deploy suite. |
||
|
|
e121d42ef0 |
fix(middleware): warn when legacy middleware filename is detected (#1561)
Next.js 16 renamed `middleware.{js,ts}` to `proxy.{js,ts}` and emits a
canonical deprecation warning when the legacy filename is detected. vinext
was emitting a custom message that didn't match the wording the Next.js
deprecation-warnings and app-middleware e2e suites assert on.
Update `findMiddlewareFile` to log the exact text Next.js uses (from
`packages/next/src/build/index.ts` adjacent to `MIDDLEWARE_FILENAME`):
The "middleware" file convention is deprecated. Please use "proxy" instead.
...migration codemod and docs link...
The warning still fires only when `middleware.{js,ts}` is found (after
checking `proxy.{js,ts}` first) so behavior is unchanged for projects on
the new convention. Existing test in tests/shims.test.ts is tightened to
assert the canonical wording.
Closes #1455
|
||
|
|
d6bf6413e9 |
feat(pages-router): consume _next/data JSON endpoint from the client (#1412)
* feat(pages-router): consume _next/data JSON endpoint from the client
The server-side `/_next/data/<buildId>/<page>.json` endpoint landed in
#1384, but the vinext client was still fetching the full HTML page on
every navigation and regex-extracting `__NEXT_DATA__` from it. This
wires the client to actually use the JSON endpoint, matching what
Next.js does and avoiding the HTML round trip.
## Client navigation (shims/router.ts)
`navigateClient` is split into two paths:
- `navigateClientData` (new): when `window.__VINEXT_PAGE_LOADERS__` has
a registered code-split loader for the target route AND we have a
buildId, fetch `/_next/data/<buildId>/<page>.json` with
`Accept: application/json` + `x-nextjs-data: 1`. On 200 parse the
envelope, look up the page module via the in-memory loader map (Vite
has already split each page into its own chunk), and re-render.
- `navigateClientHtml`: the previous HTML-fetch + regex-extract logic,
unchanged. Used in dev (where the inline hydration script does not
populate the loader map) and as a fallback for any route not in the
client loader map.
Failure modes — 404, 5xx, network error, malformed JSON, soft
redirect via `x-nextjs-redirect`, missing loader — all queue a hard
navigation (`window.location.href = url`). This is the deploy-skew
safety net: when the server's buildId has rotated, the data endpoint
returns 404 and the client lands on the new build via a full document
load.
## Code-split loader manifest (entries/pages-client-entry.ts)
Vite already code-splits each page into its own chunk via the dynamic
imports in `pageLoaders`. The generated client entry now exposes that
map on `window.__VINEXT_PAGE_LOADERS__` (route pattern → loader thunk),
plus `window.__VINEXT_PAGE_PATTERNS__` (ordered list) and
`window.__VINEXT_APP_LOADER__` (the `_app` thunk, when present). This
replaces Next.js' `_buildManifest.js` indirection — vinext already
generates the equivalent at build time, we just had to expose it.
## Data URL construction (shims/internal/pages-data-url.ts)
New pure helper module mirroring `getAssetPathFromRoute` +
`getDataHref` from Next.js. Handles:
- `/` → `/_next/data/<id>/index.json`
- `/about` → `/_next/data/<id>/about.json`
- `/index` → `/_next/data/<id>/index/index.json` (round-trip)
- `/blog/foo` → `/_next/data/<id>/blog/foo.json`
Also exports `matchPagesPattern` which uses the existing
`matchRoutePattern` to map a URL pathname to a registered route
pattern.
Round-trip tests verify the server's `parseNextDataPathname` agrees
with the client's `buildPagesDataPath` for every shape, and surfaced a
latent server bug: `/_next/data/<id>/index/foo.json` was parsing back
to `/index/foo` instead of `/foo`. Fixed in `pages-data-route.ts` so
the round-trip is now lossless.
## Prefetch (shims/link.tsx + shims/router.ts)
`<Link>` prefetch and `Router.prefetch()` now prefer the data path:
- Inject `<link rel="prefetch" as="fetch" crossorigin href={dataUrl}>`
for the JSON, matching the actual navigation's request shape.
- Invoke the loader thunk to warm the chunk in parallel. Vite's
`import()` caches the result, so the eventual click is a double
cache hit.
When no loader matches (dev, unmapped route), fall back to the legacy
`<link rel="prefetch" as="document">` so the browser at least preloads
the HTML.
Notably this also fixes a long-standing prefetch bug: the previous
Pages branch of `link.tsx`'s prefetch check
(`__NEXT_DATA__.__vinext?.pageModuleUrl`) was only populated by the
dev server's inline hydration script, so prefetch in production was a
no-op.
## Tests
- `tests/pages-data-url.test.ts` (new): 27 tests covering URL
construction and the client/server round trip.
- `tests/shims.test.ts` (+8): JSON-path navigation success, 404
hard-reload (build-skew), `x-nextjs-redirect` soft-redirect handling,
HTML fallback when no loader matches, dynamic param extraction,
malformed JSON hard-reload, prefetch JSON path, prefetch fallback.
- Ports from Next.js: `getDataHref` in
`.nextjs-ref/packages/next/src/client/page-loader.ts:154`,
`fetchNextData` in
`.nextjs-ref/packages/next/src/shared/lib/router/router.ts:498`,
`getAssetPathFromRoute` in
`.nextjs-ref/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts`.
Verified: 1393 vitest tests pass across shims, pages-router,
link/navigation, prefetch, data-route, data-url, entry-templates, and
routing.
Closes #1330 follow-up.
* fix(pages-router): drop unused export on PagesPatternMatch
Knip flagged the exported `PagesPatternMatch` type as unused. It is only
referenced as the return type of `matchPagesPattern` in the same file, so
narrow it to a local type.
* fix(pages-router): merge search params into __NEXT_DATA__.query on data-path nav
bigbonk review found that navigateClientData was synthesising
__NEXT_DATA__.query from dynamic route params only, missing the URL search
params. Code reading window.__NEXT_DATA__.query directly (a public Next.js
surface) would see incomplete data after a JSON navigation, even though
useRouter().query happened to still work because getPathnameAndQuery()
re-reads window.location.search.
Parse target.search and merge it into query alongside the route params,
with route params winning on key collision — same merge order as
getPathnameAndQuery() and Next.js itself.
Also fix a misleading comment in pages-client-entry.ts: the patterns list
is already sorted by pagesRouter()/compareRoutes (static → dynamic →
catch-all), not by the router at runtime.
* test(pages-router): e2e regression guard for JSON nav path
Adds a Playwright assertion that a Link click on a hydrated Pages Router
app on Cloudflare Workers fires a request to /_next/data/<buildId>/<page>.json
and does NOT trigger a full HTML reload. If the build pipeline ever stops
exposing __VINEXT_PAGE_LOADERS__ (or the loader map mis-keys), every other
navigation test still passes — they all assert the URL changed and the new
page rendered, neither of which fails when navigateClient silently falls
back to the HTML path. This test fails loudly in that scenario.
* test(pages-router): use hydration timestamp instead of resource-type sniffing
The original assertion 'no document-resource request to /ssr' caught false
positives: <link rel="prefetch" as="document"> can fire on initial paint
for unrelated reasons, and Chromium does not always classify prefetch hints
in a stable way. Swap to checking that __VINEXT_HYDRATED_AT survives the
click — a document reload would reset it, an in-place re-render preserves it.
Combined with the data-URL network assertion, this remains a tight guard on
the JSON nav path without the flake.
* test(pages-router): use window sentinel instead of timestamp comparison
Timestamps are timing-related signals; swap to a window-scoped sentinel
that a document reload would wipe and an in-place re-render preserves.
Same guarantee, no flake.
* fix(pages-router): handle _next/data requests in Cloudflare Workers example
The Node prod-server normalizes /_next/data/<buildId>/<page>.json before
middleware and flags it as a data request so renderPage emits the JSON
envelope. The Cloudflare worker example never did — data requests hit
matchPageRoute which can't match /_next/data/... and returned 404, so
navigateClientData saw the 404 and triggered a hard navigation. The
client-side JSON nav path was effectively dead on Workers.
Mirror the prod-server normalization in the worker: detect the data path
before middleware, validate the buildId, strip it to the page path, and
pass { isDataReq: true } through to renderPage.
Export ./server/pages-data-route from the vinext package so user-written
workers can import the helpers.
* Revert "fix(pages-router): handle _next/data requests in Cloudflare Workers example"
This reverts commit
|
||
|
|
6920568e62 |
fix(router): route hybrid pages api fallbacks through pages entry (#1446)
Hybrid App Router plus Pages Router production builds entered through the App Router server and only delegated route misses to renderPage. That meant pages/api routes were outside the fallback path and returned the App Router 404 in mixed apps. The generated RSC entry now dispatches /api misses to the Pages server entry handleApiRoute, applies middleware response effects to that API response, and the hybrid SSR bridge re-exports the API dispatcher. Regression coverage ports the Next.js og-api Pages API ImageResponse case at the production boundary.v0.0.52 |
||
|
|
88144de251 |
perf(e2e): preinstall compatible React in deploy harness (#1443)
* perf(e2e): preinstall compatible React in deploy harness App Router deploy-suite fixtures can start with React versions below the react-server-dom-webpack peer range. The first harness install then resolves that older React pair, and vinext build immediately runs its RSC compatibility upgrade, causing a second package-manager install inside each throwaway app. Normalize App Router fixture manifests before the initial install by replacing older semver React and React DOM specs with the workspace-compatible specs. Pages-only fixtures and non-semver specs are left untouched. * chore: rerun ci * fix(e2e): derive React floor from harness spec The deploy harness normalized App Router fixture React versions against a hardcoded 19.2.6 floor. That duplicated the compatibility threshold already expressed by vinext's install specs and could go stale when the workspace moves to a newer compatible React pair. Compare each fixture dependency against the replacement spec that the harness would write. This keeps React and React DOM normalization tied to the package metadata instead of a separate constant. |
||
|
|
b77dcfebdc |
test(msw): migrate font-google.test.ts from fetch hijacking to MSW (#1447)
Replaces 16 ad-hoc `globalThis.fetch = async () => new Response(...)`
swaps with a small `mockGoogleFontsCSS(body, opts?)` helper backed by
MSW's `server.use(http.get(...))`. The helper records every URL it
sees so the two tests that previously asserted on `fetchedUrls.find()`
just read `mock.urls` instead, and the one test that asserted on a
hit-count read `mock.count()`.
Two sites need bespoke handling:
- The Inter/Roboto URL-branching test uses an inline
`server.use(http.get("https://fonts.googleapis.com/*", ({ request }) => ...))`
that returns different CSS based on the URL.
- The "network error" test uses `HttpResponse.error()` to make the
fetch reject (replacing the previous `throw new TypeError(...)` in
the swapped fetch).
Handlers are reset between tests by the global `afterEach` in
`tests/_msw/setup.ts`, so the per-test `try { ... } finally
{ globalThis.fetch = originalFetch }` boilerplate disappears (the
`finally` blocks now only call `fs.rmSync` on the temp font root).
This is PR 2 of the MSW migration. PR 1 (#1444) introduced the
infrastructure and migrated `tests/font-google-build.test.ts`. A
follow-up will migrate `tests/fetch-cache.test.ts` and the in-test
fetch stub inside the font block of `tests/app-router.test.ts`.
|
||
|
|
7871730c3e |
fix(app-router): guard deployment fallback hard navigations (#1440)
App Router RSC deployment mismatches and related fallback paths could still write directly to window.location outside the navigation controller. That bypassed the persisted same-target hard-navigation guard, so a canary or rollback mismatch could repeatedly reload the current document instead of stopping after the first hard navigation. Expose the controller's guarded hard-navigation executor and route RSC compatibility fallbacks, server-action deployment fallbacks, and RSC navigation error fallbacks through it. Add skip/cache canary rollback compatibility coverage plus a direct fallback guard regression test. |
||
|
|
d03c5aa045 |
test(msw): introduce MSW infrastructure and migrate font-google-build (#1444)
* test(msw): introduce MSW infrastructure and migrate font-google-build
Replaces ad-hoc `globalThis.fetch` hijacking in tests with Mock Service
Worker. MSW intercepts both `globalThis.fetch` and `node:http`/`https`,
so it covers fetches issued by in-process Vite servers and fixture pages
without each test having to manage its own stub.
The setup file enables `onUnhandledRequest: 'error'` so any future test
that issues an unmocked external request fails loudly instead of
silently hitting the network. Loopback hostnames (localhost, 127.0.0.1,
::1) are bypassed because the integration project deliberately hits
in-process test servers via fetch.
What this PR contains:
- `tests/_msw/{handlers,server,setup}.ts` — shared infra
- `setupFiles` wired into both vitest projects (unit + integration)
- `msw` + `@mswjs/interceptors` added to the workspace catalog
- `msw` postinstall skipped via `allowBuilds` (it only copies a browser
service worker; we exclusively use `msw/node`)
- `tests/font-google-build.test.ts` migrated as the proof of pattern
Temporary handlers for `httpbin.org/uuid` and
`example.com/not-cacheable` keep the existing fixture pages
(`revalidate-tag-test`, `dynamic-error-fetch`) working under the new
guard. These are placeholders flagged with `TODO(msw-migration)` — a
follow-up will replace those live URLs with test-local endpoints.
This is PR 1 of 3:
- PR 2 will migrate `tests/fetch-cache.test.ts`
- PR 3 will migrate `tests/font-google.test.ts` and the font block in
`tests/app-router.test.ts`
* test(msw): restrict MSW to the unit project + use passthrough for loopback
Two fixes to the initial MSW wiring that surfaced when CI ran the full
suite:
1. The integration project is now excluded from MSW. Integration tests
spin up in-process HTTP servers and fixture dev servers and exercise
them via `fetch("http://127.0.0.1:<port>/...")`. Even with a loopback
`passthrough()` handler installed, the @mswjs/interceptors layer
interferes with that traffic in subtle ways:
- `tests/prerender.test.ts > errors without writing .rsc when the
middleware short-circuit fallback RSC request fails` timed out at
30s — the 5xx response body never reached the caller.
- `tests/ecosystem.test.ts > nuqs` hung in `beforeAll` waiting for
its fixture process to come up, because the readiness fetch
stalled inside the interceptor.
Integration tests already talk to real local servers, not the
internet, so the unhandled-request guard buys little there. The
value of MSW for this repo is mocking external HTTP for unit tests
of fetch wrappers (font-google, font-google-build, fetch-cache),
which is preserved.
2. The `onUnhandledRequest` callback in `setup.ts` that tried to
silently let loopback through wasn't sufficient — MSW's interceptor
still ran for those requests and could stall them. Replaced with a
default `http.all(LOOPBACK_URL_PATTERN, () => passthrough())`
handler in `handlers.ts` and a plain `onUnhandledRequest: "error"`
in `setup.ts`. Unit tests like the middleware-rewrite proxy in
`tests/shims.test.ts` and the JSX-in-JS suite need this to keep
their in-process HTTP servers reachable.
* test(msw): address BigBonk review notes
- Update PR description in the commit body to drop the stale mention of
TODO(msw-migration) handlers — they were removed when the integration
project was excluded from MSW; the fixture pages run there.
- Tighten the loopback IPv4 octet regex from `\d+` to `\d{1,3}` so it
doesn't match absurdly long octet strings. `URL.hostname` normalises
real addresses, so this is purely a readability fix.
* test(msw): address remaining BigBonk notes from the initial review
- Add `0.0.0.0` to the loopback passthrough pattern so test servers that
bind to the unspecified address keep working without per-test mocks.
- Add a `TODO(msw-3)` comment in `server.ts` flagging the
`SetupServerApi` deprecation in MSW 2.14.6 — the `FetchInterceptor`-only
construction will need to migrate to `defineNetwork`'s interceptor
config when we bump to MSW 3.
Both raised by BigBonk on the first review of this PR (the second review
focused on the stale description and the loose `\d+` octet bound, which
were addressed in the previous commit).
* test(msw): scope server.use() inside the try block in font-google-build
Minor structural fix from BigBonk's review: moves the `server.use()`
handler registration inside the `try` block so all setup and the
matching `fs.unlink` cleanup share the same try/finally scope.
No behaviour change — `server.use()` is synchronous and infallible, so
this is purely a structural tidy-up.
* test(msw): correct comment in font-google-build to match interceptor config
BigBonk caught that the comment described the default `setupServer()`
behaviour (which uses both `FetchInterceptor` and `ClientRequestInterceptor`),
but this project's `server.ts` deliberately constructs `SetupServerApi`
with only `FetchInterceptor`. Update the comment to reflect that — it
intercepts `globalThis.fetch`, not `node:http`/`node:https`.
No code change, comment only.
|
||
|
|
b6fc03cfc3 |
fix(router): preserve default locale for unprefixed root links (#1442)
* fix(router): preserve default locale for unprefixed root links Unprefixed Pages Router URLs represent the default locale, even when the browser's preferred locale has previously been detected as a non-default locale. Treating the ambient runtime locale as authoritative makes a Link from /new to / request /id and returns the wrong page locale. Derive implicit client navigation locale from the current URL first, then domain/default locale state, and render a locale-qualified default root fallback href so non-intercepted root links do not re-enter preferred-locale detection. The regression tests port the relevant Next.js i18n preferred-locale cases at the Link and router boundaries. * refactor(router): share client locale inference The review called out that Link and Router had identical browser locale fallback logic. Keeping that chain duplicated makes future i18n changes easy to apply in one place but miss in the other. Move the URL-first browser locale lookup into a shared shim helper and add SSR coverage for the intentional default-locale root fallback href. |
||
|
|
46ab691094 |
fix(i18n): normalise default-locale paths before route matching (#1409)
Mirrors Next.js's server-side default-locale path normalisation in
`packages/next/src/server/lib/router-utils/resolve-routes.ts` (lines
~250-263): every request that arrives without a locale prefix is
spliced with `/${defaultLocale}` before any `next.config.js`
redirect / rewrite / header rule (or filesystem route) match runs.
Without this, rules like `source: '/:locale/to-sv'` with
`locale: false` failed to match requests for `/to-sv` because there
was no segment for the `:locale` placeholder. After normalisation the
matcher sees `/en/to-sv` and the rule matches with `:locale=en`.
Per the merged PR #1382's description, the issue text's call for a 308
redirect from `/en/page` to `/page` is incorrect: Next.js serves
the same content under both URLs and normalises the path internally.
This change implements that internal normalisation; no redirect.
The new helper lives next to the existing pages i18n helpers so both
routers can use it. Wired into:
- `prod-server.ts` (Pages Router prod)
- `app-rsc-handler.ts` (App Router dev + prod)
- `index.ts` (Pages Router dev plugin)
- `deploy.ts` (Cloudflare Workers entry template)
The domain-mapped default locale (`i18n.domains[].defaultLocale`) is
preferred over the global default when the inbound host matches,
matching Next.js's `domainLocale.defaultLocale` branch in
resolve-routes.ts. `/_next/*` and `/__vinext/*` paths are excluded
to mirror Next.js's exception for build assets and prerender manifests.
The fallback "retain the original unprefixed source" branch in
`applyLocaleToRoutes` is intentionally kept as belt-and-braces: the
new normalisation pass renders it dead-code for ordinary requests, but
it harmlessly preserves backwards compatibility with any matcher
caller that still passes raw, unnormalised pathnames.
Refs #1336 (item 4)
|
||
|
|
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 |