* fix(app-router): emit pages client entry for hybrid builds
* fix pages router navigation params readiness
* fix(pages-router): track hashed fallback client entry
* fix(pages-router): honor navigation readiness in hooks
* fix(pages-router): derive server router.isReady from SSR readiness
Address review feedback on #1741:
- getRouterSnapshot() now reads navigationIsReady from the SSR context on
the server instead of unconditionally returning true. For pre-ready Pages
routes (auto-export dynamic / query-string / rewrite-capable builds) this
keeps useRouter().isReady consistent between SSR and client hydration,
avoiding a hydration mismatch for components that read it in JSX. Mirrors
Next.js render.tsx's server readiness rule.
- Drop the redundant guard branch in getClientParamsSnapshot.
- Add a regression test asserting server useRouter().isReady reflects the
SSR navigationIsReady context.
* fix(pages-router): align param resolution and dev/prod readiness parity
Address review feedback (ask-bonk + self-review) on #1741:
- Align client Pages navigation params to query-first, matching the server
snapshot and Next.js's adaptForPathParams (derives path params from
router.query). __NEXT_DATA__.query is kept current on client navigation, so
the two snapshots are now provably identical and avoid a latent post-
hydration mismatch.
- Populate window.__VINEXT_PAGE_PATTERNS__ in the dev server (own script tag)
so the next/navigation compat hooks can resolve a dynamic pattern from a
resolved path in dev exactly as in production. Closes a dev/prod parity gap
(prod set this via the client entry; dev never did).
- Drop the redundant pre-readiness setSSRContext in dev; the single post-
module-load call now publishes navigationIsReady before rendering, matching
the prod handler.
- Document that installPagesClientAssetGlobals is intentionally inert for
App-only builds, and that getReadonlyPagesSearchParams' string-keyed cache
is safe to share across concurrent SSR requests (immutable wrappers).
- Add a dev regression test asserting __VINEXT_PAGE_PATTERNS__ exposure.
* fix(pages-router): prefer URL params for client snapshots
* fix(app-router): gate pages asset globals
* test(app-router): add production test for hybrid basePath + assetPrefix
* fix(navigation): widen usePathname return type to string | null
* refactor(pages-router): dedupe navigation readiness modeling and cache pattern scan
Addresses code-review cleanup findings on the Pages Router navigation
readiness work:
- Extract buildPagesReadinessNextData() so the dev SSR handler and the
production Pages page handler derive the gssp/gsp/gip/appGip/autoExport
__NEXT_DATA__ readiness flags from one place, instead of two hand-kept
copies that could drift and produce a dev/prod hydration mismatch.
- Extract resolvePagesNavigationPathname() shared by the client and SSR
branches of getPagesNavigationContext, so the pathname null-ness rule
(fallback / pre-ready auto-export dynamic) cannot diverge between them.
- Memoize resolvePagesRoutePatternForPath()'s pattern scan, which ran an
uncached O(routes) loop on every useSyncExternalStore snapshot read.
- In getServerSearchParamsSnapshot, serialize the source only when the
object-identity fast path misses, avoiding a toString() on every read.
* refactor(pages-router): consistent ISR readiness data + robustness cleanups
Addresses further code-review findings:
- Thread the readiness flags (gssp/gsp/gip/appGip/autoExport) through the ISR
background-regeneration path: renderPagesIsrHtml now serializes the same
__NEXT_DATA__ readiness state the initial render emits, so a regenerated,
re-cached page hydrates with the identical initial router.isReady the server
computed. Previously these flags were dropped on that path — benign today
(ISR-eligible getStaticProps routes never trigger the flag-gated early-ready
branches) but a latent dev/prod hydration-readiness drift. Adds a shared
PagesNextDataExtras type so every render path stays in lockstep.
- readSsrManifest: degrade a malformed/unparseable SSR manifest to "no SSR
manifest" with a warning instead of throwing and aborting server startup,
restoring the previous tolerant behavior.
- pages-readiness: project PagesReadinessNextData from the canonical
VinextNextData and accept PagesPageModule, instead of bespoke types that
could drift from the __NEXT_DATA__ shape.
- getReadonlyPagesSearchParams: drop the redundant string-keyed cache slot; the
per-object WeakMap is authoritative and concurrency-safe on its own.
- isPagesRouterReady: drop the redundant server-side window check.
* fix(pages-router): restore string-keyed readonly search-params cache
Reverts the cache simplification from the previous commit. The string-keyed
slot is load-bearing, not redundant: across the Pages Router pre-ready → ready
transition the navigation context swaps in a new URLSearchParams instance even
when the query string is unchanged, so the per-object WeakMap alone returns a
fresh ReadonlyURLSearchParams wrapper and useSearchParams() loses Object.is
stability — re-firing a [searchParams] effect. Caught by the rewrite-driven
pre-ready static page e2e (search-params-snapshots saw ['',''] not ['']).
Keeps the improved comment explaining why both cache levels are needed.
---------
Co-authored-by: James <james@eli.cx>
* fix(build): correct CSS ordering for global-not-found (#1549)
* test(build): assert global-not-found CSS ordering in production (#1549)
Adds a production-build regression test for #1549: builds the global-not-found-css-order fixture, serves it via the Vite preview server, and asserts the route-miss 404 document serves global-not-found's own CSS (red wins) without leaking the root layout's stylesheet (green), while matched routes still serve the layout's CSS.
Verified the test fails without the framework-chunk split in client-build-config.ts (the 404 leaks the layout's green) and passes with it.
* chore: update lockfile for global-not-found-css-order test fixture (#1549)
* refactor(build): migrate RSC framework chunk to codeSplitting; add unit tests
Address bonk review on #1858 (issue #1549):
- Replace the deprecated rolldown `advancedChunks` output config with
the non-deprecated `codeSplitting` form for Vite 8+, mirroring the
client build. Stops the per-prod-build deprecation warning.
- Derive both RSC_FRAMEWORK_CHUNK_TEST (regex) and isRscFrameworkModule
(predicate) from a single FRAMEWORK_PACKAGES list so they can't drift.
- Add focused unit tests for createRscFrameworkChunkOutputConfig (both
the Vite 7 manualChunks and Vite 8+ codeSplitting branches) and for the
framework package matching (matches react/react-dom/scheduler/
react-server-dom-webpack, excludes react-icons / @react-aria/*).
A Pages Router app built with a `basePath` emits its page and `_app`
chunk URLs with the basePath applied twice, e.g.
`/docs/docs/_next/static/_slug_.js`. Those URLs 404, so the page never
hydrates, `next.router` never initializes, and any client-side
navigation (router.push to /404, /_error, etc.) silently does nothing.
Vite+ bakes the configured `base` into emitted chunk fileNames on disk
(`dist/client/docs/_next/static/...`) so the Cloudflare ASSETS binding
can serve them directly. Vite then prepends `base` a second time when it
writes `ssr-manifest.json`, producing `docs/docs/_next/static/...`
entries. `augmentSsrManifestFromBundle` preserved those doubled entries
verbatim (it only stripped a leading slash), while its own bundle
backfill produced the correct single-prefix path. The merged manifest
held both, and `resolveClientModuleUrl` returned the doubled one.
The on-disk chunk fileName carries the basePath exactly once, so the
public URL must too. Collapse an exact `<base>/<base>/` prefix when
normalizing the Vite-sourced manifest entries so they match the
single-base paths the backfill produces. A correctly single-prefixed
entry is left untouched.
Regression covered with a focused unit test on the manifest builder and
verified end to end against the Next.js basepath/error-pages deploy
suite (previously 2 failing client-navigation cases, now all green).
* fix(css): preserve distinct media filenames for CSS url() assets
Match Next.js `asset/resource` behaviour for CSS `url()` dependencies:
emit them as files under `/_next/static/media/` instead of inlining, and
keep byte-identical sources (e.g. `dark.svg` / `dark2.svg`) as separate
output files instead of letting Vite/Rolldown's content-based asset
dedupe collapse them onto one filename.
There is no config switch to disable that dedupe (vitejs/vite#8632). The
native escape hatch is `this.emitFile({ fileName })`, which is never
deduped. A build-only, client-scoped pair of plugins:
- mark: during the client CSS transform, tag each relative asset
`url()` with a private `?vinext_css_url_asset=<source-basename>`
query — the only durable carrier of per-reference provenance, since
once Rolldown dedupes, the bundle can't tell which reference came
from which source.
- restore: at `generateBundle`, read the marker back, emit a sibling
file under the wanted source name via the `fileName` escape hatch,
rewrite the reference, and strip the marker.
Each reference resolves from its own marker, so split CSS chunks stay
correct without a shared cursor or bundle-iteration-order dependence.
This is a slimmer reimplementation of the approach explored in #1703:
same mark/restore mechanism and public surface, but the restore side
drops the AssetIndexes machinery, filename-reservation loop, and
multi-fallback lookups in favour of a single basename map + emitted-set,
since the sibling-filename construction (source-stem + content-hash +
ext) makes those guarded cases unreachable.
Tests port the Next.js scss/url-global e2e into the committed
`pages-basic` and `app-basic` fixtures (built in isolation), covering
the single-stylesheet two-identical-urls case and cross-chunk provenance.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* fix(css): merge duplicate vite import; drop ReDoS-flagged regex; clarify comments
- tests/css-url-assets.test.ts: merge the two `vite` imports into one
(import/no-duplicates lint error that failed CI Check).
- client-build-config.ts: replace `/\/+$/` trailing-slash strip with a
linear loop so CodeQL stops flagging a (false-positive) ReDoS.
- index.ts: reword the css-url mark transform's `map: null` comment — the
marker shifts columns transiently but is stripped before final output.
- css-url-assets.ts: note the basename-uniqueness assumption in assetsByBase.
Addresses review feedback on #1725.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* test(css): use dedicated minimal fixtures for url() asset tests
The url() asset tests previously added routes to the shared app-basic /
pages-basic fixtures and built those whole apps in isolation. That made
the tests CPU-heavy and, run concurrently under CI, contended with
app-router.test.ts's static-export test enough to push it past its 30s
timeout (it passes comfortably locally and on main; the extra parallel
load was the trigger). The added app-basic route also slightly inflated
every app-basic-wide test (static export renders all routes).
Move the fixtures into dedicated minimal committed apps
(tests/fixtures/css-url-assets-{app,pages}) that build in ~1-2s. Same
observable contract (byte-identical dark.svg/dark2.svg referenced from
one stylesheet, plus the split-chunk provenance case), now isolated from
the heavyweight shared fixtures and fast enough not to contend.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* docs(test): update css-url-assets header to reference the dedicated fixtures
The file header still described the pre-refactor layout (pages-basic /
app-basic paths that no longer exist). Point it at the dedicated
css-url-assets-{pages,app} fixtures. Comment-only.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
---------
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
When a page declared `getServerSideProps` (or `getStaticProps` /
`getStaticPaths`) as a local binding and re-exported it via
`export { getServerSideProps }`, the client-bundle transform overwrote
the `export { ... }` statement with `export const getServerSideProps =
undefined;`. The original local `const getServerSideProps = ...` was
left in place, producing two declarations of the same identifier in
module scope. Rolldown/OXC rejects this with `Identifier
'getServerSideProps' has already been declared`, breaking the entire
production build (and cascading to 48 test failures in Next.js's
`test/e2e/getserversideprops` suite).
Drop the export specifier without emitting a stub, matching Next.js's
`next-ssg-transform` behaviour. The now-unused local declaration becomes
dead code and is tree-shaken later.
Closes#1354
The next/image client shim imports ipaddr.js for private-IP validation.
It's already in ssr.resolve.external, but the SSR dep optimizer would
still pre-bundle it on first request, producing
(ssr) ✨ new dependencies optimized: ipaddr.js
and the accompanying full reload. Add ipaddr.js to optimizeDeps.exclude
for the App Router and Pages Router SSR environments so the optimizer
skips it; runtime resolution still works via resolve.external on Node
and via the worker bundle on Cloudflare/Nitro. The client environment
intentionally keeps pre-bundling ipaddr.js since next/image is a 'use
client' component and needs the CJS→ESM conversion in the browser.
* fix(config): exclude React from ssr optimizeDeps when ssr.external: true
When users set `ssr.external: true` in vite.config.ts (the only practical
way to keep CJS-quirky packages like pg, protobufjs, keyv,
@launchdarkly/js-server-sdk-common, @opentelemetry/otlp-transformer
working in dev), `'use client'` components calling React.useContext from
external packages (nuqs, next-themes, launchdarkly-react, etc.) crashed
in dev with `Cannot read properties of null (reading 'useContext')`.
Root cause: @vitejs/plugin-rsc populates
environments.ssr.optimizeDeps.include with React entries via
crawlFrameworkPkgs. With external: true, the SSR env loads React via
Node's resolver, but Vite still pre-bundles a duplicate React copy into
deps_ssr/ because optimizeDeps.include overrides external: true.
react-dom-server.edge then sets the dispatcher on its bundled React
while externalized callers (vinext's runtime) see a different React,
leaving React.H null and crashing every hook call from a 'use client'
module SSR'd through it.
Fix: when userSsrExternal === true, add React entries to
environments.ssr.optimizeDeps.exclude so plugin-rsc's auto-includes
cannot pre-bundle a duplicate copy. The renderer and the runtime then
share one Node-loaded React.
The RSC env is unaffected (it still pre-bundles React with the
react-server condition), and the default mode (`noExternal: true`) is
unaffected since external: true gating the strip.
Closes#1103
* fix(config): also skip top-level ssr.noExternal: true when ssr.external: true
The previous fix only stripped React entries from
environments.ssr.optimizeDeps.exclude, but Vite uses top-level `ssr.*`
as defaults for `environments.ssr.*`. With vinext setting top-level
`ssr.noExternal: true` unconditionally (when not Cloudflare/Nitro),
that `true` leaked into `environments.ssr.resolve.noExternal` and
@vitejs/plugin-rsc's later expansion to an array (or the user's own
configResolved mirror) brought React back into the bundled list,
recreating the duplicate-React crash.
Two changes:
1. When `config.ssr.external === true`, vinext only sets the top-level
`ssr: { external: true }` block — no `noExternal: true` to leak into
the per-env config.
2. Defensive configResolved hook that strips React entries from
`environments.ssr.resolve.noExternal` if a later plugin (or
plugin-rsc itself) added them back. The strip is gated on
`external === true` so it only applies in the all-external mode.
Verified against a real-world App Router app (~80 deps including nuqs,
next-themes, launchdarkly-react, blocknote, opentelemetry, pg,
protobufjs, keyv, etc.): with ssr.external: true and these fixes
applied, 'use client' routes now render correctly in dev — the
`Cannot read properties of null (reading 'useContext')` /
`Invalid hook call` crashes are gone.
Refs #1103
* fix(rsc): exclude client shims from dep optimization
App Router dev config currently excludes the vinext package but not its client shim subpaths. @vitejs/plugin-rsc tracks package client references by the original bare import source, so optimized vinext/shims/* modules can lose the matching export metadata and crash client-package proxy loading.
Add a shared optimizeDeps exclude set for vinext's known RSC client shims and merge it into the top-level, RSC, SSR, and client optimizer configs while preserving existing user and server-external excludes.
* Update rsc-client-shim-excludes.ts
Co-authored-by: James Anderson <james@eli.cx>
* Update rsc-client-shim-excludes.ts
Co-authored-by: James Anderson <james@eli.cx>
* Update build-optimization.test.ts
Co-authored-by: James Anderson <james@eli.cx>
---------
Co-authored-by: James Anderson <james@eli.cx>
* fix(build): eliminate Vite 8 treeshake.preset warning
Add version-gated getClientTreeshakeConfigForVite() function that returns
Rollup-compatible config (with preset) for Vite 7 and Rolldown-compatible
config (without preset) for Vite 8+.
The treeshake.preset option is Rollup-specific and causes warnings in Vite 8
which uses Rolldown. The moduleSideEffects: 'no-external' option is valid in
both bundlers and is preserved in all configurations.
Changes:
- Add getClientTreeshakeConfigForVite(viteMajorVersion) function
- Update 4 usage sites to use version-gated function
- Mark clientTreeshakeConfig as @deprecated for backward compatibility
- Export getClientTreeshakeConfigForVite for testing
- Add comprehensive tests for Vite 7/8/9+ compatibility
- Update existing tests to expect Vite 8 format
Fixes#540
* refactor(build): address PR review feedback for treeshake config
Remove dead workaround plugin from playground
- Deleted strip-rolldown-incompatible-rollup-options plugin from
examples/app-router-playground/vite.config.ts
- This workaround is no longer needed since getClientTreeshakeConfigForVite()
never emits treeshake.preset for Vite 8+
Add documentation comment about Rolldown defaults
- Enhanced JSDoc for getClientTreeshakeConfigForVite() to explain behavior gap
- Documents how Rolldown defaults compare to Rollup's recommended preset
- Clarifies that moduleSideEffects: no-external is the key optimization
Fix test import style
- Changed from dynamic await import() to static imports
- Matches existing test patterns in build-optimization.test.ts
- Removes async from test cases that don't need it
Addresses review comments from PR #746
* docs(build): clarify Rolldown treeshake divergence in comments
Address PR #746 review feedback from @ask-bonk:
- JSDoc: Explicitly call out unknownGlobalSideEffects as a known acceptable
divergence (Rolldown defaults to true vs Rollup recommended preset's false).
Makes it clear this is intentional, not an oversight.
- Inline comment: Note that Rolldown's built-in defaults already cover what
Rollup's 'recommended' preset provides (annotations, correctContext,
tryCatchDeoptimization). Provides immediate context at the code site.
Both changes are documentation-only; no functional impact.
* chore: address PR review feedback on treeshake config
- Update comment to reference getClientTreeshakeConfigForVite instead of deprecated clientTreeshakeConfig
- Remove redundant not.toHaveProperty('preset') assertions from tests (toEqual already does exact matching)
Addresses review feedback from @ask-bonk[bot] on PR #745
* fix: merge top-level optimizeDeps with per-environment config
When vinext's config() hook builds per-environment optimizeDeps, it now
merges any exclude/include entries from config.optimizeDeps (populated by
earlier plugins like @lingui/vite-plugin) instead of overwriting them.
This ensures packages excluded by other plugins remain excluded across
all three environments (rsc, ssr, client).
Fixes#538https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
* fix: merge top-level optimizeDeps with per-environment config
When vinext's config() hook builds per-environment optimizeDeps, it now
merges any exclude/include entries from config.optimizeDeps (populated by
earlier plugins like @lingui/vite-plugin) instead of overwriting them.
This ensures packages excluded by other plugins remain excluded across
all three environments (rsc, ssr, client).
Add test verifying top-level optimizeDeps entries are preserved in all
per-environment configs after vinext's config hook runs.
Fixes#538https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
* docs: add PR description for optimizeDeps merge fix
https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
* chore: remove pr-description.md
https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
* fix: merge optimizeDeps for Pages Router + improve test coverage
Address PR review feedback:
- Move incomingExclude/incomingInclude capture above the hasAppDir
branch so Pages Router builds also merge entries from earlier plugins
(e.g. @lingui/vite-plugin) instead of overwriting them.
- Add @vercel/og assertion to confirm vinext's own entries survive merging.
- Add Set-based deduplication assertions to all environments.
- Test overlapping entries (incoming "vinext" + vinext's own "vinext")
to validate the Set dedup logic.
https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
* fix: preserve optimizeDeps.include for Pages Router + fix formatting
Address upstream PR review feedback:
- Spread incomingInclude into top-level optimizeDeps so Pages Router
builds preserve include entries from upstream plugins (e.g. @lingui/vite-plugin)
- Run pnpm run fmt to fix formatting in test file
https://claude.ai/code/session_01T7kpjyEcsZTUG6wHscx3nb
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* 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
* fix: stub node:async_hooks in client builds via virtual module
Several shims (headers, cache, navigation-state, etc.) import
AsyncLocalStorage from node:async_hooks. Since resolve.alias applies
globally, these shims resolve in every environment including client.
Vite externalizes node:async_hooks to __vite-browser-external in browser
builds — an empty stub with no named exports — causing Rollup errors.
Add a vinext:async-hooks-stub plugin that intercepts node:async_hooks
in the client environment and provides a no-op AsyncLocalStorage via a
virtual module. This is semantically correct: shims already guard with
`_als.getStore() ?? fallback` patterns, so undefined store returns
produce correct client-side behavior.
Unlike the external file approach in #293, this uses a virtual module
inline in the plugin — no additional files, no build artifacts.
Closes#293
* fix: address async-hooks-stub code review feedback
- Convert load hook to object form with handler() for consistency
- Match bare `async_hooks` specifier in addition to `node:async_hooks`
- Add intentionally-minimal comment explaining stub scope
- Add regression tests for resolveId and load hook behavior
* refactor: extract async-hooks-stub plugin to plugins/ and test against real implementation
Move the vinext:async-hooks-stub plugin object from an inline definition in
index.ts to packages/vinext/src/plugins/async-hooks-stub.ts, exporting it as
asyncHooksStubPlugin. Import and use it directly in index.ts.
Update the regression test to import the real plugin via _asyncHooksStubPlugin
and invoke its resolveId/load handlers with a mock context, eliminating the
duplicated logic that the test previously used as a workaround for the plugin
being unexported and inline.
* fix: add filter to load hook, evaluate stub at runtime in test
- Add filter to load hook object form for consistency with resolveId
and to opt into Vite/Rolldown hook optimization path
- Replace overlapping string-assertion test with one that evaluates
the generated source via new Function, testing actual runtime
behavior (getStore returns undefined, run/exit pass through return values)
* fix: remove control-char regex from load filter to fix lint
* fix: suppress no-control-regex for load filter via eslint-disable comment
* refactor: derive load filter regex from ASYNC_HOOKS_STUB_ID constant
* Update build-optimization.test.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* Update index.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix: widen run() type signature to accept rest args and typed callback
---------
Co-authored-by: James <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix: strip getServerSideProps/getStaticProps from client bundles
Pages Router page modules export getServerSideProps, getStaticProps,
and getStaticPaths for server-side use. These functions often import
server-only modules (database drivers, fs, etc.) that would break or
bloat the client bundle.
Added a new Vite transform plugin (vinext:strip-server-exports) that
replaces these exports with no-op stubs when the module is being
processed for the client environment. The plugin only applies to
files under the pages/ directory and skips API routes, _app,
_document, and _error files.
The transform handles multiple export patterns:
- export (async) function declarations
- export const arrow functions
- export const simple references
- TypeScript type annotations in parameters
Extracted the core logic into a standalone stripServerExports()
function for testability. Added 11 unit tests covering all patterns
including edge cases with nested braces, string literals, TypeScript
types, and multiple server exports in one file.
* Rewrite stripServerExports to use AST instead of regex
Replace the regex-based approach with parseAst (Rollup/acorn) and
MagicString. The AST approach correctly handles all export patterns:
- Function declarations (export function / export async function)
- Variable declarations with any initializer (arrow functions,
function expressions, async named function expressions, simple refs)
- Re-export syntax (export { getServerSideProps })
- Mixed re-exports (export { getServerSideProps, config })
The old regex approach had bugs with function expressions, arrow
functions with TS return type annotations, re-exports, and regex
literals in function bodies. Since this transform runs after Vite's
JSX/TS compilation, parseAst always receives plain JavaScript.
Updated tests to use post-compiled JS (no JSX/TS) matching what the
transform actually receives. Added 6 new test cases for the patterns
that broke the regex approach.
* fix: set NODE_ENV
* address review feedback: readability, test coverage, and nextConfig.env guard
- Replace nested ternary with if/else for NODE_ENV resolution
- Add comment explaining why beforeEach/afterEach is file-scoped
- Add test cases: serve (development), build without mode, user override
- Skip NODE_ENV key in nextConfig.env loop to prevent silent override
- Extract setupTmpProject helper to reduce test boilerplate
---------
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
* fix: exclude vinext from optimizeDeps to prevent virtual module resolution errors
When vinext is installed from npm (not symlinked), esbuild's dependency
optimization scans vinext/dist/ and hits virtual:vinext-* imports that
only exist at Vite plugin resolution time. This causes build failures
in all three environments (client, rsc, ssr).
Adding optimizeDeps.exclude: ['vinext'] at the top level and in each
environment prevents esbuild from scanning vinext's dist files entirely.
Fixes#1
* test: verify optimizeDeps.exclude contains vinext for all environments