mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.1
1163 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c84e383ccd |
fix(server): render progressive action not-found HTML (#1254)
* fix(server): render progressive action not-found HTML Progressive multipart server actions returned an empty status response when an action called notFound() or another HTTP fallback. That diverged from Next.js' MPA action path, where the action error re-enters app rendering so the not-found boundary produces full HTML. The root cause was treating HTTP fallback action throws as terminal responses in the action execution helper. Pass action failures through the progressive form-state path, preserve redirects as 303 responses, and rethrow action failures during page element construction so existing special-error rendering handles the response body. Regression coverage asserts the action helper handoff and the rendered not-found HTML body. * chore: retrigger CI * fix(server): forward progressive action failures in RSC entry * refactor(server): make progressive action failure state explicit * Apply suggestion from @james-elicx --------- Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
6663dd10d8 |
fix(router): decode hash scroll targets (#1255)
* fix(router): decode hash scroll targets Hash-only client navigation looked up raw URI fragments directly. That meant ids such as "hello world" and browser-compatible named anchors were skipped when links used encoded fragments or legacy anchors. The router shims now share the decoded id, named-anchor fallback, and top-fragment behavior already used by the app browser popstate path. Regression coverage exercises hash-only Pages Router pushes for decoded ids and name fallback. * fix(router): respect scroll false for hash navigation Match Next.js Pages Router behavior by gating hash target scrolling behind the scroll option for both hash-only and hash-bearing navigations. Add regression coverage for hash-only Router.push with scroll disabled. |
||
|
|
2468ae5fba |
fix(app-router): guard stale popstate scroll restoration (#1253)
* fix(server): progressive actions trigger error boundaries on HTTP fallbacks * fix(server): type progressive action errors * fix: guard stale popstate scroll restoration with active nav id * revert(server): drop action error changes from popstate PR The popstate race branch included server action commits from a separate branch. That made the PR review surface change progressive action error semantics unrelated to browser history restoration. Restore those files to main's behavior in this branch and keep only the popstate scroll restoration fix. Also correct the new popstate tests so targeted check and unit coverage pass. * test(app-router): wait for RSC cache seeding The same-build visited payload E2E asserted cache replay after visible commit from a void router.push. The visited-response cache is populated when the internal RSC navigation promise resolves, so the test could navigate away before the cache entry existed and count a second fetch. Wrap the internal navigation function in the test and wait for the captured promise before asserting the replay path. |
||
|
|
26030210b0 |
fix(app-router): throw a typed error for unrecognized server actions (#1250)
* refactor(navigation): extract UnrecognizedActionError into a shared module `UnrecognizedActionError` and `unstable_isUnrecognizedActionError` were defined inline in the `next/navigation` shim. The client server-action dispatcher needs to throw that exact class for the predicate's `instanceof` check to recognize it, but `instanceof` is identity-based per module instance, and importing the whole navigation shim into the dispatcher would be wrong layering. Move both into a dependency-free `shims/unrecognized-action-error.ts`, mirroring Next.js's own `unrecognized-action-error.ts`. The navigation shim re-exports them, so the public `next/navigation` surface is unchanged. No behavior change; existing predicate tests still pass. * fix(app-router): throw a typed error for unrecognized server actions When a server action POST returns the `x-nextjs-action-not-found` response header, the App Router client dispatcher threw a plain `Error`. `unstable_isUnrecognizedActionError` is an `instanceof UnrecognizedActionError` check, so it returned `false` for the framework's own action-not-found path. Client `catch` blocks and error boundaries that rely on the predicate to detect client/server deployment skew never fired, which is the exact case the predicate exists for. Next.js's server-action reducer throws `new UnrecognizedActionError(...)` on this same header. vinext exposed the class and predicate but never constructed the error from its dispatcher. Add `throwOnServerActionNotFound`, the client-side counterpart of `createServerActionNotFoundResponse`, and call it from the client server-action callback so the thrown error is recognizable. The two internal helpers it subsumes are no longer exported. Tests feed the real server-emitted response through the helper and assert the public predicate recognizes the result. |
||
|
|
4e558621df |
feat(cache): classify private and dynamic render downgrades (#1247)
* feat(cache): classify private and dynamic render downgrades Render observations recorded dynamic request usage but did not carry an owned cacheability downgrade decision. That left future cache proof consumers to infer privacy from raw request API observations and cacheability strings. Add pure downgrade classification for public variants, private request state, auth/session dimensions, uncacheable draft state, dynamic fetches, and incomplete observations. Bump the cache proof schema for the serialized observation shape and keep the disabled proof model as the only reuse decision. * chore(cache): rename public cache candidate flag |
||
|
|
5d622369dc |
fix(rsc): preserve binary inlined Flight chunks (#1245)
Inlined App Router Flight data could only travel through string chunks. That corrupted arbitrary binary RSC chunks during SSR hydration and static prerender extraction because non-UTF-8 bytes were decoded to replacement characters before React read them. Encode non-UTF-8 embedded chunks as base64 binary records, decode those records back to bytes in the browser, and write prerendered .rsc payloads as bytes instead of UTF-8 text. Regression coverage exercises server embedding, browser reconstruction, and prerender extraction for binary RSC chunks. |
||
|
|
7d299088b8 |
fix(app-router): promote default slot persistence through route state (#1229)
* fix(app-router): promote default slot persistence through route state Default and unmatched parallel slots were still inferred from AppElements transport markers. That made soft navigation persistence depend on wire shape instead of planner-owned route state, which breaks the #726 invariant that AppElementsWire is transport only. Thread slot binding metadata through AppElements, browser state, and the navigation planner. The commit boundary now preserves previous slot content and binding proof only when target route-state marks the slot default or unmatched and the visible state proves a mounted/default value exists. Tests cover the new planner contract, metadata validation, route wiring, mergeElements behavior, and browser commit lifecycle for default and unmatched slot targets. * fix(app-router): align intercepted slot binding metadata Intercepted slot payloads could render active modal content while their route-state metadata still described the slot as default. The browser planner could then preserve the previous default slot for an ID that the fresh payload should replace, breaking intercepted navigations in app-router E2E. Derive slot IDs through one helper for both metadata and rendering, assert graph/wire ID divergence, and compute binding state from the same resolved override default export used by the render path. Move the app hydration marker to the committed root layout effect so E2E link clicks wait for a hydrated tree. * fix(app-router): harden slot binding preservation Review follow-up for #726-CORE-14. Centralize AppElements slot binding normalization so duplicate slot ids and stale owner layout ids fail at the wire boundary instead of creating quiet last-write-wins behavior. Alias planner slot binding snapshots to the AppElements binding shape, share slot id ordering helpers, document the no-proof compatibility law, and add negative planner/wire tests plus a browser-visible soft navigation E2E. Refs #726. * fix(app-router): align slot binding proof with rendering * docs(app-router): clarify slot preservation invariants |
||
|
|
a5805b655a | fix(navigation): support prefetch invalidation callbacks (#1244) | ||
|
|
1e7005bbb6 | fix(app-router): refresh after discarded revalidating actions (#1240) | ||
|
|
d3f028206f |
fix(navigation): throw when app router context is missing (#1242)
next/navigation useRouter returned the module-level app router singleton even when no AppRouterContext provider was mounted. That masked components rendered outside the App Router boundary, diverging from Next.js and hiding integration bugs. The shim now reads the mounted AppRouterContext and throws the Next.js invariant when it is absent. The App Router browser and SSR roots provide the existing appRouterInstance so legitimate app renders keep the same public router surface. |
||
|
|
aee62a123a |
fix(app-router): hydrate initial root in transition (#1243)
App Router hydration currently calls hydrateRoot at urgent priority, unlike Next.js App Router startup. That makes the initial hydration work compete with input that React could otherwise prioritize as urgent. Move the initial App Router hydrateRoot call behind an explicit transition scheduling helper and keep returning the created root synchronously so existing navigation globals keep their current contract. Add focused regression coverage that proves hydrateRoot is invoked inside the transition callback. |
||
|
|
c7453909e2 |
fix(workflow): use correct workers.dev URL for compat ingest (#1241)
* fix: use vinext-web.vinext.workers.dev as the ingest URL The default URL I picked (cloudflare.workers.dev) was a guess — workers *.workers.dev URLs are namespaced under the account subdomain, which for this account is vinext.workers.dev (see scripts/smoke-test.sh and the deploy-examples workflow). curl was getting NXDOMAIN. Still overridable via the COMPAT_INGEST_URL repo variable for custom domains later. * fix(workflow): also archive compat-ingest.json in the report artifact When the live POST to /api/compatibility fails (DNS, expired secret, rotated secret, etc.) we previously had no way to recover the run without re-running the 40-minute suite. The ingest payload was being generated but only the human-readable deploy-suite-report.json was uploaded. Add compat-ingest.json to the same artifact so a maintainer can download it and curl it through manually. |
||
|
|
1c0bdacda6 |
feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest (#1239)
* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest
Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.
- D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
Tables are namespaced (`compat_runs`, `compat_file_results`) so
future metric kinds can live alongside.
- POST /api/compatibility ingest endpoint. Authenticates with a
`COMPAT_INGEST_SECRET` worker secret and upserts by
(kind, runKey).
- GitHub-style contribution grid (one dot per test file) and a
pass-rate-over-time line chart. Both are SVG, no chart libs.
Pass rate excludes Next.js-skipped tests from the denominator.
- Shared header/footer extracted into app/_components and rendered
by the root layout. Nav uses next/link for soft client nav.
- Workflow change: on top of the existing per-test pass/fail/skip
aggregation, the report job now POSTs results to the ingest
endpoint. Guarded to only run when (a) filter=all and
(b) targeting main, so partial runs and branch spot-checks don't
pollute the historical record.
The first nightly run after merge will seed the production D1.
* address review + add ISR with KV cache
Review fixes (apps/web/app/api/compatibility/route.ts):
- constant-time secret comparison via SHA-256 + bytewise XOR
- single-statement atomic upsert with onConflictDoUpdate (no
SELECT->INSERT race for concurrent retried runs)
- top-level try/catch with structured error body so workflow logs
show why ingest failed instead of a generic 500
- MAX_FILES=2000 cap in validation as defensive bound
Page perf (apps/web/app/compatibility/page.tsx):
- latest-run and trend D1 queries run in parallel via Promise.all
ISR + KV:
- new vinext-web-cache KV namespace bound as VINEXT_CACHE
- worker installs KVCacheHandler once per isolate
- both pages opt into ISR with revalidate = 300; compat page drops
force-dynamic
* address bonk re-review
- Drop the ?kind= query param. Only one kind exists today; gating it
behind an unused param invited ISR cache pollution. Hardcoded to
"deploy" with a comment pointing future kinds at a dedicated route.
- Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
crash mid-write can no longer leave a run with zero file results.
D1 executes batch statements inside one transaction.
- Resolve workflow expressions once via step-level `env:` and read
them as `process.env.*` from the github-script body. Removes the
fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
string literals (the nested single quotes were the worst offender).
|
||
|
|
5d5fe32a65 |
fix(app-router): forward searchParams correctly in probePage() (#1238)
The RSC entry's probePage() body read a non-existent key off collectAppPageSearchParams's return value, so the page component received undefined for searchParams. Any page that did `await searchParams` during probing threw TypeError, the probe swallowed it as a non-special error, and redirect()/notFound() calls were dropped from the probe path. The visible render path was unaffected because app-page-element-builder reads the correct key independently. Per AGENTS.md, the generated entry should be thin codegen glue. Move the probePage() body into a typed probeAppPage() helper in server/app-page-probe.ts and have the entry delegate to it, so the search-params wiring is unit-testable in isolation. Add focused tests covering thenable params, empty-query handling, the null page-component case, and redirect-throw propagation. Fixes https://github.com/cloudflare/vinext/issues/1235 |
||
|
|
d9906b94d6 |
chore: add basic vinext site (#1236)
* chore: scaffold new next app with vinext * basic app * add to deploy workflow * fmt |
||
|
|
1ac7b6514b |
fix(examples): next build failure for app-router-playground (#1234)
|
||
|
|
b4898289e7 |
fix(app-router): match Next.js RSC Content-Type and 404 plain-text body (#1233)
* fix(app-router): match Next.js RSC Content-Type (no charset suffix)
Next.js sets the RSC (React Server Components / 'flight') response
Content-Type to exactly 'text/x-component' (RSC_CONTENT_TYPE_HEADER in
.nextjs-ref/packages/next/src/client/components/app-router-headers.ts:17,
used by FlightRenderResult in
.nextjs-ref/packages/next/src/server/app-render/flight-render-result.ts:15).
vinext was sending 'text/x-component; charset=utf-8'. The suffix breaks
Next.js's own segment-cache deduplication checks (action-handler.ts uses
.startsWith(RSC_CONTENT_TYPE_HEADER) on the unsuffixed string) and fails
Next.js e2e tests that assert strict equality, e.g.:
- test/e2e/app-dir/app/index.test.ts L362, L371
- test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts L80
Replace the duplicated literal at seven sites with the existing
VINEXT_RSC_CONTENT_TYPE constant in app-rsc-cache-busting.ts.
* fix(app-router): match Next.js 404 plain-text body 'This page could not be found'
Next.js's plain-text 404 response body is exactly
'This page could not be found' (no trailing period in the response body;
the React-rendered not-found component uses the same text with a period).
vinext was returning 'Not Found' / '404 Not Found' / '404 - Not found',
which fails Next.js e2e tests that assert .toContain('This page could
not be found'), e.g.:
- test/e2e/app-dir/app/index.test.ts (several cases)
- test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts L52
- test/e2e/app-dir/prefetching-not-found/prefetching-not-found.test.ts L16
Next.js source citations (in .nextjs-ref):
- packages/next/src/server/route-modules/pages/pages-handler.ts L121, L535
- packages/next/src/build/templates/app-route.ts L170, L349
- packages/next/src/build/templates/app-page.ts L701, L1043
- packages/next/src/pages/_error.tsx L7
- packages/next/src/client/components/builtin/not-found.tsx L7 (HTML variant)
Updates the body in notFoundResponse() and the open-redirect / pages-router
SSR-fallback / worker-entry inline 404 sites. The HTTP status reason phrase
('Not Found') is unchanged. Route-specific bodies like '404 - API route not
found' are intentionally retained.
|
||
|
|
efa18a3f80 |
fix(pages-router): include _app module assets in served page HTML (#1232)
* fix(pages-router): include _app module assets in served page HTML When a Pages Router page renders, vinext's prod server collects asset tags only for the matched page module. _app.tsx — which wraps every page — is omitted, so any CSS/JS it imports is built and emitted but never <link>ed from the rendered HTML. For SCSS users this is the LHF-5 cluster from the Next.js deploy suite: when global.scss is imported via _app.tsx, Vite compiles it correctly and emits assets/_app-*.css, but the browser never sees a <link rel="stylesheet"> for it. The page renders with the user-agent default colour (rgb(0, 0, 0)) instead of the SCSS-defined colour. Fix: embed the resolved pages/_app file path at build time and include it in the moduleIds passed to collectAssetTags, so the SSR manifest's CSS/JS entries for _app are emitted alongside the page's own. Includes a regression test (tests/scss.test.ts) plus a fixture page that imports a .scss file with a variable. The test covers App Router dev, App Router production build, Pages Router dev, and the Pages Router production server end-to-end (build → start prod server → fetch page → assert linked stylesheet has the resolved colour). Skipped automatically when sass is not installed — vinext relies on Vite's built-in SCSS preprocessor, which requires the user to install sass (or sass-embedded) themselves. Matches Vite's model. * fix: isolate SCSS fixture and silence TS check for optional sass import The first revision of this PR added an SCSS fixture under tests/fixtures/app-basic/app/nextjs-compat/scss-test/. That directory is scanned by every test that boots the shared app-basic fixture, so when sass is not installed (CI default) every page in app-basic fails the route-graph scan and 500s. jsx-in-js, app-css, node-modules-css, and ~dozens of integration tests went red as a result (CI run https://github.com/cloudflare/vinext/actions/runs/25913568939). The TypeScript check job also failed because `await import("sass")` produces TS2307 when sass isn't declared as a workspace dep. Fixes: - Drop the shared fixture under app-basic. tests/scss.test.ts now materialises its Pages Router SCSS fixture in a per-test tmpdir (same pattern as the existing Pages Router scenario), so the shared fixture's route-graph scan never sees a .scss import. No other test is affected. - The App Router test scenarios from the first revision are removed. They required additional node_modules wiring for @vitejs/plugin-rsc to resolve from a tmpdir and weren't needed to validate the fix — the actual change is in the Pages Router asset-collection path, and the Pages Router tests exercise it end-to-end (dev + production build + linked stylesheet assertion). - Replace @ts-expect-error with @ts-ignore on the dynamic sass import. sass may or may not be installed depending on the environment; when it is, there's no type error to expect and @ts-expect-error itself errors as unused. @ts-ignore is the right tool for an optional peer. The pages-server-entry.ts fix from the first revision is unchanged — this commit only re-shapes the test surface so it doesn't poison the shared fixture. |
||
|
|
59a2c3bcc8 |
fix(shims): add unstable_catchError, unstable_rethrow, unstable_isUnrecognizedActionError (#1206)
* fix(shims): add unstable_catchError, unstable_rethrow, unstable_isUnrecognizedActionError
Unblocks 18 build failures in the Next.js deploy suite:
- `unstable_catchError` (12 failures) — added to `shims/error.tsx`. App
Router error-boundary HOC ported from Next.js's
`client/components/catch-error.tsx`. Implements the same
`getDerivedStateFromError` predicate that re-throws Next.js navigation
signals (redirect, notFound, …) and renders the user fallback with an
`ErrorInfo` value otherwise. `unstable_retry` throws a clear "not yet
implemented" error (tracked as follow-up).
- `unstable_rethrow` (3 failures) — added to `shims/navigation.ts` and
re-exported from `shims/navigation.react-server.ts`. Ported from
Next.js's `client/components/unstable-rethrow.{ts,server,browser}.ts`.
Specialized to the internal-error categories vinext actually emits
(redirect + HTTP access fallback); recurses through `error.cause` for
wrapped errors.
- `unstable_isUnrecognizedActionError` (3 failures) — added to
`shims/navigation.ts` along with the `UnrecognizedActionError` class.
Ported 1:1 from `client/components/unrecognized-action-error.ts`. The
react-server condition exports a throwing stub matching Next.js's
`navigation.react-server.ts`.
Also adds `isRedirectError` and `isNextRouterError` helpers to
`navigation.ts` (used internally by `unstable_rethrow`) and updates
`next-shims.d.ts` so the new exports type-check for users.
* fix(shims): document isRedirectError divergence and dedupe error-boundary copy
Addresses two /bigbonk review observations on #1206:
1. **Document the permissive prefix match.** The public `isRedirectError`
in `shims/navigation.ts` does `digest.startsWith("NEXT_REDIRECT;")`
while Next.js's `client/components/redirect-error.ts` does full
4-segment validation (type ∈ {push, replace}, non-empty destination,
status ∈ {303, 307, 308}). The divergence is intentional — vinext
emits 3-part and 4-part digests whereas Next.js's validator targets
its 5-part canary digests — but now that the predicate is reachable
from the public `next/navigation` surface via `unstable_rethrow`, it
gets a prominent JSDoc block explaining the difference, the
consequence (malformed digests return true here, false in Next.js),
and a link to the Next.js source.
2. **Consolidate duplicate `isRedirectError`.** `shims/error-boundary.tsx`
had its own private copy with the same prefix-match logic. Replaced
the duplicate with an import from `./navigation.js`, matching the
pattern already used by `shims/error.tsx`
(`import { isNextRouterError } from "./navigation.js"`). The
`RedirectBoundary` call site casts to the file-local `RedirectError`
type to preserve access to the optional `handled` field — this is
safe by construction (every error matching the prefix predicate is
produced by vinext's `redirect()` / `permanentRedirect()` helpers,
which yield `Error` instances).
No behavior change. `vp check` + `vp test run tests/shims.test.ts
tests/error-boundary.test.ts` (916 tests) pass.
* fix(shims): drop unused getErrorDigest export
After consolidating shims/error-boundary.tsx onto the public
isRedirectError from shims/navigation.ts, the only remaining caller of
getErrorDigest is inside utils/navigation-signal.ts itself. Knip flags
the export as unused, which fails the Check job in CI.
Convert getErrorDigest to a file-local function. No behavior change.
* fix(shims): extend unstable_rethrow + make unstable_retry functional
Parity audit findings on PR #1206 (post /bigbonk APPROVED) revealed two
gaps from a thorough re-read of Next.js's error-rethrow surface and
`unstable_catchError` test fixtures.
## 1. `unstable_rethrow` — added missing error categories
Next.js's server-side `unstable_rethrow` rethrows seven error categories
(client-side rethrows two). vinext was only handling #1
(`isNextRouterError`). The canonical user-facing fixture
(`test/e2e/app-dir/app-static/lib/fetch-retry.js`) breaks if any of the
other categories are not propagated.
Added the two categories vinext can plausibly encounter:
- `BailoutToCSRError` + `isBailoutToCSRError` — thrown by `next/dynamic`
with `ssr: false`. Lives in shared (non-server) code so third-party
libraries and accidentally-bundled Next.js internals can produce it.
Ported 1:1 from
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts
- `DynamicServerError` + `isDynamicServerError` — thrown by Next.js's
`cookies()`/`headers()` when called in a static render context. vinext
doesn't construct it itself but exposes the class so user code, action
wrappers, and accidental Next.js-internal bundles can interoperate.
Ported 1:1 from
https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/hooks-server-context.ts
The remaining four server-only categories (`isDynamicPostpone`,
`isPostpone`, `isHangingPromiseRejectionError`,
`isPrerenderInterruptedError`) are tied to Next.js's PPR / prerender
controller machinery that vinext does not implement. User code cannot
construct them in normal use; they're deferred as a follow-up
(documented inline). A test pins this intentional gap.
## 2. `unstable_catchError.unstable_retry` — made functional
The previous implementation threw a generic "not yet implemented" error
in both server and client contexts. Now matches Next.js's App Router
branch: on the client, calls `appRouterInstance.refresh()` inside
`React.startTransition` and resets the boundary. On the server, throws
a clear "client-only" error (refresh is meaningless during SSR setup).
Pages Router's exact error message (`unstable_retry() can only be used
in the App Router. Use reset() in the Pages Router.`) is not yet
dispatched because vinext's boundary doesn't read `PagesRouterContext`;
documented in the JSDoc as a parity follow-up.
## Test coverage
Added 8 new test cases covering:
- `BailoutToCSRError` + predicate (canonical digest, foreign-object
detection, negative cases)
- `DynamicServerError` + predicate (same shape)
- `unstable_rethrow` propagates both new categories (re-throw identity)
- `unstable_rethrow` does NOT match the four uncovered server-only
categories (pins the intentional gap)
- `unstable_catchError.getDerivedStateFromError` accepts `null` /
`undefined` thrown values (ported from
test/e2e/app-dir/catch-error/throw-null and throw-undefined fixtures)
- `unstable_retry` throws "client-only" on the server
- `unstable_retry` on the client calls `appRouterInstance.refresh()` +
resets via setState
Also added `isRedirectError` / `isNextRouterError` / new predicates
JSDoc note that these are vinext-only extensions (Next.js does NOT
export them from `next/navigation` — confirmed by reading
`packages/next/src/client/components/navigation.ts` end-to-end).
`vp test run tests/shims.test.ts tests/error-boundary.test.ts
tests/app-router.test.ts tests/app-rsc-errors.test.ts` — 1274 tests
pass. `vp check` clean.
* fix(shims): drop redundant typeof check in isDynamicServerError
Bonk-review observation on #1206: the predicate was checking
`typeof digest === "string" && digest === _DYNAMIC_SERVER_USAGE_DIGEST`,
where the `===` against a string literal already requires the operand
to be a string. Drop the redundant guard for consistency with
`isBailoutToCSRError` above.
Non-blocking cleanup; no behavior change.
|
||
|
|
c3b8948196 |
feat(compat): support declared artifact compatibility sets (#1230)
Artifact compatibility previously treated every known graph, deployment, root boundary, or render epoch mismatch as incompatible, even when a rolling deploy can prove that two versions belong to an explicit compatibility window. That preserved safety but left no typed path for #726-COMPAT-06/08 canary, rollback, and old/new client coverage. Add explicit compatibility sets to the evaluator. Matching remains strict by default, unknown, future, and legacy metadata still fall back to renderFresh, and overlapping pairs are not treated as transitive proof. Cover rolling deploy, canary/rollback, stale map, old-client/new-server, and new-client/old-server scenarios. |
||
|
|
4aa7663084 |
fix(app-router): hard navigate stale RSC build payloads (#1178)
* fix(app-router): hard navigate stale RSC build payloads App Router soft navigation currently accepts any valid RSC payload after deploy. That lets stale tabs decode RSC responses produced by a different build, which can break client navigation when the server and browser artifacts no longer match. The missing invariant was that every RSC response consumed by the browser must identify the build that produced it, and the browser must reject payloads from a different build before handing them to React. Emit a vinext-owned build ID header on App Router RSC responses, preserve it in prefetched and visited response snapshots, and hard navigate when a response is missing or mismatches the current client build ID. * chore: fix e2e fixture hook lint suppression * chore: avoid hook-like fixture callback name * fix(app-router): preserve build IDs for cached RSC payloads * fix(app-router): use public RSC compatibility IDs |
||
|
|
1d01d2989a |
feat(cache): record app render observations (#1198)
* feat(cache): record app render observations App Router cache artifacts and payloads did not carry the render-observation metadata required by #726-CACHE-05/07. That left later cache-proof work without an attached record of request APIs, dynamic fetches, output scope, cache tags, and boundary outcome. The missing boundary was that render-scoped observations lived only as dynamic/cache state, not as metadata owned by each produced payload or artifact. Record render request API usage and dynamic fetch observations in request-scoped state, build redacted RenderObservation payloads for AppElements and APP_PAGE cache writes, and preserve complete observations for HTML/RSC ISR artifacts including stale regeneration. Targeted coverage asserts payload metadata, artifact metadata, redaction, and existing cache-proof behavior. * test(cache): cover search param render observation usage The CI unit shard failed because app-page-element-builder tests fully mocked the headers shim and omitted the new markRenderRequestApiUsage export. The production path now records searchParams as a render observation whenever populated search params make the page dynamic. Update the mock to expose the new shim export and assert that populated search params mark both dynamic usage and render-observation usage, while empty search params mark neither. * refactor(cache): tighten render observation review handling Render observation metadata now reuses the app page observation state shape instead of duplicating cache-local types. The deferred cache writes also document the stream-consumption boundary that preserves late request API observations. Dynamic fetch observations are now stored in a Set so repeated no-store fetches do not inflate metadata, and the collector has focused regression coverage. The Link transition test also patches and restores the runtime React default so the full unit project is not order-sensitive when other files instantiate the module graph first. * fix(cache): preserve parent render observations |
||
|
|
c5cebaa496 |
refactor(prerender): extract shared static-paths normalization (#1231)
* refactor(prerender): extract shared static-paths normalization PR #1227 added the same getStaticPaths-entry normalization logic to three files: packages/vinext/src/build/prerender.ts, packages/vinext/src/server/pages-page-data.ts, and packages/vinext/src/server/dev-server.ts. Each site handled the string-vs-object branch, the missing-params guard, and the query-string + trailing-slash trimming on its own. Hoist the shared logic into packages/vinext/src/routing/route-pattern.ts alongside the existing matchRoutePattern helper. Two new exports: - normalizeStaticPathname(pathname): the shared query-strip and trailing-slash-strip used by every call site for path comparison. - normalizeStaticPathsEntry(entry, routePattern): the build-time helper, returning { params } | { error }; reused by prerenderPages. Both server-side matchers (matchesPagesStaticPath in pages-page-data.ts and the dev-server's request handling) now share the normalizeStaticPathname helper and the StaticPathsEntry type. Their runtime matching behaviour is preserved exactly, including the missing-params -> false (404) guard. Pure refactor: no behaviour changes. Existing tests in tests/prerender.test.ts (the safety net from #1227) still pass; new focused unit tests added to tests/route-pattern.test.ts cover the helper shape, string and object entries, catch-all patterns, percent-encoded segments, and every error path. * fix: drop unused NormalizedStaticPathsEntry export knip flagged the type as an unused exported type. It's only referenced inside route-pattern.ts as the return-type alias of normalizeStaticPathsEntry, so make it module-local. |
||
|
|
600580eec0 |
ci(e2e): cache WebKit apt archives and skip browser install on cache hit (#1228)
WebKit E2E jobs were dominated by two costs: 1. `playwright install --with-deps webkit` re-downloaded the browser binaries (or at least re-validated them) on every run even when the browser cache was hit, because the install step was unconditional. 2. The `--with-deps` flag always ran `apt-get install` for WebKit's system libraries (~30+ packages), and apt re-downloaded the .deb files on every run since runner archives aren't preserved across runs. This commit: - Splits browser install from system dep install so each can be cached on its own schedule. - Gates the browser install on `cache-hit`, so cached runs skip the Playwright CLI entirely on the hot path. - Adds a second cache for `/var/cache/apt/archives` (mirrored through `~/.cache/apt-archives`) so apt finds previously downloaded .deb files and skips redownloading them. The install itself still runs every time, but the slow network portion goes away. - Keys both caches on the resolved Playwright version from pnpm-lock.yaml rather than the full lockfile hash, with a fallback `restore-keys` so unrelated lockfile churn doesn't invalidate the cache. Chromium jobs also benefit from skipping the Playwright CLI on cache hits. |
||
|
|
617605b925 |
fix(prerender): support string paths and missing params in getStaticPaths (#1227)
Next.js allows getStaticPaths to return paths as either
Array<{ params }> or Array<string> (see Next.js docs and
.nextjs-ref/packages/next/src/build/static-paths/pages.ts). vinext only
handled the object shape, so a fixture returning string paths crashed
the prerender phase with:
TypeError: Cannot read properties of undefined (reading 'slug')
at buildUrlFromParams (packages/vinext/src/build/prerender.ts:311)
This blocked ~213 Next.js deploy-suite tests
(see https://github.com/cloudflare/vinext/actions/runs/25897889733 —
middleware-general index/node-runtime, middleware-rewrites, and
middleware-trailing-slash all gated on this single build crash).
Changes:
- buildUrlFromParams: defensively throw a descriptive error when
params itself is null/undefined, instead of crashing with a cryptic
TypeError pointing at a dist line.
- prerenderPages (Pages Router): normalize each paths entry. Strings
are run through the route pattern matcher to extract params, mirroring
Next.js _routeMatcher(cleanedEntry). Missing/null params surface as
per-route error results instead of crashing the whole prerender.
- prerenderApp (App Router): defensively guard each entry from
generateStaticParams() against missing params with a clear error
message in the existing try/catch.
- pages-page-data.ts (prod SSR): matchesPagesStaticPath now handles
string entries via URL comparison and guards undefined params.
- dev-server.ts (dev SSR): same string-path + missing-params handling
for the dev request path.
- Tests: add tests/fixtures/pages-basic/pages/string-paths/[slug].tsx
and missing-params/[slug].tsx, with two new vitest cases asserting
successful prerender and a per-route error result.
|
||
|
|
457b7dcf8d |
fix(image): exclude ipaddr.js from SSR dep optimizer (#1226)
The next/image client shim imports ipaddr.js for private-IP validation.
It's already in ssr.resolve.external, but the SSR dep optimizer would
still pre-bundle it on first request, producing
(ssr) ✨ new dependencies optimized: ipaddr.js
and the accompanying full reload. Add ipaddr.js to optimizeDeps.exclude
for the App Router and Pages Router SSR environments so the optimizer
skips it; runtime resolution still works via resolve.external on Node
and via the worker bundle on Cloudflare/Nitro. The client environment
intentionally keeps pre-bundling ipaddr.js since next/image is a 'use
client' component and needs the CJS→ESM conversion in the browser.
|
||
|
|
9277b101b2 |
feat(dev): add dev server lock file (#1193)
* feat(dev): add dev server lock file Write the running dev server's PID, port, and URL into `node_modules/.cache/vinext/dev-lock.json`. When a second `vinext dev` process starts in the same project directory, vinext reads the lock file and prints an actionable error: Another vinext dev server is already running. - Local: http://localhost:3000 - PID: 12345 - Dir: /path/to/project You can access the existing server at http://localhost:3000, or run `kill 12345` to stop it and start a new one. This is especially useful for AI coding agents, which frequently attempt to start `vinext dev` without knowing a server is already running. The structured error gives the agent the PID to kill the existing process or the URL to connect to it — no manual intervention required. If the recorded PID is dead (stale lock from a crashed run), the new process takes over the lock automatically. Set `VINEXT_NO_DEV_LOCK=1` to opt out entirely. Behavior modeled on Next.js' dev server lock file: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/lockfile.ts vinext uses a JSON file + PID liveness check (`process.kill(pid, 0)`) rather than a native `flock()`. The race window on acquisition is small and benign: at worst, two dev servers race and one fails to bind its port. * chore(dev-lockfile): unexport types only used internally knip flagged FormatErrorOptions, AcquireOptions, AcquireSuccess, AcquireFailure, and AcquireResult as unused exports. These are only referenced inside dev-lockfile.ts itself, so drop the `export` keyword to keep the public surface minimal. * review(dev-lockfile): address self-review and bot feedback - Capture ownerPid at acquire time and use it in release() instead of reading info.pid. Decouples release from update() so a future caller passing a different PID through update() can't trick release() into deleting another process's lock. Added a regression test. - Document the TOCTOU window between readLockfile() and writeLockfile() inline so future contributors don't try to 'fix' the intentionally accepted race. - Document the unreachable existing: undefined branch in formatAlreadyRunningError as a defensive fallback. - Document the process.on('exit', ...) signal semantics — exit fires on graceful shutdown and after default SIGINT/SIGTERM handlers, but not on hard crashes. The next vinext dev will take over the stale lock. - Set lock file mode to 0o600. Defense-in-depth: the PID is also discoverable via ps, so this isn't load-bearing. - Use Vite's server.resolvedUrls.local[0] in cli.ts when building the post-listen appUrl. Vite substitutes 'localhost' for wildcard binds (0.0.0.0) so the URL is actually clickable. Also substitute 'localhost' in the pre-listen initial info for the same reason. - Add tests: * exit listener is registered/deregistered correctly * update() with a different PID doesn't affect release() ownership * lock file is created with 0o600 on POSIX * review(dev-lockfile): release on listen failure, preserve startedAt Two issues caught in the bot re-review: 1. If `server.listen()` throws (e.g. strictPort and the port is taken), the dev() function previously threw without releasing the lock. The process.on('exit') handler still cleaned up afterwards, but in the brief window between listen failure and process exit, the lock file claimed a server was running at a port nobody was listening on. A concurrent `vinext dev` in that window would have shown a misleading 'already running' error. Wrap createServer + listen in try/catch and call lockfile.release() on failure. The exit listener is still registered as a safety net for unexpected exit paths. 2. update() after server.listen() was setting `startedAt: Date.now()`, which overwrote the original acquisition timestamp. `startedAt` is meant to reflect when the process started (for 'how long has this server been running?' debugging), not when the URL was resolved. Capture `startedAt` once at acquisition time and pass the same value into update(). Added a regression test. * feat(dev-lockfile): move lock file to .vinext/dev/lock.json Previously the lock file lived at node_modules/.cache/vinext/dev-lock.json. .vinext/ is already the established convention for project-local vinext state — the fonts plugin uses .vinext/fonts/ to cache self-hosted Google Fonts, and the monorepo's own .gitignore already excludes .vinext. Benefits: - Doesn't pollute node_modules (which package managers may aggressively clear during install / pnpm prune). - Co-located with other vinext state, so users find it where they expect. - Mirrors Next.js' .next/dev/lock layout more closely. Follow-up: vinext init does not currently add .vinext to user gitignores. That's a separate hygiene improvement also relevant to the fonts cache. |
||
|
|
b46e9b8c44 |
fix(font): match Next.js font result style semantics (#1186)
* fix(font): match Next.js font result style semantics
Font loaders returned incomplete style exports, emitted Google variable classes unconditionally, and accepted adjustFontFallback without producing a metric-adjusted fallback face. Apps that read next/font results or rely on the generated class rule diverged from Next.js.
The shim assumed font-family was the only exported style concern and that the runtime Google loader should always synthesize a variable class. Reuse the existing static Google font transform boundary to inject fallback CSS, share the single-face style decision between Google and local loaders, and only return variable classes when requested.
Regression coverage ports the relevant Next.js font result expectations and checks adjusted fallback injection, disabled fallback behaviour, and single local source style exports.
* fix(font): avoid fallback metrics output collision
The package build emits imported JSON as JavaScript beside compiled TypeScript. Naming the data file fallback-metrics.json let the JSON artifact collide with fallback-metrics.ts on Windows, so the installed dist module could lack the named fallback helper exports.
Rename the generated data file to fallback-metrics-data.json and update the import so the packed output has distinct fallback-metrics.js and fallback-metrics-data.js modules.
* fix(font): preserve validated Google face metadata
Static Google font calls validated and fetched one concrete face, but the runtime shim recomputed exported style fields from raw options. Italic-only families could therefore load an italic face while returning a normal class rule.
Thread validated single-face weight and style through internal transform options, prefer them in the runtime shim, and harden emitted font descriptor values before class-rule and local font-face CSS generation.
Regression tests cover italic-only Google metadata handoff and unsafe font-style values for Google and local font shims.
* fix(font): omit ambiguous Google font style
Google style resolution treated a multi-style array the same as an omitted style option. That made ambiguous requests such as normal plus italic export fontStyle normal, even though no single face was selected.
Only default to normal when the style option is actually omitted. Multi-style arrays now produce no exported fontStyle, with a regression covering the class rule output.
* fix(font): tighten Google font runtime payload
* test(font): expect internal font payload in build output
* refactor(font): consolidate duplicated CSS sanitization functions into font-utils
Address review feedback:
- Remove sanitizeCSSValue wrapper in font-local.ts, use sanitizeFontDescriptorValue directly
- Add cross-module invariant comments for '{family} Fallback' naming convention
- Consolidate escapeCSSString, sanitizeCSSVarName, and sanitizeFallback into
font-utils.ts (previously duplicated across font-google-base.ts, font-local.ts,
and fallback-metrics.ts)
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
2acec092be | chore: add next-env.d.ts to .gitignore (#1224) | ||
|
|
8418ad7829 | chore: update oxc editor settings (#1223) | ||
|
|
d5d837b720 |
fix(deploy-suite): handle __dirname, __filename, and require.resolve in CJS→ESM converter (#1217)
The CJS→ESM converter extracted in #1209 only handled require("mod") in declaration and expression contexts plus module.exports = X. Real-world next.config.{js,ts} fixtures in the deploy suite also use: - require.resolve('./my-adapter.mjs') — cache-components, edge-pages-support, adapter-dynamic-metadata - __dirname references in the config body — webpack-loader-set-environment-variable, next-config-ts/node-api-cjs - __filename references (rarer) These were left untransformed, producing ReferenceError at config load time and failing the entire test app's deploy. Fix: after running the existing regex pipeline, detect any remaining __dirname / __filename / require.* references and prepend a small ESM prelude that polyfills them via fileURLToPath / createRequire(import.meta.url). The prelude is only injected when actually referenced, so simple configs stay unchanged. Affected suites: - test/e2e/app-dir/cache-components/cache-components.server-action.test.ts - test/e2e/edge-pages-support/index.test.ts - test/e2e/edge-pages-support/edge-document.test.ts - test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts - test/e2e/app-dir/webpack-loader-set-environment-variable/...test.ts - test/e2e/app-dir/next-config-ts/node-api-cjs/...test.ts - test/e2e/app-dir/adapter-dynamic-metadata/... The 'Cannot find module @next/bundle-analyzer' subset of the cluster (converter rewrites require('@next/bundle-analyzer') to ESM but the package isn't installed) is intentionally left to the missing-deps agent. Refs run 25870737355. |
||
|
|
875f314c11 |
fix(deploy-suite): handle missing optional deps in test fixture configs (#1213)
Three classes of missing-dep build failures in the Next.js deploy harness:
1. `@next/bundle-analyzer` (test/e2e/app-dir/metadata-{font,edge,dynamic-routes})
These fixtures wrap the require in `if (process.env.ANALYZE)`, but our
CJS→ESM converter hoisted `require('mod')(args)` to a static top-level
`import`, which unconditionally tried to resolve the package and failed
the build even when ANALYZE was unset.
Fix: rewrite `const X = require('mod')(args)` to an inline
`(await import('mod')).default(args)` instead of hoisting. This preserves
CJS lazy semantics — the import only runs when the surrounding code does.
2. `webpack` (test/e2e/app-dir/next-config/next.config.js)
Top-level `require('webpack').sources.RawSource` — Next.js's own monorepo
has webpack at the workspace root, but the test fixture's package.json
doesn't declare it. Inject `webpack@^5.99.0` into the harness-deps block.
3. `ipaddr.js` (test/e2e/type-module-interop, test/e2e/app-dir/app-middleware)
vinext's image-config shim imports `ipaddr.js`, which is in vinext's
`dependencies`. But vinext is installed via `file:.vinext-local-package`,
so pnpm keeps its transitive deps under `.vinext-local-package/node_modules`
and Node's ESM resolver can't see them from `<test-app>/dist/...` at
prerender time. Inject `ipaddr.js` into the harness-deps list (spec sourced
from the vinext catalog).
Affected suites:
- test/e2e/app-dir/metadata-font
- test/e2e/app-dir/metadata-edge
- test/e2e/app-dir/metadata-dynamic-routes
- test/e2e/app-dir/next-config
- test/e2e/type-module-interop
- test/e2e/app-dir/app-middleware (prerender failure)
Verified:
- `bash -n scripts/*.sh`
- `node --check scripts/cjs-to-esm-config.mjs`
- Round-tripped each of the four fixture next.config.js files through the
patched converter; output is syntactically valid and loads without error
when ANALYZE is unset (and fails with the expected ERR_MODULE_NOT_FOUND
when ANALYZE=1, matching CJS lazy semantics).
Refs: workflow run 25870737355
|
||
|
|
f91d478d9f |
test(link-navigation): sequence link.js and importActual to fix flake (#1218)
The "clicking an RSC Link starts app-router navigation inside a React
transition" test ran link.js's dynamic import and vi.importActual("react")
in parallel via Promise.all. When importActual won the race, "react" got
cached as the actual module before the doMock factory ran, so link.tsx's
transitive `import React from "react"` resolved to the unmocked module.
That made React.startTransition the real implementation rather than the
spy, causing toHaveBeenCalledTimes(1) to occasionally see 0.
Sequencing the imports — load link.js first (with the doMock factory in
effect), then grab React via importActual — eliminates the race.
Refs: https://github.com/cloudflare/vinext/actions/runs/25878626751/job/76052351860
|
||
|
|
040eef55ae |
fix(deploy-suite): inject jiti when test app uses TypeScript config files (#1211)
Test apps using `next.config.ts`, `postcss.config.{ts,mts,cts}`, or
`.postcssrc.ts` fail at `vinext build` time with:
Failed to load PostCSS config: 'tsx' or 'jiti' is required for the
TypeScript configuration files. Make sure it is installed
Cannot find package 'jiti' imported from
.../vite/dist/node/chunks/node.js
Next.js auto-installs one of jiti/tsx as part of its own build pipeline
when it detects a TypeScript config file. Once vinext replaces Next.js
as the deploy target, the test app's package.json no longer lists
either, and Vite's bundled PostCSS / Tailwind / etc. config loaders
fail to load `*.config.ts` files.
Fix: extend the harness-deps injector in scripts/e2e-deploy.sh to scan
the test app root for `*.config.{ts,mts,cts}` files (and the dotfile
`.postcssrc.ts` variants) and inject `jiti` into devDependencies
when present. jiti is the lighter option (no native deps) and matches
what Vite's internal loader prefers.
Pinned to `^2.6.1` to match the version already resolved transitively
in this workspace via `@tailwindcss/node`. Both postcss-load-config and
Vite require `jiti >=1.21.0`; `^2.6.1` satisfies that.
Affected suites (~20 build-time failures in run 25870737355):
- test/e2e/app-dir/next-config-ts/*
- test/e2e/app-dir/next-config-ts-native-mts/*
- test/e2e/app-dir/next-config-ts-native-ts/*
- test/e2e/postcss-config-ts/
Verified locally:
- `bash -n scripts/*.sh` passes
- Manually exercised the embedded Node block against synthetic test
apps with next.config.ts, postcss.config.mts, .postcssrc.ts, and a
negative control (next.config.js only). jiti injection fires for all
three positives and stays out for the negative.
Refs: https://github.com/cloudflare/vinext/actions/runs/25870737355
|
||
|
|
eccee1843e |
fix(deploy-suite): apply tsconfig compilerOptions.paths when loading next.config.ts (#1215)
Next.js's own loader (packages/next/src/build/next-config-ts/transpile-config.ts)
reads compilerOptions.paths + baseUrl from tsconfig.json and passes them to SWC
so next.config.ts can import via tsconfig aliases like:
import { foo } from '@/foo'
vinext's loadNextConfig() calls Vite's runnerImport(), but did not configure any
resolve.alias entries, so the same imports fail with:
[vinext] Failed to load next.config.ts:
Cannot find module '@/foo' imported from '<dir>/next.config.ts'
This is a parity gap surfaced by the Next.js deploy test suite.
Fix: extract a small tsconfig-paths loader (config/tsconfig-paths.ts) that
reads compilerOptions.paths (following extends, honoring baseUrl) and returns
absolute Vite resolve.alias entries. loadNextConfig now passes those aliases
into runnerImport's inlineConfig.
Affected suites (from run 25870737355):
- e2e/app-dir/next-config-ts/import-alias-paths-only/
- e2e/app-dir/next-config-ts/import-alias-paths-with-baseurl/
- e2e/app-dir/next-config-ts/import-js-extensions-cjs/
- e2e/app-dir/next-config-ts/import-js-extensions-esm/
- e2e/app-dir/next-config-ts-native-ts/dynamic-import-esm/
- e2e/app-dir/next-config-ts-native-mts/import-from-node-modules/
Verification:
- 9 new unit tests for loadTsconfigPathAliasesForRoot
- 4 new unit tests covering loadNextConfig + tsconfig paths (ported from
Next.js's import-alias-paths-only / import-alias-paths-with-baseurl
fixtures), including extends + jsconfig fallback
- Reproduced against .nextjs-ref/test/e2e/app-dir/next-config-ts/
import-alias-paths-only/ — config now loads with `foo === "foo"`
- vp check (fmt + lint + types) clean
- bash -n scripts/*.sh clean
Refs: cloudflare/vinext#actions/runs/25870737355
|
||
|
|
289ab18b14 |
fix(deploy-suite): bump pinned sass below ^1.70.0 for Vite 8 (#1214)
The Next.js scss e2e fixtures pin `sass: '1.54.0'` (and some scss-modules fixtures pin `sass-embedded: '1.75.0'`) via createNextDescribe's `dependencies` option. Vite 8's built-in `vite:css` preprocessor calls `sass.initAsyncCompiler()`, an API added in dart-sass 1.70.0 (Feb 2024). Vite 8 declares `sass@^1.70.0` / `sass-embedded@^1.70.0` as optional peers, but the fixture's older pin wins during install. Result: every test/e2e/app-dir/scss/* and test/e2e/app-dir/scss-modules/* suite fails at vinext build time with: TypeError: [sass] sass.initAsyncCompiler is not a function (see run 25870737355, ~31 deploy-script failures from this cluster). Fix: in the deploy harness's dependency-injection step, after a test app's package.json has been augmented with vinext + Vite plugins, check for a `sass` or `sass-embedded` entry pinned below `^1.70.0` (parsed from the leading `major.minor` of common semver specs) and rewrite it to `^1.70.0`. Non-version specs (workspace:, file:, git+, tags) are left alone. This is a no-op for test apps that don't depend on sass. Verified locally by running the patched harness against the multi-page scss fixture: the resolved pnpm path goes from `sass@1.54.0` to `sass@1.99.0`, and the build advances past the SCSS preprocessor (a separate JSX-in-Pages-Router error then trips it, tracked as its own cluster). |
||
|
|
aa5ec73ada |
docs(vinext): clarify middleware.ts deprecation warning is non-fatal (#1212)
The deploy-suite investigation in run 25870737355 flagged this warning as a build-failure root cause. It is not. middleware.ts is still fully supported in Next.js 16 and in vinext; the message is purely a deprecation notice (console.warn), and none of the 26 build failures in the cluster are caused by it. The real failures are unrelated (wasm imports, missing CSS modules, URLPattern, server-only validation, and prerender param bugs) and belong to other clusters. This commit only updates the JSDoc to: - Explain why the warning may appear 2-3 times per build (Vite calls the plugin config hook once per environment). - Note that Next.js itself emits the same message via Log.warnOnce, and point to where parity would live. - Add an inline comment warning future readers not to change this console.warn to a throw / process.exit. No behaviour change. The warning is intentional and stays a warning. Refs: https://github.com/cloudflare/vinext/actions/runs/25870737355 |
||
|
|
bc8cee2385 |
fix(deploy-suite): extract CJS->ESM converter to standalone script (#1209)
* fix(deploy-suite): extract CJS->ESM converter to standalone script
The CJS->ESM next.config converter was inlined into scripts/e2e-deploy.sh
via 'node -e ...' with bash single-quote escapes ('"'"'). The escaping
rebalanced quotes such that later code (a comment containing '(e.g.') sat
outside any single-quoted bash region, causing 'bash -n' to fail with:
scripts/e2e-deploy.sh: line 490: syntax error near unexpected token `('
The script died before doing anything in every deploy-suite shard, producing
2480 identical 'Custom deploy script failed: undefined (2)' failures and a
misleading 9.5% pass rate (almost all 'passes' were 'should skip next deploy'
no-ops). See run 25869359604.
- Extract the converter to scripts/cjs-to-esm-config.mjs (same regex
pipeline, no behavioural change).
- Replace the inlined 'node -e' block with a plain 'node ./script.mjs'
invocation.
- Add a 'bash -n scripts/*.sh' gate to the Check job so a future regression
here can't ship green.
* Apply suggestion from @james-elicx
|
||
|
|
843df83de9 |
ci(deploy-suite): default to all suites and disable test retries (#1208)
The Next.js deploy suite defaulted to only running app router tests with 3 attempts per test (run-tests.js DEFAULT_NUM_RETRIES = 2). Switch the default suite-filter to 'all' so nightly runs cover pages router too, and pass --retries 0 so failures surface on the first attempt instead of masking flakes behind retries. |
||
|
|
a6d9826613 |
fix(shims): add runtime default export to next/app (#1204)
* fix(shims): add runtime default export to next/app
Pages Router fixtures using the canonical `pages/_app.js` pattern
(`import App from 'next/app'; export default class MyApp extends App`)
currently fail to build under vinext with:
[MISSING_EXPORT] "default" is not exported by
"node_modules/.../vinext/dist/shims/app.js"
The existing shim only re-exported a type as default. This adds a real
runtime class component matching Next.js's behaviour:
- `App` extends React.Component, renders `<Component {...pageProps} />`.
- `App.getInitialProps(appContext)` returns `{ pageProps }`, invoking the
wrapped page's `getInitialProps` if defined.
- `App.origGetInitialProps` is preserved alongside `getInitialProps`.
- AppProps, AppContext, AppInitialProps types are exported.
Ported from:
https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx
This unblocks 51 build failures in the Next.js deploy suite that all
trace back to the missing default export.
* fix(shims): expose App class + types in next-shims.d.ts; document null coercion
Address two nits from the PR #1204 bonk review:
1. `next-shims.d.ts` still declared the old minimal `AppProps`-only shape.
External `.d.ts` consumers (editor, type checks against the published
package) couldn't see the new runtime class, `AppContext`, or
`AppInitialProps`. Update the `declare module 'next/app'` block to
declare the full surface added in the shim:
- default `class App<P, CP, S>` extending `React.Component` with
`static origGetInitialProps` / `static getInitialProps` and a
`render(): React.ReactNode` method
- `AppContext` and `AppInitialProps<P>` type aliases
- Expanded `AppProps<P>` shape (router, __N_SSG/__N_SSP flags)
2. `appGetInitialProps` silently coerces a null/undefined return from
`Component.getInitialProps` to `{}`. Next.js's `loadGetInitialProps`
throws in that case. Add an inline comment at the coercion site
noting the divergence, linking to the upstream source, and giving
future maintainers a bread crumb when they're debugging Pages Router
pages that render with mysteriously empty props.
v0.0.50
|
||
|
|
36d2ec560d |
fix(shims): add withRouter HOC to next/router (#1203)
* fix(shims): add withRouter HOC to next/router Add the `withRouter` higher-order component export to `packages/vinext/src/shims/router.ts`, ported from `packages/next/src/client/with-router.tsx` in Next.js. `withRouter` is used primarily by class components (which cannot call hooks) to receive the Pages Router `router` instance as a prop. Real Next.js apps and the Next.js deploy test suite import it from `next/router`; without this export, bundlers fail with `[MISSING_EXPORT] "withRouter" is not exported by ".../shims/router.js"`, which is currently the cause of 9 build failures in the deploy suite. The implementation mirrors Next.js's HOC: it calls `useRouter()` and forwards the router as a `router` prop, forwards `getInitialProps` and `origGetInitialProps` from the composed component, and sets a `withRouter(<Name>)` displayName in development. Also exports the `WithRouterProps` and `ExcludeRouterProps` types (commonly imported by typed Pages Router apps) and adds matching `declare module "next/router"` entries. * fix(shims): match Next.js spread order in withRouter and tidy tests Address bonk review on PR #1203: 1. (Correctness) Match the Next.js prop-spread order in `withRouter`. Next.js's with-router.tsx renders <ComposedComponent router={useRouter()} {...props} /> so the injected `router` is placed FIRST and `{...props}` is spread after — a user-passed `router` prop wins by last-spread. The previous implementation spread `props` first and then re-assigned `router`, which silently swallowed any user-passed `router` prop. Now matches Next.js exactly. Added a regression test that passes a sentinel `router` prop and asserts the user-passed value reaches the composed component. 2. (Cleanup) Extracted the repeated globalThis.window mock and vi.resetModules() teardown in the withRouter describe block into beforeEach/afterEach. Cuts ~60 lines from tests/shims.test.ts. |
||
|
|
a713463894 |
fix(shims): add unstable_cacheLife and unstable_cacheTag to next/cache (#1202)
Next.js exports unstable_cacheLife and unstable_cacheTag as deprecation aliases that emit a one-time console.error and delegate to the stabilized cacheLife / cacheTag. Several Next.js apps in the deploy suite still import the unstable_ names, so vinext apps fail to build with [MISSING_EXPORT] errors. Match Next.js's behavior exactly: same warning text, same one-time-per-process gating, same delegation. |
||
|
|
a39047432d |
fix: expose window.next global for Next.js parity (#1201)
* fix: expose window.next global for Next.js parity
Next.js publishes window.next from both the Pages Router client bootstrap
(packages/next/src/client/next.ts) and the App Router bootstrap
(packages/next/src/client/app-bootstrap.ts). Userland code and the Next.js
deploy test suite read window.next.router directly (push, replace,
events.on, etc.), and Next.js's own client code reads
window.next.__pendingUrl and window.next.__internal_src_page for App
Router navigation bookkeeping.
vinext did not install this global, which surfaced as ~422 console errors
and 30+ runtime failures in the Next.js deploy test suite, with
"TypeError: Cannot read properties of undefined (reading 'router')" the
most cited.
Adds an installWindowNext helper in client/window-next.ts that both router
bootstraps invoke:
- Pages Router (shims/router.ts) installs window.next.router pointing at
the Router singleton at module load.
- App Router (server/app-browser-entry.ts) installs
window.next = { appDir: true, router: appRouterInstance } before
hydration starts, mirroring app-bootstrap.ts.
The helper merges fields on repeat calls so hybrid Pages+App setups
behave like Next.js (the App Router bootstrap wins for router/appDir,
matching Next.js's load order). __pendingUrl and __internal_src_page
remain undefined until the navigation paths populate them; that matches
Next.js's behavior on initial load.
* fix(window-next): address review — drop unused exports, wire real version, remove module fallback
- Drop `export` from `AppRouterPublicInstance` and `WindowNext` types
(knip flagged them as unused; they're only used within window-next.ts).
- Drop `getWindowNext()` and its export — tests read `window.next`
directly, which is the same surface the runtime callers use.
- Wire up `process.env.__NEXT_VERSION` via Vite `define` in index.ts,
reading the vinext package version from packages/vinext/package.json
at plugin load. This mirrors Next.js's own DefinePlugin substitution
(packages/next/src/client/next.ts line 5) so library code that branches
on `process.env.__NEXT_VERSION` sees a real value and
`window.next.version` lands the package version instead of the
`"vinext"` fallback.
- Drop the module-level `installedNext` fallback. `installWindowNext`
now reads and writes through `window.next` directly so deleting
`window.next` from tests/userland fully resets state — no stale
reference can be resurrected on the next call.
|
||
|
|
3f4e594e1c |
fix(deploy-suite): create .next/trace before build to avoid ENOENT noise on build failure (#1200)
The Next.js deploy test harness (test/lib/next-modes/base.ts) calls
lstat('.next/trace') during destroy() and throws ENOENT if the file
doesn't exist. vinext doesn't produce a .next/trace (it's a Next.js
telemetry file), so the deploy script creates an empty one as a stub.
That stub was created AFTER 'vinext build' (line ~520), so any run where
the build (or any earlier step like 'pnpm install' / 'vinext init')
failed left .next/trace missing. The test harness then logged ENOENT for
every failed test — 303 lines per deploy-suite workflow run.
Move the stub creation to immediately after the cleanup trap is
installed, before any step that can fail. The cleanup trap runs on EXIT,
so debug artifacts are still preserved; this just ensures .next/trace is
present for the test harness regardless of whether the build succeeded.
Note: VITE_GIT_HOOKS=0 used because vp check --fix errors out when no
JS/TS files are staged (only a .sh file is changed here).
|
||
|
|
747bb0ec76 |
ci: add Next.js deploy suite harness (#1189)
* ci: add Next.js Pages Router deploy suite harness Add the workflow, scripts, and manifest generator for running the Next.js Pages Router deploy test suite against vinext. Triggers: - Nightly cron (06:00 UTC) against main - Manual dispatch with configurable vinext-ref, next-ref, and concurrency Extracted from #922. * refactor: align deploy suite harness with Next.js adapter testing docs - Rename scripts to match Next.js convention: e2e-deploy.sh, e2e-logs.sh, e2e-cleanup.sh - Split workflow into build + test jobs (build once, shard 16 ways) per the documented adapter testing pattern - Generalize workflow from pages-only to configurable: add suite-filter input (pages/app/all, defaults to pages) - Add --filter flag to manifest generator (replaces the pages-hardcoded version) - Accept ADAPTER_DIR (Next.js docs convention) alongside VINEXT_DIR - Rename workflow file to nextjs-deploy-suite.yml - Use matrix.group strings ("1/16") instead of shard integers, matching the docs example * ci: default suite-filter to app * ci: bump Next.js default to v16.2.6, move cron to 02:00 UTC * address review: add report job, clean up cache paths, document compat shims - Add a report job that aggregates .results.json across all shards into a GitHub Actions summary (pass/fail/skip counts, failed test details, passed suite list) and uploads a JSON report artifact - Upload test results from every shard (not just on failure) - Remove redundant next.js/ from cache paths (already under .) - Add comment explaining why IS_TURBOPACK_TEST=1 is needed - Clean up stale PID file in e2e-cleanup.sh - Add header comments to all scripts explaining their purpose and contract - Document the hardcoded warning messages and .next/trace shim - Document the YAML catalog parser assumptions - Fix [[ ]] -> [ ] inconsistency in run-nextjs-deploy-suite.sh * ci: add temporary PR trigger to test workflow [remove before merge] * fix: address deploy script failures found in CI and local testing Bugs found and fixed: - IMMUTABLE_ASSET_TOKEN marker renamed to NEXT_SUPPORTS_IMMUTABLE_ASSETS to match what next-deploy.ts parseIdsFromCliOuput() actually parses. Without this, every test fails with 'Failed to get supportsImmutableAssets'. - Deploy script now injects "type": "module" into test app package.json and generates a vite.config.ts. Without type:module, Rolldown emits .mjs files but the RSC plugin's cross-environment imports expect .js. This mirrors what `vinext init` does (steps 3 and 5). - Added vp (Vite+) fallback to run_pnpm() for environments where pnpm and corepack are not available but vp is. - Cleanup script now uses process group kills (kill -TERM -PID) and a port-based lsof fallback to handle orphaned child processes from vp/pnpm exec wrappers. - Error cleanup trap now dumps the last 80 lines of the build log and last 40 lines of the server log to stderr, so failures are visible in CI output (previously only showed file sizes). - Test job uses run-install: false since the workspace is already built in the cache from the build job. Tested locally: deploy script succeeds, curl returns valid HTML, logs script outputs all three required markers (BUILD_ID, DEPLOYMENT_ID, NEXT_SUPPORTS_IMMUTABLE_ASSETS), cleanup kills the server. * fix: use vinext init in deploy script, add next.config.js to CJS rename list Replace the manual type:module / vite.config.ts / CJS rename logic in e2e-deploy.sh with a single `vinext init --skip-check --force` call. This runs after pnpm install (so deps are available) and before vinext build. Also add next.config.js to the CJS_CONFIG_FILES list in project.ts. Without this, vinext init adds type:module but doesn't rename CJS next.config.js files, causing 'module is not defined in ES module scope' errors. This was the root cause of the CI failures — Next.js test fixtures use module.exports in next.config.js. All 61 init tests pass. * fix: convert CJS next.config.js to ESM instead of renaming to .cjs Next.js doesn't support next.config.cjs, so we can't rename it. Instead, convert module.exports/require() to export default/import in-place after vinext init adds "type": "module". Also revert adding next.config.js to CJS_CONFIG_FILES — vinext init should not rename it since it's not a generic config file. Tolerate pnpm 10+ ERR_PNPM_IGNORED_BUILDS exit code 1 on install (the install completes, packages are in node_modules, but pnpm exits non-zero due to unapproved build scripts). Verify node_modules/vinext exists as a safety check. Tested against the real Next.js actions-streaming fixture: CJS next.config.js converted, app builds, server starts, HTTP 200. * fix: emit both IMMUTABLE_ASSET_TOKEN and NEXT_SUPPORTS_IMMUTABLE_ASSETS v16.2.x parses IMMUTABLE_ASSET_TOKEN, canary renamed it to NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat. * fix: enable JSX in .js files, convert CJS next.config.ts to ESM Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files. Next.js allows JSX in .js files (Babel/SWC handle it transparently), but Vite 8's built-in vite:oxc plugin excludes .js files by default (exclude: /\.js$/) AND infers lang: 'js' from the extension (which disables JSX parsing). Neither can be fixed via config alone. The plugin runs with enforce: 'pre' before vite:oxc, transforming .js files with OXC using lang: 'jsx' so JSX is compiled before vite:oxc sees the code. This unblocks ~60 test suites in the deploy suite. Also extend the deploy script's CJS-to-ESM config converter to handle next.config.ts files (not just .js), fixing ~6 more test failures. Tested: 1284 tests pass (jsx-in-js + shims + routing + app-router), zero regressions. * ci: trigger clean workflow run * fix: improve CJS-to-ESM converter for next.config files The naive regex converter missed two patterns: - require('mod')(args) produced invalid ESM (import X from 'mod'(args)) - require() calls inside function bodies were not converted New converter handles: - module.exports = X → export default X - const X = require('mod') → import X from 'mod' - const X = require('mod')(args) → import _X from 'mod'; const X = _X(args) - require('mod') in expressions → (await import('mod')).default Tested against bundle-analyzer, path, and inline require patterns. * . * Apply suggestion from @james-elicx * fix: handle destructured requires in CJS converter, read port from file in cleanup - Add pattern for const { a, b } = require('mod') → import { a, b } from 'mod' - Read port from PORT_FILE in cleanup_on_error trap instead of using $PORT variable which may be unset if the script fails before port allocation - Add TODO noting remaining CJS edge cases (dynamic require, require.resolve) * Apply suggestions from code review Co-authored-by: James Anderson <james@eli.cx> * Apply suggestion from @james-elicx |
||
|
|
81d5426ee6 |
fix: enable JSX in plain .js files (Next.js compat) (#1199)
* fix: enable JSX in plain .js files (Next.js compat) Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files. Next.js allows JSX in .js files (Babel/SWC handle it transparently), but Vite 8's built-in vite:oxc plugin excludes .js files by default (exclude: /\.js$/) AND infers lang: "js" from the extension (which disables JSX parsing). Neither can be fixed via config alone. The plugin runs with enforce: "pre" before vite:oxc, transforming .js files with OXC using lang: "jsx" so JSX is compiled before vite:oxc sees the code. Tested: 972 tests pass (jsx-in-js + shims + routing), zero regressions. * fix: suppress rules-of-hooks lint for Playwright fixture use() The oxlint-disable-next-line only covered the function signature line, not the use() call deeper in the function body. Use block-scoped disable/enable to suppress the entire fixture definition. |
||
|
|
9126344cd8 |
fix: use loader for client reference (#1141)
* fix: split client chunk * add test * fix ci deps * install deps * use loader * tweaks |
||
|
|
6cf4148ffd |
fix(link): keep auto prefetch from fetching dynamic app routes (#1177)
* fix(link): keep auto prefetch from fetching dynamic app routes
App Router Link treated null, undefined, and auto prefetch as the same full RSC prefetch used by prefetch={true}. That violates Next.js's public prefetch contract and over-fetches dynamic route payloads as soon as links enter the viewport.
The broken assumption was that prefetch only needed a boolean enabled state. Split Link prefetch into disabled, auto, and full modes, emit a small browser route manifest, and allow automatic full RSC prefetch only for matched static app routes while preserving explicit full prefetch.
Add focused tests for the mode mapping and generated route manifest.
* test(link): opt interception prefetch fixture into full mode
The interception-context E2E asserts that full RSC prefetch cache keys stay separate by source context. After auto prefetch stops full-fetching dynamic routes, that fixture needs to request the full prefetch mode explicitly.
|
||
|
|
275f8bdd73 | fix(shims): add placeholder useRouter bfcacheId (#1192) | ||
|
|
e534e9bdc3 |
fix(app-router): promote mounted parallel slot preservation (#1191)
* fix(app-router): promote mounted parallel slot preservation Mounted slots were still preserved by treating missing AppElements slot keys as proof during promoted same-root commits. That kept legacy wire absence as semantic authority after layout persistence moved into the planner. The planner snapshot now carries mounted parallel slot ownership and approves preservation only when the slot owner layout is in the retained same-root layout prefix. Approved commits pass that decision to mergeElements, while unknown-root fallbacks keep the legacy absent-slot path fenced. * test(app-router): lock traverse slot cleanup precedence Traverse commits already clear absent slots after copying planner-approved preserved ids. That ordering means a mounted parallel slot can be approved by the planner and still be removed when the traverse payload omits it. Cover that precedence explicitly so the mounted-slot preservation slice keeps navigation slot preservation and traversal cleanup as separate contracts. * fix(app-router): align traverse slot preservation metadata Traverse commits clear absent slots, so a planner proposal should not name mounted slot ids that the visible reducer will remove. That made preserveElementIds semantically wider than the commit contract. Split current-root commit preservation by lane so traverse commits preserve the retained layout prefix only, while ordinary current-root commits still preserve mounted slots owned by retained layouts. Cover both the pure planner decision and browser-state approval boundary. |