20 Commits

Author SHA1 Message Date
Jim Geurts 49a4376508 fix(config): handle ssr.external: true in App Router (closes #1103) (#1105)
* 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
2026-05-06 18:29:53 +01:00
James Anderson 3f287ff5ab chore: remove useless code assertion tests and snaps (#1023)
* chore: remove useless code assertion tests and snaps

* .
2026-05-02 19:54:31 +00:00
Nathan Nguyen dc6b11f2ce fix(rsc): exclude client shims from dep optimization (#1020)
* 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>
2026-05-02 18:50:19 +01:00
James Anderson e81a6212d5 refactor: extract remaining internal exports (#833) 2026-04-13 10:49:28 +01:00
James Anderson 3f0fce71aa refactor: extract ssr manifest utils (#832) 2026-04-13 09:32:49 +00:00
James Anderson c6dce5e483 refactor: extract client config plugin to own file (#831) 2026-04-13 09:15:40 +00:00
James Anderson a5ff8653ad refactor: remove internal re-exports from entry (#828) 2026-04-13 08:26:21 +01:00
Divanshu Chauhan (divkix) 063bf00be5 fix(build): eliminate Vite 8 treeshake.preset warning (#746)
* 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
2026-04-02 19:50:21 +01:00
James Anderson 1ff166eb92 chore: turn on more lint rules (#714)
* chore: turn on more lint rules

* fmt

* add todos
2026-03-29 20:40:57 +01:00
Steve Faulkner 05cac9d745 build: align Vite 8 bundler config (#599)
* build: align Vite 8 bundler config

* fmt

---------

Co-authored-by: James <james@eli.cx>
2026-03-28 15:16:03 +00:00
Stephen Zhou 25f6e0fc87 chore: enable typeAware and typeCheck, use vp check (#551)
* chore: enable typeAware and typeCheck, use vp check

* Build

* Try cache false

* Revert "Try cache false"

This reverts commit 5f76ed02f1.

* Update

* workaround for vp check

* Try no workaround

* Fix check

* Update to 0.1.12

* denyWarnings
2026-03-16 11:00:47 -05:00
James Bithell 2f42e05dae fix: merge top-level optimizeDeps with per-environment config (#539)
* 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 #538

https://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 #538

https://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>
2026-03-16 14:09:45 +00:00
Stephen Zhou 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 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
2026-03-15 10:50:13 +00:00
Jared Stowell f72440f082 fix: Pages Router prod hydration for inlined page modules (#394)
* Clarify chunk manifest structures

* Fix SSR manifest basePath URLs

* Fix SSR manifest basePath handling

* Fix SSR manifest basePath handling

* make ci happy

* bonk
2026-03-10 20:02:21 +00:00
James Anderson 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
2026-03-09 14:56:14 +00:00
Divanshu Chauhan (divkix) 6606006839 fix: stub node:async_hooks in client builds via virtual module (#306)
* 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>
2026-03-07 13:00:26 +00:00
Steve Faulkner 705075d878 fix: strip server-only data-fetching exports from client bundles (#282)
* 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.
2026-03-06 10:39:42 -06:00
Stephen Zhou 5b9d7756b2 fix: set correct NODE_ENV for build (#247)
* 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>
2026-03-04 21:59:40 +00:00
Sunil Pai 3ed38d1c4a fix: exclude vinext from optimizeDeps to prevent virtual module resolution errors (#3)
* 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
2026-02-24 11:13:28 -06:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00