Skip to content

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

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 plugin
Plugins.register("my-plugin", MyPlugin, { /* options */ });

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.

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

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

Plugins register symbols so they’re automatically imported when used:

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.

User-defined symbols (component classes, exports) discovered during compilation:

// Typically called with preference style
for (const component of components) {
this.#registry.registerInputSymbol(
component.className,
packageName, // e.g., "@my/components"
);
}

Project symbols can be configured per file via import preferences.

  1. Instantiation – Plugin constructor runs, registers hooks and symbols
  2. Generationhandle("generate") fires with complete IR
  3. Derivationhandle("derive") fires for each changed source file
  4. File Writing – All files are preprocessed and written to disk
  5. Cleanup – Preferences and state cleared for next pass