Skip to content

chore(deps): vite 4 → 8 + Rolldown bundler#260

Open
dnplkndll wants to merge 10 commits into
sgenoud:mainfrom
ledoent:chore/vite-8-rolldown
Open

chore(deps): vite 4 → 8 + Rolldown bundler#260
dnplkndll wants to merge 10 commits into
sgenoud:mainfrom
ledoent:chore/vite-8-rolldown

Conversation

@dnplkndll

Copy link
Copy Markdown
Contributor

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.config files 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 bumps 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 5 → 6
  • vite 6 → 7@vitejs/plugin-react ^4 → ^5, vite-plugin-pwa ^0.21 → ^1.0 (which requires workbox-build/workbox-window ^7.4.1 as new peers), vitest ^1 → ^2; .nvmrc 20.12 → 20.19 for the Vite 7 Node floor
  • vite 7 → 8 (Rolldown)@vitejs/plugin-react ^5 → ^6

Plus two follow-up commits:

  • chore: restore v-prefix in .nvmrc — cosmetic
  • fix(studio): dedupe react under Vite 8 to avoid dual-instance hook errors — see below

The 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 app when mounting the Workbench tree. Root cause: three React versions live in the workspace — replicad-app-example pins 18.2.0 exact, studio uses ^18.3.1, and replicad-docs pulls 19.x via docusaurus 3.x. Under Vite 8'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.

Standard fix: resolve.dedupe: ["react", "react-dom"] in packages/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)

Stage Before (Vite 4) After (Vite 8 + Rolldown) Δ
pnpm prepare-packages wall 7.05 s 5.40 s 1.3× faster
pnpm --filter studio build wall 19.61 s 3.00 s 6.5× faster
pnpm --filter replicad-app-example build wall 3.06 s 1.12 s 2.7× faster
studio index-*.js (gzip) 559 KB 524 KB -6%
replicad.js (lib, raw) 593 KB 588 KB -1%

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.yml Cloudflare Pages workflow this is a direct CI-time saving.

Surfaces that did NOT need changes

  • OCCT WASMreplicad-opencascadejs and replicad_single.wasm load unchanged.
  • Module worker in studio (worker.format: "es") carries across.
  • Monaco editor — self-hosts its workers; no library-side change.
  • @rollup/browser + rollup-plugin-external-globals as runtime deps of replicad-evaluator — orthogonal to the build-time bundler.
  • vite-plugin-dts with rollupTypes: true — uses API Extractor independently of Vite/Rolldown.
  • @rollup/plugin-commonjs, -node-resolve, -json, -terser in packages/replicad/package.json — Rolldown's plugin API stays Rollup-compatible.

The chunk-size advisory now reads build.rolldownOptions instead of rollupOptions — informational, expected.

Rolldown surfaces two new informational warnings during the studio build:

  • Direct eval use in Monaco editor — known upstream Monaco behavior.
  • BatchedMesh import missing from three@0.155.0 via three-mesh-bvh — pre-existing; resolves on a three.js bump.

Verification

  • pnpm install clean across all four major bumps.
  • pnpm prepare-packages clean at every step.
  • pnpm --filter studio build clean; service worker generates.
  • Vitest suites: 49/49 pass (39 replicad + 4 replicad-evaluator + 6 replicad-cli) under vitest 2.x.
  • Dev-server browser smoke in headless Chromium against http://localhost:5555/workbench: studio renders the Workbench UI byte-identical to a main baseline screenshot. The screenshots in the downstream PR (linked for reference) show the before/with/without-dedupe states.

A note on CI

Your publish-studio.yml currently uses pnpm/action-setup@v2 version: 8 with the pnpm@9.13.2 packageManager and a --no-frozen-lockfile flag, which means CI regenerates the lockfile every run. Not breaking, just wasteful. Worth bumping to pnpm/action-setup@v4 + version: 9 and 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 standalone rollup@3.10.1 (not Vite). Independent migration; deferred.
  • Vite Environment API (introduced in v6) — replicad doesn't do SSR; skip.
  • rolldownOptions.output.advancedChunks tuning — defaults are fine.

dnplkndll added 6 commits May 12, 2026 17:37
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.
@dnplkndll

Copy link
Copy Markdown
Contributor Author

Re: CI run 25765302805 — the build steps all passed (pnpm install, prepare-packages, studio + app-example builds). The failure is on the cloudflare/pages-action@v1 step with Input required and not supplied: apiToken — secrets are not exposed to fork-PR runs by GitHub Actions, so this is expected and unrelated to the upgrade. The publish step would succeed once it runs in your repo's context (e.g. on push to main after merge).

One small heads-up I should have included in the PR body: Rolldown surfaces three new info-level warnings that Rollup hid — Module "path" / "fs" / "crypto" has been externalized for browser compatibility, imported by replicad-opencascadejs/src/replicad_single.js. That's the Emscripten output of opencascade.js pulling node builtins in a path the browser bundle tree-shakes away. Harmless, just noisier than before.

@sgenoud

sgenoud commented May 18, 2026

Copy link
Copy Markdown
Owner

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
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.

2 participants