@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.
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
pnpm add @happyvertical/smrt-scannerUsually 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 returnsScanResultsvia.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 stringRawClassDefinition,RawFieldDefinition,ResolvedClassDefinition,ScanResults,OxcScannerOptions-- scanned class metadata types
How It Works
fast-globfinds.tsfiles matching include/exclude patternsoxc-parserparses each file's AST- The scanner extracts:
@smrt()config, class hierarchy, field defaults (0vs0.0heuristic), relationships, and static properties (uiSlots,adminRoutes) - 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 logicsrc/inheritance-resolver.ts--InheritanceResolver, resolves inheritance chainssrc/manifest-adapter.ts--ManifestAdapter, scan results → manifest format
Quick Start
Generate a Manifest
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.
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:
- Helper functions:
text(),integer(),decimal(),foreignKey() - Field decorators:
@field({ type: '...' }) - TypeScript annotations, using the
0vs0.0heuristic to distinguish INTEGER from DECIMAL - Default:
text
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'sManifestBuilder, 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:
OxcScannerresolves globs relative to itscwdoption (defaultprocess.cwd()). Passcwdexplicitly (or usescanFromDir) 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
uiSlotsandadminRoutesfor agent manifest generation -- if these properties aren't picked up, check they're declared asstaticwith an initializer. - pnpm symlinks resolve to workspace paths: in a monorepo, pnpm symlinks
resolve to actual workspace directories rather than via
node_modules, so anyfilePath.includes('node_modules')filter will miss them.