v0.2.1 consumes upstream Vitest directly: the @voidzero-dev/vite-plus-test wrapper is removed and vitest now comes in transitively through vite-plus.
- catalog: vite-plus 0.1.24 -> 0.2.1, vite -> @voidzero-dev/vite-plus-core@0.2.1
- catalog: vitest -> upstream 4.1.9 (was the vite-plus-test alias), @vitest/coverage-istanbul 4.1.6 -> 4.1.9
- drop the vitest peerDependencyRules accommodations for the wrapper/old vitest
- decode Vitest's flatted blob format directly in scripts/lib/vitest-blob-timings.mjs (the removed wrapper bundled the parser this script previously reached into)
* 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>