This Babel plugin transforms ES2015 block-scoped declarations (const and let) to ES5-compatible var declarations, enabling the use of modern JavaScript block scoping syntax in environments that don't support it natively. The plugin handles complex scoping scenarios including temporal dead zones (TDZ), closure requirements in loops, and maintains proper variable hoisting behavior while preserving block scoping semantics.
npm install --save-dev babel-plugin-transform-es2015-block-scopingThis plugin is typically not imported directly in application code. Instead, it's configured in Babel's configuration system:
.babelrc:
{
"plugins": ["transform-es2015-block-scoping"]
}babel.config.js:
module.exports = {
plugins: ["babel-plugin-transform-es2015-block-scoping"]
};CLI usage:
babel --plugins transform-es2015-block-scoping script.jsProgrammatic usage:
const babel = require("babel-core");
const plugin = require("babel-plugin-transform-es2015-block-scoping");
const result = babel.transform(code, {
plugins: [plugin]
});Input:
function example() {
let x = 1;
const y = 2;
if (true) {
let x = 3; // different x
console.log(x, y); // 3, 2
}
console.log(x); // 1
}Output:
function example() {
var x = 1;
var y = 2;
if (true) {
var _x = 3; // renamed to avoid collision
console.log(_x, y); // 3, 2
}
console.log(x); // 1
}Input:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}Output:
var _loop = function _loop(i) {
setTimeout(function () {
return console.log(i);
}, 100);
};
for (var i = 0; i < 3; i++) {
_loop(i);
}The plugin is built around several key components that work together to transform block-scoped variables:
VariableDeclaration, Loop, CatchClause, and block statementslet/const declarations to var with appropriate renamingThe transformation process follows these steps:
let/const)var and rename variables to avoid conflictsbreak, continue, return) within closuresControls the plugin's behavior when encountering patterns that require closure creation for semantic correctness.
{
"plugins": [
["transform-es2015-block-scoping", {
"throwIfClosureRequired": boolean
}]
]
}Default: false
When false: The plugin automatically creates closures when needed to preserve block scoping semantics.
When true: The plugin throws an error instead of creating closures, useful for performance-sensitive code where closure creation is undesirable.
Example that triggers closure creation:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1);
}With throwIfClosureRequired: true, this code will cause a compilation error instead of automatic closure generation.
The main export is a function that returns a Babel plugin configuration.
/**
* Creates a Babel plugin for transforming ES2015 block scoping
* @returns {BabelPlugin} Plugin configuration object with visitor methods
*/
function transformES2015BlockScoping(): BabelPlugin;
interface BabelPlugin {
visitor: {
VariableDeclaration(path: NodePath, state: PluginState): void;
Loop(path: NodePath, state: PluginState): void;
CatchClause(path: NodePath, state: PluginState): void;
"BlockStatement|SwitchStatement|Program"(path: NodePath, state: PluginState): void;
};
}
interface PluginState {
file: BabelFile;
opts: PluginOptions;
}
interface PluginOptions {
throwIfClosureRequired?: boolean;
}Handles transformation of let and const declarations to var while preserving scoping semantics.
let/const to var declarationsManages block scoping within loop constructs, creating closures when necessary to preserve variable binding semantics.
Transforms block scoping within catch clauses to ensure proper variable isolation.
Handles general block scoping for block statements, switch statements, and program-level blocks.
When Babel's TDZ option is enabled (tdz: true), the plugin provides runtime checks for temporal dead zone violations:
/**
* Checks temporal dead zone status for variable references
* @param {NodePath} refPath - Reference path in AST
* @param {NodePath} bindingPath - Declaration path in AST
* @returns {"inside" | "outside" | "maybe"} TDZ status
*/
function getTDZStatus(refPath: NodePath, bindingPath: NodePath): "inside" | "outside" | "maybe";/**
* Builds runtime assertion for temporal dead zone checking
* @param {Node} node - AST node for the variable reference
* @param {BabelFile} file - Babel file instance
* @returns {CallExpression} Runtime check expression
*/
function buildTDZAssert(node: Node, file: BabelFile): CallExpression;Example TDZ transformation:
Input:
console.log(x); // TDZ violation
let x = 1;Output with TDZ enabled:
console.log(babelHelpers.temporalRef(x, "x", babelHelpers.temporalUndefined));
var x = babelHelpers.temporalUndefined;
x = 1;When throwIfClosureRequired: true is set, the plugin throws descriptive errors for patterns requiring closure creation:
// This pattern will throw with throwIfClosureRequired: true
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1);
}
// Error: "Compiling let/const in this block would add a closure (throwIfClosureRequired)."With TDZ checking enabled, runtime errors are thrown for temporal dead zone violations:
// This generates runtime TDZ checking code
console.log(x); // Throws: "ReferenceError: x is not defined - temporal dead zone"
let x = 1;This plugin integrates seamlessly with Babel's transformation pipeline:
temporalRef, temporalUndefined)throwIfClosureRequired: true in performance-critical code to avoid automatic closure generationThis plugin enables ES2015 block scoping features in environments that don't natively support them:
let/const support