s-m-r-t

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

v0.19.0Core FoundationRust-powered

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

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 } 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

typescript
# 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 --benchmark

Architecture

Two-Phase Processing

  1. Phase 1 - OXC Parsing: Fast syntactic extraction using Rust-based parser. Extracts class definitions, decorators, fields, and methods without semantic analysis.
  2. 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 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

OxcScanner Class

Constructor

typescript
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

MethodReturnsDescription
scan()Promise<ScanResults>Parse files and extract raw class definitions
resolve()ResolvedClassDefinition[]Resolve inheritance after scan()
scanAndResolve(){ results, resolved }Combined scan + resolve operation
addExternalManifest()voidAdd external package manifest
getStats()StatisticsGet performance statistics

Result Types

typescript
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

typescript
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

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

typescript
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

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

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

Tutorial 3: Generating Manifests

Step 1: Scan and Resolve

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

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 { 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

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