CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-rollup--pluginutils

A set of utility functions commonly used by Rollup plugins

Pending
Overview
Eval results
Files

ast-analysis.mddocs/

AST Analysis

Tools for analyzing Abstract Syntax Trees (ASTs), tracking variable scopes, and extracting assignment patterns. These utilities are essential for plugins that need to understand code structure and variable usage.

Capabilities

attachScopes

Attaches scope objects to AST nodes for tracking variable declarations and scope chains. Each scope provides methods to check if variables are defined in the current or parent scopes.

/**
 * Attaches Scope objects to the relevant nodes of an AST
 * Each Scope object has a scope.contains(name) method that returns true
 * if a given name is defined in the current scope or a parent scope
 * @param ast - The AST to attach scopes to
 * @param propertyName - Property name to attach scope to (defaults to 'scope')
 * @returns The root scope object
 */
function attachScopes(ast: BaseNode, propertyName?: string): AttachedScope;

interface AttachedScope {
  parent?: AttachedScope;
  isBlockScope: boolean;
  declarations: { [key: string]: boolean };
  addDeclaration(node: BaseNode, isBlockDeclaration: boolean, isVar: boolean): void;
  contains(name: string): boolean;
}

Parameters:

  • ast (BaseNode): The AST root node to attach scopes to
  • propertyName (string, optional): The property name to use for attaching scopes to nodes. Defaults to 'scope'

Returns: The root AttachedScope object

AttachedScope Methods:

  • contains(name: string): boolean - Returns true if the variable name is defined in this scope or any parent scope
  • addDeclaration(node: BaseNode, isBlockDeclaration: boolean, isVar: boolean): void - Adds a variable declaration to this scope

Usage Examples:

import { attachScopes } from "@rollup/pluginutils";
import { walk } from "estree-walker";

export default function myPlugin(options = {}) {
  return {
    transform(code, id) {
      const ast = this.parse(code);
      
      // Attach scopes to AST nodes
      let scope = attachScopes(ast, 'scope');
      
      walk(ast, {
        enter(node) {
          // Update current scope when entering scoped nodes
          if (node.scope) scope = node.scope;
          
          // Check if variables are defined in current scope
          if (node.type === 'Identifier') {
            if (!scope.contains(node.name)) {
              // Variable is not defined in any scope - likely global
              console.log(`Global variable detected: ${node.name}`);
            }
          }
        },
        
        leave(node) {
          // Return to parent scope when leaving scoped nodes
          if (node.scope) scope = scope.parent;
        }
      });
      
      return { code };
    }
  };
}

// Advanced usage with variable injection
export default function injectPlugin(options = {}) {
  const { injections = {} } = options;
  
  return {
    transform(code, id) {
      const ast = this.parse(code);
      let scope = attachScopes(ast);
      
      let hasInjections = false;
      
      walk(ast, {
        enter(node) {
          if (node.scope) scope = node.scope;
          
          if (node.type === 'Identifier' && injections[node.name]) {
            // Only inject if variable is not already defined
            if (!scope.contains(node.name)) {
              // Inject the variable
              hasInjections = true;
            }
          }
        },
        leave(node) {
          if (node.scope) scope = scope.parent;
        }
      });
      
      if (hasInjections) {
        // Add imports for injected variables
        const imports = Object.keys(injections)
          .map(name => `import ${name} from '${injections[name]}';`)
          .join('\n');
        
        return { code: imports + '\n' + code };
      }
      
      return { code };
    }
  };
}

extractAssignedNames

Extracts variable names from destructuring patterns and assignment targets. Handles complex nested destructuring patterns including objects, arrays, and rest patterns.

/**
 * Extracts the names of all assignment targets based upon specified patterns
 * Handles destructuring patterns including objects, arrays, and rest patterns
 * @param param - An AST node representing an assignment pattern
 * @returns Array of extracted variable names
 */
function extractAssignedNames(param: BaseNode): string[];

Parameters:

  • param (BaseNode): An AST node representing an assignment pattern (typically from variable declarations or function parameters)

Returns: Array of strings representing all variable names that would be assigned

Supported Patterns:

  • Simple identifiers: x['x']
  • Object destructuring: {a, b: c}['a', 'c']
  • Array destructuring: [x, y]['x', 'y']
  • Rest patterns: {...rest}['rest']
  • Assignment patterns: {x = 5}['x']
  • Nested destructuring: {a: {b, c}}['b', 'c']

Usage Examples:

import { extractAssignedNames } from "@rollup/pluginutils";
import { walk } from "estree-walker";

export default function myPlugin(options = {}) {
  return {
    transform(code, id) {
      const ast = this.parse(code);
      const declaredNames = new Set();
      
      walk(ast, {
        enter(node) {
          // Extract names from variable declarations
          if (node.type === 'VariableDeclarator') {
            const names = extractAssignedNames(node.id);
            names.forEach(name => declaredNames.add(name));
            console.log(`Declared variables: ${names.join(', ')}`);
          }
          
          // Extract names from function parameters
          if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
            node.params.forEach(param => {
              const names = extractAssignedNames(param);
              names.forEach(name => declaredNames.add(name));
              console.log(`Function parameter names: ${names.join(', ')}`);
            });
          }
          
          // Extract names from catch clauses
          if (node.type === 'CatchClause' && node.param) {
            const names = extractAssignedNames(node.param);
            names.forEach(name => declaredNames.add(name));
            console.log(`Catch parameter names: ${names.join(', ')}`);
          }
        }
      });
      
      console.log(`All declared names: ${Array.from(declaredNames).join(', ')}`);
      return { code };
    }
  };
}

// Usage with variable tracking
export default function trackVariablesPlugin() {
  return {
    transform(code, id) {
      const ast = this.parse(code);
      const variableUsage = new Map();
      
      walk(ast, {
        enter(node) {
          if (node.type === 'VariableDeclarator') {
            const names = extractAssignedNames(node.id);
            
            // Track different destructuring patterns
            names.forEach(name => {
              if (!variableUsage.has(name)) {
                variableUsage.set(name, {
                  declared: true,
                  used: false,
                  line: node.loc?.start.line
                });
              }
            });
            
            // Example patterns and their extracted names:
            // const x = 1; → ['x']
            // const {a, b: c} = obj; → ['a', 'c']
            // const [x, y, ...rest] = arr; → ['x', 'y', 'rest']
            // const {a: {b, c}} = nested; → ['b', 'c']
            // const {x = 5} = obj; → ['x']
          }
          
          if (node.type === 'Identifier') {
            const usage = variableUsage.get(node.name);
            if (usage) {
              usage.used = true;
            }
          }
        }
      });
      
      // Report unused variables
      for (const [name, info] of variableUsage) {
        if (info.declared && !info.used) {
          console.warn(`Unused variable '${name}' at line ${info.line}`);
        }
      }
      
      return { code };
    }
  };
}

Common Patterns

Variable Injection with Scope Analysis

import { attachScopes, extractAssignedNames } from "@rollup/pluginutils";
import { walk } from "estree-walker";

export default function smartInjectPlugin(options = {}) {
  const { injections = {}, globals = [] } = options;
  
  return {
    transform(code, id) {
      const ast = this.parse(code);
      let scope = attachScopes(ast);
      
      const toInject = new Set();
      const declared = new Set();
      
      // First pass: collect all declared variables
      walk(ast, {
        enter(node) {
          if (node.scope) scope = node.scope;
          
          // Extract declared names from various declaration types
          if (node.type === 'VariableDeclarator') {
            extractAssignedNames(node.id).forEach(name => declared.add(name));
          }
          
          if (node.type === 'FunctionDeclaration' && node.id) {
            declared.add(node.id.name);
          }
          
          if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
            node.params.forEach(param => {
              extractAssignedNames(param).forEach(name => declared.add(name));
            });
          }
        },
        leave(node) {
          if (node.scope) scope = scope.parent;
        }
      });
      
      // Second pass: find variables to inject
      scope = attachScopes(ast);
      walk(ast, {
        enter(node) {
          if (node.scope) scope = node.scope;
          
          if (node.type === 'Identifier' && injections[node.name]) {
            // Only inject if not declared in any scope and not a global
            if (!scope.contains(node.name) && !globals.includes(node.name)) {
              toInject.add(node.name);
            }
          }
        },
        leave(node) {
          if (node.scope) scope = scope.parent;
        }
      });
      
      if (toInject.size > 0) {
        const imports = Array.from(toInject)
          .map(name => `import ${name} from '${injections[name]}';`)
          .join('\n');
        
        return { code: imports + '\n' + code };
      }
      
      return { code };
    }
  };
}

Dead Code Detection

import { attachScopes, extractAssignedNames } from "@rollup/pluginutils";
import { walk } from "estree-walker";

export default function deadCodePlugin() {
  return {
    transform(code, id) {
      const ast = this.parse(code);
      let scope = attachScopes(ast);
      
      const variables = new Map();
      
      // Track variable declarations and usage
      walk(ast, {
        enter(node) {
          if (node.scope) scope = node.scope;
          
          // Record declarations
          if (node.type === 'VariableDeclarator') {
            const names = extractAssignedNames(node.id);
            names.forEach(name => {
              variables.set(name, {
                declared: node,
                used: false,
                scope: scope
              });
            });
          }
          
          // Record usage
          if (node.type === 'Identifier') {
            const variable = variables.get(node.name);
            if (variable) {
              variable.used = true;
            }
          }
        },
        leave(node) {
          if (node.scope) scope = scope.parent;
        }
      });
      
      // Report unused variables
      for (const [name, info] of variables) {
        if (!info.used) {
          console.warn(`Dead code: unused variable '${name}'`);
        }
      }
      
      return { code };
    }
  };
}

Install with Tessl CLI

npx tessl i tessl/npm-rollup--pluginutils

docs

ast-analysis.md

code-generation.md

file-processing.md

index.md

regex-utilities.md

tile.json