Compiler

Internals

The pipeline, output format and design decisions.

Pipeline

Scan Link Generate Manifest + files

The stages and terms follow bundlers like Rolldown, so the concepts carry over.

StageWhat it does
ScanCreate a module for each entry of components, and read the import/export records of modules.
LinkresolveId() each import record, and decide its outcome, see Imports. Imported files matching a rule become modules, and are linked in turn.
GenerateReplace 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
_registry.json
{
  "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" }
      ]
    }
  }
}
  • imports are the positions for installer to fill, so installing needs no parser. Only a merge analyzes the file of consumer.
  • The file content has the id at start, or the package specifier when defined. No end position is stored.
  • bindings are the names taken from a tree-shaken file, omitted otherwise, or when the whole module is used.
  • An import with package is a preserved one, the file content already contains the package specifier.
  • stmtInfos is 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 from fuma-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] }
  ]
}
FieldDescription
endIt starts from the end of the last statement, with the comments and blank lines before it.
declaresTop-level names and export names, default for the default export.
referencesIndices of the statements it needs.
importIt is an import declaration.
dependencies, devDependenciesPackages 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 imports belongs to the statement containing its span. Only imports of selected statements are followed.

Toolchain

TaskTool
Parsing & symbol analysisyuku-analyzer, in utils/analyze.ts
Resolutionoxc-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:

TaskTime
oxc-parser, module record27 ms
yuku-analyzer, import/export records21 ms
yuku-analyzer, records + spans (our scan)26 ms
yuku-analyzer, decoding top-level AST61 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:

ResolverColdWarmNotes
oxc-resolver59 ms3 msCovers everything above.
Own probing + get-tsconfig18 ms3 msNo references and imports, more code to own.
enhanced-resolve94 ms27 msNo 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

On this page