CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ember-cli-htmlbars

A library for adding HTMLBars template compilation to Ember CLI applications

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

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

Install with Tessl CLI

npx tessl i tessl/npm-ember-cli-htmlbars

docs

ast-plugin-system.md

broccoli-plugin.md

ember-cli-integration.md

index.md

template-compilation.md

tile.json