Commit Graph

1107 Commits

Author SHA1 Message Date
github-actions[bot] 948a0d9d72 chore: version packages (#2147)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-19 16:53:06 +01:00
James Anderson 656a381de9 fix(og): constrain inlined assets to trusted roots (#2172)
* fix(og): constrain inlined assets to trusted roots

* fix(og): canonicalize asset package boundaries

* test(og): cover canonical boundary intersections

* fix(og): support file-symlinked dependency modules

* docs(og): clarify logical asset boundary resolution

* fix(og): pin symlinked modules to matching packages

* fix(og): keep linked package boundaries narrow

* fix(og): reject workspace-root asset boundaries

* fix(og): name-pin external package boundaries

* fix(og): validate linked package ownership

* fix(og): track linked package ownership

* fix(og): harden linked package provenance

* fix(og): confine captured alias packages

* fix(og): pin captured directory aliases

* fix(og): require alias package ownership

* fix(og): support single-segment aliases

* perf(og): preserve alias lookup precedence

* refactor(og): extract asset ownership policy

* test(og): cover linked package fonts in e2e

* fix(og): recognize resolved package aliases

* fix(og): support regex package aliases

* test(og): isolate linked font fixture

* docs(test): clarify linked font coverage
2026-06-19 16:08:46 +01:00
Nathan Nguyen 301418a3f1 perf(app-router): defer layout, template and boundary modules to first request (#2145)
* perf(app-router): defer layout, template and boundary modules to first request

The generated RSC entry statically imported every route's layout, template,
loading/error/not-found/forbidden/unauthorized boundary and parallel-slot
module at the top level. On a cold isolate — and on the dev server's first
request — that eagerly evaluated the whole app's segment tree, including
routes never visited in that lifetime. Page and route-handler modules were
already lazy; the surrounding segment modules were not.

Emit those modules as lazy `() => import()` loaders paired with `null`
placeholders in the manifest, and hydrate them onto the route's synchronous
fields in `ensureAppRouteModulesLoaded` before any read (extended from
page/handler to layouts, templates, boundaries and parallel-slot modules).
Intercept layouts hydrate via the dedicated `loadAppInterceptLayouts` because
they live on the match object, not the route. Every reader already runs behind
`ensureAppRouteModulesLoaded` (handler dispatch and `buildPageElements`) or the
intercept resolvers, so no synchronous module field is read before hydration.

The shared intercept `interceptLayouts` arrays keep their `readonly` public
types; the loader is their sole writer and fills slots through one
boundary-local cast, so route metadata stays immutable to callers.

Dev cold start is unchanged on page-heavy apps — pages were already lazy — but
on a boundary-heavy app (80 independent sections, each with
layout+template+error+loading+not-found) first-request module compile drops
704ms -> 534ms (~24%) and time-to-first-200 1917ms -> 1665ms. The saving scales
with the number of unrelated segments deferred.

* test(app-router): localize react max header font fixture

* fix(app-router): dedupe lazy intercept module loads

* fix(app-router): share intercept load state across matches

* docs(app-router): explain root layout loader reuse

---------

Co-authored-by: James <james@eli.cx>
2026-06-19 14:49:29 +01:00
MaxtuneLee 42f8700f15 fix(router): enhance navigation with as mask for URL handling (#2076)
* fix(router): enhance navigation with `as` mask for URL handling

* fix(router): implement bracket-pattern interpolation for dynamic routes in Pages Router

* fix(router): preserve masked state in beforePopState

* fix(router): cover basepath navigation in ci

* docs(router): clarify navigation state invariants

* refactor(router): require complete history state

* refactor(router): share dynamic href interpolation

* test(router): cover localized masked navigation

* fix(router): keep interpolation internals private

---------

Co-authored-by: James <james@eli.cx>
2026-06-19 00:07:03 +01:00
James Anderson b2ea860284 fix(proxy): match Next.js file conventions (#2155)
* fix(proxy): match Next.js file conventions

* test(proxy): keep convention fixture out of workspace

* test(proxy): provide middleware path in handler fixture
2026-06-18 23:54:39 +01:00
Divanshu Chauhan 7487886b38 fix(prod-server): send headers-only for HEAD in sendCompressed (#1980) (#2058)
sendCompressed wrote the full body for every request. HEAD requests routed
through it (bot/crawler-buffered Pages HTML and Pages API routes) still
compressed and emitted a payload that Node then discards at the socket level.

Short-circuit with res.end() after writeHead when req.method is HEAD,
mirroring sendWebResponse. This skips spinning up a compressor for a body
that would be thrown away and keeps the buffered sender consistent with the
streamed and static-file senders, which already handle HEAD.

Co-authored-by: James <james@eli.cx>
2026-06-18 22:13:07 +00:00
Jerry Zhao 3e5f676666 refactor(shims): normalize the shim alias base to forward slashes on Windows (#2166) 2026-06-18 22:28:25 +01:00
Jerry Zhao b352d671fa fix(rsc): resolve the app-rsc-handler shim to a forward-slash id on Windows (#2165) 2026-06-18 22:18:03 +01:00
Jerry Zhao e99b8c6f54 refactor(project): unify directory detection into a shared findDir helper (#2162) 2026-06-18 21:43:11 +01:00
Jilles Soeters 8354b98d64 fix(init): ignore vinext output directory (#2160) 2026-06-18 21:26:05 +01:00
James Anderson 12f04d6d63 fix(app-router): block dangerous RSC redirect URLs (#2154)
* fix(app-router): block dangerous RSC redirect URLs

* refactor(app-router): reuse blocked navigation reporter

* style(app-router): format redirect guard import
2026-06-18 17:51:07 +00:00
Alexander Lichter 2fc9845ed3 fix(fonts): preload only requested google font subsets (#2152)
Co-authored-by: James Anderson <james@eli.cx>
2026-06-18 17:20:05 +01:00
Alexander Lichter 9639c88231 perf(app-router): defer prerender endpoint & file metadata runtimes (#2150)
* perf(app-router): defer prerender endpoint runtime

* perf(app-router): defer file metadata runtimes
2026-06-18 16:30:03 +01:00
MaxtuneLee ef003c0ebe fix(server): q-value-aware Accept-Encoding negotiation (#2047)
* fix(server): implement q-value-aware Accept-Encoding negotiation and add tests

* docs(server): update comments for Accept-Encoding parsing to clarify q-value handling

* fix(server): enhance q-value handling in Accept-Encoding parsing and add tests

* fix(server): enhance q-value handling in Accept-Encoding parsing and add tests for isEncodingAccepted

* fix(tests): update negotiateEncoding tests to reflect zstd as highest preference in prod-server.ts

* fix(tests): update negotiateEncoding tests

* fix(tests): remove redundant test for explicit refusal overriding wildcard in negotiateEncoding

* fix(server): normalize encoding checks in isEncodingAccepted function

* refactor(server): extract accept-encoding helpers

* fix(tests): remove redundant zlib check and import HAS_ZSTD from accept-encoding module

* fix(server): honor Accept-Encoding quality weights

* fix(server): repair accept-encoding CI checks

* fix(server): harden content encoding negotiation

* fix(server): align encoding negotiation with Next.js

* refactor(server): remove unused encoding parse state

* fix(server): normalize empty vary headers

---------

Co-authored-by: James <james@eli.cx>
2026-06-18 14:07:05 +01:00
James Anderson 55d9a23860 fix(image): resolve aliased static imports (#2142) 2026-06-18 13:59:31 +01:00
github-actions[bot] 39ce2ba170 chore: version packages (#2113)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-18 09:54:39 +00:00
James Anderson eeac5db0a3 fix(image): preserve unoptimized remote URLs (#2139)
* fix(image): preserve unoptimized remote URLs

* fix(image): honor overrideSrc when unoptimized
2026-06-18 10:49:42 +01:00
Jerry Zhao d077d67b79 refactor(instrumentation): drop redundant normalizePath on instrumentation-client path (#2130) 2026-06-18 00:33:30 +01:00
Jerry Zhao 33b24a27c9 refactor(instrumentation): resolve instrumentation hook files in forward-slash space (#2128) 2026-06-18 00:06:17 +01:00
Alexander Lichter 55b1417d22 perf(app-router): defer cache-only runtimes (#2126) 2026-06-18 00:05:31 +01:00
Alexander Lichter 01c3abbe38 perf(app-page-cache): reuse HTML ISR key during regeneration (#2127) 2026-06-18 00:04:30 +01:00
Jerry Zhao d98adce4bc fix(image-imports): normalize watchChange id so image cache invalidates on Windows (#2125) 2026-06-17 23:50:19 +01:00
Jerry Zhao a85170c74a fix(transforms): emit sourcemaps from strip-server-exports and remove-console (#2124) 2026-06-17 23:45:59 +01:00
Jerry Zhao 29248465c6 perf(app-page-cache): reuse the ISR cache key on the stale-regeneration path (#2123) 2026-06-17 23:39:11 +01:00
Jerry Zhao 19d8f04cbb perf(image-imports): hoist image-extension regex to module scope (#2122) 2026-06-17 21:51:49 +00:00
Alexander Lichter cc8d4d6dea perf(ssr): isolate open redirect detection (#2120)
* perf(ssr): isolate open redirect detection

* perf(ssr): isolate stream cleanup runtime

* Apply suggestions from code review

Co-authored-by: James Anderson <james@eli.cx>

* remove comments

---------

Co-authored-by: James Anderson <james@eli.cx>
2026-06-17 21:29:13 +00:00
Alexander Lichter c3ff8a7028 perf(dev): skip JSX transform for compiled runtime (#2121) 2026-06-17 22:13:21 +01:00
Alexander Lichter d2edaf80b5 perf(build): rely on plugin hook filters (#2119) 2026-06-17 19:13:42 +01:00
Alexander Lichter 2eb394f0a4 perf(dev): skip typeof window folding for prebundles (#2118)
* perf(dev): skip typeof window folding for prebundles

* perf(dev): move prebundle skip into hook filter

* test: provide cacheDir in resolved config mock

* fix(dev): handle reused typeof-window plugins

* refactor(dev): share cache directory normalization

---------

Co-authored-by: James <james@eli.cx>
2026-06-17 18:13:31 +00:00
Jerry Zhao 0ef6e10538 fix(middleware): match middleware taint ids by forward-slash on Windows (#2088) 2026-06-17 19:02:38 +01:00
Jerry Zhao 54cb9e3a2f fix(build): normalize canonicalized module ids to forward slashes on Windows (#2089) 2026-06-17 18:43:20 +01:00
Alexander Lichter 59a2de53a3 perf(app-router): defer action and route-handler runtimes (#2079)
* perf(app-router): defer action and route-handler runtimes

* perf(app-router): defer middleware runtime

* perf(rsc): isolate server navigation runtime

* perf(ssr): isolate bfcache identity runtime

* perf(ssr): avoid browser navigation runtime

* refactor(navigation): share server-safe primitives

* fix(ssr): track error boundary pathname in fallback shells

---------

Co-authored-by: James <james@eli.cx>
2026-06-17 17:41:25 +00:00
James Anderson 284cbb443e fix(css): keep module hashes stable with url assets (#2114)
* fix(css): keep module hashes stable with url assets

* fix(css): restore url assets in server output
2026-06-17 18:21:59 +01:00
Alexander Lichter 4fdf617004 perf(cache): isolate handler and request state (#2107)
* perf(cache): isolate handler and request state

* perf(rsc): isolate page cache key marker
2026-06-17 18:17:41 +01:00
James Anderson 144c30d47e fix(middleware): normalize trailing slash before matching (#2112)
* fix(middleware): normalize trailing slash before matching

* test(middleware): preserve unmatched API fixture

* fix(middleware): normalize matcher source slashes

* fix(middleware): normalize direct matcher inputs

* fix(middleware): preserve explicit slash matchers
2026-06-17 13:19:49 +01:00
Alexander Lichter 73d01f085e perf(build): filter compatibility transform hooks (#2103) 2026-06-17 12:32:42 +01:00
Alexander Lichter 56d7311994 perf(dev): externalize App Router request handler (#2108) 2026-06-17 11:55:48 +01:00
github-actions[bot] 58154448cf chore: version packages (#2046)
* chore: version packages

* Apply suggestion from @james-elicx

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: James Anderson <james@eli.cx>
2026-06-16 14:45:52 +01:00
James Anderson 2f8b0ef78b fix(build): preserve lazy RSC framework chunks (#2074)
* fix(build): preserve lazy RSC framework chunks

* test(image): wait for completed watch output

* test(build): isolate RSC startup regression

* fix(build): preserve lazy framework chunks on Vite 7

* test(build): cover transitive framework reachability

* test(build): harden framework chunk regressions
2026-06-16 14:28:15 +01:00
James Anderson 06ffe4661c fix(middleware): fail closed for unsafe matchers (#2078) 2026-06-16 14:24:04 +01:00
Alexander Lichter 5495682f6a perf(rsc): prebundle static renderer entry (#2077) 2026-06-16 12:56:27 +00:00
Alexander Lichter f0e6be73f2 perf(dev): prune NODE_ENV during dependency optimization (#2075)
* perf(dev): fold NODE_ENV during dependency optimization

* fix(dev): preserve NODE_ENV optimizer defines

* test(dev): inspect optimized dependencies via metadata

* docs(dev): clarify dependency optimizer defines
2026-06-16 12:42:44 +00:00
James Anderson c845c0af05 fix(build): align Pages data export transforms (#2055)
* fix(build): align Pages data export transforms

* fix(build): address Pages export review feedback

* perf(build): narrow page export-all parsing

* perf(build): scan page export-all candidates linearly
2026-06-16 12:04:53 +01:00
Xplod13 b891b01d41 fix(fonts): detect trailing comma past comments when injecting font options (#2051)
The font plugins inject an extra option into the user's `next/font` options
object by string-splicing before the closing `}`. They chose the separator by
testing whether the text before `}` ends with `,`, but that test ran on the
raw source, so a trailing comment hid a real trailing comma:
`Inter({ weight: "400", /* x */ })` produced `{ weight: "400", /* x */, _vinext: … }`
— a double comma and a syntax error that broke the build.

Add a shared, string-aware `hasTrailingComma`/`lastSignificantChar` helper
(`utils/has-trailing-comma.ts`) that forward-scans tracking string and comment
state, and use it at both font injection sites and in dynamic-preload-metadata
(which previously kept a private comment-stripper). A stateful scan — rather
than stripping comments with a regex — is required so a `//` inside a string
literal (e.g. a URL) can never be mistaken for a comment and corrupt the
trailing-comma decision.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:42:16 +01:00
Divanshu Chauhan 067a7c6000 fix(csp): match script-src-elem/attr when extracting script nonce (#2049)
Select the CSP nonce directive via startsWith("script-src") /
startsWith("default-src"), matching Next.js get-script-nonce-from-header.
Previously the exact-match predicate skipped script-src-elem/script-src-attr
directives, so a CSP declaring the nonce only on script-src-elem fell through
to default-src and SSR scripts could be emitted without the expected nonce
and blocked by the browser CSP.

Closes #1989
2026-06-15 23:39:55 +01:00
Xplod13 33be48648d fix(script): register src beforeInteractive scripts and mark hoisted output (#2019)
## Summary
- Register `beforeInteractive` Scripts with `src` through
  BeforeInteractiveContext so they are hoisted into <head> ahead of
  interactive, exactly like the inline form (previously they bypassed the
  registry and rendered in source order, after React Float's head content).
- Suppress the client React render for both inline AND src beforeInteractive
  Scripts in the App Router so the hoisted tag is never duplicated.
- Tag every hoisted script with Next.js's `data-nscript="beforeInteractive"`
  marker.
- Map `crossOrigin` -> `crossorigin` and `referrerPolicy` -> `referrerpolicy`
  on hoisted scripts (parity with Next.js set-attributes-from-props).
- Extract `renderBeforeInteractiveInlineScripts` into a dedicated, pure module
  (before-interactive-head.ts) so the emit path is unit-testable.

## Root Cause
The beforeInteractive SSR branch computed `inlineContent = src ? null : ...`
and only registered when `inlineContent !== null`, so any Script with a `src`
was never captured by the registry and never spliced into <head>; it stayed
where the user wrote it, behind React Float's hoisted resource hints, defeating
the before-interactive guarantee. Hoisting src scripts then requires the client
to stop rendering them through React (the React-created <script> never executes
and would only add a duplicate DOM node), so the client dedupe condition is
aligned with the SSR registration condition. The hoisted output also lacked
Next.js's `data-nscript` marker, and REACT_TO_HTML_ATTR omitted
crossOrigin/referrerPolicy, so those attribute names did not match Next.js's
lowercased output.

## References
- Fixes #2016
- Next.js: .nextjs-ref/packages/next/src/client/script.tsx (data-nscript,
  src/inline registry parity), set-attributes-from-props.ts (lowercased attrs)
- Ported: test/e2e/app-dir/script-before-interactive/script-before-interactive.test.ts

## Verification
- CI=true pnpm test tests/script.test.ts tests/script-head-ordering.test.ts
  tests/nextjs-compat/script-nonce.test.ts tests/shims.test.ts -> 1139 passed
- CI=true npx vp check on all changed files -> pass

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:38:06 +01:00
Xplod13 59ddd0aded fix(i18n): match NEXT_LOCALE cookie case-insensitively (#2050)
## Summary
- Resolve the `NEXT_LOCALE` cookie case-insensitively and return the
  canonical configured locale (e.g. `NEXT_LOCALE=EN-US` -> `en-US`).
- Add unit tests covering uppercase/mixed-case cookies, exact-case, and
  non-configured values.

## Root Cause
`parseCookieLocaleFromHeader` matched the cookie value against
`i18nConfig.locales` with a case-sensitive `Array.includes`. A cookie like
`NEXT_LOCALE=EN-US` therefore failed to match a configured `en-US`, so the
sticky locale was silently dropped and locale detection fell through to the
default. Next.js's `getLocaleFromCookie` lowercases the cookie value and
finds the configured locale case-insensitively, returning the canonical
form; vinext now mirrors that.

## References
- Next.js: packages/next/src/shared/lib/i18n/get-locale-redirect.ts
  (`getLocaleFromCookie`).
- Next.js test: test/e2e/i18n-support/shared.ts (`addDefaultLocaleCookie`
  sets `NEXT_LOCALE=EN-US` "to ensure it's case-insensitive").

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:35:22 +01:00
Xplod13 50f435ce13 fix(metadata): gate twitter app and player tags on card type (#2053)
The Twitter metadata renderer emitted `twitter:app:*` tags whenever a
`twitter.app` object was present and `twitter:player:*` tags whenever
`twitter.players` was present, regardless of the resolved card type. It also
emitted `twitter:app:id`/`twitter:app:url` for falsy values (`0`, `""`).

Next.js gates these blocks on the resolved card: `if (card === 'app')` and
`if (card === 'player')`, and uses truthy checks for app id/url. Mirror that:
gate the app block on `card === 'app'`, the player block on `card === 'player'`,
and skip falsy app ids/urls. The card is already defaulted in
postProcessMetadata (summary_large_image / summary), matching Next.js.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:34:53 +01:00
Xplod13 157cf7ab70 fix(config): support tsconfig extends array form (#2054)
## Summary
- Handle the `extends` array form (TypeScript 5.0+) when resolving
  tsconfig/jsconfig path aliases and `baseUrl` for `next.config.ts` loading.
- Normalize `extends` (string | string[] | undefined) into an ordered list at
  both loader sites; iterate parents so later entries override earlier ones.
- Add array-extends regression tests (single parent, multi-parent precedence,
  child override).

## Root Cause
Both tsconfig loaders only handled the string form of `extends`
(`typeof parsed.extends === "string"`). When `extends` was an array
(`["./a.json", "./b.json"]`), it was silently dropped, so inherited
`compilerOptions.paths`/`baseUrl` were lost and path-aliased imports in
`next.config.ts` failed to resolve. Next.js normalizes via
`Array.isArray(config.extends) ? config.extends : [config.extends]` and merges
parents in order; this change mirrors that behavior at both vinext sites
(packages/vinext/src/config/tsconfig-paths.ts and packages/vinext/src/index.ts).

## References
- Fixes #1974
- Next.js parity: packages/next/src/build/next-config-ts/transpile-config.ts (loadTsConfigFile)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:29:27 +01:00
Nathan Nguyen 5b3ea11153 fix(navigation): promote OperationToken to the eligibility authority (#2009)
The App Router client carried an OperationToken on every navigation
decision but only read its `lane` field. The authority the token was
meant to own lived inline elsewhere: the commit-staleness gate was a raw
boolean (startedNavigationId !== activeNavigationId or a visible-commit
version mismatch) evaluated before the token was even built, and cache
reuse was gated separately. The token also lacked the lifecycle
navigation id, so it could not answer "does this result belong to the
active navigation?" at all — its operationId is a per-render counter.

Promote OperationToken to the single proof-of-eligibility object. A new
operation-token.ts module owns the token type and verifyOperationToken,
a pure verifier over four dimensions (active-navigation, visible-commit,
graph-version, cache-variant) that returns a branded VerifiedOperationToken
on success. The token verifies; ApprovedVisibleCommit still mutates — the
two stay separate.

- Add navigationId to the token (sourced from startedNavigationId) so it
  can carry the active-navigation authority.
- Route the commit-staleness gate through verifyOperationTokenForCommit
  and delete the inline boolean. The staleOperation skip + trace are
  byte-identical; planPendingRootBoundaryFlightResponse now requires the
  VerifiedOperationToken brand, making "verify before plan" a compile-time
  guarantee.
- Share the authority with cache reuse: planFlightResponseArrived gates an
  accepted cache-entry reuse decision through verifyOperationTokenForCacheReuse,
  hard-navigating (cacheReuseTokenRejected) when the proof's graph version no
  longer matches the installed route graph. Behavior-preserving today (the
  token's graphVersion is minted from the same manifest); a real guard once
  cross-document and segment reuse can diverge (PR 6/7).

Absence is not permission: a checked dimension tolerates an absent
authority fact, but a *required* one fails closed, and require implies
evaluation — the contract segment-cache/BFCache writes need in PR 6/7.

Tests: operation-token.test.ts covers every verdict dimension, the brand,
the absent/mismatch split, and require⇒evaluate; navigation-planner and
app-browser-entry suites stay green, proving the gate refactor preserved
behavior; two new planner tests cover the cache-reuse graph-version seam.
2026-06-15 23:21:38 +01:00