Internals
The pipeline, output format and design decisions.
Pipeline
The stages and terms follow bundlers like Rolldown, so the concepts carry over.
| Stage | What it does |
|---|---|
| Scan | Create a module for each entry of components, and read the import/export records of modules. |
| Link | resolveId() each import record, and decide its outcome, see Imports. Imported files matching a rule become modules, and are linked in turn. |
| Generate | Replace import spans in the original text. The rest stays byte-identical, including comments and formatting. |
Tree-shaken modules do more in the same scan: import declarations are split per binding, and statements are analyzed. Generate only shifts their spans by the edits.
- Module: an installable file. External: left to a package, or to the consumer.
- Scanning reads the import/export records of the analyzer, it doesn't walk the AST. Statements are only indexed for tree-shaken modules.
- The export index of a package is built on first use.
- There is no chunking: a component is only a named list of files.
Output
registry/
_registry.json manifest of root registry
files/button.tsx raw files
acme/charts/ sub registry
_registry.json
files/chart.tsx{
"name": "acme-ui",
"components": [{ "name": "card", "files": ["card/index.tsx"] }],
"files": {
"card/index.tsx": {
"type": "components",
"target": "<dir>/card/index.tsx",
"dependencies": { "acme-ui": "^1.2.0" },
"imports": [
{ "start": 24, "id": "acme-ui:button.tsx" },
{ "start": 65, "id": "acme-ui:layout.tsx", "package": "acme-ui/layout" }
]
}
}
}importsare the positions for installer to fill, so installing needs no parser. Only a merge analyzes the file of consumer.- The file content has the
idatstart, or thepackagespecifier when defined. No end position is stored. bindingsare the names taken from a tree-shaken file, omitted otherwise, or when the whole module is used.- An import with
packageis a preserved one, the file content already contains the package specifier. stmtInfosis defined for tree-shaken files, see below.- The manifest has everything needed to link, file contents are only downloaded for files that get installed. Binary files are served as-is.
- The types and
assertManifest()are exported fromfuma-cli/registry/schema. The validator is generated from the types at build time by typia, so it has no runtime dependency.
Statement Info
Modelled on StmtInfo of Rolldown, one entry per top-level statement of a tree-shaken file:
{
"type": "lib",
"imports": [{ "start": 223, "id": "acme-ui:lib/format.ts", "bindings": ["trim"] }],
"stmtInfos": [
{ "end": 14 },
{ "end": 43, "declares": ["clsx"], "import": true, "dependencies": { "clsx": "^2.0.0" } },
{ "end": 327, "declares": ["cn"], "references": [2, 3, 1] }
]
}| Field | Description |
|---|---|
end | It starts from the end of the last statement, with the comments and blank lines before it. |
declares | Top-level names and export names, default for the default export. |
references | Indices of the statements it needs. |
import | It is an import declaration. |
dependencies, devDependencies | Packages imported by the statement. The file itself has none. |
- Statements partition the file: selecting every statement gives back the whole file.
- The installer starts from statements declaring a requested binding, or declaring nothing, and follows
references. - An entry of
importsbelongs to the statement containing its span. Only imports of selected statements are followed.
Toolchain
| Task | Tool |
|---|---|
| Parsing & symbol analysis | yuku-analyzer, in utils/analyze.ts |
| Resolution | oxc-resolver |
One native toolchain covers every need: import/export records for scanning, resolved symbols for statement info, and the AST for macros. The installer only loads it for a merge, a route handler, or a React Router config.
Measured on 318 files (1 MB) of Fumadocs:
| Task | Time |
|---|---|
oxc-parser, module record | 27 ms |
yuku-analyzer, import/export records | 21 ms |
yuku-analyzer, records + spans (our scan) | 26 ms |
yuku-analyzer, decoding top-level AST | 61 ms |
- Nodes are decoded lazily, so scanning reads records and only decodes the nodes it needs a span from.
- Codegen is not used: printing an unmodified AST changed 49% of lines in 188 files (blank lines, JSX & import wrapping). Installed code is meant to be read and diffed, so we edit spans of the original text.
Resolver
Yuku has no resolver. The compiler needs relative imports with extension probing (./foo.js to foo.ts included), paths of the nearest tsconfig.json with extends and references, and imports of package.json. Measured on the 1653 imports of the same files:
| Resolver | Cold | Warm | Notes |
|---|---|---|---|
oxc-resolver | 59 ms | 3 ms | Covers everything above. |
Own probing + get-tsconfig | 18 ms | 3 ms | No references and imports, more code to own. |
enhanced-resolve | 94 ms | 27 ms | No automatic tsconfig.json discovery. |
Most of the cold time of oxc-resolver is spent on packages in node_modules, which are external anyway. It is the only one that is complete without code of our own, so it stays.
Last updated on