or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

ast-plugin-system.mdbroccoli-plugin.mdember-cli-integration.mdindex.mdtemplate-compilation.md
tile.json

ast-plugin-system.mddocs/

AST Plugin System

Extensible system for registering custom HTMLBars AST transformations. Supports both simple transforms and dependency-aware plugins with sophisticated caching strategies and parallel processing capabilities.

Capabilities

Plugin Registration

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

Plugin Implementation

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

Dependency Tracking

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

Plugin Setup and Configuration

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

Parallel Processing

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;

Caching Strategies

Cache Key Generation

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

Dependency Invalidation

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

Error Handling

The AST plugin system provides comprehensive error handling:

  • Plugin Loading Errors: Missing or invalid plugin modules
  • Transform Errors: Runtime errors during AST processing
  • Dependency Errors: Issues with dependency tracking
  • Parallelization Errors: Worker process failures

Error messages include:

  • Template file being processed
  • Plugin name causing the error
  • AST node information when applicable
  • Stack traces for debugging