Extensible system for registering custom HTMLBars AST transformations. Supports both simple transforms and dependency-aware plugins with sophisticated caching strategies and parallel processing capabilities.
Register AST plugins through Ember CLI's preprocessor registry system:
/**
* Register an AST plugin with ember-cli-htmlbars
* Called in addon's included() hook
*/
interface ASTPluginWrapper {
/** Plugin name for debugging and identification */
name: string;
/** Plugin builder function */
plugin: ASTPluginBuilder;
/** Enable dependency invalidation tracking */
dependencyInvalidation?: boolean;
/** Function returning cache key for plugin state */
cacheKey?: () => string;
/** Function returning plugin base directory for cache invalidation */
baseDir?: () => string;
/** Configuration for parallel Babel processing */
parallelBabel?: ParallelBabelConfig;
}
/**
* AST Plugin builder function signature
* @param env - Plugin environment with moduleName and meta
* @returns AST plugin with visitor pattern
*/
type ASTPluginBuilder = (env: ASTPluginEnvironment) => ASTPlugin;
interface ASTPluginEnvironment {
/** Module name of template being processed */
moduleName: string;
/** Additional metadata */
meta: object;
}
interface ASTPlugin {
/** Plugin name */
name: string;
/** AST visitor object with node type handlers */
visitor: ASTVisitor;
}
interface ASTVisitor {
/** Handle element nodes (e.g., <div>) */
ElementNode?: (node: any) => void;
/** Handle mustache expressions (e.g., {{helper}}) */
MustacheStatement?: (node: any) => void;
/** Handle block statements (e.g., {{#each}}) */
BlockStatement?: (node: any) => void;
/** Handle text nodes */
TextNode?: (node: any) => void;
/** Handle other AST node types */
[nodeType: string]: ((node: any) => void) | undefined;
}Usage Example:
// In addon's index.js
module.exports = {
name: 'my-addon',
included() {
this.app.registry.add('htmlbars-ast-plugin', {
name: 'my-transform',
plugin: require('./lib/my-ast-transform'),
dependencyInvalidation: true,
baseDir: () => __dirname,
cacheKey: () => JSON.stringify(this.getPluginConfig())
});
this._super.included.apply(this, arguments);
}
};AST plugins follow the visitor pattern for transforming HTMLBars syntax trees:
/**
* Example AST plugin that transforms element attributes
* @param env - Plugin environment
* @returns Plugin with visitor methods
*/
function MyASTTransform(env) {
return {
name: 'my-transform',
visitor: {
ElementNode(node) {
// Transform element nodes
node.attributes.forEach(attr => {
if (attr.name === 'data-old') {
attr.name = 'data-new';
}
});
},
MustacheStatement(node) {
// Transform mustache expressions
if (node.path.original === 'oldHelper') {
node.path.original = 'newHelper';
}
}
}
};
}Advanced dependency tracking for plugins that depend on external files:
/**
* Plugin interface with dependency tracking capabilities
* Used when dependencyInvalidation: true is set
*/
interface ASTPluginWithDeps extends ASTPlugin {
/**
* Reset dependency tracking for a new file
* @param relativePath - Path to template file being processed
*/
resetDependencies?(relativePath: string): void;
/**
* Return array of file dependencies discovered during processing
* @param relativePath - Path to template file that was processed
* @returns Array of dependency file paths
*/
dependencies(relativePath: string): string[];
}
/**
* Alternative: Plugin builder with dependencies method
*/
interface ASTPluginBuilderWithDeps extends ASTPluginBuilder {
/**
* Return dependencies for a template file
* @param relativePath - Template file path
* @returns Array of dependency paths
*/
dependencies(relativePath: string): string[];
}Usage Example:
function DependencyAwareTransform(env) {
const fileDependencies = new Set();
return {
name: 'dependency-transform',
visitor: {
ElementNode(node) {
// Check for external template references
const src = node.attributes.find(attr => attr.name === 'src');
if (src && src.value.type === 'TextNode') {
fileDependencies.add(src.value.chars);
}
}
},
resetDependencies(relativePath) {
fileDependencies.clear();
},
dependencies(relativePath) {
return Array.from(fileDependencies);
}
};
}Internal utilities for processing plugin configurations:
/**
* Process plugin wrappers into executable configuration
* @param wrappers - Array of plugin wrapper configurations
* @returns Plugin setup information
*/
function setupPlugins(wrappers: ASTPluginWrapper[]): PluginInfo;
interface PluginInfo {
/** Processed plugin instances */
plugins: ASTPlugin[];
/** Plugin names for debugging */
pluginNames: string[];
/** Cache keys from all plugins */
cacheKeys: string[];
/** Parallel processing configurations */
parallelConfigs: ParallelBabelConfig[];
/** Whether all plugins support parallelization */
canParallelize: boolean;
/** Names of plugins that prevent parallelization */
unparallelizableWrappers: string[];
/** Whether any plugin uses dependency invalidation */
dependencyInvalidation: boolean;
}Support for parallel Babel processing to improve build performance:
interface ParallelBabelConfig {
/** Path to worker module */
requireFile: string;
/** Function name to call in worker */
buildUsing: string;
/** Parameters to pass to worker function */
params: object;
}
/**
* Create a parallelized Babel plugin configuration
*/
function buildParalleizedBabelPlugin(
pluginInfo: PluginInfo,
projectConfig: object,
templateCompilerPath: string,
isProduction: boolean,
requiresModuleApiPolyfill: boolean
): ParallelBabelPlugin;Plugins can contribute to cache invalidation through multiple mechanisms:
// Base directory monitoring - invalidates when plugin files change
baseDir: () => path.join(__dirname, 'lib'),
// Custom cache key - invalidates when key changes
cacheKey: () => {
return JSON.stringify({
version: require('./package.json').version,
config: this.getPluginConfig(),
timestamp: this.getConfigModificationTime()
});
}For plugins that depend on external files:
// Plugin marks dependencyInvalidation: true
// Plugin implements dependencies() method
// Build system watches returned dependency files
// Cache invalidates when dependencies change
const plugin = {
name: 'external-deps',
plugin: MyTransform,
dependencyInvalidation: true
};The AST plugin system provides comprehensive error handling:
Error messages include: