Symbol Resolution
Preprocessing automatically collects symbol references and injects missing imports before writing files to disk.
The Problem
Section titled “The Problem”Generated code references sp(), dce(), mc(), INTERNALS without importing them. Preprocessing ensures all symbols are imported before files are written.
Additionally, barrel files (files that only re-export symbols) should not have imports added for the re-exported symbols, since they’re being re-exported, not used as references.
Architecture
Section titled “Architecture”Single universal processor works for all files:
class SourcePreprocessor { process(sourceFile: SourceFile): SourceFile { const usedSymbols = collector.collect(sourceFile) // Find all identifiers const requirements = builder.build(usedSymbols) // Lookup modules return injector.injectImports(sourceFile, requirements) // Inject AST }}Three-bucket registry (well-known + project symbols + file symbols) means same processor handles all symbol types.
Components
Section titled “Components”| Component | Purpose |
|---|---|
| SymbolRegistry | Symbol → module mapping; well-known + project + file symbol buckets |
| SymbolCollector | AST walk to find identifiers; filters declarations, imports, exports, and built-ins |
| ImportBuilder | Convert symbols to requirements; group, deduplicate |
| ImportInjector | Add/merge imports into AST using ts.factory |
| SourcePreprocessor | Orchestrate: collect → build → inject |
SymbolCollector Details
Section titled “SymbolCollector Details”The collector performs a three-phase symbol collection process:
-
Pre-extraction phase – Before traversing the AST, it pre-extracts:
- Symbols from import specifiers (e.g.,
import { Symbol }) - Symbols from export specifiers (e.g.,
export { Symbol }) - These symbols are skipped during collection to prevent them from being treated as missing references
- Symbols from import specifiers (e.g.,
-
Import extraction phase – Collects symbols that are already imported via import declarations
-
Main traversal phase – Walks the AST collecting identifier references, filtering out:
- Already-imported symbols
- Export/import specifier symbols
- Built-in globals (
Promise,Array,console, etc.) - Local declarations (variables, functions, classes, parameters, properties)
Integration
Section titled “Integration”Runs in FileWriter.writeAllFiles() before printing:
for (const sourceFile of files.values()) { const preprocessed = sourcePreprocessor.process(sourceFile)
const printed = sourcePrinter.printFile(preprocessed) await writeFile(outputPath, printed)}Symbol Buckets
Section titled “Symbol Buckets”The registry maintains three separate buckets for different symbol types:
Well-Known Symbols
Section titled “Well-Known Symbols”Stable APIs registered by Pencel and plugins—always imported regardless of file location. Examples: sp, dce, INTERNALS from @pencel/runtime; provideAppInitializer from @angular/core.
Registered on startup; never change during compilation. Enables plugins to ensure their framework symbols (Angular, React, Vue) auto-import everywhere.
Project Symbols
Section titled “Project Symbols”Discovered from generated .gen.ts files (component classes, etc.):
TestFeatures, PenCardElement, PenInputElement // Component classesRegistered via registerInputSymbol() as symbols are discovered. Configurable per plugin via ImportPreference:
preference.style === "package" ? { symbol: "MyComp", module: "@org/components" } : { symbol: "MyComp", module: "./my-comp.gen" }File Symbols
Section titled “File Symbols”Cached mapping of which symbols were exported from each file:
this.#fileSymbols: Map<filePath, Set<symbolNames>>Used for incremental updates and rescanning when files change. Enables upsertFileSymbols() to track and update project symbols efficiently.
Deduplication
Section titled “Deduplication”Imports from same module are consolidated:
// Before preprocessingimport { State } from "@pencel/runtime";export class Comp { render() { sp(el, props); // Uses sp, dce dce("div"); }}
// After preprocessingimport { State, sp, dce } from "@pencel/runtime";export class Comp { render() { sp(el, props); dce("div"); }}Existing imports are preserved; only missing symbols are added.
Plugin Registration
Section titled “Plugin Registration”See Plugin System for details on how plugins register and control symbols.
Plugins register symbols during initialization:
this.#registry.registerWellKnown([ { symbol: "myHelper", module: "@my-org/helpers" }]);Import Preferences
Section titled “Import Preferences”Plugins can control how symbols are imported in specific files using ImportPreference. See Plugin System - File Creation with Preferences for complete examples.
Quick reference:
const preference: ImportPreference = { style: "package", // "package" | "relative" | "deep" packageName: "@my/lib", // for "package" style symbolOverrides: [ { match: "Component1", packageName: "@other/lib" }, { match: "*Helper", style: "relative" }, ],};
this.#sourceFiles.newFile(fileName, statements, { preference });During preprocessing, preferences control the import resolution:
- Well-Known symbols – Always use their registered module
- Symbol overrides – Apply pattern-matched rules if present
- Global style – Fall back to file’s default preference
Performance
Section titled “Performance”- Collection – Linear AST walk per file
- Registry lookup – O(1) hash table, pattern matching on symbol overrides
- Injection – Single AST pass
- Total – Typically less than 1ms per file
Symbol Lookup Decision Tree
Section titled “Symbol Lookup Decision Tree”graph TD
Symbol["Symbol"]
Symbol --> WK["Well-known?"]
WK -->|Yes| Import1["Use registered module"]
WK -->|No| Pref["File has preferences?"]
Pref -->|Yes| Override["Match symbol overrides?"]
Override -->|Yes| Import2["Apply override rules"]
Override -->|No| GlobalStyle["Apply global style"]
Pref -->|No| Input["Input (user-defined)?"]
Input -->|Yes| Import3["Use registered module"]
Input -->|No| Ignore["Ignore"]
style Symbol fill:#fff59d,stroke:#fbc02d,color:#000
style WK fill:#42a5f5,stroke:#1565c0,color:#fff
style Pref fill:#ab47bc,stroke:#6a1b9a,color:#fff
style Override fill:#ab47bc,stroke:#6a1b9a,color:#fff
style GlobalStyle fill:#ab47bc,stroke:#6a1b9a,color:#fff
style Import1 fill:#66bb6a,stroke:#2e7d32,color:#fff
style Import2 fill:#66bb6a,stroke:#2e7d32,color:#fff
style Import3 fill:#66bb6a,stroke:#2e7d32,color:#fff
style Ignore fill:#e0e0e0,stroke:#616161,color:#000