Plugin System
Plugins extend the compiler’s capabilities by hooking into the compilation pipeline. The system supports both generators (global files) and derivatives (per-source framework adapters).
Plugin Architecture
Section titled “Plugin Architecture”Plugins are registered singleton classes that respond to compiler hooks:
class MyPlugin extends PencelPlugin { readonly #registry = inject(SymbolRegistry); readonly #sourceFiles = inject(SourceFiles);
constructor(userOptions: MyPluginOptions) { super();
// Register symbols this.#registry.registerWellKnown([...]);
// Handle hooks this.handle("generate", async (hook) => { /* ... */ }); this.handle("derive", (hook) => { /* ... */ }); }}
// Register pluginPlugins.register("my-plugin", MyPlugin, { /* options */ });Hook Types
Section titled “Hook Types”Generate Hook
Section titled “Generate Hook”Fully rebuilds global files from the complete IR tree every compilation.
When to use: Files that aggregate data across multiple source files (e.g., ir.json, components.d.ts, directives.ts)
this.handle("generate", async (hook) => { // hook.irs: Array<FileIRSnapshot> // All file IRs available at once
for (const fileIR of hook.irs) { for (const component of fileIR.components) { // Process all components from all files } }});No 1:1 mapping – The handler sees the entire project state. Multiple source files contribute to one output file.
Derive Hook
Section titled “Derive Hook”Creates framework-specific per-source adapters. Runs once per source file that changed.
When to use: Files that map 1:1 to source files (e.g., component.gen.ts → component.angular.ts)
this.handle("derive", (hook) => { // hook.irr: IRRef<FileIR, SourceFile> // Single file being processed
const { ir, node: sourceFile } = hook.irr; // Generate adapter for this specific file});Incremental – Only processes files that changed. Can be much faster than full regeneration.
File Creation with Preferences
Section titled “File Creation with Preferences”When generating files, plugins can specify import preferences to control how symbols are resolved. See Symbol Resolution - Import Preferences for detailed examples.
const preference: ImportPreference = { style: "package", packageName: "@my/components",};
this.#sourceFiles.newFile( "out/index.ts", [exportStatement], { preference });const preference: ImportPreference = { style: "relative", consumerPath: sourceFile.fileName, symbolOverrides: [ { match: /^provide|inject/, packageName: "@angular/core", style: "package", }, { match: "*Internal", style: "deep", }, ],};
this.#sourceFiles.newFile( "out/helpers.ts", [helperExports], { preference });const componentNames = hook.irs .flatMap(f => f.components) .map(c => c.className);
// Register each componentfor (const name of componentNames) { this.#registry.registerInputSymbol(name, "@pencel/components");}
// Create directives with package importsconst preference: ImportPreference = { style: "package", packageName: "@pencel/components", symbolOverrides: componentNames.map(name => ({ match: name, packageName: "@pencel/components", })),};
this.#sourceFiles.newFile( "out/directives.ts", [directivesArray, provideFunction], { preference });Symbol Registration
Section titled “Symbol Registration”Plugins register symbols so they’re automatically imported when used:
Well-Known Symbols
Section titled “Well-Known Symbols”Stable APIs that should always be imported from a fixed module:
this.#registry.registerWellKnown([ { symbol: "provideAppInitializer", module: "@angular/core", importStyle: "named", }, { symbol: "component", module: "@angular/core", importStyle: "named", },]);Once registered, these symbols are imported automatically in all generated files, regardless of file location or preferences.
Project Symbols
Section titled “Project Symbols”User-defined symbols (component classes, exports) discovered during compilation:
// Typically called with preference stylefor (const component of components) { this.#registry.registerInputSymbol( component.className, packageName, // e.g., "@my/components" );}Project symbols can be configured per file via import preferences.
Plugin Lifecycle
Section titled “Plugin Lifecycle”- Instantiation – Plugin constructor runs, registers hooks and symbols
- Generation –
handle("generate")fires with complete IR - Derivation –
handle("derive")fires for each changed source file - File Writing – All files are preprocessed and written to disk
- Cleanup – Preferences and state cleared for next pass