@vivtel/discovery
v1.1.3
Published
AST scanning and decorator discovery utilities for Vivtel packages
Downloads
587
Maintainers
Readme
@vivtel/discovery
AST scanning and code discovery utilities for Vivtel packages.
Features
- ✅ Decorator Scanning - Find and extract metadata from TypeScript decorators
- ✅ Export Scanning - Discover exported functions, classes, and constants
- ✅ Smart Caching - File-based caching with mtime validation
- ✅ Import Resolution - Track and resolve import statements
- ✅ Flexible Filtering - Filter by name, type, or patterns
- ✅ Comprehensive Types - Full TypeScript support
- ✅ Detailed Documentation - JSDoc comments on all APIs
Installation
pnpm add @vivtel/discoveryUsage
Decorator Scanning (ASTScanner)
Scan for TypeScript decorators on classes:
import { ASTScanner } from '@vivtel/discovery';
// Using static factory method
const scanner = ASTScanner.make({
decorators: ['Module', 'Resource', 'Provider'],
include: ['src/**/*.ts'],
debug: true,
});
const results = await scanner.scanDirectory('/path/to/project');
results.forEach((result) => {
console.log(`File: ${result.filePath}`);
result.decorators.forEach((decorator) => {
console.log(` @${decorator.decoratorName} on ${decorator.className}`);
console.log(` Config:`, decorator.config);
});
});Export Scanning (ExportScanner)
Scan for exported functions, classes, and constants:
import { ExportScanner } from '@vivtel/discovery';
// Scan for specific exports (like Remix patterns)
const scanner = ExportScanner.make({
exports: ['ErrorBoundary', 'loader', 'action'],
types: ['function'],
include: ['src/**/*.page.tsx'],
debug: true,
});
const results = await scanner.scanDirectory('/path/to/project');
results.forEach((result) => {
console.log(`File: ${result.filePath}`);
result.exports.forEach((exp) => {
console.log(` export ${exp.type} ${exp.name}`);
if (exp.isAsync) console.log(` (async)`);
if (exp.isDefault) console.log(` (default)`);
});
});Scan All Exports
// No filter - get all exports
const scanner = new ExportScanner({
include: ['src/**/*.ts'],
});
const results = await scanner.scanDirectory('/project');
const allExports = results.flatMap((r) => r.exports);
console.log(`Found ${allExports.length} total exports`);Filter by Type
// Only scan for class exports
const classScanner = new ExportScanner({
types: ['class'],
include: ['src/**/*.ts'],
});
// Only scan for function exports
const functionScanner = new ExportScanner({
types: ['function'],
include: ['src/**/*.ts'],
});
// Only scan for constants
const constScanner = new ExportScanner({
types: ['const'],
include: ['src/**/*.ts'],
});With Caching
const scanner = new ASTScanner({
decorators: ['Route'],
cache: true, // Enable caching (default: true)
debug: true,
});
// First scan - parses all files
const results1 = await scanner.scanDirectory('/project');
// Second scan - uses cached results for unchanged files
const results2 = await scanner.scanDirectory('/project');
// Clear cache if needed
scanner.clearCache();Follow Imports
const scanner = new ASTScanner({
decorators: ['Module'],
followImports: true, // Track import statements
debug: true,
});
const results = await scanner.scanDirectory('/project');API
ASTScanner
Scanner class for discovering decorators on classes.
Constructor
new ASTScanner(options: IScannerOptions)Methods
scanDirectory(projectRoot: string): Promise<IScanResult[]>- Scan all files in directoryscanFile(filePath: string): IScanResult- Scan a single fileclearCache(): void- Clear the scan cachegetCacheStats()- Get cache statistics
ExportScanner
Scanner class for discovering exports (functions, classes, constants).
Constructor
new ExportScanner(options?: IExportScannerOptions)Methods
scanDirectory(projectRoot: string): Promise<IExportScanResult[]>- Scan all files in directoryscanFile(filePath: string): IExportScanResult- Scan a single fileclearCache(): void- Clear the scan cachegetCacheStats()- Get cache statistics
Interfaces
IScannerOptions (for ASTScanner)
interface IScannerOptions {
decorators: string[]; // Required: ['Module', 'Route']
include?: string[]; // Default: ['src/**/*.ts(x)']
exclude?: string[]; // Default: test files, node_modules
debug?: boolean; // Default: false
cache?: boolean; // Default: true
includeRawNodes?: boolean; // Default: false
followImports?: boolean; // Default: false
maxFileSize?: number; // Default: 1MB
concurrency?: number; // Default: 10
}IExportScannerOptions (for ExportScanner)
interface IExportScannerOptions {
exports?: string[]; // Filter by name: ['ErrorBoundary', 'loader']
types?: ExportType[]; // Filter by type: ['function', 'class', 'const']
include?: string[]; // Default: ['src/**/*.ts(x)']
exclude?: string[]; // Default: test files, node_modules
debug?: boolean; // Default: false
cache?: boolean; // Default: true
includeRawNodes?: boolean; // Default: false
maxFileSize?: number; // Default: 1MB
}
type ExportType = 'function' | 'class' | 'const' | 'let' | 'var' | 'default';IScanResult (for ASTScanner)
interface IScanResult {
filePath: string;
decorators: IDecoratorMetadata[];
errors: string[];
warnings: string[];
parseTime?: number;
fromCache?: boolean;
}IExportScanResult (for ExportScanner)
interface IExportScanResult {
filePath: string;
exports: IExportMetadata[];
errors: string[];
warnings: string[];
parseTime?: number;
fromCache?: boolean;
}IDecoratorMetadata
interface IDecoratorMetadata {
decoratorName: string; // 'Module', 'Route', etc.
className: string; // Class the decorator is on
filePath: string; // Source file path
config: Record<string, any>; // Decorator configuration object
sourceLocation?: {
line: number;
column: number;
};
rawNode?: any; // Raw AST node (if includeRawNodes: true)
}IExportMetadata
interface IExportMetadata {
name: string; // Export name: 'ErrorBoundary', 'CONFIG'
type: ExportType; // 'function', 'class', 'const', etc.
filePath: string; // Source file path
isDefault: boolean; // true for `export default`
isAsync?: boolean; // For async functions
isGenerator?: boolean; // For generator functions
isReExport?: boolean; // For `export { x } from './y'`
reExportSource?: string; // Source module for re-exports
sourceLocation?: {
line: number;
column: number;
};
rawNode?: any; // Raw AST node (if includeRawNodes: true)
}Use Cases
Route Error Boundaries (Remix-like Pattern)
import { ExportScanner } from '@vivtel/discovery';
import type { Plugin } from 'vite';
export function routingPlugin(): Plugin {
const exportScanner = new ExportScanner({
exports: ['ErrorBoundary', 'loader', 'action'],
types: ['function'],
include: ['src/**/*.page.tsx'],
});
return {
name: 'routing-plugin',
async buildStart() {
const results = await exportScanner.scanDirectory(process.cwd());
// Find pages with ErrorBoundary exports
for (const result of results) {
const errorBoundary = result.exports.find((e) => e.name === 'ErrorBoundary');
if (errorBoundary) {
console.log(`${result.filePath} has ErrorBoundary`);
}
}
},
};
}Combined Decorator + Export Scanning
import { ASTScanner, ExportScanner } from '@vivtel/discovery';
// Scan decorators for route registration
const decoratorScanner = new ASTScanner({
decorators: ['Route', 'Layout'],
include: ['src/**/*.page.tsx'],
});
// Scan exports for error boundaries
const exportScanner = new ExportScanner({
exports: ['ErrorBoundary'],
include: ['src/**/*.page.tsx'],
});
const [decoratorResults, exportResults] = await Promise.all([
decoratorScanner.scanDirectory(process.cwd()),
exportScanner.scanDirectory(process.cwd()),
]);
// Merge results by file path
const routeConfigs = decoratorResults.map((dr) => {
const exportResult = exportResults.find((er) => er.filePath === dr.filePath);
const hasErrorBoundary = exportResult?.exports.some((e) => e.name === 'ErrorBoundary');
return {
filePath: dr.filePath,
decorators: dr.decorators,
hasErrorBoundary,
};
});In Vite Plugins
import { ASTScanner } from '@vivtel/discovery';
import type { Plugin } from 'vite';
export function myPlugin(): Plugin {
const scanner = new ASTScanner({
decorators: ['MyDecorator'],
cache: true,
});
return {
name: 'my-plugin',
async buildStart() {
const results = await scanner.scanDirectory(
this.meta.watchMode ? process.cwd() : './src'
);
// Process results...
},
};
}In Build Tools
import { ASTScanner } from '@vivtel/discovery';
const scanner = new ASTScanner({
decorators: ['Component', 'Injectable'],
include: ['src/**/*.ts'],
});
const results = await scanner.scanDirectory(process.cwd());
// Generate code based on found decorators
const code = generateCode(results);Performance
- Caching: Unchanged files are skipped on subsequent scans
- File Size Limit: Large files (>1MB by default) are skipped
- Parallel Processing: Multiple files parsed concurrently
- Typical Performance: ~500ms for 100 files (cold), ~50ms (warm)
License
MIT
