@happyvertical/smrt-scanner
AST-based scanner using oxc-parser (Rust) for class/field metadata extraction. Discovers @smrt() classes, resolves inheritance, and generates manifests for code generators.
Overview
The smrt-scanner package uses the Rust-based OXC parser to scan TypeScript
source files and extract class, field, method, and decorator metadata without executing them.
It discovers @smrt() decorated classes, resolves inheritance hierarchies,
and generates manifests consumed by code generators, the vitest plugin, and CLI.
Key Features
- 2-3x faster than TypeScript compiler using Rust-based OXC parser
- Automatic class discovery via
@smrt()decorator detection - Inheritance resolution with full chain tracking
- STI support with automatic field merging from parent classes
- Field type inference from TypeScript annotations and helper functions
- Cross-package resolution via external manifests
- CLI and programmatic API for flexible integration
- Manifest generation compatible with smrt-core
Installation
npm install @happyvertical/smrt-scanner
# or
pnpm add @happyvertical/smrt-scanner
# or
bun add @happyvertical/smrt-scannerQuick Start (5 Minutes)
Programmatic Usage
import { OxcScanner, InheritanceResolver, ManifestAdapter } from '@happyvertical/smrt-scanner';
import { parseFile, parseSource, extractSmrtImports } from '@happyvertical/smrt-scanner';
// Scan a single file
const result = parseFile('/path/to/Product.ts');
// result.classes → RawClassDefinition[]
// Full scanner with glob support
const scanner = new OxcScanner({ include: ['src/**/*.ts'] });
const results = await scanner.scan();
// Resolve inheritance across files
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
// Convert to SMRT manifest format
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved);CLI Usage
# Scan and output manifest
smrt-scan src/**/*.tsArchitecture
Processing Pipeline
- fast-glob finds
.tsfiles matching include/exclude patterns - oxc-parser parses each file's AST
- Extraction:
@smrt()config, class hierarchy, field defaults (0 vs 0.0 heuristic), relationships, static properties - Manifest output: JSON consumed by code generators, vitest plugin, and CLI
Class Discovery
The scanner finds classes in three ways:
- Classes with
@smrt()decorator - Classes extending
SmrtObject - Classes extending
SmrtCollectionorSmrtClass
Field Type Inference
Type inference follows this priority:
- Helper functions:
text(),integer(),decimal(),foreignKey() - Field decorators:
@field({ type: "..." }) - TypeScript annotations with 0 vs 0.0 heuristic for numbers
- Default: 'text' type
STI (Single Table Inheritance)
Classes with tableStrategy: 'sti' automatically merge fields from their entire inheritance
chain. All descendants inherit the STI strategy and share the same table.
API Reference
Exports
Classes
| Export | Description |
|---|---|
OxcScanner | Scans TypeScript files for @smrt() decorated classes |
InheritanceResolver | Resolves class inheritance chains across files |
ManifestAdapter | Converts raw scan results to SMRT manifest format |
Functions
| Export | Description |
|---|---|
parseFile | Parse a single TypeScript file and extract metadata |
parseSource | Parse a TypeScript source string |
extractSmrtImports | Extract SMRT-related imports from a file |
Key Types
The following types are exported from the package:
// Core scan result types
RawClassDefinition
RawFieldDefinition
RawMethodDefinition
RawDecoratorConfig
ResolvedClassDefinition
ScanResults
FileScanResult
OxcScannerOptions
// Field type inference
InferredFieldType
FieldTypeInferenceManifestAdapter
import { OxcScanner, InheritanceResolver, ManifestAdapter } from '@happyvertical/smrt-scanner';
// Full pipeline: scan → resolve → manifest
const scanner = new OxcScanner({ include: ['src/**/*.ts'] });
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved);Tutorials
Tutorial 1: Basic Project Scanning
Step 1: Create SMRT Classes
// src/models/User.ts
import { smrt, SmrtObject } from '@happyvertical/smrt-core';
@smrt()
export class User extends SmrtObject {
name: string = '';
email: string = '';
age: number = 0;
}Step 2: Run Scanner
import { OxcScanner } from '@happyvertical/smrt-scanner';
const scanner = new OxcScanner({ cwd: './src' });
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const user = resolved.find(c => c.className === 'User');
console.log(`User has ${user.fields.length} fields`);
user.fields.forEach(f => {
console.log(` - ${f.name}: ${f.typeAnnotation}`);
});Tutorial 2: STI Hierarchy Management
Step 1: Create STI Base
// Base class with STI
@smrt({ tableStrategy: 'sti' })
export class Vehicle extends SmrtObject {
make: string = '';
model: string = '';
year: number = 0;
}
// Car inherits STI strategy
@smrt()
export class Car extends Vehicle {
numDoors: number = 0;
trunkSize: number = 0.0; // 0.0 → DECIMAL
}
// Truck also inherits STI
@smrt()
export class Truck extends Vehicle {
bedLength: number = 0.0;
towingCapacity: number = 0;
}Step 2: Scan and Examine
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const car = resolved.find(c => c.className === 'Car');
console.log('Car inheritance chain:', car.inheritanceChain);
// ["SmrtObject", "Vehicle", "Car"]
console.log('Car is STI:', car.isSTI);
// true
console.log('STI base:', car.stiBase);
// "Vehicle"
console.log('All fields (merged from Vehicle):', car.allFields.length);
// 5 fields: make, model, year, numDoors, trunkSizeTutorial 3: Generating Manifests
Step 1: Scan and Resolve
import { OxcScanner, InheritanceResolver, ManifestAdapter } from '@happyvertical/smrt-scanner';
import fs from 'fs/promises';
const scanner = new OxcScanner({ include: ['src/**/*.ts'] });
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved);
await fs.writeFile(
'manifest.json',
JSON.stringify(manifest, null, 2)
);Key Files
src/oxc-scanner.ts-- core AST scanning logicsrc/manifest-builder.ts-- orchestrates scanning to manifestsrc/base-class-discovery.ts-- resolves base classes from node_modules
Internally, ManifestBuilder provides a high-level API that scans source files
and builds a SmartObjectManifest in one call. It is used by smrtVitestPlugin() at startup to generate the build-time manifest.
Examples
Example 1: Find All STI Hierarchies
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const stiClasses = resolved.filter(c => c.isSTI);
const hierarchies = new Map();
stiClasses.forEach(cls => {
const base = cls.stiBase || cls.className;
if (!hierarchies.has(base)) {
hierarchies.set(base, []);
}
hierarchies.get(base).push(cls.className);
});
hierarchies.forEach((children, base) => {
console.log(`${base} -> [${children.join(', ')}]`);
});Example 2: Analyze Field Types
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
resolved.forEach(cls => {
const requiredFields = cls.fields.filter(f => !f.optional);
const optionalFields = cls.fields.filter(f => f.optional);
console.log(`${cls.className}:`);
console.log(` Required: ${requiredFields.map(f => f.name).join(', ')}`);
console.log(` Optional: ${optionalFields.map(f => f.name).join(', ')}`);
});Example 3: Extract API Endpoints
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
resolved.forEach(cls => {
const api = cls.decoratorConfig?.api;
if (api?.include) {
const basePath = cls.className.toLowerCase();
api.include.forEach(endpoint => {
console.log(`GET /api/${basePath}/${endpoint}`);
});
}
});Integration Patterns
Vite Plugin Integration
// vite.config.ts
import { defineConfig } from 'vite';
import { OxcScanner, ManifestAdapter } from '@happyvertical/smrt-scanner';
export default defineConfig({
plugins: [{
name: 'smrt-manifest',
async buildStart() {
const scanner = new OxcScanner({ cwd: './src' });
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved);
await fs.writeFile(
'src/generated/manifest.json',
JSON.stringify(manifest, null, 2)
);
}
}]
});Monorepo Scanning
// Scan all packages in monorepo
const packages = ['packages/users', 'packages/commerce', 'packages/assets'];
const allClasses = [];
for (const pkg of packages) {
const scanner = new OxcScanner({ cwd: pkg });
const results = await scanner.scan();
const resolver = new InheritanceResolver();
resolver.addClasses(results.classes);
const resolved = resolver.resolveAll();
allClasses.push(...resolved);
}
console.log(`Total classes across monorepo: ${allClasses.length}`);Best Practices
✅ DO
- Always exclude test files:
**/*.test.ts,**/*.spec.ts - Use
0.0for decimal fields,0for integers in initializers - Use helper functions (
text(),integer()) for clearer type inference - Specify
tableStrategy: 'sti'on base class before extending - Keep inheritance chains reasonably shallow (2-4 levels)
- Cache external manifests during build process
- Use
scanAndResolve()for most use cases (convenience method)
❌ DON'T
- Don't forget to add
@smrt()decorator on SMRT classes - Don't mix STI and CTI strategies in same hierarchy
- Don't scan unnecessary directories (use tight include/exclude patterns)
- Don't forget to resolve inheritance when working with STI classes
- Don't rely on external manifests being automatically discovered
Troubleshooting
Classes not found
Problem: Scanner doesn't detect SMRT classes.
Solution: Ensure classes have @smrt() decorator or extend SMRT base
classes. Check include/exclude glob patterns.
Inheritance not resolved
Problem: Parent classes in external packages not found.
Solution: Load external manifest via addExternalManifest().
STI fields not merged
Problem: allFields doesn't include parent fields.
Solution: Use scanAndResolve() or call resolve() after scan().
Parse errors
Problem: Syntax errors in files.
Solution: Check results.errors array for detailed error messages with
file paths and line numbers.
Decimal/Integer confusion
Problem: Wrong numeric types inferred.
Solution: Use 0.0 for decimals, 0 for integers, or
use helper functions: decimal(), integer().
Performance
The smrt-scanner is 2-3x faster than the TypeScript compiler thanks to the Rust-based OXC parser. Benchmark your project:
smrt-scan --benchmarkPerformance Tips
- Use tight include/exclude patterns to minimize files scanned
- Disable unused options like
includePrivateMethods - Cache scan results when possible
- Use
cwdto scope scanning to specific directories