Skip to content

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 pathSourcePurpose
@mrmt/vite/libpackages/vite/src/lib.tsLib(), LibConfig()
@mrmt/vite/docspackages/vite/src/docs/index.tsDocs()
@mrmt/vite/dtspackages/vite/src/dts.tsdts(), getDTSPlugin()
@mrmt/vite/lib/contextpackages/vite/src/lib/context.tsVitePluginContext, contextFactory()
@mrmt/vite/codegenpackages/vite/src/codegen.tsregisterCommentTypeResolver() (re-export of the inlined utils)
@mrmt/vite/base-configpackages/vite/src/base-config.tsBaseConfig(), BaseBundle()
@mrmt/vite/package-metapackages/vite/src/package-meta/PackageMeta() plugin
@mrmt/unplugin-typedocpackages/unplugin-typedoc/src/typedoc.tsvite, rolldown, bootstrapTypeDoc()
@mrmt/vitepress-plugin-sidebar (+ /indent.css)packages/vitepress-plugin-sidebar/src/sidebar.tssidebar plugin, re-exports vitepress-sidebar
@mrmt/vitepress-plugin-sidebar/workspacepackages/vitepress-plugin-sidebar/src/workspace.tsworkspaceDocs(): monorepo docs config
@mrmt/vite-uipackages/vite-ui/src/index.tsUILib(), UIApp(), UICommon()
@mrmt/vitest (+ /tmpdir)packages/vitest/src/withTmpDir(), createTmpDir()
@mrmt/utils/*packages/utils/src/internal only (private package, inlined)
@mrmt/tsconfig/tsconfig.*.jsonpackages/tsconfig/tsconfig bases
marmotte/apipackages/cli/src/cli/api.tsrunCreate(), 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, and bare (only package.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 with add-<id>.
  • package.json is composed, not mutated. blocks/package-json.ts owns the file. Other blocks add scripts, devDependencies and properties to it as addons, and Stratum merges them.
  • Setup vs transition. setup creates a project; the setup() hook of a block (starter code) only runs then, so later edits are never overwritten. transition updates an existing project: the existing package.json is 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 edited scripts.lint is reset. Blocks the template doesn't know about still abort on a conflict.
  • Custom templates (marmotte create -t user/repo or a local path) are plain directories copied by custom-template.ts ( and are substituted in package.json); selected features are then added to the result with the bare preset 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:

ts
// 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:

ts
const ctx = new DefaultVitePluginContext({ root: cfg.root });
ctx.resolve("sourceDir", "index.ts"); // → /abs/path/src/index.ts

DefaultVitePluginContext maps sourceDir → src/. Use contextFactory() to create contexts with custom named paths and typed plugin options. It takes two arguments:

  1. Options object: plugin config with defaults (empty {} if none). Keys become non-optional on ctx.options.
  2. Factory config: { paths: { ... } }. Path values can be a string (relative to root) 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.

ts
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 .bak is 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.