Skip to content

chore(deps): vite 4 → 8 + Rolldown, vitest 4, snapshot-matcher fixes#1

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

chore(deps): vite 4 → 8 + Rolldown, vitest 4, snapshot-matcher fixes#1
dnplkndll wants to merge 10 commits into
mainfrom
chore/vite-8-rolldown

Conversation

@dnplkndll

@dnplkndll dnplkndll commented May 12, 2026

Copy link
Copy Markdown

Summary

Step-wise upgrade of Vite (4 → 5 → 6 → 7 → 8) across the four Vite-using packages in the monorepo (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.

Direct analog to ledoent/cad-plm PR sgenoud#43.

What changed

Dependency bumps (packages/*/package.json)

  • vite ^4 → ^8 (4 packages).
  • @vitejs/plugin-react 3.0.1 / ^4.3.4 → ^6.0.1 (studio, replicad-app-example).
  • vite-plugin-pwa ^0.16.7 → ^1.0.3 (studio).
  • vite-plugin-dts ^3.5.2 → ^4.5.0 (replicad, replicad-evaluator).
  • vitest ^0.28.3 → ^4.1.10 (replicad, replicad-evaluator, replicad-cli) — see the vitest section below.
  • workbox-window ^7.3.0 → ^7.4.1 + new workbox-build ^7.4.1 — vite-plugin-pwa 1.x peers.

Toolchain

  • .nvmrc v20.12.1 → v20.19.0 — Vite 7+ Node floor.

Studio dev-mode fix

  • packages/studio/vite.config.js gains resolve.dedupe: ["react", "react-dom"]. Three React versions live in the workspace (18.2.0, 18.3.1, 19.1.0 — pinned by replicad-app-example, used by studio, and pulled transitively by replicad-docs/docusaurus respectively). 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, producing Invalid hook call. You might have more than one copy of React in the same app at runtime when mounting the Workbench tree. Dedupe is the standard remedy and is a dev-mode-only concern (Rolldown collapses duplicates in prod).

Each upgrade major lives in its own commit so any regression bisects to the exact bump. The dedupe fix is a separate commit on top so the review pattern matches.

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). Dev startup wasn't measured here; upstream reports ~3× faster.

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 for in-browser script bundling — orthogonal to the build-time bundler, unaffected.
  • 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 — Rollup-compatible plugin API in Rolldown means these stay as-is.

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

vitest 2 → 4 (folded into this PR)

The initial pass bumped vitest only to 2.x, which left the dependency tree carrying two Vite copies: vite@8.0.12 for the packages and vite@5.4.21 nested under vitest/vite-node, because vitest 2 and 3 depend on their own Vite rather than taking it as a peer. That is the same dual-instance class as the studio React fix above, one layer down. vitest 4 declares Vite as a peer (^6 || ^7 || ^8), so the tree collapses to a single vite@8.0.12. It's folded in here rather than shipped as a follow-up so Vite 8 never lands alongside a stale nested Vite 5.

The upgrade surfaced two defects in the repo's custom SVG snapshot matcher (packages/replicad/__tests__/toMatchSVGSnapshot.ts), both of which let the SVG suite go green while asserting nothing:

  • Orphaned baselines. The snapshot filename was built from currentTestName, which vitest 2 prefixed with the relative test path and vitest 4 does not. Every committed baseline was silently renamed out from under the suite. The identifier now strips any file-path prefix and depends only on the test's own name (the snapshot dir is already per-file, so the prefix was always redundant).
  • Fail-open on a missing baseline. A missing golden file was auto-written and reported as a pass in plain vitest run; only --ci failed it. Combined with the rename, the suite reported all-green while regenerating every baseline from scratch. A missing baseline is now a hard failure unless the run explicitly records with -u.

One genuinely flaky test fixed. build from triangles shuffled its input with Math.random() before fusing. The fused geometry is identical regardless of order, but toSVG() serializes subpaths in input order, so the exact-string snapshot varied every run — it passed historically only by luck. Input order is now a fixed permutation; verified deterministic across three fresh processes.

Baseline re-record. The identifier change renames every baseline, so the golden files are re-recorded: 26 are pure renames (byte-identical, git R100), build from triangles reflects its new fixed input, and four offset vase baselines were stale — offsetting a spline is an iterative approximation whose control points are toolchain-sensitive, so those 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, confirming curve math didn't shift — only the spline-offset approximation. One dead baseline (offset vase, with offset -5, no matching case) was removed.

⚠️ Follow-up: offset-of-spline snapshots remain toolchain-sensitive. If this suite ever gains CI, those four vase cases should assert a geometric invariant (bounding box + normalized path set) rather than an exact SVG string.

Rolldown surfaces a couple of new informational warnings during the studio build:

  • Direct eval use in Monaco editor — known upstream behavior.
  • BatchedMesh import missing from three@0.155.0 (via three-mesh-bvh) — pre-existing; would resolve 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 4.1.10, single vite@8.0.12 in the tree. Full replicad suite verified deterministic across repeated fresh processes.
  • Dev-server browser smoke in headless Chromium against http://localhost:5555/workbench:
    • Before dedupe fix (with the dual-React regression): React error boundary tripped before the Workbench tree mounted — see replicad-vite8-broken.png below.
    • After dedupe fix on this branch: studio renders the same Workbench UI as main — editor, toolbar, language dropdown, etc. — see replicad-vite8-fixed.png below. Both are byte-identical to a main baseline screenshot (replicad-main-ok.png); the remaining WebGL context could not be created console error is a headless-browser artifact unrelated to the upgrade (it's present on main too).

Visual evidence

Step Screenshot
main baseline (pre-upgrade) main
Branch without dedupe fix (the regression) broken
Branch with dedupe fix (this PR) fixed

Out of scope

  • packages/replicad-threejs-helper — uses standalone rollup@3.10.1 (not Vite). Independent migration; deferred.
  • CI workflow (.github/workflows/publish-studio.yml) currently uses pnpm/action-setup@v2 version: 8 with our pnpm@9.13.2 lockfile and actions/setup-node@v3 node-version: 20. Pre-existing and tangentially related; left to a follow-up.
  • Migrating to Vite's Environment API (introduced in v6) — we don't do SSR; skip.
  • Tuning rolldownOptions.output.advancedChunks for the studio bundle — defaults are fine.

dnplkndll added 4 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.
@dnplkndll dnplkndll force-pushed the chore/vite-8-rolldown branch from 45183e6 to b1ce543 Compare May 12, 2026 21:38
dnplkndll added 2 commits May 12, 2026 17:48
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.
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
@dnplkndll dnplkndll changed the title chore(deps): vite 4 → 8 + Rolldown bundler chore(deps): vite 4 → 8 + Rolldown, vitest 4, snapshot-matcher fixes Jul 11, 2026
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