Skip to content

Upgrade to Vite 8#3383

Draft
adamziel wants to merge 14 commits intotrunkfrom
adamziel/vite-8-upgrade
Draft

Upgrade to Vite 8#3383
adamziel wants to merge 14 commits intotrunkfrom
adamziel/vite-8-upgrade

Conversation

@adamziel
Copy link
Copy Markdown
Collaborator

Summary

Vite 8 replaces esbuild+Rollup with Rolldown as the bundler — the biggest architectural change in Vite's history. This brings faster builds and consistent dev/build behavior.

Dependency updates:

  • vite 5 → 8
  • vitest 2 → 4
  • @vitejs/plugin-react 4 → 6
  • vite-plugin-dts 3 → 4
  • vite-tsconfig-paths 4 → 6
  • Removed direct rollup dependency (Vite 8 bundles Rolldown internally)

Config changes across 31 vite.config.ts files:

  • build.rollupOptionsbuild.rolldownOptions
  • worker.rollupOptionsworker.rolldownOptions
  • Removed deprecated vitest cache.dir option
  • Removed resolve.alias: { fs: false } in blueprints (incompatible with vite-plugin-dts 4, redundant with external modules list)

Library builds and tests verified working. The vite-tsconfig-paths plugin still works but Vite 8 has native tsconfig path resolution — migrating away from the plugin can be done separately.

Test plan

  • Verify library package builds produce correct ESM+CJS output
  • Run npx nx run-many --target=test across packages
  • Build the website and verify it works in a browser
  • Test the CLI npx nx dev playground-cli server

adamziel added 14 commits March 12, 2026 23:58
Vite 8 replaces esbuild+Rollup with Rolldown as the bundler. This is the
biggest architectural change in Vite's history and brings faster builds
and consistent dev/build behavior.

Dependency updates: vite 5→8, vitest 2→4, @vitejs/plugin-react 4→6,
vite-plugin-dts 3→4, vite-tsconfig-paths 4→6. Removed direct rollup
dependency since Vite 8 bundles Rolldown internally.

Config migrations across 31 vite.config.ts files: renamed
build.rollupOptions to build.rolldownOptions, removed the deprecated
vitest cache.dir option, and removed the resolve.alias fs:false hack
in blueprints that was incompatible with vite-plugin-dts 4 and redundant
with the external modules list.
Rolldown (the bundler in Vite 8) has three behavioral differences from
Rollup that required adaptation:

1. resolveDynamicImport hook is not called for files Rolldown can resolve
internally. The icu.dat externalization in php-wasm-web now uses an
inline resolveId plugin instead of viteExternalDynamicImports.

2. The base64-loader plugin needed resolveId+load hooks instead of just
transform, because Rolldown requires explicit resolution for files with
query parameters like ?base64.

3. vitest 4 no longer augments Vite's defineConfig type. All vite configs
now import defineConfig from vitest/config instead of vite.

Also added isomorphic-git to the external modules list, added
getExternalModules() to the client config to match other library
packages, and updated viteExternalDynamicImports with resolveId support
alongside resolveDynamicImport for Rolldown compatibility.
@nx/vite@22.5.1 declares peer vite@"^5 || ^6 || ^7" which
conflicts with vite@8. npm ci enforces strict peer resolution
by default, causing every CI job to fail at the install step.

Adding legacy-peer-deps=true to .npmrc lets npm ci proceed
until Nx releases a version that accepts Vite 8.
Vitest 4 removed the test(name, fn, timeout) signature — the timeout
option must now be the second argument: test(name, { timeout }, fn).
Updated all affected test files across 6 packages.

Vitest 3+ changed the default pool from 'forks' to 'threads', which
broke --expose-gc and JSPI flags that rely on poolOptions.forks.execArgv.
Added explicit pool: 'forks' to the php-wasm-node test config.

Made viteIgnoreImports build-only so it doesn't intercept .so file
asset imports (?url) during dev server mode in Vite 8.
Handle .so?url imports in Vite 8's dev server by adding an
asset-url-dev plugin to the Playwright config. Vite 8's dev server
doesn't handle ?url imports for custom file extensions (.so, .wasm)
correctly — this plugin intercepts those imports and returns a JS
module that exports the /@fs/ URL.

Pass --expose-gc via both forks and threads pool options so the
flag reaches test workers regardless of which pool Vitest 4 uses.

Fix xdebug bridge tests that broke because Vitest 4 changed how
vi.spyOn on prototypes interacts with class instances. Replace
the EventEmitter.prototype.on spy with a proper MockCDPServer class.

Catch errors in the onMessage listener loop in php.ts to prevent
unhandled rejections from bleeding into subsequent tests.

Fix remaining deprecated test(name, fn, {timeout}) signatures
in import-wxr.spec.ts.
…t loading

Vitest 4 moved all poolOptions to top-level test options, so
test.poolOptions.forks.execArgv became test.execArgv. This was
preventing --expose-gc from reaching test workers.

Also migrates the remaining it(name, fn, timeout) signatures to
it(name, { timeout }, fn) in rotate-php-runtime and php-crash tests.

Fixes the MockCDPServer constructor mock by using a regular function
instead of an arrow function (arrow functions can't be called with new).

Updates viteIgnoreImports to track ?url imports via resolveId so it
doesn't intercept asset URL imports during build – Rolldown may strip
query strings before calling the load hook, which was causing the intl
extension .so file to be replaced with an empty object instead of
being served as an asset URL.
The dts-bundle-generator calls were picking up tsconfig.json (which
only lists "vitest" in types) instead of tsconfig.lib.json (which
includes "node"). Vitest 2/3 used to pull in @types/node transitively,
masking the issue. Now we pass --project explicitly so all Node.js
types resolve correctly during declaration bundling.

Vitest 4 also tightened toThrowError() to check the error constructor,
not just the message. The writeFile directory test was passing a plain
Error but receiving an ErrnoError, so now it matches on the message
string instead.

The custom ESM loader didn't handle Node.js built-in modules like
"tls" which Vitest 4's module-evaluator resolves to file:// URLs
before calling our loader. Two guards detect bare and file:// forms
of built-in specifiers and redirect them to node: protocol.

The intl extension Playwright tests failed because the
externalize-icu-dat plugin was running during dev serve (breaking
import resolution) and the dev server's fs.allow list didn't include
the web-builds/ sibling directory where .so files live.
… issues

Vitest 4 changed how vi.spyOn chains resolve. Re-spying an already-spied
method now creates infinite recursion because the inner spy delegates back
to the outer one. The xdebug-cdp-bridge tests used
`cdpServer.sendMessage.bind(cdpServer)` to capture the "original" method,
but that reference was already the first spy. Fixed by binding directly
from `CDPServer.prototype.sendMessage`.

The php-part-2 "should not log an error" CLI test was picking up a stale
`console.error("Failure!")` call from a previous PHP version's onMessage
test. The EM_ASYNC_JS catch handler in php_wasm.c fires console.error
when a message handler rejects, and this call leaked across the
describe.each PHP version boundary. Fixed by setting up the spy, flushing
pending callbacks with setTimeout(0), then clearing the spy before
assertions.

Also: removed isomorphic-git from the global external modules list so it
gets bundled into the storage package (it's a local submodule, not an npm
dependency), fixed xdebug-bridge vite config to import defineConfig from
vitest/config, relaxed MockInstance type annotations in start-bridge tests,
and added a postinstall patch for @vitejs/plugin-react's d.ts which uses
`export as "module.exports"` syntax that TypeScript 5.4.x can't parse.
…lures

Rolldown (Vite 8's production bundler) wraps ES module initialization in
async functions that await their dependencies. Circular imports that work
fine with standard ESM live bindings become deadlocks under this scheme.

The Redux store files had circular imports: store.ts imported from
slice-sites.ts and init-mcp-bridge.ts, which both imported selectActiveSite
back from store.ts. This caused the production website to hang on load,
failing all E2E and Playwright tests.

The fix defines selectActiveSite locally in each consumer file (it's a
trivial state accessor) and uses dynamic import() for setActiveSite in the
one thunk that needs it. This breaks the import cycle without changing any
runtime behavior.

Also fixes: Vitest 4 Mock type incompatibility in cross-tab-sync tests,
adds timers to the Node.js externals list for npm package builds, increases
WordPress boot test timeout for CI, and uses path.basename() instead of
string splitting in the ESM loader so it works on Windows.
…bridge require shim

The ESLint import/first rule requires all imports before non-import statements.
The local selectActiveSite definitions were placed between imports – move them
after all imports in slice-sites.ts and init-mcp-bridge.ts.

Rolldown replaces import.meta.url with {}.url in CJS output, which evaluates
to undefined and breaks new URL() and createRequire() calls. Fix by replacing
{}.url with the CJS equivalent in the CLI's renderChunk hook.

The xdebug-bridge bundles CJS dependencies that use require() for external
modules, but Rolldown's ESM output doesn't provide require(). Fix by injecting
createRequire via the output banner.
The ~400MB heap memory tests take ~5.2-5.6s, right at the edge of the
default 5000ms timeout. Vitest 4's worker pool adds enough overhead to
push them past the limit. Bump to 30s to match other packages.
The PHP 7.4 WASM binary has a race condition where a socket data
callback fires after a pipe is destroyed, triggering "Cannot read
properties of null (reading 'length')" in PIPEFS. Vitest 3 only
warned about unhandled errors, but Vitest 4 treats them as failures.
Use onUnhandledError to suppress this specific WASM runtime error.
## Summary

The proc_open implementation in the compiled PHP WASM binary registers
`cp.stdout.on('data')` callbacks that write directly to PIPEFS pipe
streams via `stream.stream_ops.write()`. When the child process emits
data after PHP closes both ends of the pipe, `pipe.buckets` is null and
the write crashes with `Cannot read properties of null (reading
'length')`.

This was always a latent race condition, but Vitest 3 only warned about
unhandled errors while Vitest 4 treats them as test failures — surfacing
it during the Vite 8 upgrade.

The fix patches `PIPEFS.stream_ops.write` at runtime to guard against
null buckets. All pipe streams share a single `stream_ops` object, so
patching once protects all pipes. The patch hooks into `FS.createStream`
to detect the first pipe stream and wrap the shared write handler.

This replaces the `onUnhandledError` suppression from the parent PR with
a proper fix at the source.

## Test plan

- [x] `npx nx test-group-1-asyncify php-wasm-node` passes with 826
tests, 0 errors, 0 unhandled exceptions
- [ ] CI passes on all test-unit-asyncify shards

Stacked on `adamziel/vite-8-upgrade`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant