A set of utility functions commonly used by Rollup plugins
—
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.
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 topropertyName (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 scopeaddDeclaration(node: BaseNode, isBlockDeclaration: boolean, isVar: boolean): void - Adds a variable declaration to this scopeUsage 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 };
}
};
}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:
x → ['x']{a, b: c} → ['a', 'c'][x, y] → ['x', 'y']{...rest} → ['rest']{x = 5} → ['x']{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 };
}
};
}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 };
}
};
}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