Plugin API

Rust plugin API

Implement plugins with the Rust Plugin trait and SharedPluginable.

The telarel crate re-exports the plugin interface from telarel_plugin. A Rust plugin implements the Plugin trait and is wrapped into a shared, object-safe handle with Plugin::new_shared.

For the hook contract shared by both languages, see the Plugin API.

The Plugin trait

pub trait Plugin: Any + Debug + Send + Sync + 'static {
    fn new_shared(plugin: Self) -> SharedPluginable
    where
        Self: Sized;

    fn name(&self) -> Cow<'static, str>;

    fn register_hook_usage(&self) -> HookUsage;

    // ... one method per hook, each defaulting to a no-op ...
}
  • name returns the plugin name.
  • register_hook_usage declares which hooks the plugin implements. The partitioned stages — parse, prepare, transform, finalize, and generate — are only called when declared. options, start, and end run on every settled plugin regardless of usage.
  • new_shared wraps a sized plugin into a SharedPluginable, which is Arc<dyn Pluginable>. Pluginable is the object-safe mirror of the trait and is implemented for every Plugin.

Every hook has a default implementation that does nothing, so a plugin only writes the hooks it needs.

HookUsage

HookUsage is a bit flag set with one flag per hook:

use telarel::HookUsage;

let usage: HookUsage = HookUsage::Prepare | HookUsage::End;

The flags are Options, Start, Parse, Prepare, Transform, Finalize, Generate, and End.

Hooks

Each hook returns an impl Future, so its body can be written as an async block. The context borrows the pipeline state, and the hook resolves to its return type.

HookArgumentsReturn
options&CommonPluginContext, &OptionsArgsOptionsReturn
start&PluginContext, &StartArgsNotifyReturn
parse&PluginContext, ParseArgsParseReturn
prepare&PluginContext, PrepareArgsNotifyReturn
transform&PluginContext, TransformArgsTransformReturn
finalize&PluginContext, FinalizeArgsNotifyReturn
generate&PluginContext, GenerateArgsGenerateReturn
end&PluginContext, &EndArgsNotifyReturn

Arguments

TypeFields
OptionsArgsoptions: ResolvableOptions, plugins: Vec<SharedPluginable>
StartArgsoptions: ResolvedOptions
ParseArgscode: &str, options: &ParserOptions
PrepareArgsast: &Ast
TransformArgsast: Ast (owned)
FinalizeArgsast: &Ast
GenerateArgsast: &Ast, options: &GeneratorOptions
EndArgserror: Option<CompileError>

The options bag is shared by every entry point and always uses source_map. parse and rewrite expose a flat input_source_map at the entry point; it is normalized to source_map.input before the options hook runs. Those AST-producing entry points ignore source_map.output and output, so a stage-agnostic plugin may set them for every stage without failing.

A parse hook replacement is used as returned: its own Ast / Program.source_type carries the grammar that later stages and code generation see. ParserOptions are passed to the hook, but the driver applies them only to the builtin parser that runs when every hook defers. The JavaScript binding differs: it rebuilds a parse replacement with the parser options' grammar.

TransformArgs owns a cheap Ast handle: Ast::clone is O(1), and Ast::with_mut copies the arena on write when the driver still holds the carried AST. TransformArgs is Clone, not Copy. Returning None leaves the driver's AST untouched.

with_mut may rewrite the tree, but changing the source text requires building a new Ast and returning it. Debug builds assert that Program::source_text still matches Ast::source.

TransformOutput is the value a transform hook returns to replace the carried AST; it wraps the new owned ast.

Returns

Every fallible hook returns Result<_, PluginError>. NotifyReturn is Result<(), PluginError>; the stages that hand back a value return Option inside the result, where None means "no change".

TypeDefinition
OptionsReturnResult<Option<OptionsArgs>, PluginError>
NotifyReturnResult<(), PluginError>
ParseReturnResult<Option<AstResult>, PluginError>
TransformReturnResult<Option<TransformOutput>, PluginError>
GenerateReturnResult<Option<CodeResult>, PluginError>

Returning Some from options replaces the whole options bag; fields the returned OptionsArgs omits are dropped and fall back to their defaults.

Errors

A hook reports failure with PluginError. The error reaches the entry point as a CompileError with its message and attached source unchanged.

Ordering

Each Rust hook exposes a <hook>_meta method returning Option<PluginHookMeta>, all defaulting to None. PluginOrder is Pre or Post.

use telarel::{PluginHookMeta, PluginOrder};

fn transform_meta(&self) -> Option<PluginHookMeta> {
    Some(PluginHookMeta { order: Some(PluginOrder::Pre) })
}

Context

  • options receives CommonPluginContext, which carries no fields.
  • Every later hook receives PluginContext:
FieldTypeDescription
cwd&PathFilesystem root used to resolve relative paths.
moduleModuleInfoModule being compiled.

ModuleInfo holds file: &str (the file being compiled), language: Language, and source_type: SourceType.

Passing plugins

Wrap each plugin in Plugin::new_shared and pass the list to any entry point.

use telarel::{CodeResult, CompileOptions, Plugin, SharedPluginable, compile};

let plugins: Vec<SharedPluginable> = vec![
    Plugin::new_shared(TracePlugin),
    Plugin::new_shared(ReplaceRootPlugin),
];

let result: CodeResult = compile(
    String::from("const x: number = 1;"),
    CompileOptions::default(),
    plugins,
)
.await?;

Example

A plugin that rewrites the module specifier react to preact/compat in transform. The hook owns a cheap AST handle; mutating it through Ast::with_mut copies the arena on write, and the mutated AST is returned as TransformOutput:

use std::borrow::Cow;
use std::future::Future;

use telarel::allocator::Allocator;
use telarel::ast::ast::{ImportDeclaration, Program};
use telarel::ast_visit::VisitMut;
use telarel::ast_visit::walk_mut;
use telarel::str::Str;
use telarel::{
    Ast, HookUsage, Plugin, PluginContext, TransformArgs, TransformOutput,
    TransformReturn,
};

const FROM: &str = "react";

const TO: &str = "preact/compat";

struct ImportRewriter<'x> {
    allocator: &'x Allocator,
}

impl<'x> VisitMut<'x> for ImportRewriter<'x> {
    fn visit_import_declaration(
        &mut self,
        declaration: &mut ImportDeclaration<'x>,
    ) {
        if declaration.source.value.as_str() == FROM {
            declaration.source.value = Str::from_str_in(TO, &self.allocator);
        }
    }
}

#[derive(Debug)]
struct ImportRewritePlugin;

impl Plugin for ImportRewritePlugin {
    fn name(&self) -> Cow<'static, str> {
        "import-rewrite".into()
    }

    fn register_hook_usage(&self) -> HookUsage {
        HookUsage::Transform
    }

    fn transform<'a>(
        &'a self,
        _ctx: &'a PluginContext<'_>,
        args: TransformArgs,
    ) -> impl Future<Output = TransformReturn> + Send + 'a {
        async move {
            let mut ast: Ast = args.ast;

            ast.with_mut(|allocator: &Allocator, program: &mut Program<'_>| {
                let mut rewriter: ImportRewriter<'_> =
                    ImportRewriter { allocator };

                walk_mut::walk_program(&mut rewriter, program);
            });

            Ok(Some(TransformOutput { ast }))
        }
    }
}

Last updated on

Copyright © 2026, Alpheus

On this page