mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.7
50 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef78685beb | test(next-config): build the cacheHandler file:// fixture with pathToFileURL (#2163) | ||
|
|
10b5086f4d |
fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior. The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks. * fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan * test(app-router): add full-chain scroll intent integration test * fix(app-router): match legacy scroll focus parity * fix(app-router): preserve latest scroll navigation intent * fix(app-router): scope scroll and error recovery * fix(app-router): gate navigation failure recovery * fix(app-router): match navigation failure handling * fix(app-router): align scroll completion parity * fix(app-router): retain latest failure target * fix(app-router): preserve route error boundaries * fix(app-router): clear discarded failure targets * fix(app-router): align failure recovery boundaries * fix(app-router): disarm refused recovery targets * fix(app-router): preserve failure target ownership * fix(app-router): match boundary recovery parity --------- Co-authored-by: James <james@eli.cx> |
||
|
|
3d9639510e |
fix(build): honour experimental.lightningCssFeatures include/exclude (#1498) (#1664)
vinext silently ignored `experimental.lightningCssFeatures` because the options were never plumbed into Vite's CSS pipeline. `light-dark()` and other modern CSS features were still lowered to polyfills even when the user explicitly listed them under `exclude`. Resolve `experimental.useLightningcss` and convert the dash-case feature names from `experimental.lightningCssFeatures` into the numeric bitmask expected by lightningcss (mirroring Next.js's `featureNamesToMask` in `packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts` and `crates/next-core/src/next_config.rs`). When `useLightningcss` is on, forward `transformer: 'lightningcss'` plus the resolved `include`/`exclude` masks to Vite's `css.lightningcss` bridge. Adds a regression test that builds a fixture with `exclude: ['light-dark']` and asserts the function survives untouched in the emitted CSS, plus an inverse `include: ['light-dark']` case and unit coverage for the feature-name → bitmask helper. Ported from Next.js: test/e2e/app-dir/experimental-lightningcss-features/experimental-lightningcss-features.test.ts |
||
|
|
a84a038fe6 |
fix(config): share CommonJS module fallback (#1990)
* fix(config): share CommonJS module fallback * fix(config): harden CommonJS retry detection |
||
|
|
5e19a2dd38 |
fix(config): honor resolve extensions and improve webpack config processing (#1959)
* fix(config): honor resolve extensions * fix(config): preserve resolve extension semantics * fix(config): preserve resolve extensions fallback * fix(config): preserve resolver environment semantics * fix(config): treat Vite preview as production |
||
|
|
e018a08708 |
fix(pages-router): restore scroll across reload history traversal (#1905)
* fix(pages-router): restore scroll across reload history traversal
Pages Router pages with experimental.scrollRestoration currently expose window.next.router before the popstate runtime is installed and only keep Vinext scroll coordinates in the in-memory history state. After a reload, a back or forward traversal can run before the runtime is ready and has no durable scroll snapshot for the target history entry.
The violated invariant is that the Pages Router owns browser scroll restoration whenever the Next.js scroll restoration flag is enabled, with one stable history key per entry and routeChangeComplete emitted after the new page commits.
Resolve the Next.js config flag into process.env.__NEXT_SCROLL_RESTORATION, install the Pages Router runtime during next/router evaluation, persist Next-style sessionStorage scroll snapshots per history key, and render navigations through a stable commit boundary before emitting routeChangeComplete.
Regression coverage ports Next.js reload-scroll-backforward-restoration and verifies manual restoration plus exact scroll positions across reload, back, and forward traversal.
* fix(pages-router): sequence routeChangeComplete after scroll restoration and settle on render failures
* fix(router): guard against missing window.history in stampInitialHistoryState
* fix(router): defer hash scroll until after routeChangeComplete
Move hash scrolling out of renderPagesRouterElement's commit callback
and into the post-routeChangeComplete path, matching Next.js ordering:
x/y restoration happens during render commit, hash scrolling happens
after the completion event.
Also fix E2E test expectRouteChangeComplete to use one-shot event
listeners instead of checking for stale text, and add listener cleanup
to the fixture to prevent accumulation.
* fix(test): remove post-push deadlock and throw on missing events in E2E helper
expectRouteChangeComplete() now throws if window.next.router.events is
absent instead of silently resolving. Remove the redundant
expectRouteChangeComplete call after pushWithPagesRouter in the third
test — the awaited router.push already resolves after routeChangeComplete,
so registering a listener after it would deadlock.
* fix(test): replace as any casts with type-safe window.next narrowing in E2E scroll tests
* fix(router): narrow renderPagesRouterElement scroll param, drop dead hash branch
* fix(router): reset x/y scroll on hash routes before routeChangeComplete and cancel pending render on supersede
- Pass {x:0, y:0} through commit boundary for all scrollable
navigations, not just hash-less ones (hash scroll deferred to after
routeChangeComplete as before).
- Cancel pending render-commit Promise when a newer navigation starts,
preventing hang when the first root.render() commit is dropped by
React re-reconciliation.
- Add regression tests for both edge cases.
* fix(router): use SSR-safe layout effect in commit boundary and pin shallow scroll ordering
Address bonk review on #1905:
- swap the commit boundary's raw useLayoutEffect for the SSR-safe
useNonWarningLayoutEffect pattern from shims/image.tsx so Pages Router
SSR renders no longer log React's useLayoutEffect warning
- document that the default-noop commit boundary intentionally also wraps
SSR and initial hydration
- document the intentional shallow-scroll-before-routeChangeComplete
ordering and add a unit test pinning it
* fix(router): keep history-state scroll restore on popstate when scrollRestoration flag is off
Without experimental.scrollRestoration, popstate must still honour the
__vinext_scrollX/Y saved into history state on push, since a soft popstate
re-renders content the browser's native restoration can't position. Also
document why the popstate-time sessionStorage snapshot depends on
history.scrollRestoration = "manual".
* fix(router): guard render-commit canceller against late commits from superseded trees
* fix(router): defer current-history-key commit until beforePopState allows navigation
When beforePopState cancels a popstate the app stays on the previous
history entry, but _currentHistoryKey had already been updated to the
target key, skewing subsequent scroll-position bookkeeping. Move the
key commit below the beforePopState early-return, matching the
deliberate deferral of _lastPathnameAndSearch.
* chore(router): note intentional eager scroll snapshot; fix fixture link increment
- Document that the outgoing-entry scroll snapshot is deliberately written
before the beforePopState gate: on cancellation the app stays on
currentKey, so the saved value remains that entry's live scroll.
- Coerce id to a number in the pages-scroll-restoration fixture Link so
the href increments numerically instead of string-concatenating.
* fix(router): keep manual scroll restoration opt-in Pages-Router-only
Gate installManualScrollRestoration on the document not being an App
Router one (window.next.appDir === true): with the now-global
__NEXT_SCROLL_RESTORATION define, an App Router app that value-imports
next/router for compat would otherwise have its native browser scroll
restoration disabled at module eval. Upstream only flips
history.scrollRestoration inside the Pages Router constructor, which
never runs on App Router documents.
Also document the hash-only popstate path discarding the per-entry
__next_scroll_<key> snapshot as intentional upstream parity (change()'s
onlyAHashChange branch passes null to this.set and only scrollToHash
runs), and add unit coverage for both behaviors.
* ci: run pages-scroll-restoration E2E project in the CI matrix
Add the missing E2E matrix entry so the ported Next.js regression spec
(reload-scroll-backforward-restoration.spec.ts) actually executes in CI,
matching the shape of the sibling single-shard Pages Router entries.
Also document why the soft-popstate scroll fallback differs between
manual restoration on ({ x: 0, y: 0 }) and off (null -> native auto
restoration handles unstamped entries), per review.
* refactor(router): fold App Router gate into manualScrollRestoration constant
The popstate handler's sessionStorage save/read branches gated on the
module-level constant, which lacked the window.next?.appDir check that
installManualScrollRestoration used, leaving those branches relying
solely on the __N state filter to stay inert on App Router documents.
Fold the appDir check into the constant so every consumer is
Pages-Router-only by the same gate; the App Router bootstrap stamps
window.next.appDir before any client module evaluates, so the
module-eval-time check is reliable.
* test(pages-scroll-restoration): default getServerSideProps id to string "0"
Route params always arrive as strings, so the numeric 0 fallback was
inconsistent with the string values the component relies on
(Number(id) + 1 href, id === "error" check). Default to "0" for type
consistency.
* test(pages-scroll-restoration): align fixture deps and vinext import with pages-basic
Import vinext via the workspace package name instead of reaching into
dist by relative path, and declare the same react/react-dom/vinext
dependencies as the other fixtures so the fixture is self-describing
and the pnpm-lock.yaml importer entry is populated.
* fix(router): snapshot outgoing scroll on hash-only pushes
Upstream Next.js snapshots the outgoing entry's scroll into
__next_scroll_<key> inside Router.push() itself, before change()'s
onlyAHashChange short-circuit, so hash-only pushes still capture the
departed entry's position. Our hash-only push branch returned before
saveScrollPosition() ran, so a later back-popstate to that entry
restored {x: 0, y: 0} instead of the position the user had reached.
Save the outgoing scroll before updateHistory mints the new key,
matching the full-navigation push path and upstream ordering.
* docs(router): clarify manual scroll-target fallback chain precedence
* test(shims): tag scrollRestoration App Router test as eval-order guard
* docs(router): note missing-snapshot fallback intent at forcedScroll default
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
fd1023321b |
fix(router): support experimental gesture push (#1909)
* fix(router): support experimental gesture push Vinext did not expose useRouter().experimental_gesturePush when Next.js apps enabled experimental.gestureTransition. Upstream gesture-transition fixtures call the method inside a held async transition, so the target route never became visible before the canonical router.push completed. The fix resolves the config flag into the same __NEXT_GESTURE_TRANSITION define used by Next.js, exposes the optional router method only under that flag, and lets gesture navigation use the existing RSC navigation pipeline with an immediate approved visible commit. The ported e2e fixture preserves the upstream held-transition flow and target/static/dynamic assertions. * test(router): update navigate mock assertions to expect default transition commit mode * fix(router): address ask-bonk review comments on gesture push - track scheduled app router navigation in experimental_gesturePush for parity with push/replace, so a refresh() issued in the same task skips its redundant re-fetch - document the synchronous visible-commit mode's null pendingRouterState invariant in the browser navigation controller - add a unit test asserting experimental_gesturePush is only attached when the __NEXT_GESTURE_TRANSITION define is set * style(tests): fix formatting in shims.test.ts * docs(router): clarify gesture push normalization and flushSync commit paths * test(router): fix unbound-method lint in shims test and document gesture spec coverage * docs(router): clarify gesture define boolean form and spec held-state rationale * docs(test): note gestureTransition is fixture-wide for the gesture spec only * fix(router): no-op experimental_gesturePush before the navigation runtime is ready Upstream gesturePush early-returns when getCurrentAppRouterState() is null, so a gesture dispatched before hydration is a no-op. Previously vinext fell through navigateClientSide's non-runtime branch and performed a real history push. Guard on the runtime's navigate function (the same readiness check navigateClientSide uses) and document that the un-awaited void navigateClientSide(...) deliberately matches push/replace's fire-and-forget rejection shape. * docs(router): note gesturePush guard/dispatch runtime reads have no TOCTOU window * docs(test): document gesture fixture's double-navigation divergence from upstream --------- Co-authored-by: James <james@eli.cx> |
||
|
|
6d9d16da1e | test(next-config): make CJS config fixture deterministic on Windows (#1922) | ||
|
|
89a12bb203 | fix(build): don't path-resolve bare specifier aliases for esmExternals (#1507) (#1843) | ||
|
|
de2ad5278f |
fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) (#1841)
* fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) * test(app-router): cover reactMaxHeadersLength config resolution * refactor(app-router): prioritize React preloads in Link header; cover buildAppPageLinkHeader Address review feedback on #1841: - Order React preload hints before font preloads so render-critical entries survive a tight reactMaxHeadersLength cap. - Rename the buildAppPageHtmlResponse option to linkHeader (it now carries the combined header, not just fonts). - Add unit coverage for buildAppPageLinkHeader (cap, 0-disables, ordering, whole-entry truncation). |
||
|
|
a843ea2cfa |
fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828) (#1834)
* fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828) Oversized fetch (client-invoked) server-action requests were rejected with a bare 413 Payload Too Large response, which bypasses the client error boundary and surfaces the wrong status/content-type. Next.js instead throws the body-exceeded error before the action runs and, for fetch actions, emits a 500 Flight response carrying the rejected action result so the nearest error boundary catches it. Mirror that: when an RSC fetch action body exceeds the configured limit (content-length pre-check or stream read), render a Flight stream with returnValue { ok: false } and no page root, returning status 500. The no-JS progressive form path keeps its 413 (no client boundary to catch it). Closes #1828 * fix(actions): use verbatim bodySizeLimit string in body-exceeded error (#1828) Address bonk review: the body-exceeded error message must contain the verbatim serverActions.bodySizeLimit config string (e.g. "2mb") to match Next.js byte-for-byte — the upstream suites assert the log message literally ('Body exceeded 2mb limit'). The previous reconstruction from the parsed byte count produced '2 MB', diverging on casing and spacing. Thread serverActionsBodySizeLimitLabel (the verbatim config value, defaulting to "1mb") through next-config -> index -> app-rsc-entry -> the RSC action handler options, and use it directly in createBodyExceededError. Removes the lossy formatBodySizeLimit reconstructor. Tests assert the verbatim string. * fix(actions): default bodySizeLimit label to Next.js' '1 MB' literal (#1828) Address bonk re-review nit: when serverActions.bodySizeLimit is unset, Next.js emits 'Body exceeded 1 MB limit.' (its defaultBodySizeLimit = '1 MB' literal). Default serverActionsBodySizeLimitLabel to '1 MB' (was '1mb') so the unconfigured-case message matches upstream byte-for-byte too. |
||
|
|
5a1ea0d967 |
fix(config): avoid duplicate CJS global shims (#1771)
* fix(config): avoid duplicate CJS global shims * fix(config): guard require injection too The createRequire ESM polyfill (const require = createRequire(import.meta.url)) commonly appears alongside the __dirname/__filename polyfills and hits the same duplicate-const Rolldown parse error. Skip the createRequire import and the const require line when the user already declares require. Add a regression test covering the scenario. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
49f3905b7b |
fix(config): keep packages externalized when loading TypeScript next.config (#1710)
PR #1701 added `resolve.noExternal: true` while loading a TypeScript next.config so a baseUrl-local file could shadow an installed package of the same name. That de-externalizes every installed package and forces it through Vite's module runner, which breaks CJS config plugins that call `require` / `require.resolve` at runtime — e.g. `@next/mdx`, which failed the app-router-playground production build with "require is not defined". This was not caught pre-merge because deploy-examples is skipped for fork PRs. Drop the blanket `noExternal: true`. Installed packages stay externalized (so CJS config plugins keep working), and the native Vite 8 `resolve.tsconfigPaths` resolver still resolves baseUrl-local bare imports that have no installed package of the same name (the real Next.js tsconfig-extends fixture). The only behavior we give up is shadowing an installed package with a baseUrl-local file of the same name, which no Next.js fixture exercises. - Update the shadowing test to assert the installed package wins. - Add a regression test for an @next/mdx-style CJS plugin that calls `require.resolve` at runtime (red before this change). - Document the package-precedence behavior in the README limitations entry. |
||
|
|
5991406324 |
fix(config): resolve next.config baseUrl imports from tsconfig (#1701)
* fix(config): resolve next.config baseUrl imports from tsconfig next.config.ts could resolve tsconfig paths aliases but not bare imports covered by compilerOptions.baseUrl. This broke configs that inherit baseUrl through tsconfig extends and import local modules such as "bar" from next.config.ts. Carry the effective baseUrl out of the tsconfig reader and add a narrow Vite runner resolver for bare specifiers so local files resolve before normal package fallback. Add a regression ported from the Next.js tsconfig-extends deploy fixture. * fix(config): preserve baseUrl priority over packages next.config.ts imports that matched both a local tsconfig baseUrl target and an installed package resolved to the package first. That violated the TypeScript and Next lookup order where baseUrl local resolution wins before package fallback. Vite's module runner externalized installed bare packages before resolveId hooks ran, so the baseUrl resolver only handled missing packages. Keep config imports inside the resolver pipeline, run the baseUrl resolver before package resolution, and scope it to project-owned importers so package dependencies are not rewritten through the app baseUrl. Adds focused regression coverage for local-over-package shadowing, package fallback, node_modules importer isolation, and existing CJS/ESM package imports from next.config.ts. * fix(config): keep baseUrl lookup scoped to TypeScript configs next.config.mjs could inherit TypeScript baseUrl package shadowing because the config runner noExternal override and baseUrl resolver were enabled for every config file type. That leaked Next's TypeScript config transpilation semantics into non-TypeScript config loading. Gate the baseUrl resolver and noExternal runner override behind TypeScript config extensions while keeping existing alias loading behavior unchanged. Add a regression for mjs package shadowing and update the tsconfig helper comment to name baseUrl explicitly. * refactor(config): resolve next.config baseUrl via native Vite 8 tsconfigPaths Replace the hand-rolled tsconfigBaseUrlResolverPlugin with Vite 8's native resolve.tsconfigPaths (oxc-resolver tsconfig: 'auto'), which already mirrors Next.js's SWC paths + baseUrl handling: follows extends, resolves baseUrl-local bare imports before package fallback, and scopes the app baseUrl to project-owned importers via per-importer tsconfig discovery. Gate it behind the existing Vite-major check (matching index.ts). Vite 7 has no native equivalent, so baseUrl bare imports in next.config.ts are a documented Vite 7/8 capability gap; paths aliases still work on both via resolve.alias. Extract getViteMajorVersion into utils/vite-version.ts so next-config.ts and index.ts share one detection path without a circular import. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
66519412c3 | fix(app-router): reuse pending prefetched RSC payloads (#1704) | ||
|
|
19dfd5e009 | feat(config): add experimental.appShells config plumbing and validation (#1692) | ||
|
|
00f585969d |
feat: recognize Sparkle experimental flags (varyParams, optimisticRouting, cachedNavigations) (#1655)
* feat(check): recognize Sparkle experimental flags in CONFIG_SUPPORT * test(check): add tests for Sparkle experimental flags * feat(config): warn when cachedNavigations is set without cacheComponents * test: address ask-bonk review on cachedNavigations tests - Remove redundant mockRestore() calls (afterEach handles it) - Add negative test cases for cachedNavigations not set / false - Match existing swcEnvOptions test style (warn.mock.calls.find()) |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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>
|
||
|
|
6a806ecee1 |
feat(instrumentation): support nextConfig.instrumentationClientInject (#1416)
* feat(instrumentation): support nextConfig.instrumentationClientInject (#1326) Add support for the Next.js instrumentationClientInject config option that lets users inject client bootstrap modules for side effects and an optional onRouterTransitionStart hook ahead of the user's instrumentation-client file. Changes: - New generateInstrumentationClientInjectModule() generates a virtual ESM module with side-effect imports in array order + composed hook fan-out. - Accept instrumentationClientInject in NextConfig / ResolvedNextConfig. - New vinext:instrumentation-client-inject Vite plugin with resolveId/load hooks; virtual module string is precomputed once in config() for efficiency. - Unit tests cover empty injects, single/multi specifiers, hook ordering, and empty-module fallback when no user file exists. * test(instrumentation): fix oxfmt formatting and assertion indentation The test expected 2-space indent for the generated code, but the implementation produces 4-space indent within if blocks. Also fixes oxfmt quote style and line wrapping. * fix(ci): lint warnings and missing field in test helper - Replace unnecessary escaped quotes inside template literals to fix eslint(no-useless-escape) warnings in instrumentation-client-inject.ts - Add missing instrumentationClientInject field to makeResolved() test helper in next-config.test.ts to fix TS2322 type errors * Address review feedback for instrumentationClientInject. Make clientInjectModule self-clearing on config re-eval, filter non-string inject specifiers, document dual resolution paths, and add escaping coverage. Signed-off-by: Divanshu Chauhan <divkix@divkix.me> * test(instrumentation): add plugin pipeline integration tests and fix inject resolution Add client-environment tests for instrumentationClientInject virtual module wiring, omit the alias when injects are active so resolveId/load run, reload next.config when root changes, and pre-resolve relative inject paths. Share empty-module path via INSTRUMENTATION_CLIENT_EMPTY_MODULE and simplify tests. * fix(instrumentation): resolve empty-module.js in production builds CI E2E builds failed because INSTRUMENTATION_CLIENT_EMPTY_MODULE pointed at empty-module.ts, which is not shipped in dist. Prefer .js when present. * test(instrumentation): address PR review comments for client inject - Revert nextConfigRoot guard changes in config loading to avoid buildId divergence risk - Add cache-busting to dynamic ESM module imports in tests and document the reason - Add integration test case for empty-inject and missing user-file fallback path --------- Signed-off-by: Divanshu Chauhan <divkix@divkix.me> |
||
|
|
4b6b438b94 |
fix(app-router): stream generated metadata for non-html bots (#1585)
* fix(app-router): stream generated metadata for non-html bots Dynamic App Router metadata was always folded into the route head. That diverged from Next.js for normal browser-like requests, where generated metadata is sent through a body outlet while html-limited bots still receive blocking head metadata. Track whether the matched route uses generateMetadata, thread next.config htmlLimitedBots into the generated RSC entry, and choose head versus body placement at page element construction. * test(app-router): cover streaming metadata bot edge cases Generated metadata must keep the default html-limited bot list when serialized config contains a falsy regex source. Empty config strings previously produced an empty regex, which treated every user agent as blocking and moved generated metadata back into the head. Add regression coverage for the default Twitterbot path and the streaming body serializer, then match Next.js by falling back to the default bot regex for any falsy htmlLimitedBots value. * fix(app-router): validate html-limited bot regex config Invalid serialized htmlLimitedBots values could reach request handling and throw while building the bot matcher. That puts a config error on the request path and recompiles the same matcher for repeated requests. Validate serialized regex sources while resolving next.config and reuse compiled bot matchers in the streaming metadata helper. The focused tests cover invalid config, falsy fallback, and matcher reuse. * refactor(config): narrow next config option reads Config resolution had several local assertions around experimental options, Turbopack aliases, output mode, and Sass options. Those assertions made the boundary wider than necessary in the code adjacent to htmlLimitedBots handling. Read optional records, strings, arrays, and body-size inputs through typed narrowing helpers so the resolved config path keeps the same semantics without spreading unchecked asserted values. * fix(config): keep bot regex helper out of server graph Config resolution imported the streaming metadata server module to validate htmlLimitedBots. In RSC-backed App Router integration tests, that cross-layer import can pull server runtime modules through config resolution and surface rsc reference-validation errors. Move the shared html-limited bot matcher cache to a neutral utils module and import it from config and the server streaming metadata helper. This keeps config validation independent from the server runtime graph while preserving the cached matcher behavior. |
||
|
|
c1c51fdbed |
fix(config): warn on deprecated experimental.rootParams (#1417)
has been stable since Next.js stabilized the feature upstream. - Emit a deprecation warning when users still set - Matches Next.js upstream wording for parity Closes #1325 |
||
|
|
9415a238f4 |
feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time Wire `assetPrefix` from next.config through to the Vite build so every emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors Next.js semantics: `assetPrefix` controls asset URLs only, independent of `basePath` (which still controls route URLs). * Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via a new `normalizeAssetPrefix` helper that trims trailing slashes, accepts absolute URLs (`https://cdn.example.com[/sub]`) and path prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable values. * New `packages/vinext/src/utils/asset-prefix.ts` module collects the shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix` (emitted URL prefix), `assetPrefixPathname` (path portion for runtime matching), and the canonical `_next/static` directory constant. * In the Vite plugin, set `build.assetsDir` so on-disk layout matches the URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's default `base + url` URL emission so `assetPrefix` and `basePath` stay independent — routes continue to anchor at `base = basePath + '/'`. * Persist `assetPrefix` in the embedded `vinextConfig` for the Pages Router server and in a new `vinext-runtime-config.json` sibling for the App Router prod server (the RSC entry only embeds `__basePath`). @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix * feat(server): serve assetPrefix-prefixed assets at runtime Update the production server to resolve incoming asset requests under `<assetPrefix>/_next/static/<filename>` (path-prefix form) and `/_next/static/<filename>` (absolute-URL form) in addition to the historical `/assets/` URLs. Both routers (Pages and App) share a new `resolveAppRouterAssetPath` helper that returns the lookup path on disk or `null` to fall through. * App Router prod server loads `assetPrefix` from the new `vinext-runtime-config.json` sibling written by the Vite plugin. * Pages Router prod server reads `assetPrefix` from the embedded `vinextConfig` and threads it through the existing `stripBasePath` lookup. * StaticFileCache marks any path under `_next/static/` as content-hashed (alongside the existing `assets/` rule) so emitted bundles get the long-lived `immutable` cache headers. Same change in the slow-path branch of `tryServeStatic`. Cloudflare Workers needs no code change: the ASSETS binding serves `<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...` because the on-disk layout already mirrors the URL. Absolute-URL prefixes are expected to be served by the CDN, so they don't reach the worker. * test(asset-prefix): add unit + e2e build tests for next.config assetPrefix Covers the surface area added in the previous two commits: * Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL, trailing-slash trimming, validation), `resolveAssetsDir` / `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout), `assetPrefixPathname` (runtime matching), and `resolveAppRouterAssetPath` (request → on-disk lookup translation). * End-to-end build tests that exercise `vinext build` against the `tests/fixtures/app-basic` app patched with each `assetPrefix` shape: - Path prefix (`/custom-asset-prefix`): SSR HTML references `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches, and fetching the bootstrap URL from the prod server returns 200. - Absolute URL (`https://cdn.example.com`): emitted asset URLs are fully qualified; no `/assets/` leaks. - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes stay under `/app/` while asset URLs live under `/cdn-prefix/` — proving the two configs stay independent. - Unset (default): historical `/assets/` layout is preserved. Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts * test(next-config): add assetPrefix field to makeResolved helper ResolvedNextConfig requires assetPrefix; the test helper was missing it which broke the type check. * fix(config): replace polynomial regex with loop (CodeQL) CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "") as polynomial-time backtracking on uncontrolled input. Swap both for explicit while loops with linear-time semantics. Same fix applied to the URL pathname trim in assetPrefixPathname(). * refactor(prod-server): drop dead null fallback; tighten hashed-asset detection - Remove the `?? null` after resolveAppRouterAssetPath; the helper already returns null on miss, so the fallback (and its obsolete comment) were no-ops. - Add explicit startsWith("_next/static/") / startsWith("/_next/static/") prefix checks before the substring fallback in both static-file-cache and prod-server. The substring check stays so assetPrefix-prefixed layouts (e.g. "cdn/_next/static/") are still recognised. * fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix` is configured the build writes hashed assets to a different subdirectory (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs) and the precompression step walked an empty directory, silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds with assetPrefix would then serve uncompressed assets. Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix) at the plugin call site, default to `"assets"` for backward compat, and add focused tests for both assetPrefix layouts. * refactor: drop redundant isHashed startsWith; harden test cleanup against build failures - prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")` check; `pathname` always has a leading slash so the existing `includes("/_next/static/")` already covers it. - static-file-cache.ts: keep both `startsWith("_next/static/")` and `includes("/_next/static/")` because `relativePath` has no leading slash — the prefix and substring checks cover different layouts (absolute-URL prefix vs path-prefix). Clarify the comment. - tests/asset-prefix.test.ts: register the tmp-dir cleanup with the shared `cleanups` array synchronously inside `buildFixtureWithConfig`, right after `mkdtempSync`. Previously the cleanup was only pushed after the build returned, so a thrown `createBuilder`/`buildApp` would leak the tmp dir. * refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts) and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry. The runtime-config.json sidecar was unnecessary — the App Router entry template already inlines config values as compile-time constants, so assetPrefix should be inlined the same way. Changes: - Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts. - Emit `export const __assetPrefix = <json>` from the generated RSC entry alongside the existing `__basePath` / `__configHeaders` / `__expireTime` constants. Single-line declaration to stay within the "thin generated entry" guideline. - Thread `nextConfig.assetPrefix` through the generateRscEntry call site in index.ts. - App Router prod-server reads `rscModule.__assetPrefix` (with a string fallback for older builds) instead of `vinext-runtime-config.json`. - Delete the vinext-runtime-config.json write in index.ts. Nothing else in the repo reads this file (verified via rg on packages/ tests/ examples/). Pages Router behaviour is unchanged: it already gets assetPrefix from the embedded `vinextConfig` exported by pages-server-entry.ts. Addresses james-elicx review feedback on #1311. * fix(ssr): emit bootstrap as <script src> via bootstrapModules The App Router SSR pipeline previously passed only `bootstrapScriptContent` to React's renderToReadableStream, producing inline `<script>import("/path/to/entry.js")</script>` tags. The URL was therefore inaccessible to anything that reads `script.attribs.src` — including the Next.js asset-prefix fixture test "bundles should return 200 on served assetPrefix" which iterates script tags looking for a `src` attribute prefixed by the configured assetPrefix. Extract the bootstrap URL from the plugin-rsc-generated content and pass it as `bootstrapModules: [url]` so React emits a real `<script type="module" src="url">` tag. The existing `extractModulePreloadHtml` helper already did the same extraction for the modulepreload <link>, so the URL was already exposed (just renamed `extractBootstrapModuleUrl` and lifted out of the head-injection helper to share between the bootstrap and the preload tags). The inline `bootstrapScriptContent` is no longer needed — it was only carrying the dynamic-import call that bootstrapModules now performs natively. Passing both would load the bootstrap module twice. `bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`) that the browser fetches as a module from the dev server; in prod it's the hashed bundle URL, optionally prefixed by `assetPrefix`. Mirrors Next.js's app-render path which combines both `bootstrapScriptContent` (for session state) and `bootstrapScripts` (for the actual chunk URL). See .nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx. We use `bootstrapModules` instead of `bootstrapScripts` because vinext's chunks are native ES modules (Vite output) — a `type="module"` tag is the correct loader. Tests updated where they asserted on the old `import("URL")` shape: tests/asset-prefix.test.ts, tests/app-router.test.ts, tests/e2e/cloudflare-workers/navigation.spec.ts. Verified all related suites pass locally (asset-prefix, app-router, script-nonce, script-preload, use-client-page-pathname, document, prerender, ISR, fetch-cache, entry-templates, precompress). * fix(build): use resolveAssetsDir for Pages Router client entry fallback scan The Pages Router Cloudflare Workers post-build step scans the on-disk assets directory for the client entry chunk when the SSR manifest lookup doesn't surface one. The directory path was hardcoded to `<clientDir>/assets/`, which is the Vite default — but when `assetPrefix` is configured, the chunk lives under `<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL prefix). The fallback would silently miss the entry and the worker would ship without its client bootstrap reference. Use the shared `resolveAssetsDir(assetPrefix)` helper that already drives `build.assetsDir` and the prod-server lookup, so this scan works for every supported asset layout. Addresses round-4 reviewer feedback on #1311 (low-priority follow-up item — only fires when the SSR manifest lookup fails). * feat(config): mirror Next.js basePath → assetPrefix fallback Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531). Without this, an app deployed under a basePath serves its routes correctly but emits its assets from `<basePath>/assets/...` (Vite's default `base + assetsDir` composition) rather than from the Next.js- canonical `<basePath>/_next/static/...`. Add the same fallback in resolveNextConfig, gated by Next.js's own conditions: - basePath !== "" (skip when basePath is unset) - basePath !== "/" (Next.js rejects this earlier; we mirror the guard so we never produce assetPrefix === "/") - assetPrefix === "" (user did not opt out) The fallback feeds the rest of the asset-prefix pipeline (Vite's `build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server URL→file resolver) so no other code paths need to change. Tests: - tests/next-config.test.ts: 5 unit tests for the four edge cases of the fallback condition. - tests/asset-prefix.test.ts: new end-to-end test that builds with `basePath: "/app"` (no assetPrefix), starts the prod-server, and asserts the bootstrap <script src> lands at /app/_next/static/... (and that the legacy /app/assets/... layout is gone). - tests/pages-router.test.ts: updated the "preserves basePath on backfilled SSR manifest entries" test to expect the new /docs/_next/static/ layout (was /docs/assets/). The manifest assertion is relaxed to startsWith("docs/") because Vite's raw SSR manifest now emits some duplicate-prefixed entries ("docs/docs/_next/static/...") alongside the backfilled ones; both forms anchor under basePath so the prod-server's URL→file lookup remains correct — and the user-visible HTML assertions in the same test are the source of truth. Addresses round-4 reviewer feedback on #1311. * fix(server): Pages Router must not strip basePath before asset lookup The Pages Router prod-server stripped `basePath` from the request pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke the asset lookup whenever the assetPrefix path-prefix branch was active with a path that happened to equal basePath. Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's `basePath → assetPrefix` fallback, an app configured with `basePath: "/docs"` (and no explicit `assetPrefix`) now resolves `assetPrefix = "/docs"`. Emitted asset URLs are `/docs/_next/static/…` (correct). But the Pages Router branch did: const staticLookupPath = stripBasePath(pathname, basePath); // "/docs/_next/static/x.js" → "/_next/static/x.js" resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs"); // path-prefix branch needs pathname to start with "/docs/" → null → 404 The App Router branch already passes the un-stripped `pathname` to the same helper (see line ~1140). Match that behaviour. `staticLookupPath` is still computed and used downstream by non-asset paths (image-optimization endpoint, page routing), where the basePath-stripped form is correct. Test coverage: - tests/pages-router.test.ts: extended the existing "preserves basePath on backfilled SSR manifest entries" test to FETCH every emitted asset URL and assert HTTP 200 (previously it only inspected HTML for the URL string). This is the missing assertion that would have caught the bug the first time. - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test that builds an inline pages/ fixture with `basePath: "/docs"`, starts the prod-server, collects every script/link URL from the SSR HTML, and asserts every URL serves a 200 + correct content-type. Confirmed to fail on `bac8425` before this fix and pass after. Addresses round-5 reviewer feedback on #1311. * refactor(ssr): accept either quote style in bootstrap URL regex plugin-rsc currently emits the bootstrap as `import("<url>")` with double quotes, but the contract isn't documented and a future version could switch to single quotes. Tighten the URL extraction regex to `import(["'])([^"']+)\1` so either form works. Addresses non-blocking round-5 review feedback on #1311. * docs(ssr): note CSP implications of bootstrapModules switch The round-3 commit `3a2c886` switched the App Router SSR bootstrap from inline `bootstrapScriptContent` (`<script>import(…)</script>`) to React's `bootstrapModules` option (`<script type="module" src=…>`). That has user-visible CSP consequences worth documenting at the call site so app authors hit them in code search rather than at runtime: - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap (improvement — inline imports previously required it). - Apps that restrict script sources need `'self'` for same-origin, or the CDN origin when assetPrefix is an absolute URL. - React still threads nonce through to the emitted <script> tag, so nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps working unchanged. Addresses non-blocking round-5 review feedback on #1311. |
||
|
|
e69721f392 |
fix(config): provide CJS globals when loading next.config.ts (#1278)
* fix(config): provide CJS globals when loading next.config.ts
Next.js's `next.config.ts` loader transpiles the file to CommonJS via SWC
and evaluates it through Node's `Module._compile`, which exposes the CJS
globals (`__filename`, `__dirname`, `module`, `exports`, `require`) even
when the source uses ESM syntax. vinext previously loaded the config
through Vite's ESM-only module runner, so configs that read `__dirname`,
called `require()`, or used `module.exports` threw `ReferenceError`s
during build.
Inject the same globals via a small Vite `transform` plugin that runs
only on the `.ts`/`.mts`/`.cts` config entry point. Reassigning
`module.exports` is preserved by exposing the wrapper object as a named
export and reading it back in `unwrapConfig`.
Reference: packages/next/src/build/next-config-ts/{transpile-config,require-hook}.ts
* fix(config): guard CJS-globals injection on static reference check
Skip cjsGlobalsInjectorPlugin's transform entirely when the next.config
source contains no bareword reference to __filename, __dirname, require,
module, or exports. The vast majority of next.config.ts files are pure
ESM (every upstream `test/e2e/app-dir/next-config-ts` fixture except
node-api-cjs is) and pay no cost from the transform.
Substring matching is intentional: a false positive (e.g. the word
`module` inside a comment or `node:module` specifier) costs at most one
wasted, idempotent transform, while a false negative would be a
correctness regression. Bareword `\b` boundaries already exclude
`requireSomething` and `myModule` style identifiers, and `\bexports\b`
does not match `export default`.
* fix(config): skip module.exports wrapper when source uses ESM-only
Statically detect whether a `next.config.ts` source assigns to
`module.exports` (via `module.exports =`, `module.exports.foo =`, or
`module.exports[...] =`) with a new `reassignsModuleExports` helper.
Only emit the `__vinext_cjs_exports` named export and its companion
initial-reference export when that check matches. Pure-ESM configs —
the overwhelming majority — get a slightly smaller injected preamble
and a simpler unwrap path that just reads the ESM `default` export.
The previous sentinel-on-module.exports approach tagged the initial
exports object with a `Symbol.for("vinext:cjs-initial")` property on
every config load and checked for it during unwrap. That worked but
ran unconditionally and left a stray symbol-keyed property on the
final exports object whenever the user reassigned. The new shape only
pays for the wrapper when the source plausibly needs it, and keeps a
referential-identity check (`module.exports !== __vinext_cjs_initial_exports`)
as belt-and-braces so that a `reassignsModuleExports` false positive
inside a string or comment still falls back to the ESM default
instead of returning an empty `{}`.
|
||
|
|
9252ca2966 |
fix(css): pass sassOptions from next.config to Vite preprocessor (#1277)
* fix(css): pass sassOptions from next.config to Vite preprocessor
Next.js threads `sassOptions` from `next.config.js` into sass-loader,
destructuring `prependData` / `additionalData` / `implementation` and
forwarding the rest (e.g. `includePaths`, `loadPaths`) as Sass options.
vinext was dropping this entirely, so SCSS fixtures that rely on
`additionalData` to define variables, `includePaths`/`loadPaths` to
resolve partials, or a custom `implementation` all failed to build
with `Undefined variable` / `Can't find stylesheet to import`.
Resolve `sassOptions` from next.config into `ResolvedNextConfig`, then
translate it onto Vite's `css.preprocessorOptions.scss` (and `.sass`)
shape:
- prependData precedence matches Next.js (prependData || additionalData)
- legacy includePaths is merged into modern loadPaths (Vite uses
modern Sass, which dropped includePaths)
- implementation and any remaining Sass options are passed through
Affected upstream fixtures:
- test/e2e/app-dir/scss/basic-module-include-paths
- test/e2e/app-dir/scss/basic-module-additional-data
- test/e2e/app-dir/scss/basic-module-prepend-data
Refs: cloudflare/vinext CI run 25951111875
Source: packages/next/src/build/webpack/config/blocks/css/index.ts
* fix(css): address sass passthrough review feedback
- Drop `export` on `VitePreprocessorOptions` so Knip stops flagging it
as unused (CI Check blocker).
- Use truthy-OR (`prependData || additionalData`) to match Next.js's
`sassPrependData || sassAdditionalData` exactly — empty-string
`prependData` now falls through to `additionalData` instead of
overriding it.
- Widen `additionalData` type to accept Vite's function form
(`(source, filename) => string | Promise<string>`) and drop the
inaccurate `as string` cast.
- Add tests covering the empty-`prependData` fallthrough and the
function-form `additionalData` passthrough.
|
||
|
|
1f2030b8d2 |
fix(config): load CJS next.config.js under "type": "module" (#1275)
`createRequire` of a `.js` file in a `"type": "module"` package still gets
resolved as ESM by Node (extension + parent type wins over the require()
caller), so the existing CJS fallback in loadNextConfig threw `require is
not defined` for every plain `module.exports = {}` config after `vinext
init`. Worked around in the Nextjs deploy suite by rewriting every
fixture's config from CJS to ESM in-place via scripts/cjs-to-esm-config.mjs.
Copy the source to a sibling temp `.cjs` (extension overrides the parent
type field) and require that instead. Relative imports inside the config
still resolve against the original directory. Drop the deploy-suite
rewrite step and its 145-line shim.
|
||
|
|
eccee1843e |
fix(deploy-suite): apply tsconfig compilerOptions.paths when loading next.config.ts (#1215)
Next.js's own loader (packages/next/src/build/next-config-ts/transpile-config.ts)
reads compilerOptions.paths + baseUrl from tsconfig.json and passes them to SWC
so next.config.ts can import via tsconfig aliases like:
import { foo } from '@/foo'
vinext's loadNextConfig() calls Vite's runnerImport(), but did not configure any
resolve.alias entries, so the same imports fail with:
[vinext] Failed to load next.config.ts:
Cannot find module '@/foo' imported from '<dir>/next.config.ts'
This is a parity gap surfaced by the Next.js deploy test suite.
Fix: extract a small tsconfig-paths loader (config/tsconfig-paths.ts) that
reads compilerOptions.paths (following extends, honoring baseUrl) and returns
absolute Vite resolve.alias entries. loadNextConfig now passes those aliases
into runnerImport's inlineConfig.
Affected suites (from run 25870737355):
- e2e/app-dir/next-config-ts/import-alias-paths-only/
- e2e/app-dir/next-config-ts/import-alias-paths-with-baseurl/
- e2e/app-dir/next-config-ts/import-js-extensions-cjs/
- e2e/app-dir/next-config-ts/import-js-extensions-esm/
- e2e/app-dir/next-config-ts-native-ts/dynamic-import-esm/
- e2e/app-dir/next-config-ts-native-mts/import-from-node-modules/
Verification:
- 9 new unit tests for loadTsconfigPathAliasesForRoot
- 4 new unit tests covering loadNextConfig + tsconfig paths (ported from
Next.js's import-alias-paths-only / import-alias-paths-with-baseurl
fixtures), including extends + jsconfig fallback
- Reproduced against .nextjs-ref/test/e2e/app-dir/next-config-ts/
import-alias-paths-only/ — config now loads with `foo === "foo"`
- vp check (fmt + lint + types) clean
- bash -n scripts/*.sh clean
Refs: cloudflare/vinext#actions/runs/25870737355
|
||
|
|
671d782062 |
fix(cache): scope use cache keys by deployment id (#1092)
* fix(cache): scope use cache keys by deployment id Shared "use cache" entries only used the vinext build ID as their cache-key seed. That diverges from the upstream Next.js change for cacheHandlers and can let a Cloudflare Worker deployment reuse entries from another deployment when the deployment identity changes independently of the build fallback. The cache runtime now resolves a deployment-aware key seed at execution time, with request-scoped Worker version metadata or NEXT_DEPLOYMENT_ID taking precedence over the build ID. Generated Wrangler config includes the CF_VERSION_METADATA binding, and the App Router Worker entry threads it into the runtime without process-global request races. Tests cover deployment ID precedence, concurrent deployment scope isolation, build ID fallback, and generated Worker config wiring. * fix(config): resolve deployment id for cache seeds Deployment-aware "use cache" keys had runtime Worker and NEXT_DEPLOYMENT_ID support, but vinext did not resolve the Next.js deploymentId config field. That left config-defined deployment identity out of the build-time cache seed. Resolve deploymentId from next.config.js before NEXT_DEPLOYMENT_ID, validate the same string shape Next.js documents, and feed the resolved value into the __VINEXT_DEPLOYMENT_ID define. The process-wide cache-runtime setter is also documented as a fallback rather than the per-request API. Tests cover config precedence, env fallback, empty config values, and invalid deploymentId shapes. * refactor(cache): carry deployment id through request context The Worker version metadata path still needs request-scoped state, but cache-runtime should not own a cache-specific deployment ALS or public-looking setter. Move runtime deployment ID storage into the unified request-context layer. The Worker boundary now writes deploymentId into request runtime context, cache-runtime reads that generic request metadata before falling back to inlined deployment/build IDs, and the tests cover direct runtime scopes plus inheritance into unified request contexts. * refactor(cache): reuse request context for deployment id * test(app-elements): expect artifact compatibility metadata * refactor(cache): inline deployment id seed * chore: rerun ci |
||
|
|
f45fce00d5 |
fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses. Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules. Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known. * fix(isr): address cache life review follow-ups * chore: address latest review follow-ups * ci: pin vite-plus setup version * Revert "ci: pin vite-plus setup version" This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1. * fix: avoid blocking ISR page streams on cache metadata * fix: preserve headers for speculative cacheLife probes * fix: preserve prerender cacheLife metadata * fix: preserve legacy ISR cache metadata behavior * fix: preserve prerender seed revalidate context * fix: harden app page cache policy metadata * fix: resolve app router prerender conflict |
||
|
|
ae0440b4f7 |
fix: support enablePrerenderSourceMaps config with default true (#973)
* fix: support enablePrerenderSourceMaps config with default true (#960) Adds the `enablePrerenderSourceMaps` config option matching Next.js behavior. Defaults to `true` so prerender stack traces resolve through sourcemaps to original source files. Error results in prerender output now include full stack traces when enabled. Node.js sourcemap support is enabled before the prerender phase in both `cli.ts` (build) and `deploy.ts` (deploy). Config parity: silently accepts the option; recognized as "supported" in `vinext check` output. * refactor: address review comments for enablePrerenderSourceMaps (#960) - prerender.ts: simplify getErrorMessageWithStack and fix comment (V8's Error.stack always prepends message, remove startsWith guard) - cli.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled - deploy.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled - next-config.test.ts: move enablePrerenderSourceMaps tests to own describe block and remove redundant explicit-true test * fix(prerender): simplify getErrorMessageWithStack and add test coverage Simplify the error stack handling in getErrorMessageWithStack by removing the redundant nullish coalescing with empty string. The expression `err.stack || err.message` handles all cases correctly. Add test coverage proving that enablePrerenderSourceMaps: true (default) includes stack traces in prerender errors. New test fixture page `error-throw.tsx` throws during render to exercise the error path. * fix(prerender): address review feedback - update snapshot, add config comment - Regenerate entry-templates.test.ts.snap after adding error-throw fixture page - Add comment clarifying enablePrerenderSourceMaps default in prerender test * feat(config): add enablePrerenderSourceMaps config option Adds support for the experimental.enablePrerenderSourceMaps config option that Next.js 15+ introduced for debugging prerender issues. Also includes merged changes from main: - hashSalt: support experimental.outputHashSalt + NEXT_HASH_SALT env - swcEnvOptions warning: warn about unsupported SWC polyfill option All 75 next-config tests passing. Closes #960 |
||
|
|
721be81709 |
fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#978)
* fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#754) Add support for Next.js experimental.outputHashSalt config option and NEXT_HASH_SALT environment variable. When set, the salt is mixed into content-addressed build output filenames via Rollup/Rolldown's output.hashSalt option, producing different hashes without modifying source code — useful for cache-busting after CDN poisoning. Config resolution: concatenates config salt + env var salt (matching Next.js behavior: config first, then env). When neither is set, no salt is applied and behavior is unchanged. * ci: re-trigger CI (Windows create-next-app flake, STATUS_STACK_BUFFER_OVERRUN) * fix: use augmentChunkHash for outputHashSalt instead of output.hashSalt The output.hashSalt option is webpack-specific and is silently ignored by Rollup/Rolldown. Use the augmentChunkHash plugin hook instead, which is supported by both bundlers and actually affects chunk content hashes. Changes: - Remove hashSalt parameter from getClientOutputConfigForVite() - Add vinext:hash-salt plugin with augmentChunkHash hook - Add experimental.outputHashSalt to CONFIG_SUPPORT map * refactor: simplify hash-salt plugin comments - Simplify comment about augmentChunkHash (just Rolldown, not Rollup) - Add scope-gap note about CSS/static asset hashing not being salted |
||
|
|
155be501e1 |
fix: accept experimental.swcEnvOptions config without erroring (#977)
* fix: accept experimental.swcEnvOptions config without erroring (#774) Next.js added experimental.swcEnvOptions for SWC preset-env polyfill injection. vinext uses Vite for transforms, so polyfill injection is not applicable, but the config key should be accepted without errors. Add: - CONFIG_SUPPORT entry in check.ts (unsupported with detail) - Warning during config resolution in next-config.ts - Tests for analyzeConfig detection and dot-notation matching * refactor: align swcEnvOptions messaging and add test coverage - Change runtime warning from "not yet supported" to "not applicable" - Update CONFIG_SUPPORT detail for consistent messaging - Add test coverage for swcEnvOptions warning in next-config.test.ts * fix(check): update test assertion for swcEnvOptions message The CONFIG_SUPPORT message for experimental.swcEnvOptions was updated to indicate it's not applicable since vinext uses Vite instead of SWC. This updates the corresponding test assertion to match the new message. Fixes CI failure in tests/check.test.ts |
||
|
|
e0854972eb |
fix: support file:// URLs in cacheHandler config path (#944)
* fix: support file:// URLs in cacheHandler config path (#728) - Add cacheHandler and cacheMaxMemorySize to NextConfig type - Add cacheHandler to ResolvedNextConfig with file:// URL handling - Create resolveCacheHandlerPathToFilesystem() helper using fileURLToPath() - Convert file:// URLs from import.meta.resolve() to filesystem paths during config resolution to prevent broken path concatenation Matches Next.js commit 9a0214a1 which added the same file:// URL handling for ESM projects using import.meta.resolve() for the cache handler path. * fix: drop unused export on resolveCacheHandlerPathToFilesystem * fix: resolve cacheMaxMemorySize and add cacheHandler tests - Add cacheMaxMemorySize to ResolvedNextConfig and resolve it from input config, preventing silent config swallowing - Add 7 tests for resolveNextConfig cacheHandler: file:// URL resolution, absolute/relative path passthrough, null/undefined defaults - Also add cacheMaxMemorySize resolution and default tests |
||
|
|
fa03a06bea |
feat: optimize barrel imports for RSC-incompatible packages (#138)
* feat: add vinext:optimize-imports plugin for barrel import rewriting
Barrel imports like `import { Slot } from "radix-ui"` cause RSC crashes
because Vite eagerly evaluates all re-exported sub-packages, some of which
call React.createContext() — unavailable in the react-server condition.
This plugin rewrites barrel imports into direct sub-module imports on the
server, matching Next.js's optimizePackageImports behavior:
- Parses barrel entry files to build export maps (namespace, named,
default re-exports, and import-then-export patterns)
- resolveId hook handles pnpm strict hoisting via Vite's own resolver
- Only runs on server environments; client uses Vite's dep optimizer
- Lazy-initialized Set<string> for O(1) package lookups
- Respects experimental.optimizePackageImports from next.config
- Includes Next.js default package list plus radix-ui
Closes cloudflare/vinext#100
Closes cloudflare/vinext#137
* fix: add missing packages to DEFAULT_OPTIMIZE_PACKAGES
Add the full effect ecosystem (17 packages) and all react-icons/*
sub-packages (32 packages) to match Next.js's built-in defaults.
* fix: address bonk review — entryPathCache, buildStart init, Windows path fix, trailing semicolons, fixture-based transform tests
* fix: correct bySource grouping for mixed namespace+named imports, clear barrelCaches on rebuild
- Key bySource map on `${source}::${isNamespace}` instead of just source
so that a namespace re-export and a named re-export from the same sub-module
produce two separate import statements rather than one corrupted one.
Caught by the new 'produces separate statements for namespace and named
imports from the same source' test.
- Clear barrelCaches.exportMapCache and barrelCaches.subpkgOrigin in buildStart
alongside entryPathCache so stale barrel analysis doesn't survive rebuilds.
- Add regression test for the mixed namespace+named grouping bug.
* fix: resolve relative barrel re-export paths to absolute, emit default imports for default re-exports
Barrels like lodash-es and lucide-react use relative re-export paths
(e.g. `export { chunk } from './chunk.js'`). Writing those verbatim caused
Vite to resolve './chunk.js' against the importing user file rather than
the barrel package directory.
Fix: resolve any relative source path against the barrel entry's directory
before emitting the replacement import statement, producing an absolute
filesystem path that Vite resolves correctly regardless of where the
importing file lives.
Also fix: `export { default as X }` re-exports now produce `import X from`
(a default import) instead of `import { default as X }` (invalid syntax
that only works via ASI / bundler leniency).
Tests updated to assert absolute paths and added a dedicated test for
default re-export handling.
* refactor: extract barrel optimization to plugins/optimize-imports.ts, add Windows path fix and react-server TODO
- Move all barrel types, helpers, DEFAULT_OPTIMIZE_PACKAGES, and the
vinext:optimize-imports IIFE from index.ts into a new
packages/vinext/src/plugins/optimize-imports.ts file with a factory
function createOptimizeImportsPlugin(getNextConfig, getRoot)
- Forward-slash normalize resolved absolute paths so emitted import
statements use '/' separators on Windows (path.sep join)
- Add TODO comment in resolveExportsValue noting that the 'react-server'
condition should be added in a future pass
- Add test for ImportDefaultSpecifier: import MyFoo from 'pkg' rewrites
to import MyFoo from '<abs>/sub' (22 tests total, all passing)
* fix(optimize-imports): address sixth-pass review comments
- Fix astName() to throw on unexpected nodes instead of String(value)
- Normalize all resolvePackageEntry return values to forward slashes (Windows fix)
- Fix originalName falsy guard: && → !== undefined
- Pre-build quotedPackages in buildStart to avoid per-file template literal allocations
- Add first-wins comment on subpkgOrigin registration
- Add clarifying comment to lucide-react test fixture
* fix(optimize-imports): remove EnvironmentPluginContext cast, expand subpkgOrigin comment
Vite augments rolldown's MinimalPluginContext with `environment: Environment`
via declaration merging, so both resolveId and transform have `this.environment`
typed without any cast. Drop the EnvironmentPluginContext interface and the
`as unknown as EnvironmentPluginContext` cast in the transform handler, using
direct `this.environment` access consistently with resolveId.
Also expand the subpkgOrigin first-wins comment to make explicit why first-wins
is safe: the sub-package specifier resolves to the same path regardless of which
barrel's importer context is used.
* fix(optimize-imports): address seventh-pass review nits
- Fix test imports: vitest → vite-plus/test, Plugin from vite → vite-plus,
following the convention used by 82 of 85 test files in this repo
- Add TODO comment for extensionless absolute paths produced when barrel
sources lack file extensions (Vite resolves these correctly in practice)
- Expand subpkgOrigin first-wins comment to explicitly note the nested
node_modules edge case and why it is out of scope for the default list
* fix(optimize-imports): address james-elicx review comments
- Support wildcard re-exports (export * from './sub') by recursively
parsing sub-modules and merging their exports into the barrel map
- Add circular-reference guard to prevent infinite loops in recursive
export map building
- Refactor buildBarrelExportMap into buildExportMapFromFile helper
with shared recursive logic
- Use switch statements for spec.type handling instead of if-else chains
- Remove non-null assertions and type assertions; use optional chaining
and typeof guards throughout
- Extract resolvePackageInfo helper to separate concerns in
resolvePackageEntry
- Tests now import directly from optimize-imports.ts instead of via
the full vinext() plugin array
- Remove _buildBarrelExportMap re-export from index.ts (no longer needed
since the plugin has its own file)
- Fix type assertion for optimizePackageImports in next-config.ts using
a proper type-guard filter
- Apply README nit: 'is always optimized' → 'are always optimized'
- Add tests for wildcard export resolution, no-overwrite semantics,
and circular re-export safety
* fix(optimize-imports): address ask-bonk review comments
- Resolve relative sources to absolute paths inside buildExportMapFromFile
so nested wildcard re-exports (e.g. antd-style components/) resolve against
the correct sub-module directory instead of the barrel root
- Add react-server condition to resolveExportsValue (before node/import) for
RSC-compatible entry points in packages like react and react-dom
- Add NOTE comment in resolvePackageEntry documenting that only exports["."]
is checked, not subpath exports
- Eliminate double readFile call in buildBarrelExportMap by passing the
already-read content to buildExportMapFromFile via new initialContent param
- Update subpkgOrigin guard to check for absolute paths (not startsWith ".")
since relative sources are now stored as absolute paths in the export map
- Add test for nested subdirectory wildcard path resolution correctness
- Update existing unit tests to expect absolute paths in export map entries
* fix(optimize-imports): only prefer react-server condition in RSC environment
SSR renders with the full React runtime and must not resolve react-server
export condition entries. Thread preferReactServer (env.name === 'rsc')
through resolveExportsValue and resolvePackageEntry, and key entryPathCache
by environment prefix to keep RSC and SSR barrel entries separate.
* fix(optimize-imports): address review nits — TODO comment, type annotation, test names, wildcard dir test
- Add TODO comment on allResolved bail-out suggesting debug logging
- Remove redundant ': string[]' annotation on optimizePackageImports (inferred)
- Rename namespace re-export test to use generic X/barrel names instead of Slot/lucide-react
- Update syntax-error test to match new empty-map-on-parse-error behavior (not null)
- Add test for directory-style 'export * from "./components"' where components/index.js exists
* fix(optimize-imports): address review nits — wildcard extensions, double-read, readFile hoisting, subpkg registration guard
- Add /index.mjs, /index.tsx, and .tsx to wildcard candidate list for ESM-first and TypeScript-first packages
- Pass candidateContent to buildExportMapFromFile in wildcard loop to avoid reading each sub-module file twice
- Add comment clarifying the no-op resolveEntry callback pattern
- Hoist readFileSafe closure outside the import-declaration for-loop (one allocation per transform, not per barrel import)
- Gate subpkgOrigin registration loop with registeredBarrels Set so 50 files importing the same barrel don't each iterate the full export map
* fix(optimize-imports): remove redundant double-cache, extract PluginCtx type alias
- Remove the redundant exportMapCache.set() in buildBarrelExportMap — buildExportMapFromFile
already stores the result under the same key; the extra set was a no-op but misleading
- Extract PluginCtx type alias for the environment cast so all three hook sites share one
definition and the verbose inline cast doesn't repeat across resolveId and transform
* fix(optimize-imports): hoist readFileSafe to module level, add namespace import test
- Move readFileSafe from handler scope to module level since it captures
nothing from the per-file closure; avoids a per-call function allocation
- Add test verifying import * as Pkg from 'barrel' is left unchanged
(ImportNamespaceSpecifier sets allResolved = false — can't optimize a
full namespace import to a sub-module)
* fix(optimize-imports): make barrel resolution async, add debug logging, update tests
* fix(optimize-imports): address review feedback — extensions, env-keyed subpkgOrigin, debug gate, graceful astName
* fix: scope registeredBarrels key to env to prevent RSC registration blocking SSR
When RSC and SSR resolve the same barrel entry path (common — most packages
have no react-server export condition), the bare barrelEntry key in
registeredBarrels caused RSC's registration to suppress SSR's inner loop
entirely. SSR's subpkgOrigin map was left empty, making resolveId depend
solely on the cross-env fallback — fragile coupling that could silently break.
Change the registeredBarrels key from barrelEntry to `${envKey}:barrelEntry`
so each environment runs the inner loop and populates its own subpkgOrigin
map independently. Matches the existing entryPathCache key pattern.
Add a regression test covering the shared-barrel-entry scenario.
* fix: handle inline export declarations in barrel sub-modules (date-fns)
date-fns barrels use `export * from "./formatDistanceToNow.js"` where the
sub-module directly declares `export function formatDistanceToNow(...) {}`.
buildExportMapFromFile only handled ExportNamedDeclaration with a source
(re-exports) or specifiers (local re-bindings) — it silently skipped inline
declarations (export function/class/const/let/var), leaving the name absent
from the export map and causing the plugin to log "skipping: could not
resolve specifier" and leave the barrel import unchanged.
Handle the declaration branch: FunctionDeclaration and ClassDeclaration
contribute their id.name; VariableDeclaration iterates all declarators.
The source for inline declaration entries is the sub-module file itself.
Also extend AstBodyNode.declaration typing from `unknown` to the minimal
shape covering all three declaration kinds.
Add three unit tests covering export function, export const (multiple
declarators), and export class; all 38 tests pass.
* refactor: remove DEBUG-gated logging from optimize-imports plugin
* refactor: always emit skip log without DEBUG env gating
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
|
||
|
|
70e7553ae0 |
fix: throw errors when next config file loading fails (#593)
* fix: throw errors when next config parse fails * add unit test |
||
|
|
c17d6941be |
chore: migrate to vite plus (#535)
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit
|
||
|
|
23f9e1e13d |
feat: propagate next config serverExternalPackages (#526)
* feat: propagate next config serverExternalPackages * . |
||
|
|
735a443169 |
chore: upgrade vitest to 4.1 (#512)
* chore: upgrade vitest to 4.1 * fix: clear console.warn spy state before asserting call counts Vitest 4 changed vi.spyOn to return the same mock when called on an already-intercepted function. This means console.warn call records from earlier tests accumulate in the spy, causing toHaveBeenCalledTimes(2) to see 4 calls instead of 2. Adding mockClear() after each spy creation resets the call count so the assertions only reflect calls made within the test itself. |
||
|
|
ddf4d337d5 |
docs/runtime: warn on external rewrites, sanitize Content-Disposition (#418)
* fix: warn on external rewrites and sanitize Content-Disposition
Add a startup warning when next.config contains rewrites targeting
external origins (e.g. https://...), alerting that credential headers
(cookie, authorization, x-api-key, proxy-authorization) are forwarded.
Sanitize contentDispositionType in image optimization and prod server
to only allow "attachment" — any other value defaults to "inline",
preventing header injection via arbitrary config values.
Closes #414
* fix: improve external rewrite warning and add Content-Disposition sanitization test
Use proper singular/plural in the external rewrite warning message
instead of "rewrite(s)". Add test assertions verifying the warning
includes the source pattern and arrow separator. Add a test confirming
that invalid contentDispositionType values default to "inline".
* fix: correct external rewrite warning to reflect actual header forwarding
proxyExternalRequest copies all request headers, not just credential
headers. Updated warning text to accurately state this and suggest
server-side fetch as an alternative. Tightened test assertion for
exact plural form.
* fix: address PR review feedback on external rewrite warning
Clarify why API routes are preferable (control over forwarded headers),
tighten test assertion to prevent false singular/plural match.
Refs: #430
* fix: refine external rewrite warning wording and add source path assertions
Mention route handlers alongside API routes in the warning suggestion,
and assert that source paths (/proxy1, /proxy2, /proxy3) appear in the
multi-rewrite warning message.
* fix: strip credential headers from outbound proxy requests
External rewrites via proxyExternalRequest now strip authorization,
cookie, proxy-authorization, and x-api-key headers before forwarding
to third-party origins. This prevents leaking user credentials to
external services.
Updated the warning message to reflect that credentials are stripped
(not forwarded) and updated tests to assert the new behavior.
Closes #430
* fix: align app-router credential stripping test with implementation
The integration test in app-router.test.ts was missed when
|
||
|
|
0faf62ac4c |
feat: support generateBuildId in next.config and inject build ID at runtime (#408)
* feat: support generateBuildId in next.config and inject build ID at runtime Resolves generateBuildId() from next.config, falling back to a random UUID (ad-blocker-safe) when not configured or when the function returns null. The resolved ID is injected as process.env.__VINEXT_BUILD_ID via Vite define and embedded into __NEXT_DATA__ for Pages Router (dev, prod, and static export). NextRequest gains a buildId accessor for middleware deployment-skew detection. * regen snaps * refactor: extract safeUUID helper and drop buildId param from createSSRHandler Address review nits: deduplicate the UUID+retry loop into a safeUUID() helper, and read process.env.__VINEXT_BUILD_ID directly in dev-server instead of threading it as an explicit parameter through createSSRHandler. * regen snaps * fix: stabilize entry-templates snapshot by setting generateBuildId in pages-basic fixture The random UUID from resolveBuildId was embedded in the server entry snapshot, making it fail on every run. Set generateBuildId to a fixed string in the pages-basic test fixture so the snapshot is deterministic. * fix: move buildId to NextURL to match Next.js API surface Next.js exposes buildId on nextUrl (request.nextUrl.buildId), not directly on NextRequest. Move the getter to NextURL and add a pass-through on NextRequest for convenience. Update tests to assert both paths. |
||
|
|
764a496ce7 |
add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs * rebuild lockfile * fix: add Format to required checks list, remove dead ignore pattern * run fmt * add format to agents.md again |
||
|
|
cb66cc793c |
feat(next-intl): auto-detect config without createNextIntlPlugin (#366)
* fix: resolve NEXT_DATA type conflict with next package types
next/dist/client/index.d.ts declares Window.__NEXT_DATA__ as NEXT_DATA,
which conflicts with our inline type in global.d.ts. The vinext-specific
extensions (__vinext, __pageModule, __appModule) aren't in NEXT_DATA,
causing TS2339 errors.
Extract vinext extensions into a VinextNextData interface and cast at
the two usage sites (client/entry.ts, shims/link.tsx) instead of
re-declaring __NEXT_DATA__ with a conflicting type.
* feat(next-intl): auto-detect next-intl config without createNextIntlPlugin
next-intl's createNextIntlPlugin() crashes in vinext because it calls
require('next/package.json') to check the Next.js version, and next
isn't installed. Instead, vinext now detects next-intl in project
dependencies and registers the next-intl/config alias automatically.
Detection probes i18n/request.{ts,tsx,js,jsx} then
src/i18n/request.{ts,tsx,js,jsx}, matching next-intl's own discovery
order. Explicit aliases from webpack/turbopack config take precedence.
Also adds a targeted warning when config loading fails due to
next-intl/plugin, telling users the wrapper is unnecessary.
Closes #202
* fix: address review feedback
- Update check.ts: next-intl status "partial" → "supported"
- Extend VinextNextData from NEXT_DATA in shims/internal/utils.ts
instead of duplicating fields
- Add JSDoc note about monorepo resolution walk-up behavior
|
||
|
|
8a4929dd63 |
feat: support experimental.serverActions.bodySizeLimit from next.config (#338)
* feat: support experimental.serverActions.bodySizeLimit from next.config * fix: address PR review feedback for parseBodySizeLimit - Accept bare number strings (e.g. "1048576") as bytes, matching Next.js bytes library behavior - Add tb and pb unit support - Throw on zero/negative values to match Next.js validation behavior - Remove unreachable type guard (value is always string at that point) - Update tests: add tb/pb/bare-number/validation cases, fix "10tb" assertion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: warn on invalid bodySizeLimit string instead of silently falling back Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
032a2afb41 |
fix: preserve next.config plugin aliases for next-intl (#342)
* fix: preserve next.config plugin aliases * refactor: probe wrapped webpack config once * fix: tighten next.config alias warnings |
||
|
|
d056e50467 |
fix(config): pass correct Next.js phase to function-form configs + instrumentation test coverage (#273)
* fix(config): pass correct Next.js phase to function-form configs unwrapConfig() hardcoded "phase-development-server" regardless of whether vinext was running in dev or build mode. Apps that branch on the phase argument in next.config.ts received wrong config during production builds. Map Vite's env.command to the correct Next.js phase constant: - "serve" → "phase-development-server" - "build" → "phase-production-build" * test: add instrumentation module test coverage The instrumentation module had zero tests. Add 11 tests covering: - findInstrumentationFile: file discovery across root and src/, priority ordering, null when absent - runInstrumentation: register() lifecycle, onRequestError handler storage, graceful handling of empty modules, error isolation when ssrLoadModule fails - reportRequestError: handler invocation with correct args, no-op without handler, error isolation when handler throws * fix(test): use shared helper for App Router static export test The static-export test had its own startFixtureServer that loaded the fixture's vite.config.ts via configFile. This caused RSC plugin timing issues on Node 25 / macOS — the App Router dev server returned 404 for all pages. The Pages Router section (same approach) worked because it doesn't use the RSC plugin. Switch the App Router section to use the shared startFixtureServerHelper from helpers.ts, which uses configFile: false with explicit plugin registration. This matches how app-router.test.ts (193 passing tests) creates its dev server and avoids the RSC timing issue. * fix: address PR review feedback - Import PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants from shims/constants.ts instead of hardcoding string literals in index.ts and config/next-config.ts - Add afterEach cleanup for temp directories in instrumentation tests * refactor: use phase constants in tests, simplify pure function test setup - Replace hardcoded phase strings in next-config.test.ts with PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants - Remove unnecessary vi.resetModules() from findInstrumentationFile tests — it's a pure function with no module-level state to reset * refactor: use shared startFixtureServer helper in static export tests Remove the local startFixtureServer function — the shared helper from tests/helpers.ts does the same thing. The Pages Router fixture's vite.config.ts is just `vinext()` with no special options, so the shared helper (which uses configFile: false + vinext plugin) works identically. |