@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.

v0.20.44Core FoundationRust-powered

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

typescript
npm install @happyvertical/smrt-scanner
# or
pnpm add @happyvertical/smrt-scanner
# or
bun add @happyvertical/smrt-scanner

Quick Start (5 Minutes)

Programmatic Usage

typescript
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

typescript
# Scan and output manifest
smrt-scan src/**/*.ts

Architecture

Processing Pipeline

  1. fast-glob finds .ts files matching include/exclude patterns
  2. oxc-parser parses each file's AST
  3. Extraction: @smrt() config, class hierarchy, field defaults (0 vs 0.0 heuristic), relationships, static properties
  4. 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 SmrtCollection or SmrtClass

Field Type Inference

Type inference follows this priority:

  1. Helper functions: text(), integer(), decimal(), foreignKey()
  2. Field decorators: @field({ type: "..." })
  3. TypeScript annotations with 0 vs 0.0 heuristic for numbers
  4. 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

ExportDescription
OxcScannerScans TypeScript files for @smrt() decorated classes
InheritanceResolverResolves class inheritance chains across files
ManifestAdapterConverts raw scan results to SMRT manifest format

Functions

ExportDescription
parseFileParse a single TypeScript file and extract metadata
parseSourceParse a TypeScript source string
extractSmrtImportsExtract SMRT-related imports from a file

Key Types

The following types are exported from the package:

typescript
// Core scan result types
RawClassDefinition
RawFieldDefinition
RawMethodDefinition
RawDecoratorConfig
ResolvedClassDefinition
ScanResults
FileScanResult
OxcScannerOptions

// Field type inference
InferredFieldType
FieldTypeInference

ManifestAdapter

typescript
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

typescript
// 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

typescript
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

typescript
// 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

typescript
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, trunkSize

Tutorial 3: Generating Manifests

Step 1: Scan and Resolve

typescript
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 logic
  • src/manifest-builder.ts -- orchestrates scanning to manifest
  • src/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

typescript
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

typescript
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

typescript
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

typescript
// 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

typescript
// 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.0 for decimal fields, 0 for 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:

typescript
smrt-scan --benchmark

Performance Tips

  • Use tight include/exclude patterns to minimize files scanned
  • Disable unused options like includePrivateMethods
  • Cache scan results when possible
  • Use cwd to scope scanning to specific directories