mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.8
1420 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1d26b2b229 |
fix(shims/script): emit <link rel="stylesheet"> for next/script stylesheets prop (#1646)
Refs #1517. The vinext `next/script` shim ignored the `stylesheets` prop, so an `<Script src="/x.js" stylesheets={['/x.css']} />` element emitted only the `<script>`/preload and silently dropped the associated stylesheet (and worse, leaked the prop onto the rendered `<script>` as a `stylesheets="..."` attribute on the beforeInteractive branch). Mirror the App-Router branch of Next.js's component (`.nextjs-ref/packages/next/src/client/script.tsx:309-313` and `:48-59`): - SSR: call `ReactDOM.preinit(href, { as: 'style' })` for each entry — React Float hoists `<link rel="stylesheet">` into `<head>`. - Client load path (`loadClientScript` + `handleClientScriptLoad`): same preinit preferred; fall back to direct `document.head.appendChild` of `<link rel="stylesheet">` when `ReactDOM.preinit` is unavailable. - Destructure `stylesheets` out of `rest` everywhere so it never lands as an attribute on the emitted `<script>`. - Add `stylesheets` to the `RESERVED` set used by the hoisted-script attribute collector for inline beforeInteractive scripts. Scope: stylesheets prop only. Nonce propagation (the other half of #1517) is already handled across SSR (`buildBeforeInteractiveScriptProps`, `ReactDOM.preload`) and client (`loadClientScript`) paths — see the existing "Script nonce resolution" suite. Bootstrap-script preinit is a separate, larger change tracked under #1328. Tests: extend `tests/script.test.ts` with a `Script stylesheets prop` suite covering SSR emission, multi-entry lists, prop leakage on the rendered `<script>`, omitted/empty lists, and the imperative `handleClientScriptLoad` path. |
||
|
|
a6a279509c |
fix(shims): throw Pages Router parity error from unstable_retry (#1448) (#1670)
Closes the Pages Router parity gap noted as a follow-up in #1206. When `unstable_catchError`'s boundary is rendered under the Pages Router, `unstable_retry()` now throws Next.js's verbatim error message ("`unstable_retry()` can only be used in the App Router. Use `reset()` in the Pages Router.") instead of calling App Router's `refresh()` (which is a no-op there and leaves the user stuck on the error). Detection mirrors Next.js's own implementation in `packages/next/src/client/components/catch-error.tsx`: the inner class component reads `RouterContext` via `static contextType`. The wrapper stays a pure JSX factory so existing introspection-style unit tests that invoke it as a bare function continue to work. Regression test ported from `test/e2e/app-dir/catch-error/catch-error.test.ts` — "should throw when unstable_retry is called on Pages Router". |
||
|
|
f4687b3537 |
fix(app-router): treat module-only Vite ref errors as action-not-found (#1340) (#1668)
When a server action id resolved from request headers carries the `#<exportName>` suffix (e.g. `/app/foo.ts#bar`), `loadServerAction()` splits the id at `#` and calls `requireModule(file)` with the module path only. `@vitejs/plugin-rsc`'s reference-validation virtual module therefore emits `[vite-rsc] invalid server reference '/app/foo.ts'`, omitting the `#<exportName>` portion. The previous match in `isServerActionNotFoundError` compared the full actionId against the Vite error string, so missing/stale references surfaced as a generic 500 instead of the Next.js-compatible 404 with the `x-nextjs-action-not-found` header. Also accept the pre-`#` module-path form for both the dev-time `[vite-rsc] invalid server reference '<id>'` and prod-time `server reference not found '<id>'` error shapes. Adds a focused unit test reproducing the dev-mode case and integration tests asserting that throwing `notFound()` from a server action returns 404 (both fetch and progressive form paths), porting fixture from Next.js: test/e2e/app-dir/actions/app/server/form.js. |
||
|
|
c2f9f5b53f |
fix(app-router): cache pages with revalidate=Infinity/false (#1487) (#1666)
* fix(app-router): cache pages with revalidate=Infinity/false (#1487) App Router pages exporting `revalidate = Infinity` (or `revalidate = false`, which resolves to Infinity in segment config) were re-rendered on every request: the HTML response policy returned `shouldWriteToCache: false`, and the cache-write policy rejected non-finite revalidate values outright. The downstream Cache-Control header was correct (1y s-maxage), but vinext itself never served identical bytes for repeat requests inside the same process. This change treats `revalidate = Infinity` as "cache indefinitely" rather than "never write to cache": - `resolveAppPageHtmlResponsePolicy` now writes the cached HTML/RSC bundle in production and reports `MISS` on first render, matching the standard ISR write flow. Dev mode continues to emit `STATIC` with no cache write. - `resolveAppPageCacheWritePolicy` accepts `Infinity` as a valid revalidate interval; only `null`, `NaN`, and non-positive values reject the write. The MemoryCacheHandler (used by tests and as the default) already handles Infinity correctly: `revalidateAt = now + Infinity` keeps the entry fresh forever until `revalidatePath`/`revalidateTag` invalidates it. Adds two production-server regression tests: - `export const revalidate = 60` returns HIT with identical timestamp on the second request (covers the standard finite case). - `export const revalidate = Infinity` returns HIT with identical timestamp on the second request (covers the previously broken indefinite case). Refs #1487. Other symptoms listed in the issue (`unstable_cache`, fetch cache directives, force-static, force-no-store, on-demand revalidate) are out of scope here and tracked separately. * test(app-page-response): update Infinity revalidate assertions Production + revalidateSeconds: Infinity now writes to the ISR cache (cacheState: "MISS", shouldWriteToCache: true). Update the existing assertion to match, and add a dev-mode case asserting Infinity stays as a no-write STATIC response in development. |
||
|
|
2354ca6e60 |
test(trailing-slash): pin core enforcement contract for App Router (#1332) (#1635)
* test(trailing-slash): pin core enforcement contract for App Router (#1332) Adds focused integration tests against a dev-server fixture built from app-basic with a minimal `next.config.ts` overriding `trailingSlash`. The core enforcement contract documented in Next.js test/e2e/trailing-slashes and test/e2e/app-dir/trailingslash was already implemented across the dev server, prod-server, app-rsc-handler, and the deploy worker entry by #1395 and #1413, but no end-to-end test asserted the full contract end-to-end through the dev server. These regression tests close that gap so future refactors of the trailing-slash redirect or `<Link>` normalisation paths fail loudly. Coverage: - trailingSlash: true - GET /about → 308 Location: /about/ - GET /about/ → 200 - <Link href="/about"> renders href="/about/" - trailingSlash: false (default) - GET /about/ → 308 Location: /about - GET /about → 200 - <Link href="/about"> renders href="/about" (no trailing slash) Refs #1332. * test(trailing-slash): address bonk review nits - Rename tests/trailing-slash-1332.test.ts → tests/trailing-slash.test.ts to match repo naming convention (the issue ref lives in the header comment and describe titles). - Switch to the shared `startFixtureServer` helper from tests/helpers.ts; drop the local `startServer` and the now-unused `createServer` / `Server` imports. - Tighten the second describe title to "trailingSlash: false" — the fixture explicitly sets `false`, not omits the field. The default-vs- explicit behaviour is identical today, so the merged-name was misleading. - Document why we keep `fs.cpSync` rather than `createIsolatedFixture`: app-basic's node_modules carries fixture-local symlinks (fake-context-lib, fake-css-lib, …) that `createIsolatedFixture` would break by replacing node_modules with a workspace symlink, and the isolated copy also gets its own `.vite` optimizer cache (preventing races against the workspace cache used by other tests). Verified empirically: the bot-suggested `createIsolatedFixture` swap reproduces 500s on every request, mirroring the rationale already captured in tests/favicon-short-circuit.test.ts. Refs #1332. * test(trailing-slash): bump 200-serve timeouts to 30s for dev-mode compile CI showed both 200-serve cases hitting the 5s default while the dev server was compiling the /about route on first hit. Match the headroom we already use elsewhere (e.g. tests/app-router.test.ts). * style: apply biome formatter to trailing-slash.test.ts |
||
|
|
5d9548dc92 |
fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1633)
* fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1361) `pages/_document.tsx` files that override `static async getInitialProps` to inject extra props were rendered with an empty `this.props`, because both the dev-server and the production pages response builder created the Document element with `React.createElement(DocumentComponent)` and never called the hook. The Next.js async-modules e2e fixture (`test/e2e/async-modules/pages/_document.jsx`) uses exactly that pattern to surface a `docValue` from `await Document.getInitialProps(ctx)` plus a top-level `await` constant. PR #1381 (issue #1361) fixed the class construction crash, but the deploy-suite continued to fail "csr async page modules" because `<div id="doc-value">` rendered empty. This change adds a small `loadUserDocumentInitialProps` helper used by both the Pages Router dev-server (`streamPageToResponse` shell and the custom-error-page shell) and the production response builder (`buildPagesShellHtml`). It invokes the override (if any) and spreads the resolved props on the Document element, mirroring Next.js's `render.tsx` `<Document {...htmlProps} {...docProps} />` call. The helper short-circuits when the user did not override the base shim, so the existing fast path keeps the same number of awaits. Reference: vercel/next.js packages/next/src/server/render.tsx (`loadDocumentInitialProps`, `documentElement`). Tests: - Adds a new pages-router production e2e case that builds a `_document.tsx` with `static async getInitialProps` + top-level `await`, plus a custom `404.tsx` that uses top-level `await`, and asserts the resolved values reach the rendered HTML. - Unit-tests `loadUserDocumentInitialProps` directly for the override, no-override, and throwing-override cases. Refs #1361 * fix(pages-router): address bonk review on Document.getInitialProps helper Two changes from review feedback on PR #1633: 1. Cache the base getInitialProps reference at module level. The previous helper did `await import("vinext/shims/document")` on every request to compare identity — unnecessary per-request work and an extra await on the fast path where the user has no override. Switched to a static import + module-scoped constant so the identity check is a synchronous reference compare. No-override callers now resolve with one less await. 2. Let user `getInitialProps` errors propagate. Next.js's `loadGetInitialProps` does not catch — a throw surfaces as a 500 to the caller. The previous version logged-and-swallowed the error and returned null, which silently erased docProps on every failing render and made user bugs harder to find. vinext now matches Next.js's contract. Updated the corresponding unit test to assert the throw propagates rather than that the helper resolves to null. Refs #1361, #1633 |
||
|
|
809218f6c4 |
fix(pages-router): mark app routes as detected on prefetch (#1526) (#1653)
* fix(pages-router): mark app routes as detected on prefetch (#1526) In a hybrid Pages + App Router app, when the Pages Router prefetches a `<Link href>` whose target is actually an App Router route, Next.js marks the route on the Pages Router singleton with `router.components[urlPathname] = { __appRouter: true }` (driven by the `_bfl` bloom filter at packages/next/src/shared/lib/router/router.ts:2525). The Next.js deploy test `test/e2e/app-dir/app/index.test.ts` ("should successfully detect app route during prefetch") asserts this via `window.next.router.components["/dashboard"]`. Vinext previously did not expose `components` and did not record App Router prefetch targets, so the assertion never resolved. This commit: - adds a shared `components` map (via `Symbol.for` for module-split safety) on the Pages Router singleton, mirrored onto `window.next.router`, - threads the App Router prefetch manifest into the Pages Router client entry so the same matcher Link uses for App Router auto-prefetch can run in Pages-only-entry pages, - marks the target on `components` with `{ __appRouter: true }` from both `Router.prefetch` and `<Link>` Pages-mode prefetch when the target matches the App Router manifest. * fix(app-route-detection): drop unused exports flagged by knip Both `matchesAppRoute` and `PagesRouterComponentsMap` are only used inside this module — knip rightly flagged them as unused exports. Internalize both. |
||
|
|
3ff06784a0 |
fix(app-router): forbidden()/unauthorized() escalate past intermediate layouts (#1547) (#1647)
When a deep page called `forbidden()`/`unauthorized()`/`notFound()` and the
intermediate layout segments had no matching boundary file, vinext's
page-error fast path wrapped the resolved root boundary in *all* of the
route's layouts. That left the intermediate layout's chrome ("Dynamic with
Layout") rendered next to the boundary fallback, diverging from Next.js
where the nearest ancestor boundary owns the entire fallback subtree.
`renderPageSpecialError` now resolves the deepest layout-level boundary
via `resolveAppPageParentHttpAccessBoundary` and slices the layouts to
`[0..boundaryLayoutIndex]`, so layouts beneath the boundary owner stop
rendering. When the route-level boundary file lives next to the page
(not aligned to a layout), all layouts are kept as before.
Ported the Next.js regression cases from
`test/e2e/app-dir/forbidden/basic/forbidden-basic.test.ts` and
`test/e2e/app-dir/unauthorized/basic/unauthorized-basic.test.ts`.
|
||
|
|
cd62cb2ebb |
fix(app-router): honor experimental.staleTimes in client prefetch cache (#1490) (#1660)
* fix(app-router): honor experimental.staleTimes in client prefetch cache (#1490) The App Router's client-side prefetch cache previously used a hard-coded 30s TTL and ignored `experimental.staleTimes` from next.config. As a result, users could not extend (or shorten) how long prefetched route segments were considered fresh, breaking parity with Next.js' `test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts`. Surface `experimental.staleTimes.{dynamic,static}` through `ResolvedNextConfig` and inject it into the client bundle as `process.env.__NEXT_CLIENT_ROUTER_{DYNAMIC,STATIC}_STALETIME` (matching the Next.js variable names plumbed by `build/define-env.ts`). The prefetch cache TTL in `shims/navigation.ts` now reads the static value at module load time, converting seconds to ms. Defaults mirror Next.js (`dynamic: 0`, `static: 300`). When the env var is absent — primarily in unit tests that import the module outside the plugin's `define` pipeline — we fall back to vinext's historical 30s default rather than the Next.js 300s so existing tests keep passing. Refs: .nextjs-ref/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts Refs: .nextjs-ref/packages/next/src/build/define-env.ts * review: address bot feedback on staleTimes PR - Remove invokeLoaderSideEffects scope creep (belongs to #1500 / PR #1657) - Reject negative staleTimes at resolution time alongside Number.isFinite, matching the seconds < 0 guard in resolvePrefetchCacheTtl - Drop unused __NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME define; vinext does not yet distinguish prefetch kinds, so the value was never consumed |
||
|
|
90a09df893 |
fix(build): invoke webpack loader side effects to land process.env mutations (#1500) (#1657)
Some Next.js webpack loaders mutate `process.env.X = ...` at compile time, expecting the value to be visible to other modules during the same build. vinext doesn't run the webpack loader pipeline, so those env mutations never happened and downstream code (defines, `NEXT_PUBLIC_*`, server-side reads) saw `undefined`. Walk the rules collected from the user's `webpack(config)` function and invoke each referenced loader once with a dummy source. Loaders that only mutate `process.env` get a chance to land their changes in the shared Node process. Failures are swallowed since vinext doesn't use the loader's transform output. Well-known framework loaders (next-babel-loader, @next/mdx, next/dist/build/webpack/*) are skipped to avoid pulling in webpack-runtime-dependent code at probe time. |
||
|
|
c1a6adc19a |
fix(routing): treat @children as transparent so explicit page wins over catchall (#1535) (#1659)
In Next.js, the `@children` directory is special: it provides the layout's `children` prop rather than acting as a parallel slot. `app/foo/@children/page.tsx` should therefore register a real page route at `/foo` and take priority over a sibling `[...catchAll]` catch-all. Previously vinext excluded all `@`-prefixed directories from the page scan, so `@children/page.tsx` was hidden and `/` (or `/nested`) was either served by the catch-all or failed to resolve when no `default.tsx` existed. Three small changes restore Next.js parity: - Let `@children` directories through the page-scan exclusion so the page file at `@children/page.tsx` registers as a route. - Strip a trailing `@children` segment in `fileToAppRoute` so the route is anchored at the parent directory — sibling `@slot` directories remain owned (not inherited) and layout/boundary discovery is sourced from the parent. - Skip `@children` in `discoverParallelSlots` and `hasParallelSlotDirectory` so it never appears in `parallelSlots` (matching Next.js' types-plugin behaviour). Ported from Next.js: test/e2e/app-dir/parallel-routes-catchall-children-slot/ https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-catchall-children-slot/parallel-routes-catchall-children-slot.test.ts References Next.js' `normalizeAppPath` and `normalize-catchall-routes` which both treat `@children` as a non-matchable slot: - packages/next/src/shared/lib/router/utils/app-paths.ts - packages/next/src/build/normalize-catchall-routes.ts - packages/next/src/build/webpack/plugins/next-types-plugin/index.ts Fixes #1535. |
||
|
|
1671a46c35 |
fix(app-router): dedupe action Set-Cookie headers by name (#1481) (#1665)
Multiple `cookies().set(name, ...)` calls on the same name inside a server action emitted multiple Set-Cookie response headers, diverging from Next.js's `MutableRequestCookiesAdapter`, whose underlying `ResponseCookies` Map collapses repeated sets to the final value per name. Apply `dedupePendingCookies` at every cookie-emitting action exit (form action redirects, fetch action redirects, fetch action responses) so the last write per name wins and insertion order is preserved. Reference: packages/next/src/server/web/spec-extension/adapters/request-cookies.ts Refs #1481. |
||
|
|
2d9f3ffc36 |
fix(app-router): accept icons.other as single descriptor (#1492) (#1667)
Next.js's `metadata.icons.other` accepts either a single
`IconDescriptor` or an array of them, but `MetadataHead` iterated
it directly and threw `TypeError: metadata.icons.other is not iterable`
when given a single object. That crash bubbled up into the page
render so no `<link>` tags (favicon, shortcut, apple, custom) reached
the head at all for any page declaring `icons.other` as an object.
Normalize `metadata.icons.other` to an array before iterating, widen
its type to `OtherIconDescriptor | OtherIconDescriptor[]`, and also
forward the optional `type` attribute so descriptors like
`{ rel: "mask-icon", url, type: "image/svg+xml" }` round-trip
correctly. Ported coverage from
`test/e2e/app-dir/metadata-icons/metadata-icons.test.ts`.
Refs cloudflare/vinext#1492
|
||
|
|
33be785d43 |
fix(app-router): allow draftMode() reads inside cache scopes (#1489) (#1669)
Calling `await draftMode()` and reading `.isEnabled` inside a `"use cache"` or `unstable_cache()` callback now works the same as Next.js: the non-mutating reads succeed and reflect the `__prerender_bypass` cookie, while only the side-effecting `enable()` / `disable()` calls throw when invoked inside a cache scope. Previously vinext threw eagerly on `draftMode()` itself, which caused fixtures such as `app-static`'s `unstable-cache/dynamic` page (which reads `(await draftMode()).isEnabled` inside the cached function) to surface as empty/undefined values in the Next.js deploy suite. Ported from `.nextjs-ref/packages/next/src/server/request/draft-mode.ts` (`getDraftModeProviderForCacheScope` + `trackDynamicDraftMode`). |
||
|
|
d61c5cbafe |
fix(app-router): propagate cookies/headers from no-JS server actions (#1483) (#1673)
When a progressive (no-JS) server action POST runs without redirecting, the
pending `cookies().set(...)` / `draftMode().enable()` mutations made during
the action were dropped on the floor — the form-state result returned from
`handleProgressiveServerActionRequest` never read them out of the
request-scoped headers context, and the subsequent page rerender never
flushed them onto the response.
Mirror Next.js' `res.setHeader('set-cookie', ...)` flush in
`action-handler.ts` / `app-render.tsx`: capture pending cookies, the draft
cookie, and the resolved revalidation kind at the end of the non-redirect
action path, expose them on the form-state result, then append them to the
rerender response (and the `x-action-revalidated` marker) in
`app-rsc-handler`. Falls back to rebuilding the response when its headers are
immutable so cookies still ride a `redirect()` issued during the rerender.
Fixes #1483.
|
||
|
|
51a63131b5 |
test(pages-router): regression coverage for URL-encoded CSS paths (#1472) (#1675)
Adds focused tests verifying that the static-asset layer serves CSS files when the browser percent-encodes special characters in the path: * `tests/serve-static.test.ts` — three cases against `tryServeStatic`: the cache fast path with an encoded URL, the slow path with an encoded URL, and the literal-character pathname that the request pipeline produces after `normalizePathnameForRouteMatchStrict` runs. * `tests/pages-css-url-encoding.test.ts` — full build-and-serve test for the Pages Router that imports `my@style.css` and `with space.css` from `pages/index.tsx`, then asserts every emitted stylesheet link resolves to a 200 after `encodeURI` is applied (matching browser behaviour). All three tests pass against the current implementation: `tryServeStatic` decodes percent-encoded pathnames before cache lookup, and the cache indexes files by their literal on-disk names. These tests document the contract and guard against future regressions. Refs cloudflare/vinext#1472 |
||
|
|
76fe5d8b05 |
fix(pages-router): render custom pages/500 on SSR throw (#1676)
When getServerSideProps (or any other render-time code) threw in the production server entry, vinext collapsed the response into a plain "Internal Server Error" text body. Next.js instead renders the user's custom pages/500.tsx (with pages/_error.tsx as a fallback). Match that behaviour so frameworks that rely on custom error pages — and the Next.js getserversideprops deploy suite — see the expected output. The catch block in `_renderPage` now re-renders through the same entry using `pages/500.tsx` (or the `_error` route) with status 500. A new `__isInternalErrorRender` guard prevents infinite recursion if the error page itself throws; data (`/_next/data/...`) requests still short-circuit to the plain text body since they can't return HTML. Mirrors Next.js base-server.ts `renderToResponseImpl` 500 fallback. The dev server already renders custom 500/error pages via `renderErrorPage()` — this aligns prod with that behaviour and adds explicit regression coverage for both paths via a new gssp-throw fixture. Refs #1458 |
||
|
|
98c8286fdc |
test(pages-router): shallow Router.push bypasses redirect for as (#1522) (#1681)
Port the Next.js "should not apply client router filter on shallow" test
(test/e2e/app-dir/app/index.test.ts) so we lock in vinext's behaviour:
`Router.push(url, as, { shallow: true })` must update history to `as`
(/redirect-1) without fetching the page HTML, even when `as` matches a
server-side `redirects` rule from next.config.js.
vinext already produces the correct behaviour — `performNavigation`
skips `runNavigateClient` when `shallow` is true, so the server-side
redirect table never runs and `window.history.pushState` lands on
`as`. The test guards against future regressions (e.g. introducing a
Next.js-style client router bloom filter without exempting shallow
updates) and serves as the parity counterpart to Next.js's own bypass
in `_bfl` in packages/next/src/shared/lib/router/router.ts.
Refs #1522.
|
||
|
|
3e7d7f8f8b |
fix(build): inline process.env.NEXT_DEPLOYMENT_ID for client and worker bundles (#1538) (#1683)
User code such as `new Worker(new URL('./w.ts', import.meta.url))` reads
`process.env.NEXT_DEPLOYMENT_ID` from inside the worker scope, but vinext
only defined the internal `__VINEXT_DEPLOYMENT_ID` mirror — leaving the
public Next.js identifier as `undefined` in every browser-side bundle.
Mirror Next.js' webpack DefinePlugin behavior (see
packages/next/src/build/define-env.ts) and statically inline the value
into the global Vite `define`. The same map is applied to client, SSR,
RSC and Vite worker bundles, so workers transparently see the deployment
id without needing a globalThis bridge (which can't be shared across
the main-thread / worker boundary anyway).
When no deploymentId is configured, inline `false` — the same fallback
Next.js uses, so feature checks like `if (process.env.NEXT_DEPLOYMENT_ID)`
remain dead-code eliminated.
|
||
|
|
7be428dbc3 |
feat(app-router): support inline CSS parity (#1595)
* feat(app-router): support inline CSS parity
App Router production builds ignored Next.js experimental.inlineCss, so HTML responses kept stylesheet links, next/font CSS lived in a second style tag, and Next-style RSC requests with RSC: 1 plus _rsc could return HTML.
The missing invariant was that inlineCss changes the production CSS delivery boundary: non-RSC HTML gets inlined style resources, RSC payloads keep CSS bodies out, and client navigations must not reintroduce duplicate stylesheet resources for CSS already present inline.
Resolve the config flag into the generated RSC entry, collect emitted CSS assets into an inline manifest for Node and Workers production, rewrite matching React stylesheet links into hoistable style tags, merge next/font CSS into the first inline style with a fallback path, and recognize Next-style RSC requests.
Regression coverage now ports the upstream app-inline-css behavior at production-browser level and adds focused tests for request normalization, stream rewriting, font merging, and fallback emission.
* feat(app-router): preserve inline CSS import ordering
Inline CSS font merging could prepend generated font rules before a stylesheet import. CSS imports must stay at the beginning of their stylesheet, so that ordering caused imported styles to be ignored when experimental.inlineCss and local font CSS were combined.
Keep import-sensitive stylesheet preambles untouched and let the existing font fallback style carry the font rules instead. Add stream-helper coverage for the import case.
* feat(app-router): harden inline CSS review edges
Inline CSS rewriting only saw complete link tags in a single decoded chunk and treated rel as an exact value. That left valid stylesheet links un-inlined when rel had multiple tokens or when streaming split the tag across chunks.
The production manifest injection also assumed the RSC worker bundle was always at dist/server/index.js, so custom rscOutDir builds missed the inline CSS manifest.
Buffer incomplete link tags across stream flushes, match stylesheet as a rel token, and inject the manifest into the configured RSC output entry. Add regressions for the streaming and custom-output cases.
* feat(app-router): match inline CSS cleanup semantics
Client navigation cleanup only pruned links whose rel was exactly stylesheet and whose precedence marker used data-precedence. That diverged from the server inline CSS rewrite, so tokenized rel values or legacy precedence links could survive after navigation as duplicate stylesheet resources.
The fix moves the cleanup into a testable helper, reuses shared HTML token-list matching for server and browser paths, accepts both precedence marker forms, and only buffers split link tags when an inline CSS manifest is present.
Regression coverage now covers browser cleanup and non-inlineCSS streaming chunk granularity.
* feat(app-router): harden inline CSS HTML rewriting
Inline CSS rewriting could prepend SSR font CSS ahead of namespace-bearing stylesheets and could treat stylesheet-like text inside script bodies as real link tags. That violates CSS ordering constraints and corrupts streamed script text when experimental.inlineCss is enabled.
The stream rewrite now treats @namespace as an unsafe preamble, skips link replacement inside raw-text and comment regions, and buffers incomplete rewrite boundaries across flushes. Regression coverage pins namespace fallback, complete script text, split script text, and unterminated script text.
* test(e2e): specify junction type for fs.symlink to support Windows
* refactor(app-router): drop write-only __PAGE__ wire marker
Every outgoing App Router payload carried `__PAGE__: true` in its wire
metadata, but nothing in the runtime ever read it. Its only consumer was
an inline-css E2E assertion checking that a dynamic navigation returned an
RSC payload rather than HTML, so the marker added permanent per-response
wire overhead with no behavioural effect.
The `__route` key is already present on every payload and proves the same
thing: the response carries page metadata. Remove the marker key, its type
field, and its re-export, and point the unit and E2E assertions at
`__route` instead.
* perf(app-router): skip inline-css manifest scan on the streaming hot path
The tick-buffered SSR transform recomputed `Object.keys(manifest).length`
on every flush through `rewriteInlineCssStylesheetLinks`, even when inline
CSS was disabled and the manifest empty. Streaming flushes are a hot path,
so this ran per chunk of every response for no benefit.
Compute the manifest-present flag once at transform creation and reuse it
to gate both the split-link boundary buffering and the link rewrite, so
non-inline-css responses skip the rewrite entirely. Also document the
shared-lastIndex constraint on the module-level rewrite regexes and the
startup/build-time population contract for `__VINEXT_INLINE_CSS__`.
* fix(app-router): forward SSR script nonce to inline-css <style> tags
Sites running `Content-Security-Policy: style-src 'nonce-…'` saw the
inlined `<style>` blocks blocked at parse time and rendered unstyled
when `experimental.inlineCss` was enabled. The `<link rel="stylesheet">`
tags the feature replaces aren't subject to inline-style CSP, but the
new inline blocks are — and React Fizz typically doesn't put a nonce on
the `<link>`, so the rewrite path had no nonce to carry over.
Thread the SSR-time `scriptNonce` through `createTickBufferedTransform`
into `rewriteInlineCssStylesheetLinks`, and prefer the link's own nonce
when present (otherwise fall back to the SSR nonce). Add unit coverage
for the SSR-only, link-only, and missing-nonce cases.
Also replace `html.toLowerCase().lastIndexOf("<link")` in
`splitTrailingIncompleteLinkTag` with a forward `g+i` regex scan so the
streaming hot path no longer allocates a lowercased copy of every flush
(can be tens of KB on large responses).
* chore(check): register experimental.inlineCss as supported
So `vp check` advertises App Router CSS inlining alongside the other
experimental flags. Notes that next/font CSS is folded into the first
inline `<style>` block.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
9e049261e1 |
fix(otel): inject experimental.clientTraceMetadata into SSR head (#1644)
* fix(otel): inject experimental.clientTraceMetadata into SSR head (#1494) When `experimental.clientTraceMetadata` is configured in next.config, read the active OpenTelemetry context via the registered propagator and emit `<meta name="..." content="...">` tags in the SSR HTML head for each allow-listed key. Both App Router and Pages Router are covered (dev and prod servers). `@opentelemetry/api` is resolved as an optional peer at runtime, so apps without OTel get a zero-cost no-op. Mirrors Next.js: packages/next/src/server/lib/trace/utils.ts and packages/next/src/server/app-render/make-get-server-inserted-html.tsx. * fix(otel): drop unused getOpenTelemetryTraceData export knip flagged the helper as unused — it is only consumed internally by `getClientTraceMetadataHTML`. Keep it module-private. * fix(otel): normalize clientTraceMetadata allow-list to undefined-only Pick one convention for the optional allow-list across the App Router pipeline (was mixing `string[] | null` and `string[] | undefined`). Drops the redundant `?? null` / `?? undefined` coercions and aligns every server-side type with `ResolvedNextConfig.clientTraceMetadata`, which is `string[] | undefined`. Also adds a resolveNextConfig test asserting that an array passed via experimental.clientTraceMetadata is preserved verbatim, non-string entries are filtered, and unset / non-array values resolve to undefined. |
||
|
|
5bcbee60e0 |
fix(app-router): isolate global-not-found chunk to fix CSS cascade (#1549) (#1654)
When `app/global-not-found.tsx` is defined, route-miss 404s render that
module standalone (it ships its own `<html>`/`<body>` and replaces the
root layout). Previously the entry registered global-not-found.tsx as a
static `import * as`, which co-located its CSS with the root layout's CSS
in a single chunk. Lightningcss then dropped overlapping declarations
between the two stylesheets as dead code, so any rule global-not-found
defined that the layout also defined was silently removed — breaking the
CSS cascade for 404s.
Switching the entry to `() => import("…/global-not-found.tsx")` puts the
module in its own JS chunk and lets the SSR pipeline (and `Resources`
metadata) treat its CSS independently of the rest of the app. The
fallback renderer now accepts a `loadGlobalNotFoundModule` loader (cached
per worker) instead of an eagerly-imported module.
Also adds a regression test asserting the SSR markup for `/does-not-exist`
links only global-not-found's stylesheet — never the root layout's — so
the cascade can never resolve to the wrong color.
Ported from Next.js: test/e2e/app-dir/initial-css-order/initial-css-order.test.ts
|
||
|
|
9f3ae31f49 |
fix(app-router): propagate unstable_rootParams to actions and route handlers (#1452) (#1652)
`setRootParams` was only called after the post-action route match, so server actions, progressive (form) actions, and route handlers all ran before the unified request context's `rootParams` slice was populated. User code calling `unstable_rootParams()` from those contexts therefore saw `undefined` instead of the matched root layout's params. Seed `setRootParams` from a pre-action route match against the current cleanPathname so that: - route handlers observe rootParams during dispatch - server actions observe rootParams while the action runs and during the page rerender that follows a successful action - `"use cache"` functions invoked from any of those contexts read rootParams from the unified request context The legacy post-rewrite `setRootParams` call is preserved so the value is refreshed if `afterFiles`/`fallback` rewrites land on a different route (and consequently a different set of root param names). |
||
|
|
b5da92e717 |
fix(app-router): drop space before /> in streamed error meta tags (#1491) (#1651)
When notFound() or redirect() is called from inside a Suspense boundary the error surfaces after the shell has already streamed, so the not-found / redirect intent is communicated to the client via inline <meta> tags injected into the head section of the stream. vinext was emitting these as `<meta name="robots" content="noindex" />` (with a space before the self-closing slash), but Next.js serializes them via React's HTML renderer, which produces `<meta ... content="..."/>` without the space. The Next.js navigation tests assert on that exact substring, so this mismatch caused ~5 deploy-suite failures. Drop the extra space so the streamed meta tags match Next.js byte-for-byte, and add ported regression tests against the suspense fixtures. Ref: packages/next/src/server/app-render/make-get-server-inserted-html.tsx Ref: test/e2e/app-dir/navigation/navigation.test.ts (SEO describe block) |
||
|
|
bdc6fe0935 |
fix(build): forward pageExtensions to Vite resolve.extensions (#1502) (#1649)
When the user configured `pageExtensions` with values beyond Vite's defaults (e.g. `["platform.tsx", "tsx", "mdx"]`), extensionless imports of files with those extensions failed to resolve and the build crashed with `Custom deploy script failed: undefined (1)`. Mirror Next.js / Turbopack `resolveExtensions` semantics: prepend the configured `pageExtensions` (each prefixed with `.`) to Vite's default extension list, preserving the user's priority so e.g. `.web.tsx` resolves before `.tsx`. Ported from Next.js: test/e2e/app-dir/resolve-extensions/ Refs: cloudflare/vinext#1502 |
||
|
|
15fbe66c47 |
test(pages-router): regression coverage for edge runtime & OG image API routes (#1338) (#1648)
Adds Pages Router fixtures and dev + production integration tests for the
two scenarios reported as failing in the Next.js Deploy Suite:
- `export const config = { runtime: 'edge' }` API route returns a Web
Response (issue #1338 case 1 — edge API routes were observed as 500).
- `next/og` `ImageResponse` with `runtime: 'edge'` inside a Pages Router
API route returns `image/png` (issue #1338 case 2 — OG routes were
observed as 404).
Ports route handlers from `next.js/test/e2e/edge-pages-support/app/pages/api/hello.js`
and `next.js/test/e2e/og-api/app/pages/api/og.js`. The existing
implementation already wires both code paths correctly via
`handlePagesApiRoute` and the `vinext:og-inline-fetch-assets` plugin; this
PR locks in coverage so a Pages Router-only build cannot silently regress
either path.
Refs #1338
|
||
|
|
34c8669182 |
fix(config): implement compiler.define / compiler.defineServer (#1638)
* feat(config): implement compiler.define and compiler.defineServer (#1496) Forward `compiler.define` to Vite's top-level `define` (applies to both client and server bundles) and `compiler.defineServer` to non-client environments only via a `configEnvironment` hook, matching Next.js Webpack `DefinePlugin` semantics. Values are JSON-stringified at config resolution time so they slot directly into Vite's substitution format. Refs Next.js: packages/next/src/build/define-env.ts. Fixes #1496 * fix(config): reject compiler.define keys that collide with built-ins Mirrors Next.js (`packages/next/src/build/define-env.ts`) — throw when a user-provided `compiler.define` or `compiler.defineServer` key would silently overwrite an internal define (e.g. `process.env.NODE_ENV`, `__NEXT_*`, `__VINEXT_*`) or when `define` and `defineServer` define the same key. Without this, a misconfigured project would produce subtly wrong output instead of failing fast. Addresses /bigbonk review on #1638. |
||
|
|
f6e1525e22 |
fix(pages-router): throw clear error on non-serializable gSP/gSSP props (#1478) (#1642)
When `getStaticProps` or `getServerSideProps` returned props containing non-JSON-serializable values (e.g. `Date`, `Map`, class instances), vinext silently rendered an empty page instead of surfacing an error. This ports Next.js's `isSerializableProps` check from `packages/next/src/lib/is-serializable-props.ts` and invokes it from `pages-page-data.ts` and `dev-server.ts` after the redirect/notFound short-circuits, mirroring `render.tsx` (~lines 982 and 1200). The thrown `SerializableError` is caught by the existing per-request error boundary, which returns a 500 with a Next.js-style message naming the offending key. |
||
|
|
5e90f16a56 |
fix(app-router): preserve push autoscroll across refresh (#1602)
Calling router.push() and router.refresh() in the same transition could leave vinext on the current page instead of committing the pushed route. That diverged from Next.js and broke the default autoscroll target for the pushed page. The refresh path dispatched a current-route RSC refresh before the nested push transition could finish scheduling, so the refresh superseded the intended navigation. Track App Router push/replace scheduling for the current task, keep refresh cache invalidation, and skip only the overtaking current-route refresh. Add Next parity coverage for refresh, server-action refresh, focus targets, fragments, hoisted children, and metadata routes. |
||
|
|
07be579048 | test(app-router): cover clean replace after search load (#1626) | ||
|
|
a0bebe0353 |
test(metadata): regression test for #1493 (dynamic icon hrefs with metadataBase) (#1643)
Locks in the fix from #1436: when `metadataBase` is configured (on a parent layout) and `generateImageMetadata` produces multiple dynamic icon / apple-icon images, the rendered `<link>` href values must stay relative, matching Next.js behavior in test/e2e/app-dir/metadata-dynamic-routes/index.test.ts ("should support generate multi images with generateImageMetadata"). The existing unit-level coverage in features.test.ts verifies MetadataHead output directly. This adds an integration test through the full pipeline (applyFileBasedMetadata -> MetadataHead) covering route groups, dynamic segments, and parent-layout metadataBase inheritance — the exact shape of the failing Next.js deploy test in #1493. Refs #1493 |
||
|
|
c8b8fc3370 |
fix(cache): updateTag throws outside Server Action (#1485) (#1641)
Match Next.js semantics: `updateTag` from `next/cache` must throw when
called outside a Server Action context (e.g. in a Route Handler or
during render), and only the in-action invariant invokes the cache
tag invalidation.
Detect the headers access phase ("action") via the existing shim and
throw the canonical error message otherwise. Existing in-action
behavior (immediate tag expiry via the active cache handler) is
preserved.
|
||
|
|
f75845ef14 |
fix(pages-router): emit page scripts with defer in <head> by default (#1636)
* fix(pages-router): emit page scripts with defer in <head> by default Next.js' Pages Router emits page scripts with the `defer` attribute in `<head>` when `experimental.disableOptimizedLoading` is false (the default). vinext emits `<script type="module">` (which already defers implicitly), but upstream tests assert the literal `defer` attribute and were failing against the deploy suite. Add `defer` to module scripts emitted by `collectAssetTags` when `experimental.disableOptimizedLoading` is not enabled. Surface the flag in `ResolvedNextConfig` so prod-server respects an explicit opt-out. Browser behaviour is unchanged (module scripts already defer), but parity tests now pass. Fixes #1519. * test(e2e): update hydration regex to allow defer attribute The script tag regex in `tests/e2e/cloudflare-pages-router/hydration.spec.ts` required `type="module"` to be immediately followed by `src="…"`, but the optimized-loading fix inserts a `defer` attribute between them. Relax the regex to allow any whitespace-separated attributes between `type="module"` and `src` so future attribute additions don't break it either. |
||
|
|
53a97e2002 |
fix(navigation): stabilize Pages Router useParams snapshot for SSR (#1639)
`useParams()` / `useSearchParams()` / `usePathname()` from `next/navigation`
work as cross-router compat shims under Pages Router, deriving their state
from `getPagesNavigationContext()` published by `next/router`. React's
`useSyncExternalStore` requires `getServerSnapshot` to return a stable
reference (Object.is) across calls in a single render — otherwise React
logs:
"The result of getServerSnapshot should be cached to avoid an infinite loop"
and may re-render until the snapshots stabilize, which is wasteful and a
hydration-mismatch hazard. The SSR branch of `getPagesNavigationContext`
was returning a fresh object each call.
Cache the shape on a `WeakMap` keyed on the ALS-isolated SSR context, so
the same `{ pathname, searchParams, params }` shape (with reference-stable
`params` / `searchParams`) is returned for every hook call inside a single
request. The WeakMap key is per-request, so concurrency safety is
preserved.
Adds a Vitest unit assertion (reference equality across repeated SSR
calls) and a Playwright regression matching the Next.js deploy-suite case
(`test/e2e/app-dir/use-params/use-params.test.ts` — 'should work on pages
router'): a Pages Router dynamic page using `useParams()` inside a fixture
that also has an `app/` directory must render the matched param after
hydration.
Refs #1466
|
||
|
|
a20c49ead2 |
fix(middleware): stop merging original query into middleware rewrite target (#1632)
* fix(middleware): stop merging original query into middleware rewrite target
Next.js middleware constructs the rewrite-target URL itself — by mutating
`request.nextUrl` or by passing a fresh URL — and whatever search params
that URL carries IS the final query. vinext was silently re-merging the
original request's query on top, which resurrected keys that middleware
had explicitly removed via `searchParams.delete(...)`.
Tests:
- Add `mw-clear-query-params` Pages Router fixture + dev test ported from
Next.js `test/e2e/middleware-rewrites/test/index.test.ts` ("should
clear query parameters").
- Add prod-server tests for the original-query-preservation paths
(middleware now propagates query via `request.nextUrl.clone()` in the
fixture, mirroring Next.js `app/middleware.js`).
- Add App Router parity test for original-query-preservation.
Closes #1342
* test(middleware): address bonk review nits for #1342
- Fix misleading comment in app-router.test.ts that referenced Object.assign
merge semantics — the PR's premise is that vinext should NOT auto-merge.
- Add prod-server regression test for searchParams.delete on rewrite target,
mirroring the dev coverage so both code paths are exercised explicitly.
|
||
|
|
d458b71e6b |
test(link): port Next.js OnNavigate fixture as Pages Router regression (#1357) (#1628)
* test(link): port Next.js OnNavigate fixture as a Pages Router regression Add a local Playwright fixture mirroring `.nextjs-ref/test/e2e/link-on-navigate-prop/shared/OnNavigate.tsx` so the upstream `onNavigate` parity scenarios are exercised against vinext's own Pages Router runtime. The `onNavigate` prop itself was implemented in #1371 and is covered by unit tests in `tests/link-navigation.test.ts`. The nightly Next.js deploy suite (workflow run 26552059147) still surfaces failures under `test/e2e/link-on-navigate-prop/index.test.ts` for the Pages Router half of the suite, where even a plain `<button onClick={() => setIsLocked(...)}>` does not update state. The App Router half of the same suite passes, suggesting a Pages Router-specific hydration regression rather than a missing `onNavigate` implementation. This commit ports the upstream fixture so the same scenarios run against vinext in CI's existing Playwright suite, narrowing the reproduction without depending on the full Next.js deploy harness. Refs #1357. * test(link): address bot review on OnNavigate Pages Router port - Assert `isClicked` / `isNavigated` flip to `true` in the internal-click test by stubbing `history.pushState` so vinext's Link runs its synthetic handlers in full but the URL never commits and the source DOM stays available for state assertions. Split the navigation check into its own test that asserts `#subpage-heading`. - Port the three remaining upstream scenarios from `.nextjs-ref/test/e2e/link-on-navigate-prop/index.test.ts`: modifier-key click, plain external link (alert-driven), and external link with `replace` (history-length check). Add the corresponding `#external-link` and `#external-link-with-replace` anchors to the fixture to match `shared/OnNavigate.tsx`. - Explain in the download-test why the native `click` listener doesn't interfere with React's synthetic onClick / onNavigate. - Switch `setIsLocked((v) => !v)` to `setIsLocked(!isLocked)` to match upstream's exact callback shape (parity port). Refs #1357. |
||
|
|
35bb1a2250 |
fix(pages-router): return 405 with Allow: GET, HEAD on POST to static pages (#1631)
Pages Router previously rendered HTML for any HTTP method, including POST/PUT/DELETE, on static (no `getServerSideProps`) pages — masking client bugs that Next.js surfaces as 405. Mirrors Next.js's behavior in `base-server.ts` (the `(typeof components.Component === 'string' || isSSG)` gate), which rejects non-GET/HEAD methods on static pages with `Allow: GET, HEAD` and a `Method Not Allowed` body. Pages with `getServerSideProps` continue to accept any method, since user code may legitimately read `req.method` inside gSSP. The new `resolvePagesPageMethodResponse` helper is wired into both the dev-server SSR handler and the prod-server pages-server-entry, and skips `/_error`, `/404`, `/500`, data requests, and explicit status overrides — matching the upstream carve-outs. Refs #1463. The `res.revalidate()` portion of #1463 is left for a follow-up. |
||
|
|
fc317081fd |
fix(deploy): plain-text 404 for invalid _next/static in Pages Router worker (#1630)
The Pages Router Cloudflare worker template generated by `vinext deploy` forwarded asset-shaped misses to `renderPage`, which renders the full HTML 404 page (with bootstrap scripts + CSS) instead of the plain-text "Not Found" body Next.js emits. The App Router worker entry and prod server already short-circuit these requests; only the Pages Router worker template was missing the check. Adds `isNextStaticPath` + `notFoundStaticAssetResponse` to `generatePagesRouterWorkerEntry()` so invalid `_next/static/*` requests (including those under `basePath` or `assetPrefix`) return `text/plain; charset=utf-8` "Not Found" before the renderer runs. Matches Next.js: packages/next/src/server/lib/router-server.ts. Fixes #1337 |
||
|
|
b463b8eb30 |
fix(script): translate React DOM prop names to HTML attrs in hoisted beforeInteractive scripts (#1629)
Inline `<Script strategy="beforeInteractive">` content is captured via BeforeInteractiveContext and spliced directly into <head> as raw HTML by the SSR pipeline. The attribute collector preserved React's camelCase prop names verbatim, so `className="foo"` round-tripped as `classname="foo"` (HTML attribute names are case-insensitive), making CSS selectors on `.foo` fail to match. Same hazard for `htmlFor`, `httpEquiv`, `acceptCharset`. Translate those four prop names to their HTML attribute equivalents at the collection boundary, mirroring Next.js's set-attributes-from-props.ts. Fixes #1518 |
||
|
|
a1a448a49f |
fix(route-handler): default Path=/ on returned Set-Cookie when merging mutable cookies (#1484) (#1627)
When a route handler called `cookies().set()` alongside a returned
`Response` that carried raw `Set-Cookie` headers without a `Path`
attribute, vinext appended the raw line verbatim. The resulting comma-
joined `set-cookie` view then read like `bar=bar2, baz=baz2` with no
per-cookie `Path=/`, diverging from Next.js whose `appendMutableCookies`
re-runs every returned cookie through `ResponseCookies.set` (which
normalises a missing `path` to `/`).
`applyMutableCookieFallbacks` now ensures each returned cookie carries
an explicit `Path=` attribute, matching Next.js's behaviour in
`packages/next/src/server/web/spec-extension/adapters/request-cookies.ts`.
Ported the Next.js coverage from
`test/e2e/app-dir/actions/app-action.test.ts` ("should support setting
cookies in route handlers with the correct overrides") as
`nextjs-compat/set-cookies.test.ts`.
Fixes #1484
|
||
|
|
f7ea402cd7 |
fix(link): forward href/onClick to child in legacyBehavior (#1623)
* fix(link): forward href/onClick to child in legacyBehavior (#1469) When `<Link legacyBehavior>` is set, the shim previously rendered its own wrapping `<a>` around the user's child anchor, producing nested anchors and duplicated content (e.g. a hidden `<a id="custom-button"></a>` alongside the user's anchor) and breaking onClick propagation. Mirror Next.js: when `legacyBehavior` is true, forward `href`/`onClick`/ `onMouseEnter`/`onTouchStart` to the single child via `React.cloneElement` instead of wrapping. `href` is only forwarded when `passHref` is set or the child is an `<a>` without its own `href`. String/number children are wrapped in a plain `<a>` first so they have an element to clone, matching Next.js. Ported from .nextjs-ref/packages/next/src/client/link.tsx (legacyBehavior branch) and covered by .nextjs-ref/test/e2e/legacy-link-behavior-pages. Fixes #1469 * fix(link): address bigbonk legacyBehavior parity gaps (#1469) - Skip <Link>'s own onClick prop when invoked via legacyBehavior. The child's onClick is the canonical handler; calling Link's onClick too caused a double-fire. Added a dev console.warn matching Next.js when onClick/onMouseEnter are passed to <Link legacyBehavior>. - Use the child element's own ref in legacyBehavior (matches Next.js) instead of routing through Link's forwardedRef. Merged with the internal intersection-observer ref so prefetching still works. - Use `'href' in child.props` (matches Next.js) instead of `!== undefined` so an explicit `href={undefined}` on the child <a> is treated as the child owning its href and is not overwritten. - Clone the child in the dangerous-scheme branch when legacyBehavior is set, so blocked javascript:/data:/vbscript: URLs do not produce nested anchors. The dangerous href is intentionally not forwarded to the child. Added 3 unit tests covering the new behaviors. |
||
|
|
30e374cd68 |
test(link): cover javascript: URL console.error for App + Pages Router Link (#1622)
Fixes #1576. PR #1575 hoisted the dangerous-scheme guard out of the async `performNavigation` body for Pages Router `router.push`/`router.replace`, which covers two of the six Next.js `test/e2e/app-dir/javascript-urls/javascript-urls.test.ts` scenarios. This adds regression coverage for the remaining four Link-flavoured scenarios — App Router and Pages Router, both `href` and `as` — by toggling `appNavigation` in `renderIsolatedLink` and swapping `href` <-> `as`. Each test clicks the rendered anchor and asserts: - `console.error` is emitted with a message containing "has blocked a javascript: URL as a security precaution." (the substring the Next.js E2E matcher asserts on). - `navigate` / `fetch` are never invoked, so navigation does not proceed. - A user-provided `onClick` still fires, so analytics or local UI state is preserved. The Link shim's existing `handleDangerousClick` path (`shims/link.tsx`) and the `assertSafeNavigationUrl` guard inside the app router push/replace already emit the matching message; this PR locks that contract behind a test matrix so a future Link refactor cannot regress it silently. The two `router.push` / `router.replace` scenarios from the issue are already covered by `tests/router-javascript-urls.test.ts:96-128` (App Router) and `tests/router-javascript-urls.test.ts:180-302` (Pages Router, from #1575). Ported from Next.js: test/e2e/app-dir/javascript-urls/javascript-urls.test.ts |
||
|
|
bdbd1f833d |
fix(app-router): preserve source identity for intercepted renders (#1589)
* fix(app-router): preserve source identity for intercepted renders Intercepted App Router renders used the visible target pathname as the payload route identity. That let a modal navigation such as /feed to /photos/42 replace the retained source tree with target-scoped route and page ids, so refreshes, server actions, chained modals, and history traversal could lose source page state. The violated invariant is that an intercepted render has separate display and matched-route identities. This commits a fenced render identity helper, keeps source route and page ids stable for retained source-route payloads, leaves the target pathname in interception proof, and includes source context in RSC cache variants. Regression coverage asserts source payload keys, cache variants, modal refresh/action/history flows, and source client/search state preservation. * fix(app-router): normalize intercepted rsc cache source * fix(app-router): harden intercepted cache regeneration * refactor(app-router): share intercepted path normalization * fixup! fix(app-router): preserve source identity for intercepted renders fixup! fix(app-router): preserve source identity for intercepted renders |
||
|
|
8e3438d267 |
fix(app-router): complete default autoscroll page target parity (#1601)
* fix(app-router): complete default autoscroll page target parity App Router page navigations can commit loading shells, final payloads, and superseded payloads out of order. A pending scroll intent that is only keyed by navigation start can be consumed by the wrong committed tree, which makes default page target scrolling and focus timing diverge from Next.js when loading boundaries or rapid navigations are involved. Bind scroll intents to approved render commits, consume only the matching intent from the committed scroll target, and clear hard-navigation failures only for their own intent. Extend the Next.js compat fixture coverage for skipped first children, loading boundary timing, same-page search param changes, focus behavior, horizontal scroll preservation, and stale intent races. * test(app-router): update link navigation scroll intent assertion * fix(app-router): address review — consume scroll intent on no-commit, tighten test - Consume scroll intent before returning no-commit in renderNavigationPayload, preventing the navigateClientSide fallback from scrolling for an abandoned navigation. - Replace waitForTimeout(700) with expect.poll for robust race-condition stability assertion (CI-flake-safe, stronger proof of stability). - Update wrapperless targeting comment to clarify outer function / inner class split. - Add three unit tests covering no-commit cleanup, stale intent rejection, and claim-without-commitId consumption paths. |
||
|
|
b6ac0c9abd |
test(pages-router): regression test for literal form action=action:foo (#1476) (#1613)
* test(pages-router): regression test for literal form action=action:foo (#1476) Adds a Pages Router fixture page with a literal action:foo string in a form action attribute, plus an integration test asserting the attribute survives SSR serialization intact. This guards against future regressions where React 19 or the RSC plugin might strip the literal string because it resembles a server action ID. Ported from Next.js test/e2e/app-dir/action-in-pages-router. * test: clarify action:foo regression test comment Update the comment to accurately describe that this test verifies React 19 SSR preserves literal string action attributes, not server action invocation. This addresses review feedback on the PR adding the regression test for #1476. |
||
|
|
fdcc850638 | fix(routing): serve pages/_foo.tsx when app/ also exists (#1610) | ||
|
|
8b4210f38f | fix(app-router): dev-only favicon.ico short-circuit to avoid expensive 404 render (#1612) | ||
|
|
aae4b50fe8 |
fix(build): implement compiler.removeConsole option (#1611)
* fix(build): implement compiler.removeConsole option
Implements Next.js parity for compiler.removeConsole, which strips
console.* calls from the client bundle during production builds.
Behavior:
- compiler.removeConsole: true removes all console.<method>() calls
- compiler.removeConsole: { exclude: ['error'] } preserves excluded
methods (case-insensitive)
- Shadowed console bindings (local vars, params, destructured) are
preserved per Next.js SWC behavior
- Computed access console[prop]() is preserved
- Removed calls in expression statements become ; for clean output
- Removed calls in other contexts (JSX, arguments, returns) become void 0
Files:
- packages/vinext/src/plugins/remove-console.ts (new transform)
- packages/vinext/src/config/next-config.ts (types + resolution)
- packages/vinext/src/index.ts (Vite plugin registration)
- tests/remove-console.test.ts (24 unit tests)
- tests/next-config.test.ts (update default resolved config)
Fixes #1497
* fix(knip): un-export RemoveConsoleConfig type
Remove the declare -x ABBR_TIPS_PROMPT="\\n💡 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}"
declare -x ABBR_TIPS_REGEXES="(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*"
declare -x BUN_PTY_LIB="/\$bunfs/root/librust_pty_arm64-b7hv765h.dylib"
declare -x COLORTERM="truecolor"
declare -x COMMAND_MODE="unix2003"
declare -x CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include"
declare -x DEBIAN_FRONTEND="noninteractive"
declare -x DIVMEMORY_API_KEY="3b3f7180985460433de23604da244b8e4c1917f81f992199ebeb0ebc75e7a51e"
declare -x DIVMEMORY_WORKER_URL="https://divmemory.divkix.workers.dev"
declare -x DOTNET_CLI_TELEMETRY_OPTOUT="= 1"
declare -x DO_NOT_TRACK="= 1"
declare -x EDITOR="nano"
declare -x FORCE_COLOR="0"
declare -x FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT="= 1"
declare -x GATSBY_TELEMETRY_DISABLED="= 1"
declare -x GCM_INTERACTIVE="never"
declare -x GIT_ASKPASS=""
declare -x GIT_EDITOR="true"
declare -x GIT_SEQUENCE_EDITOR="true"
declare -x GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
declare -x GIT_TERMINAL_PROMPT="0"
declare -x GPG_TTY="/dev/ttys005"
declare -x HOME="/Users/divkix"
declare -x HOMEBREW_CELLAR="/opt/homebrew/Cellar"
declare -x HOMEBREW_NO_ANALYTICS="= 1"
declare -x HOMEBREW_PREFIX="/opt/homebrew"
declare -x HOMEBREW_REPOSITORY="/opt/homebrew"
declare -x INFOPATH="/opt/homebrew/share/info"
declare -x LANG="en_US.UTF-8"
declare -x LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib"
declare -x LOGNAME="divkix"
declare -x LaunchInstanceID="EB1B14BF-88FC-4DF6-A808-CFA3E9E33401"
declare -x MORPH_API_KEY="sk-sKIOR_4BfuniAInrzxgMoovjMKqlBxa2Ae6EC2oVOe44hOl-"
declare -x NEXT_TELEMETRY_DISABLED="= 1"
declare -x NODE_EXTRA_CA_CERTS="/Users/divkix/.factory/cache/certs/factory-cli-certs.pem"
declare -x OLDPWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x OPENCODE_ENABLE_EXA="true"
declare -x OSLogRateLimit="64"
declare -x PATH="/Users/divkix/.factory/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/divkix/.antigravity-ide/antigravity-ide/bin:/Users/divkix/.antigravity/antigravity/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/imagemagick-full/bin:/opt/homebrew/opt/ffmpeg-full/bin:/opt/homebrew/opt/ruby/bin:/Users/divkix/.bun/bin:/bin:/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home/bin:/opt/homebrew/opt/python/libexec/bin:/opt/homebrew/opt/python/bin:/opt/homebrew/opt/curl/bin:/opt/homebrew/opt/libpq/bin:/Users/divkix/go/bin:/Users/divkix/.local/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/Users/divkix/.lmstudio/bin:/Users/divkix/.orbstack/bin:/Applications/Warp.app/Contents/Resources/bin"
declare -x PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@3/lib/pkgconfig"
declare -x POWERSHELL_TELEMETRY_OPTOUT="= 1"
declare -x PWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x PYTHONUNBUFFERED="1"
declare -x SECURITYSESSIONID="186b0"
declare -x SHELL="/opt/homebrew/bin/fish"
declare -x SHLVL="2"
declare -x SSH_ASKPASS=""
declare -x SSH_AUTH_SOCK="/var/run/com.apple.launchd.VER64w7Euk/Listeners"
declare -x SSH_SOCKET_DIR="~/.ssh"
declare -x STARSHIP_CONFIG="/Users/divkix/.config/starship.toml"
declare -x STARSHIP_SESSION_KEY="5594177922001814"
declare -x STARSHIP_SHELL="fish"
declare -x SUDO_ASKPASS=""
declare -x TERM="dumb"
declare -x TERM_PROGRAM="WarpTerminal"
declare -x TERM_PROGRAM_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x TMPDIR="/var/folders/l6/72dpyfhj03x4z9lvndxkdbxr0000gn/T/"
declare -x USER="divkix"
declare -x VSCODE_TELEMETRY_LEVEL="= off"
declare -x WARP_CLIENT_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x WARP_CLI_AGENT_PROTOCOL_VERSION="1"
declare -x WARP_HONOR_PS1="1"
declare -x WARP_IS_LOCAL_SHELL_SESSION="1"
declare -x WARP_USE_SSH_WRAPPER="1"
declare -x XPC_FLAGS="0x0"
declare -x XPC_SERVICE_NAME="0"
declare -x __ABBR_TIPS_KEYS="l la ll lla g gcl gph gphf gpl gplr gst gcm gcam gco gca gcan gitweb gwp gwl reload c df less www untar bu oc fastdl a__cat a__ls a__rmd"
declare -x __ABBR_TIPS_VALUES="ls ls -a ls -l ls -la git git clone git push git push -f git pull git pull --rebase git status git commit -m git commit -am git commit git commit -a --amend git commit --amend -a --no-edit open (git remote get-url origin) git worktree prune -v git worktree list exec fish \"clear && printf '\\\\e[3J'\" df -h more python3 -m http.server 8000 tar -zxvf fisher update && brew update && brew upgrade opencode aria2c -x16 -s16 -k1M bat lsd --icon-theme=unicode trash"
declare -x __CFBundleIdentifier="dev.warp.Warp-Stable"
declare -x __CF_USER_TEXT_ENCODING="0x1F5:0:0" keyword from — it is only
used internally within and knip flagged it as an
unused export.
* ci: trigger CI re-run
Previous E2E failures (font-google, error-interactive, hooks) appear
to be flaky/unrelated to the removeConsole changes.
* fix(remove-console): address bonk review feedback
- Document client-only scoping as a known parity gap from Next.js
- Correct the node_modules skip comment (Next.js does apply to deps)
- Hoist SCOPE_ENTERING_TYPES to a module-level Set for O(1) lookup
- Push a scope for CatchClause params so `catch (console)` shadows
- Document the var-hoisting limitation in BlockStatement scope handling
- Add resolution tests covering all removeConsole config shapes
* fix(remove-console): gate to build + cover remaining shadow patterns
- Set `apply: "build"` on the plugin so console stripping no longer fires
in `next dev`. Matches Next.js, which documents removeConsole as a
production-only feature.
- Extend `checkBinding` to recognize AssignmentPattern, ArrayPattern, and
RestElement so destructured defaults, array destructuring, and rest
elements correctly shadow `console`.
- Mark FunctionDeclaration / ClassDeclaration `id` in the enclosing scope
before pushing the function's own scope (function and class declarations
hoist their name into the surrounding scope).
- Mark named FunctionExpression `id` in the function's own scope (named
function expressions bind their name only inside their body).
- Add 10 tests covering all newly-supported shadow patterns plus a
CatchClause regression test.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
217aecd84e |
fix(ci): grant contents:read to publish workflow's CI gate (#1620)
The publish workflow declares `permissions: {}` at the top level, which
revokes all permissions. When it invokes the reusable ci.yml workflow,
the called workflow inherits this restriction and cannot request
`contents: read` for actions/checkout, failing with:
The workflow is requesting 'contents: read', but is only allowed
'contents: none'.
Grant the permission explicitly at the ci job level so the reusable
workflow can use it.
v0.0.53
|
||
|
|
a0f4e26655 |
fix(security): honor pages API bodyParser config (F-PROD-4) (#1617)
* fix(security): honor pages API bodyParser config (F-PROD-4)
Pages Router API routes export configuration like:
export const config = { api: { bodyParser: false } };
export const config = { api: { bodyParser: { sizeLimit: '4mb' } } };
vinext silently ignored this, hard-coding a 1 MB limit and always
parsing the body. That created a security downgrade for webhook
handlers (Stripe, GitHub, Slack, etc.) that depend on
'bodyParser: false' to read the raw request stream for HMAC
signature verification. By the time the user handler ran, the body
had already been consumed and parsed into 'req.body', so HMAC
verification would either fail closed (no signature match) or fail
open (handlers that fall back to an empty body for unsupported
content types).
Legitimate large uploads were also rejected with 413 even when the
route declared 'sizeLimit: "4mb"' or higher.
This change introduces a shared helper that resolves the route
module's 'config.api.bodyParser' option and patches both code paths:
- Node/dev path: packages/vinext/src/server/api-handler.ts
- Workers/prod: packages/vinext/src/server/pages-api-route.ts
(+ packages/vinext/src/server/pages-node-compat.ts)
Behaviour now matches Next.js
(packages/next/src/server/api-utils/node/api-resolver.ts):
- bodyParser: false -> skip parsing entirely. Node
handlers receive the raw
IncomingMessage stream;
Workers handlers receive
request.body as a
ReadableStream<Uint8Array>
on 'req.body'.
- bodyParser: { sizeLimit: '4mb' } -> parse with the user-supplied
limit instead of the
hard-coded 1 MB.
- bodyParser: true / undefined -> existing 1 MB default behaviour.
The sizeLimit parser accepts the same suffix grammar Next.js does
('500b', '100kb', '4mb', '1gb'), implemented inline rather than
pulling the 'bytes' package.
Tests cover both Node and Workers paths plus parser-level unit
tests, including:
- bodyParser: false leaves the stream intact for HMAC verification
- sizeLimit '4mb' accepts a 2 MB POST that used to 413
- sizeLimit '4mb' rejects a 5 MB POST with 413
- sizeLimit '100kb' rejects a 200 KB POST with 413
- No config preserves the 1 MB default
config.api.responseLimit is not in scope for this PR and remains a
follow-up.
Reference: Finding F-PROD-4 in SECURITY-AUDIT-2026-05.md
* chore(security): drop unused ResolvedBodyParserConfig export to satisfy knip
|