This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called vp. Vite+ is distinct from Vite, and it invokes Vite through vp dev and vp build. Run vp help to print a list of commands and vp <command> --help for information about a specific command.
Docs are local at node_modules/vite-plus/docs or online at https://viteplus.dev/guide/.
vp <name> runs a built-in command. vp run <name> runs a package.json script or a vite.config.ts task. Scripts cannot overwrite built-ins, so vp dev and vp run dev may do different things. Check package.json and vite.config.ts first, and run vp run <name> when the project defines a script or task with that name.
Run vp toolchain to show versions and relationships in the active Vite+
release. Add a tool name to select part of the graph. For example, run
vp toolchain vite. Use --global to ignore the local vite-plus package. Use
vp why <package> to show the package-manager dependency graph.
- Run
vp installafter pulling remote changes and before getting started. - Run
vp checkandvp testto format, lint, type check and test changes. - Check if there are
vite.config.tstasks orpackage.jsonscripts necessary for validation, run viavp run <script>. - If setup, runtime, or package-manager behavior looks wrong, run
vp env doctorand include its output when asking for help.
This workspace uses Effect 4, pinned in the catalog
in pnpm-workspace.yaml and currently on a release candidate. Before writing
Effect code, read node_modules/effect/AGENTS.md in full, and search
node_modules/effect/src for anything it does not cover.
Two things about this version are worth knowing before designing anything on
top of it, both found the hard way while building
packages/effect-xml-codec:
- The schema derivations are internal.
toCodecJson,toCodecIsoandtoCodecStringTreewalk a schema AST throughSchemaAST.replaceEncodingand a per-noderecurwalker, both/** @internal */and stripped from the published.d.ts;./internal/*is not on the export map. A derivation of your own cannot reuse them, and casting overeffect/SchemaASTto reach them pins a library to one patch release. Schema.decodeTodeclares its transformation getters backwards. It typesdecodeasTo.Encoded → From.TypeandencodeasFrom.Type → To.Encoded, and its implementation runs them the other way round. Every use inside Effect's own source is a target that is aDeclarationor a primitive — whereTypeandEncodedcoincide and the inversion is invisible — so composing two genuine codecs through it silently needs casts.test/behaviour, not the types, is what to trust here.
Benchmarks are Vitest benchmarks, not scripts. A file named *.bench.ts is
collected by the benchmark project, which vp test skips and vp test bench
runs alone.
vp run bench— every benchmark in the workspace (vp test benchunder the hood).vp -C packages/benchmarks test bench— the benchmark project.vp -C packages/benchmarks test bench -t <name>narrows by test name.vp checkcoversbench/like any other source: the benchmark package'stsconfig.jsonalready listsbench/**/*.tsininclude, so a benchmark that stops type-checking fails the review checklist like anything else.
Two things the Vitest benchmarking guide warns about, both of which the existing files already handle and any new one needs to:
-
A discarded result is a result the JIT may delete. Fold it into a module-scope counter and read that back in
afterAll, or the benchmark measures nothing and reports a very fast number. -
benchis a test-context fixture, not an import. Vite+ 1.0 bundles Vitest 5, which dropped the top-levelbenchthe v4 files imported. A benchmark is atestthat takesbenchoff its context and registers its workloads inside:test('encoding — a small document', async ({ bench }) => { await bench.compare(bench('this codec', measure(...)), bench('fast-xml-builder', measure(...)), BUDGET); });
One
testper configuration, withbench.compareover the workloads in it, so the printed table is the comparison and the fastest row is marked; a single workload callsbench(name, fn).run(BUDGET)instead.BUDGET— the sample and warmup window — moved with the rest:timeandwarmupTimeare run options in v5, handed torunorcompare, not tobenchitself. A benchmark option that moved without being noticed is a silently changed measurement, so check the durations after a Vite+ upgrade rather than assuming the numbers mean what they meant.
A third thing the guide does not warn about, and the one that actually bit:
- A benchmark is a
test, and atesthas a 60s timeout. Tinybench runs a task until eithertimeelapses oriterationssamples are collected — whichever comes last — and its defaults aretime: 1000withiterations: 64. A workload heavy enough to outrun the time window (a full-document parse of a large fixture, say) is therefore run 64 times regardless, and 64 samples of a one-second task is a minute of work in a suite that cannot exceed 60. The fix is to piniterationson any suite whose single iteration is not tens of microseconds —iterations: 3samples a slow task three times, and a longertimewindow still smooths the sample. The current suites finish well inside the timeout, so none of them pins it; add the pin, and a comment saying why, the moment a workload gets slow enough to need it.
- After a task runs and its checks pass, commit the work. Do not wait to be asked.
- Skip the commit when the user says not to commit, or when there is nothing worth committing (no file changes).
- Never push. Leave the commit on the current branch for the user to review.
- Before committing, review the diff and leave out unrelated changes, secrets, and generated noise.
- One logical change per commit. Don't fold unrelated edits into a single commit.
- If a commit hook rewrites files (formatter, linter), re-stage and amend rather than starting a new commit.
Every commit message follows Conventional Commits with the Angular convention. The subject and any scope are lowercase, the subject is imperative and under 72 characters, and no line in the body exceeds 100 characters.
<type>(<scope>): <subject>
<body>
<footer(s)>
type— one offeat,fix,perf,refactor,docs,style,test,build,ci,chore. Usedocsfor documentation-only changes,chorefor things that touch no source or tests (tooling, dependencies, generated files).scope— optional, but include it when the change is confined to one package or area:refactor(builder): make XMLBuilder an Effect service,perf(effect-xml-codec): resolve names without an Effect per call. Omit it for workspace-wide changes.subject— imperative mood, no leading capital, no trailing period. "add", not "added" or "adds".body— optional. Separate it from the subject with one blank line, and explain what and why rather than repeating the diff.footer— optional. Reference issues (Refs #123) or note a breaking change.- Breaking changes — mark them with
!after the type or scope (feat(api)!: drop the legacy parse entry point) and add aBREAKING CHANGE: <description>footer. - Match the existing history:
refactor(parser): make XMLParser an Effect service,fix(parser): stop the parse benchmarks timing out, anddocs: record the effect-services refactor progress.