@happyvertical/smrt-scanner

AST-based scanner using oxc-parser (Rust, 2-3x faster than tsc) for class/field metadata extraction. Powers manifest generation for code generators, the vitest plugin, and the CLI.

v0.29.34Core FoundationRust-poweredESM-only

Overview

@happyvertical/smrt-scanner reads your TypeScript source with the Rust-based OXC parser and extracts the metadata that smrt-core's code generators need: @smrt() config, class hierarchy, field defaults (with the 0 vs 0.0 heuristic for INTEGER vs DECIMAL), relationships, and static properties like uiSlots and adminRoutes.

It outputs a manifest JSON consumed downstream by code generators, the vitest plugin, and the CLI.

Installation

bash
pnpm add @happyvertical/smrt-scanner

Usually you don't install this directly -- it's a dependency of smrt-vitest and smrt-cli.

Key Exports

  • OxcScanner -- scans source files with the OXC parser and returns ScanResults via .scan()
  • InheritanceResolver -- resolves class inheritance chains across files (.addClasses() / .resolveAll())
  • ManifestAdapter -- converts resolved scan results into the smrt-core manifest format via .toManifest()
  • parseFile, parseSource, extractSmrtImports -- low-level parsing helpers for a single file or source string
  • RawClassDefinition, RawFieldDefinition, ResolvedClassDefinition, ScanResults, OxcScannerOptions -- scanned class metadata types

How It Works

  1. fast-glob finds .ts files matching include/exclude patterns
  2. oxc-parser parses each file's AST
  3. The scanner extracts: @smrt() config, class hierarchy, field defaults (0 vs 0.0 heuristic), relationships, and static properties (uiSlots, adminRoutes)
  4. The output is a manifest JSON consumed by code generators, the vitest plugin, and the CLI

Key Files

  • src/oxc-parser.ts -- low-level OXC parsing (parseFile, parseSource)
  • src/scanner.ts -- OxcScanner, glob-based scanning logic
  • src/inheritance-resolver.ts -- InheritanceResolver, resolves inheritance chains
  • src/manifest-adapter.ts -- ManifestAdapter, scan results → manifest format

Quick Start

Generate a Manifest

typescript
import {
  OxcScanner,
  InheritanceResolver,
  ManifestAdapter,
} from '@happyvertical/smrt-scanner';

// 1. Scan source files for @smrt() classes
const scanner = new OxcScanner({
  include: ['src/**/*.ts'],
  exclude: ['**/*.test.ts', '**/*.spec.ts'],
});
const results = await scanner.scan();

// 2. Resolve inheritance chains across files
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();

// 3. Convert to the smrt-core manifest format
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved);

Discover Base Classes

Base-class discovery lives in @happyvertical/smrt-core/manifest. discoverBaseClasses() returns an array of base class names (e.g. ['SmrtObject', 'SmrtCollection', ...]) that you feed into OxcScanner's baseClasses option so it can resolve classes that extend framework or cross-package bases.

typescript
import { discoverBaseClasses } from '@happyvertical/smrt-core/manifest';
import { OxcScanner } from '@happyvertical/smrt-scanner';

// Always pass cwd when projectRoot != process.cwd()
const baseClasses = await discoverBaseClasses({ cwd: projectRoot });
const scanner = new OxcScanner({ baseClasses, cwd: projectRoot });

Field Type Inference

The scanner infers field types in this priority order:

  1. Helper functions: text(), integer(), decimal(), foreignKey()
  2. Field decorators: @field({ type: '...' })
  3. TypeScript annotations, using the 0 vs 0.0 heuristic to distinguish INTEGER from DECIMAL
  4. Default: text
typescript
class Product extends SmrtObject {
  name: string = '';        // → TEXT
  quantity: number = 0;     // → INTEGER  (no decimal point)
  price: number = 0.0;      // → DECIMAL  (has decimal point)
  active: boolean = true;   // → BOOLEAN
  tags: string[] = [];      // → JSON
  createdAt: Date = new Date(); // → DATETIME
}

Used By

  • smrt-vitest -- smrtVitestPlugin() generates manifests at vitest startup via smrt-core's ManifestBuilder, which scans through this package
  • smrt-cli -- introspection and code-generation commands
  • smrt-core -- the vite-plugin loads scanner modules from dist/ (see core's "Vite plugin loads scanner from dist first" gotcha, #1139)

Gotchas

  • CWD-relative: OxcScanner resolves globs relative to its cwd option (default process.cwd()). Pass cwd explicitly (or use scanFromDir) when you're not at the project root.
  • ESM-only (PR #1219): the package ships only ESM exports. CJS consumers must switch to ESM or use dynamic import().
  • Static property capture: the scanner captures uiSlots and adminRoutes for agent manifest generation -- if these properties aren't picked up, check they're declared as static with an initializer.
  • pnpm symlinks resolve to workspace paths: in a monorepo, pnpm symlinks resolve to actual workspace directories rather than via node_modules, so any filePath.includes('node_modules') filter will miss them.

Next Steps