Build system
The toolchain is Vite+ (vite-plus) on pnpm: vp pack (tsdown/rolldown) builds, vp test (Vitest) tests, vp check/lint/fmt (oxlint/oxfmt) checks, vp run runs cached tasks, and vp staged with .vite-hooks replaces husky and lint-staged. pnpm is pinned through devEngines and downloaded by vp.
Per-package build
Each buildable package has a vite.config.ts calling packageConfig(import.meta.dirname, { pack: { entry: {...} }, test: true }) from tooling/pack.ts. It returns:
- the
packblock: ESM only, bundled per entry (so code inlined from private packages is tree-shaken),.js/.d.tsoutput, hidden sourcemaps (excluded from tarballs); - the
buildandtesttasks.
build and test are tasks in the config, not package.json scripts (a task cannot be defined in both).
Task ordering
Workspace dependencies use the workspace:^ protocol; that is what lets vp run -r and --filter '<pkg>...' order builds. build and test first build the package's dependencies and devDependencies.
A package that imports another workspace package (or extends @mrmt/tsconfig) must declare it. pnpm's node_modules is strict: nothing is hoisted by accident.
API reference
A generated API reference is opt-in, for packages whose option types people look up: today @mrmt/vite and @mrmt/vite-ui. The other packages are small and covered by their handwritten pages plus the JSDoc in their .d.ts files; a reference for them was mostly noise (the CLI generated 17 pages for a single public entry).
A package opts in with one line in its vite.config.ts:
pack: { entry, plugins: [apiDocs(import.meta.dirname, entry)] },apiDocs (tooling/typedoc.ts) runs the TypeDoc plugin (@mrmt/unplugin-typedoc, Rolldown flavour) during vp pack, documents only the package's public entries (its pack.entry) and writes packages/<pkg>/docs/api/ (gitignored). To use it a package must:
- list
@mrmt/unplugin-typedocas aworkspace:^dependency (devDependency is enough), sovpbuilds the plugin first; - not be
@mrmt/utilsor@mrmt/unplugin-typedocitself: the plugin depends onutils, so that would make the build graph cyclic.
The plugin is imported lazily, in buildStart: package configs are evaluated to build the task graph, before anything (the plugin included) is built. docs/** is excluded from the build task's cache inputs since TypeDoc cleans and rewrites it.
See Documentation for how the site reads these files.
Private packages are inlined
@mrmt/utils is private: consumers list it under devDependencies and tsdown bundles (and tree-shakes) what they import, types included.
- A private package's own runtime dependencies must be declared by every consumer that inlines it (
@mrmt/vitelistszodfor the inlinedpackage-jsonschema). - Modules with global state (the comment-type registry in
codegen) must be inlined by one package only, otherwise each package gets its own copy. Today only@mrmt/viteusescodegen; if another package needs it, re-export it from@mrmt/vite(@mrmt/vite/codegendoes this for the public part). - Don't import
@mrmt/utilsin files that run outside a build of the consumer: it is not installed for end users.
Packing and releasing
pnpm pack,vp pm packandvp pm publishrewriteworkspace:^to real ranges (^0.6.1) in the tarballs. Never usenpm pack/npm publish.- Releases use changesets with a fixed (lockstep) group:
vp run versionthenvp run release. Untested end to end: check thatchangeset publishfinds pnpm before the first real release. - Scaffold tests pack every publishable package (not the private root or
@mrmt/utils) and install all the tarballs together: see Testing.
Type-checking
dts: true generates declarations, but vp check does not type-check. Use vp exec tsc --noEmit -p packages/<name>/tsconfig.json, and -p tsconfig.json at the root (it covers every package's vite.config.ts and tooling/). Cross-package types come from built output, so build first.
Known issues
qsupin in@mrmt/vitepress-plugin-sidebar.qsu@1.100.0(2026-10-23) renamedcapitalizeEachWordstocapitalizeEachWordin a minor release, which breaksvitepress-sidebar@1.40.0(it still allowsqsu ^1.18.0). The package therefore depends onqsu ~1.21.0directly: npm reuses that copy forvitepress-sidebar, which keeps fresh installs (and the scaffold tests) working. pnpm consumers still resolve the newestqsuforvitepress-sidebarand needoverrides: { qsu: "~1.21.0" }until upstream is fixed. Remove the pin once avitepress-sidebarrelease supports the newqsu.