chore(deps): vite 4 → 8 + Rolldown bundler#260
Conversation
Bumps vite ^4 → ^5 across replicad, replicad-evaluator, studio, and replicad-app-example. Tags along: vite-plugin-dts ^3 → ^4, vite-plugin-pwa ^0.16 → ^0.21, @vitejs/plugin-react 3.0.1 → ^4.3.4, vitest ^0.28 → ^1.6. vite.config files untouched.
Bumps vite ^6 → ^7, @vitejs/plugin-react ^4 → ^5, vite-plugin-pwa ^0.21 → ^1, vitest ^1 → ^2. Adds workbox-build/workbox-window ^7.4.1 peers required by vite-plugin-pwa 1.x. Vite 7 floor is Node 20.19/22.12 — .nvmrc bumped 20.12.1 → 20.19.0.
Vite 8 ships with Rolldown as the default bundler — replacing esbuild (dev transform) and Rollup (production) with a single Rust toolchain. Plugin API stays Rollup-compatible; vite.config files unchanged across all four major bumps. Studio build wall: 19.6s → 3.3s (~6× faster). replicad lib build: ~2.4s → 1.7s.
Earlier upgrade dropped the original 'v' prefix when bumping to 20.19.0. Pure cosmetic — both forms work with nvm.
…rors Three react versions live in the workspace: 18.2.0 (replicad-app-example pins exact), 18.3.1 (studio uses ^), 19.1.0 (transitive via docusaurus). Under Vite 8 + Rolldown's dep optimizer, studio's pre-bundled chunks for mobx-react and @devbookhq/splitter were resolving react against a different instance than the app's, producing 'Invalid hook call. You might have more than one copy of React in the same app' at runtime when mounting the Workbench tree. resolve.dedupe forces both 'react' and 'react-dom' to a single instance across the bundle. Prod build is unaffected (Rolldown already collapses duplicates); the fix is dev-mode-load-time.
|
Re: CI run 25765302805 — the build steps all passed ( One small heads-up I should have included in the PR body: Rolldown surfaces three new info-level warnings that Rollup hid — |
|
Thanks! This looks great, I need to take a bit of time to test it locally and then I'll merge! |
vitest 2 and 3 depend on their own copy of Vite rather than taking it as a peer, so vite@8 + vitest@2 resolved two Vite instances (8.0.12 for the packages, 5.4.21 nested under vitest/vite-node). vitest 4 declares Vite as a peer (^6 || ^7 || ^8), collapsing the tree to a single vite@8.0.12. Same failure class as the dual-React instance fixed earlier in studio. Folded into this PR (rather than a follow-up) so Vite 8 never lands with a nested Vite 5. Claude-Session: https://claude.ai/code/session_01A1L7NpgecJPis1mJzxHKDb
Two defects surfaced by the vitest 4 upgrade, both of which let the SVG
suite pass while asserting nothing:
- The snapshot filename was derived from `currentTestName`, which in
vitest 2 carried a leading "<relative-path> > " segment that vitest 4
dropped. Every committed baseline was silently orphaned. The
identifier now strips any file-path prefix, so it depends only on the
test's own name and survives the version change (the snapshot dir is
already scoped per test file, so the prefix was always redundant).
- A missing baseline was auto-written and reported as a pass in normal
`vitest run`; only `--ci` failed it. Combined with the rename above,
the suite reported all-green while regenerating baselines from scratch.
A missing baseline is now a failure unless the run explicitly records
with `-u`, so a renamed or deleted golden file fails loudly.
Claude-Session: https://claude.ai/code/session_01A1L7NpgecJPis1mJzxHKDb
The test shuffled its triangles with `.sort(() => Math.random() - 0.4)` before fusing. fuseAll is commutative so the geometry was identical every run, but toSVG() emits subpaths in input order, so the serialized SVG differed every run while the shape did not — an exact-string snapshot of that is flaky by construction (it passed historically only by luck). Use a fixed non-identity permutation (reverse) so the test still exercises fuseAll on scrambled input while producing a reproducible snapshot. Verified deterministic across three fresh processes. Claude-Session: https://claude.ai/code/session_01A1L7NpgecJPis1mJzxHKDb
The matcher identifier change renames every baseline, so the committed
golden files are re-recorded to match. Breakdown:
- 26 files are pure renames — byte-identical content, only the filename
changed (git detects them as R100). Confirms those tests' output is
unchanged; the rename is purely the vitest-4 identifier fix.
- "build from triangles" content changed because its input order is now
fixed rather than randomised (see prior commit).
- Four "offset vase" baselines re-recorded. These were stale: the vase
is a spline, and offsetting a spline is an iterative approximation
(blueprints/offset.ts) whose control points are sensitive to the
toolchain they were recorded on. The old baselines (recorded 2026-03)
no longer reproduce — coordinates drift at the 4th–5th significant
figure while the geometry is identical (viewBox byte-for-byte equal,
coords agree to 3 dp). Arc/curve output is unaffected: "fuses two
circles" (arc commands) re-records byte-identically, so curve math did
not shift — only the spline-offset approximation.
- One dead baseline removed: "offset vase, with offset -5" had no
corresponding case (the test.each covers -1/1/5/10), a leftover from a
removed parameter.
Note: offset-of-spline snapshots remain toolchain-sensitive. If this suite
ever gains CI, those four cases should assert a geometric invariant rather
than an exact SVG string.
Claude-Session: https://claude.ai/code/session_01A1L7NpgecJPis1mJzxHKDb
Summary
Step-wise upgrade of Vite (4 → 5 → 6 → 7 → 8) across the four Vite-using packages (
replicad,replicad-evaluator,studio,replicad-app-example). Vite 8 ships with Rolldown as the default bundler — replacing both esbuild (dev transform) and Rollup (production) with a single Rust toolchain. Plugin API stays Rollup-compatible;vite.configfiles needed no behavior changes across all four majors.Sending upstream because I've been running this against
replicad-based work in another project for a few weeks, and the perf win plus the dev-mode dual-React fix (described below) both apply to your tree exactly as-is. Feel free to take it, split it, or pass.Commits
Each Vite major lives in its own bisectable commit:
vite 4 → 5— also bumpsvite-plugin-dts^3 → ^4,vite-plugin-pwa^0.16 → ^0.21,@vitejs/plugin-react3.0.1 → ^4.3.4,vitest^0.28 → ^1.6vite 5 → 6vite 6 → 7—@vitejs/plugin-react^4 → ^5,vite-plugin-pwa^0.21 → ^1.0 (which requiresworkbox-build/workbox-window^7.4.1 as new peers),vitest^1 → ^2;.nvmrc20.12 → 20.19 for the Vite 7 Node floorvite 7 → 8 (Rolldown)—@vitejs/plugin-react^5 → ^6Plus two follow-up commits:
chore: restore v-prefix in .nvmrc— cosmeticfix(studio): dedupe react under Vite 8 to avoid dual-instance hook errors— see belowThe React dedupe fix
In headless-browser smoke-testing the dev server, I hit
Invalid hook call. You might have more than one copy of React in the same appwhen mounting the Workbench tree. Root cause: three React versions live in the workspace —replicad-app-examplepins 18.2.0 exact,studiouses ^18.3.1, andreplicad-docspulls 19.x via docusaurus 3.x. Under Vite 8's dep optimizer,studio's pre-bundled chunks formobx-reactand@devbookhq/splitterwere resolving react against a different instance than the app's.Standard fix:
resolve.dedupe: ["react", "react-dom"]inpackages/studio/vite.config.js. Prod build is unaffected (Rolldown collapses duplicates either way); the fix is dev-mode-load-time. Worth taking even if you choose not to bump Vite, since the same conditions exist on Vite 4 — the optimizer just happens not to trip the resolver the same way today.Build perf (local, identical machine)
pnpm prepare-packageswallpnpm --filter studio buildwallpnpm --filter replicad-app-example buildwallindex-*.js(gzip)replicad.js(lib, raw)Studio's headline 6.5× win is the most visible: production-mode Workbench/index bundles plus PWA manifest generation, with code-splitting that Rolldown produces more aggressively than Rollup did (Monaco workers + Workbench load lazily as separate chunks). For your
publish-studio.ymlCloudflare Pages workflow this is a direct CI-time saving.Surfaces that did NOT need changes
replicad-opencascadejsandreplicad_single.wasmload unchanged.worker.format: "es") carries across.@rollup/browser+rollup-plugin-external-globalsas runtime deps ofreplicad-evaluator— orthogonal to the build-time bundler.vite-plugin-dtswithrollupTypes: true— uses API Extractor independently of Vite/Rolldown.@rollup/plugin-commonjs,-node-resolve,-json,-terserinpackages/replicad/package.json— Rolldown's plugin API stays Rollup-compatible.The chunk-size advisory now reads
build.rolldownOptionsinstead ofrollupOptions— informational, expected.Rolldown surfaces two new informational warnings during the studio build:
evaluse in Monaco editor — known upstream Monaco behavior.BatchedMeshimport missing fromthree@0.155.0viathree-mesh-bvh— pre-existing; resolves on a three.js bump.Verification
pnpm installclean across all four major bumps.pnpm prepare-packagesclean at every step.pnpm --filter studio buildclean; service worker generates.http://localhost:5555/workbench: studio renders the Workbench UI byte-identical to amainbaseline screenshot. The screenshots in the downstream PR (linked for reference) show the before/with/without-dedupe states.A note on CI
Your
publish-studio.ymlcurrently usespnpm/action-setup@v2 version: 8with thepnpm@9.13.2packageManagerand a--no-frozen-lockfileflag, which means CI regenerates the lockfile every run. Not breaking, just wasteful. Worth bumping topnpm/action-setup@v4+version: 9and dropping--no-frozen-lockfile. Happy to do that in a follow-up if you prefer it separate.Out of scope
packages/replicad-threejs-helper— uses standalonerollup@3.10.1(not Vite). Independent migration; deferred.rolldownOptions.output.advancedChunkstuning — defaults are fine.