@happyvertical/smrt-scanner
High-performance TypeScript scanner using OXC for automatic SMRT class discovery, inheritance resolution, and manifest generation. 2-3x faster than TypeScript compiler.
Overview
The smrt-scanner package uses the blazing-fast Rust-based OXC parser to scan TypeScript projects and generate manifests for the SMRT framework. It automatically discovers classes, resolves inheritance hierarchies, and handles STI (Single Table Inheritance) field merging.
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 } from '@happyvertical/smrt-scanner';
// Create scanner
const scanner = new OxcScanner({
cwd: process.cwd(),
include: ['src/**/*.ts'],
exclude: ['**/*.test.ts']
});
// Scan and resolve inheritance
const { results, resolved } = await scanner.scanAndResolve();
console.log(`Found ${resolved.length} SMRT classes`);
resolved.forEach(cls => {
console.log(`- ${cls.className}: ${cls.fields.length} fields`);
if (cls.isSTI) {
console.log(` STI base: ${cls.stiBase}`);
}
});CLI Usage
# Basic scan
smrt-scan
# Custom directory and patterns
smrt-scan ./src -i "**/*.ts" -e "**/*.test.ts"
# Output manifest to file
smrt-scan -o manifest.json --stats
# Benchmark performance
smrt-scan --benchmarkArchitecture
Two-Phase Processing
- Phase 1 - OXC Parsing: Fast syntactic extraction using Rust-based parser. Extracts class definitions, decorators, fields, and methods without semantic analysis.
- Phase 2 - Inheritance Resolution: Builds class hierarchy, resolves inheritance chains, merges STI fields, and identifies framework base classes.
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
OxcScanner Class
Constructor
new OxcScanner(options?: OxcScannerOptions)
interface OxcScannerOptions {
include?: string[]; // Glob patterns (default: ['**/*.ts', '**/*.tsx'])
exclude?: string[]; // Exclude patterns
cwd?: string; // Base directory
tsconfig?: string; // Path to tsconfig.json
followImports?: boolean; // Follow imports for base classes
baseClasses?: string[]; // Known base classes
includePrivateMethods?: boolean; // Include private methods
includeStaticMethods?: boolean; // Include static methods (default: true)
externalManifests?: Map<string, ExternalManifest>;
}Key Methods
| Method | Returns | Description |
|---|---|---|
scan() | Promise<ScanResults> | Parse files and extract raw class definitions |
resolve() | ResolvedClassDefinition[] | Resolve inheritance after scan() |
scanAndResolve() | { results, resolved } | Combined scan + resolve operation |
addExternalManifest() | void | Add external package manifest |
getStats() | Statistics | Get performance statistics |
Result Types
interface ScanResults {
files: FileScanResult[]; // Per-file results
classes: RawClassDefinition[]; // All classes (flattened)
errors: ScanError[]; // Parse errors
totalParseTimeMs: number; // Total parse duration
fileCount: number; // Number of files scanned
}
interface ResolvedClassDefinition {
className: string;
filePath: string;
extendsClause: string | null;
inheritanceChain: string[]; // Full chain from base to this class
stiBase: string | null; // STI base if in STI hierarchy
effectiveTableStrategy: 'sti' | 'cti';
isSTI: boolean;
isFrameworkBase: boolean;
decoratorConfig: RawDecoratorConfig | null;
fields: RawFieldDefinition[];
allFields: RawFieldDefinition[]; // Merged fields for STI
methods: RawMethodDefinition[];
}ManifestAdapter
import { ManifestAdapter } from '@happyvertical/smrt-scanner';
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolvedClasses, {
packageName: '@my/package',
packageVersion: '1.0.0'
});
// Write to file
await fs.writeFile('manifest.json', JSON.stringify(manifest, null, 2));Tutorials
Tutorial 1: Basic Project Scanning
Step 1: Create SMRT Classes
// src/models/User.ts
import { SmrtObject, smrt } from '@happyvertical/smrt-core';
import { text, integer } from '@happyvertical/smrt-core/fields';
@smrt()
export class User extends SmrtObject {
name = text();
email = text();
age = integer();
}Step 2: Run Scanner
import { OxcScanner } from '@happyvertical/smrt-scanner';
const scanner = new OxcScanner({ cwd: './src' });
const { resolved } = await scanner.scanAndResolve();
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 = text();
model = text();
year = integer();
}
// Car inherits STI strategy
@smrt()
export class Car extends Vehicle {
numDoors = integer();
trunkSize = decimal();
}
// Truck also inherits STI
@smrt()
export class Truck extends Vehicle {
bedLength = decimal();
towingCapacity = integer();
}Step 2: Scan and Examine
const { resolved } = await scanner.scanAndResolve();
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, ManifestAdapter } from '@happyvertical/smrt-scanner';
import fs from 'fs/promises';
const scanner = new OxcScanner();
const { resolved } = await scanner.scanAndResolve();
const adapter = new ManifestAdapter();
const manifest = adapter.toManifest(resolved, {
packageName: '@my/models',
packageVersion: '1.0.0'
});
await fs.writeFile(
'manifest.json',
JSON.stringify(manifest, null, 2)
);
console.log('Manifest generated with', manifest.classes.size, 'classes');Tutorial 4: Cross-Package Resolution
Step 1: Load External Manifest
import externalManifest from '@external/package/manifest.json';
const scanner = new OxcScanner();
scanner.addExternalManifest({
packageName: '@external/package',
classes: new Map(Object.entries(externalManifest.classes))
});
// Now scanner can resolve classes extending @external/package classes
const { resolved } = await scanner.scanAndResolve();Examples
Example 1: Find All STI Hierarchies
const { resolved } = await scanner.scanAndResolve();
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 { resolved } = await scanner.scanAndResolve();
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 { resolved } = await scanner.scanAndResolve();
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 { resolved } = await scanner.scanAndResolve();
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 { resolved } = await scanner.scanAndResolve();
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