* 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>
* 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>
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>
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
## 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>
## 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>
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>
## 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>
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.
* fix(pages): emit canonical next data script
* test(pages): expect canonical next data markup
* fix(pages): initialize canonical next data before routing
* test(pages): parse canonical next data markup
* fix(pages): preserve router bootstrap ordering
* test(pages): expect readiness bootstrap import
* fix(pages): guard readiness bootstrap without next data