Skip to content

Build & Deploy

RMT Compose is a plain Vite ES-module app — no framework, no TypeScript in src/ — plus a separate VitePress site in docs/. One repo, two independent Vercel projects.

Requirements

Node20.19+ or 22.12+. package.json declares no engines field, but Vite 7.1.12's own engines is ^20.19.0 || >=22.12.0. Node 18 will not run it.
Runtime dependencyfraction.js ^5.3.4 (BigInt-backed) — the only one
devDependenciesplaywright, pngjs, vite, vitepress
BrowserWebGL2 is a hard requirement — the workspace does not initialise without it. WASM support is not required.
Rust + wasm-packOptional. Only for rebuilding the crate — see Building WASM
ffmpegOptional. Only for npm run samples:build
bash
npm ci
npm run dev     # http://localhost:3000, opens your browser

The dev server port (3000) and auto-open are set in vite.config.js.

The scripts

All 13, and what each one really does.

ScriptCommandWhat it does
devviteDev server on port 3000, auto-opens the browser
buildnpm run wasm:build && vite buildFull build. Requires Rust + wasm-pack. Vercel does not use this
previewvite previewServes the built dist/ on Vite's default preview port (4173)
testnode scripts/validate-modules.mjs && node scripts/test-exactness.mjsNot a unit-test runner. Validates every library module, then proves arbitrary-precision exactness end to end
gen:intervalsnode scripts/gen-interval-catalog.mjsRegenerates public/modules/intervals/ and patches the intervals section of library.json
wasm:buildcd rust && wasm-pack build --target web --out-dir pkg && cd .. && node scripts/sync-wasm.mjsBuilds the Rust crate, then copies the artifacts into src/wasm/
wasm:syncnode scripts/sync-wasm.mjsThe copy step alone
perf:gennode scripts/perf/generate-stress-module.mjsWrites stress modules to public/modules/perf/
perf:benchnode scripts/perf/bench-node.mjsHeadless Node evaluation benchmark
samples:buildnode scripts/build-samples.mjsRebuilds the piano and violin multisamples. Needs ffmpeg and network access
docs:devvitepress dev docsDocs dev server
docs:buildvitepress build docsBuilds the docs
docs:previewvitepress preview docsPreviews the built docs

npm run build is not the command for a fresh contributor

It chains wasm:build, which needs a Rust toolchain. To produce a production bundle without Rust, run npx vite build — byte for byte what Vercel runs.

npm test is not a test suite

There is no Jest and no Vitest anywhere in this repo. npm test runs two scripts and exits non-zero if either fails: scripts/validate-modules.mjs, which validates every module referenced by public/modules/library.json, and scripts/test-exactness.mjs, which proves exact rational arithmetic survives a 200-note (3/2) chain (checked against native BigInt) and that a ~98-digit LOAD_CONST_BIG literal round-trips digit-exact. Run it before you touch anything under public/modules/, the compilers, or the evaluator. The checks are detailed in Development Setup.

Scripts with no npm alias

Run these with node directly.

ScriptPurpose
scripts/gen-chords-progressions.mjsGenerates the chords / progressions / cadences modules and patches library.json
scripts/gen-melodies.mjsGenerates the public-domain melodies and the scale-systems category
scripts/perf/test-wasm-swap.mjsHeadless end-to-end test of the WASM evaluator hot-swap
scripts/perf/bench-render.mjs, bench-drag.mjs, bench-hover.mjs, bench-pick.mjs, bench-marquee.mjs, profile-sync.mjs, converge.mjs, who-dirties.mjsPlaywright render/interaction benchmarks. They drive a running dev server
scripts/perf/visual-regress.mjsPlaywright pixel-diff regression harness (--capture / --compare)

All the app-facing Playwright harness scripts default to --url http://localhost:3000 — the dev server's pinned port — so --url is only needed for a non-default server. The only exceptions are the two docs-site scripts: check-docs-rendered.mjs (4173, the VitePress preview port) and shot-docs.mjs (3005).

See Performance for how to use them.

What the Vite build produces

npx vite build on the current commit:

ChunkRawGzip
index-*.js (entry)422.70 kB110.97 kB
renderer-*.js264.21 kB54.14 kB
dsl-*.js25.70 kB7.86 kB
settings-panel-*.js (async)24.80 kB8.46 kB
rmt_core-*.js (WASM glue)16.72 kB4.69 kB
instruments-*.js9.46 kB3.03 kB
perf-harness-*.js (async, ?perf only)6.00 kB2.34 kB
vendor-*.js (fraction.js)6.81 kB2.48 kB
rmt_core_bg-*.wasm384.01 kB146.96 kB
index.html8.80 kB2.97 kB

Output goes to dist/ (gitignored). publicDir: 'public' is copied verbatim, so modules/, samples/, images/, styles.css, favicon.ico, robots.txt, sitemap.xml and license.html all ship as-is.

Chunking

manualChunks in vite.config.js splits the entry monolith into independently-cacheable pieces. The rules run in order:

ChunkMatchesWhy
vendor/node_modules/fraction.js — effectively immutable, highest cache value
rendererrenderer/webgl2/renderer.js, renderer-config.jsA verified singleton-free leaf: it imports only renderer-config.js
dsl/src/dsl/, binary-note.js, binary-utils.jsThe bytecode foundation is bundled here deliberately — it breaks the core↔dsl cross-chunk cycle
instruments/src/instruments/Singleton-free leaf
(entry)everything elseplayer.js, module.js, workspace, menu bar, modals, audio, theme, settings store

The singletons (eventBus, app-state, settingsStore) stay coalesced in the entry chunk on purpose, so they can never be duplicated across chunks. id is normalised to forward slashes so the matching works on Windows.

Two chunks come from dynamic imports rather than the manualChunks rules. settings-panel-*.js is import()ed during initApp() (src/main.js:112), so it is out of the entry bundle but still fetched on every boot. perf-harness-*.js is genuinely conditional — it is only ever downloaded with ?perf in the URL (src/main.js:133).

optimizeDeps.exclude: ['rmt-core'] is vestigial

rmt-core is not an npm dependency — the WASM glue is imported by relative path. The exclusion matches nothing. Harmless, but do not read it as evidence of a package.

The committed WASM artifacts

This is the one non-obvious thing about the build.

rust/pkg/                    ← wasm-pack output. GITIGNORED.
  └── (copied by scripts/sync-wasm.mjs)

src/wasm/rmt_core.js         ← COMMITTED. 42,725 bytes.
src/wasm/rmt_core_bg.wasm    ← COMMITTED. 384,008 bytes.

Vercel's build command is plain vite build. It never runs wasm-pack, and the build image has no Rust toolchain. The committed src/wasm/ files are what gets bundled and shipped. After any change under rust/, run npm run wasm:build and commit the two synced files, or the deploy keeps serving the old binary with no warning.

The binary ships in the bundle but is not fetched on a normal page load: initWasm() returns early unless ?evaluator=wasm is present, because the WASM evaluator is opt-in and currently blocked. Only the opt-in (or headless Node) pays the download and instantiation.

Deploys

Two Vercel projects, one repo.

AppDocs
Domainhttps://rmt.worldhttps://docs.rmt.world
Vercel root directoryrepo rootdocs/
Config filevercel.jsondocs/vercel.json
frameworkvitevitepress
installCommandVercel defaultnpm install
buildCommandvite buildnpm run buildvitepress build
outputDirectorydist.vitepress/dist

The app's buildCommand deliberately overrides npm run build with bare vite build — that is what skips wasm:build and consumes the committed artifacts instead.

docs/ has its own package.json and its own package-lock.json, and its scripts (dev, build, preview) call vitepress with no path argument, because Vercel's root directory is already docs/. From the repo root, use the docs:* scripts instead.

Docs site config

docs/.vitepress/config.ts sets the sitemap hostname to https://docs.rmt.world, local search (provider: 'local'), the GitHub edit link, and lineNumbers: true on code fences.

A dead link fails the docs build

ignoreDeadLinks: false. One broken root-relative link and npm run docs:build exits non-zero, naming the file. Write cross-links without the .md extension (/developer/wasm/overview, not /developer/wasm/overview.md), and run npm run docs:build before you push.

Release checklist

  1. npm test — the module library still validates.
  2. If you touched rust/: npm run wasm:build, then commit src/wasm/rmt_core.js and src/wasm/rmt_core_bg.wasm.
  3. npx vite build — reproduces the Vercel build locally, without Rust.
  4. npm run preview — sanity-check the bundle.
  5. If you touched the renderer: measure and pixel-diff. See Performancevisual-regress.mjs defaults to a 300-pixel tolerance (MSAA resolution is not bit-deterministic across runs, so a 0-pixel gate would be permanently red).
  6. If you touched the docs: npm run docs:build.

Licensing

The project is MIT (LICENSE.md, and license = "MIT" in rust/Cargo.toml). Third-party components, per THIRD_PARTY_NOTICES.md: fraction.js (MIT), the VSCO-2 Community Edition sample sets (CC0), Roboto Mono (Apache-2.0).

See also

Released under the MIT License