Architecture
Repository layout
package.json private root: vite-plus, devEngines (pnpm), scripts
pnpm-workspace.yaml workspace globs, dependency overrides (Vite+ core, vitest), allowed build scripts
vite.config.ts root: staged hooks, docs tasks (docs, build:docs)
tooling/
pack.ts packageConfig(): shared `vp pack` baseline + build/test tasks for a package
packages/
tsconfig/ @mrmt/tsconfig tsconfig bases (JSON only, no build)
utils/ @mrmt/utils PRIVATE internal helpers, inlined, never published
vitest/ @mrmt/vitest withTmpDir(), createTmpDir()
vite/ @mrmt/vite Lib, Docs, dts, externals, PackageMeta, VitePluginContext
unplugin-typedoc/ @mrmt/unplugin-typedoc TypeDocPlugin() for Vite and Rolldown
vitepress-plugin-sidebar/ @mrmt/vitepress-plugin-sidebar sidebar plugin, workspace docs helper, indent.css
vite-ui/ @mrmt/vite-ui UILib / UIApp / UICommon (Vue + Vuetify)
cli/ marmotte CLI + MCP server (bins: marmotte, marmotte-mcp) + scaffold tests
docs/ site shell: home page and cross-cutting docs (this section included)
.vite-hooks/ git hooks (pre-commit runs `vp staged`)Dependency graph: utils (private, inlined as a devDependency of the others) → unplugin-typedoc → vite → vite-ui; utils → vitepress-plugin-sidebar, cli; vitest and tsconfig stand alone. vitepress-plugin-sidebar is not used by vite: it is only named in files generated by Docs, so the scaffold templates list it.
Package exports
| Import path | Source | Purpose |
|---|---|---|
@mrmt/vite/lib | packages/vite/src/lib.ts | Lib(), LibConfig() |
@mrmt/vite/docs | packages/vite/src/docs/index.ts | Docs() |
@mrmt/vite/dts | packages/vite/src/dts.ts | dts(), getDTSPlugin() |
@mrmt/vite/lib/context | packages/vite/src/lib/context.ts | VitePluginContext, contextFactory() |
@mrmt/vite/codegen | packages/vite/src/codegen.ts | registerCommentTypeResolver() (re-export of the inlined utils) |
@mrmt/vite/base-config | packages/vite/src/base-config.ts | BaseConfig(), BaseBundle() |
@mrmt/vite/package-meta | packages/vite/src/package-meta/ | PackageMeta() plugin |
@mrmt/unplugin-typedoc | packages/unplugin-typedoc/src/typedoc.ts | vite, rolldown, bootstrapTypeDoc() |
@mrmt/vitepress-plugin-sidebar (+ /indent.css) | packages/vitepress-plugin-sidebar/src/sidebar.ts | sidebar plugin, re-exports vitepress-sidebar |
@mrmt/vitepress-plugin-sidebar/workspace | packages/vitepress-plugin-sidebar/src/workspace.ts | workspaceDocs(): monorepo docs config |
@mrmt/vite-ui | packages/vite-ui/src/index.ts | UILib(), UIApp(), UICommon() |
@mrmt/vitest (+ /tmpdir) | packages/vitest/src/ | withTmpDir(), createTmpDir() |
@mrmt/utils/* | packages/utils/src/ | internal only (private package, inlined) |
@mrmt/tsconfig/tsconfig.*.json | packages/tsconfig/ | tsconfig bases |
marmotte/api | packages/cli/src/cli/api.ts | runCreate(), runSetup(), applyTemplate(), catalogs |
There are no ./* wildcard exports: every public path is listed explicitly in the package's package.json.
Scaffolding
marmotte create and marmotte setup write files with a Bingo template (the Stratum engine) in packages/cli/src/template/. The prompts and flags stay ours (src/cli/); applyTemplate() in src/cli/scaffold.ts runs the template offline, without GitHub, git or installs.
- Presets are the project types:
node-library,ui-library,ui-app, andbare(onlypackage.json, for projects that already exist). - Blocks are the pieces: a build block (
vite.config.ts, tsconfigs, scripts, devDependencies) and a starter block per preset, and the five features (lint,format,pre-commit,changesets,version-lifecycle), which are in no preset and added withadd-<id>. package.jsonis composed, not mutated.blocks/package-json.tsowns the file. Other blocks addscripts,devDependenciesandpropertiesto it as addons, and Stratum merges them.- Setup vs transition.
setupcreates a project; thesetup()hook of a block (starter code) only runs then, so later edits are never overwritten.transitionupdates an existing project: the existingpackage.jsonis read back (intake) so what the user added survives. - The template wins on what it manages. Stratum aborts if an existing value differs from the one a block adds. The package.json block therefore leaves out of what it reads back the keys that the selected blocks set (
managedByTemplate), so an editedscripts.lintis reset. Blocks the template doesn't know about still abort on a conflict. - Custom templates (
marmotte create -t user/repoor a local path) are plain directories copied bycustom-template.ts(andare substituted inpackage.json); selected features are then added to the result with thebarepreset in transition mode. - Blocks are tested in memory with
testBlock/testIntake(bingo-stratum-testers); see Testing.
Bingo is 0.x (bingo 0.13, bingo-stratum 0.8) and its documentation lags the code: read the .d.ts files in node_modules/bingo-stratum when the docs and the types disagree.
Code patterns
Plugin composition
Plugins are ordinary arrays of Vite Plugin objects. Higher-level plugins (Lib, UILib) delegate to lower-level ones and accept their options as pass-through:
// Lib() = BaseBundle() + dts() + nodeExternals() + Docs() + LibConfig()
export function Lib(options: LibPluginOptions = {}) { … }VitePluginContext: path resolution
All plugins that need filesystem paths use VitePluginContext (or the pre-configured DefaultVitePluginContext). It provides typed, root-relative path resolution:
const ctx = new DefaultVitePluginContext({ root: cfg.root });
ctx.resolve("sourceDir", "index.ts"); // → /abs/path/src/index.tsDefaultVitePluginContext maps sourceDir → src/. Use contextFactory() to create contexts with custom named paths and typed plugin options. It takes two arguments:
- Options object: plugin config with defaults (empty
{}if none). Keys become non-optional onctx.options. - Factory config:
{ paths: { ... } }. Path values can be a string (relative toroot) or a tuple["baseName", "relative"](relative to another named path).
Option keys and path keys must not overlap; TypeScript errors if they share a name.
const MyContext = contextFactory(
{ serve: "/docs/" as string | false },
{ paths: { sourceDir: "./src", testDir: ["sourceDir", "__tests__"] } },
);
// ctx.options.serve → string | false (default applied)
// ctx.options.sourceDir → string | undefined (path override)
// ctx.resolve("testDir") → "<root>/src/__tests__"Auto-managed vs default files
- Auto-managed (
writeAutoGeneratedFile): always overwritten by the tool; humans should not edit them; a.bakis created if a manual edit is detected. - Default (
writeDefaultFile): written once; users are free to edit; will not be overwritten.
Entry point discovery
resolveEntries(dir, filter) (from @mrmt/utils/fs) walks a directory and returns a Rollup entry map. filter is a RegExp or (path: string) => boolean.
Comment type detection
resolveCommentType(filePath) auto-detects //, #, or <!-- --> from the file extension. New types can be registered with registerCommentTypeResolver() (public via @mrmt/vite/codegen).
Package name and version injection
vp pack replaces import.meta.env.VITE_PACKAGE_NAME and import.meta.env.VITE_PACKAGE_VERSION with the package's own name and version at build time (see tooling/pack.ts). A generated-file header therefore names the package whose code wrote it. Packages are versioned in lockstep.