This package is part of the Mochi-CSS project. It provides the core infrastructure for statically extracting Mochi-CSS styles from TypeScript/TSX source files and generating plain CSS at build time.
Note: This package is primarily consumed by integrations such as
@mochi-css/postcss. You only need it directly when building a custom integration or tooling on top of Mochi-CSS.
npm i @mochi-css/builder --save-devThe builder is configured through plugins. The typical setup loads a plugin into a FullContext, then passes the collected stages and hooks to Builder:
import { Builder, RolldownBundler, VmRunner } from "@mochi-css/builder"
import { FullContext } from "@mochi-css/config"
import { writeFile } from "fs/promises"
// Load plugins into a context (see @mochi-css/plugins and @mochi-css/vanilla/config)
const ctx = new FullContext(() => {})
myPlugin.onLoad(ctx)
const builder = new Builder({
roots: ["./src"],
stages: [...ctx.stages.getAll()],
bundler: new RolldownBundler(),
runner: new VmRunner(),
sourceTransforms: [...ctx.sourceTransforms.getAll()],
emitHooks: [...ctx.emitHooks.getAll()],
emitDir: "./dist/mochi",
cleanup: () => ctx.cleanup.runAll(),
})
const { global, files } = await builder.collectMochiCss()
if (global) await writeFile("dist/global.css", global)
if (files) {
for (const [source, css] of Object.entries(files)) {
await writeFile(source.replace(/\.(ts|tsx)$/, ".css"), css)
}
}The builder performs multiphase processing:
- Preprocessing — Runs the optional
filePreProcesscallback on every source file before parsing. - Analysis — Scans source files, builds a dependency graph, and identifies all style function calls and the variables they depend on.
- sourceTransforms — Hooks that run after analysis on the canonical AST index. Mutations persist and are visible to
postEvalTransforms. - preEvalTransforms — Hooks that run on a deep copy of the ASTs, used only for bundling and evaluation. Mutations do NOT persist.
- Extraction — Generates minimal executable code from relevant style expressions, bundles it, and executes it in an isolated VM context.
- postEvalTransforms — Hooks that run after execution. The evaluator is populated — use
context.evaluator.getTrackedValue()to read runtime values. - emitHooks — Hooks that run after
postEvalTransforms. Callcontext.emitChunk(path, content)to produce output files written toemitDir.
The main orchestration class.
class Builder {
constructor(options: BuilderOptions)
// Analyze a pre-parsed set of modules; returns chunks keyed by path
collectStylesFromModules(modules: Module[]): Promise<Map<string, Set<string>>>
// High-level: discover files, extract styles, and generate CSS
collectMochiCss(options?: CollectCssOptions): Promise<{
global?: string
files?: Record<string, string>
sourcemods?: Record<string, string>
}>
}| Option | Type | Description |
|---|---|---|
roots |
RootEntry[] |
Directories (or named root entries) scanned recursively for source files. |
stages |
StageDefinition[] |
Analysis pipeline stages. Populated by calling plugin.onLoad(ctx) and passing ctx.stages.getAll(). |
bundler |
Bundler |
Bundler implementation. Use RolldownBundler. |
runner |
Runner |
Code runner implementation. Use VmRunner. |
splitCss |
boolean |
When true, CSS is split per source file instead of merged. Default: false. |
onDiagnostic |
function |
Callback for structured warnings and non-fatal errors. |
filePreProcess |
function |
Optional callback invoked on every source file before parsing. Receives { content, filePath } and returns the (possibly transformed) source string. |
sourceTransforms |
AstPostProcessor[] |
Hooks that run after analysis. May mutate AST nodes. Mutations persist in the canonical index. |
preEvalTransforms |
AstPostProcessor[] |
Hooks that run on a deep copy of the ASTs before evaluation. Mutations do NOT persist in the canonical index. |
postEvalTransforms |
AstPostProcessor[] |
Hooks that run after execution. The evaluator is populated — use context.evaluator.getTrackedValue() to read runtime values. |
emitHooks |
EmitHook[] |
Hooks that run after postEvalTransforms. Call context.emitChunk(path, content) to emit files. |
emitDir |
string |
Base directory for files produced via context.emitChunk(). |
cleanup |
function |
Called once at the end of the pipeline. Use to release caches built during transforms. |
When splitCss is false (default), all generated CSS is merged into a single global string.
When splitCss is true, the files map contains per-source-file CSS keyed by the source file path, and any truly global styles (from globalCss) are in global.
Passed to sourceTransforms, preEvalTransforms, postEvalTransforms, and emitHooks.
type AnalysisContext = {
onDiagnostic?: OnDiagnostic
evaluator: Evaluator
// Emit a chunk of content to a file in emitDir. Multiple chunks for the same path are
// deduplicated by content and joined with "\n\n".
emitChunk(path: string, content: string): void
// Mark an AST expression for inclusion in the eval bundle for a given file.
// The expression's identifier dependencies are traced and its value is captured at runtime.
// Wrap with evaluator.valueWithTracking() first to read back the runtime value later.
markForEval(filePath: string, expression: SWC.Expression): void
}A hook that runs after code execution. Call context.emitChunk() to emit files into emitDir. Files emitted on a previous run that are absent on the next run are automatically deleted.
type EmitHook = (runner: StageRunner, context: AnalysisContext) => void | Promise<void>RolldownBundler uses Rolldown to bundle the extracted minimal source code into a CommonJS module that can be executed.
interface Bundler {
bundle(rootFilePath: string, files: FileLookup): Promise<string>
}You can provide an alternative Bundler implementation if your environment requires a different bundler.
VmRunner executes bundled code in an isolated Node.js vm.Context, injecting extractors as context variables.
interface Runner {
execute(source: string, context: Record<string, unknown>): Promise<void>
}You can provide an alternative Runner implementation if your environment requires a different js runner.
Parse TypeScript/TSX source code into a Module AST.
async function parseSource(source: string, filePath: string): Promise<Module>
async function parseFile(filePath: string): Promise<Module>Useful when you want to supply pre-parsed modules to collectStylesFromModules.
Recursively discover all .ts and .tsx files in a directory.
async function findAllFiles(dir: string): Promise<string[]>Provide an onDiagnostic callback to receive structured warnings and non-fatal errors.
const builder = new Builder({
// ...
onDiagnostic: (diagnostic) => {
console.warn(`[${diagnostic.code}] ${diagnostic.message}`)
if (diagnostic.file) console.warn(` at ${diagnostic.file}:${diagnostic.line}`)
},
})Fatal errors are thrown as MochiError instances:
class MochiError extends Error {
readonly code: string // e.g. "MOCHI_BUNDLE"
readonly file?: string
}| Error Code | Cause |
|---|---|
MOCHI_FILE_READ |
Cannot read a source file |
MOCHI_PARSE |
TypeScript parsing failed |
MOCHI_BUNDLE |
Bundling the extracted code failed |
MOCHI_EXEC |
Execution of bundled code failed |
MOCHI_UNRESOLVED_IMPORT |
A local import cannot be resolved |
MOCHI_INVALID_STYLE_ARG |
A css()/styled() argument is not an object |
MOCHI_INVALID_GLOBAL_CSS_ARG |
A globalCss() argument is not an object |
MOCHI_INVALID_KEYFRAMES_ARG |
A keyframes() argument is not an object |
MOCHI_STYLE_GENERATION |
CSS generation threw an error |
MOCHI_INVALID_EXTRACTOR_USAGE |
Derived extractor was not properly destructured |
Plugin authoring — StyleExtractor, StyleGenerator, createExtractorsPlugin, stage definitions, and utilities like propagateUsagesFromExpr — is covered in the @mochi-css/plugins README.