跳到主要内容

@babel/helper-environment-visitor

@babel/helper-environment-visitor is a utility package that provides a current this context visitor.

Installation

npm install @babel/helper-environment-visitor

Usage

To use the package in your Babel plugin, import the required functions from @babel/helper-environment-visitor:

my-babel-plugin.js
import environmentVisitor, {
requeueComputedKeyAndDecorators
} from "@babel/helper-environment-visitor";

environmentVisitor

It visits all AST nodes within the same this context to the root traverse node. Running this visitor alone is no-op as it does not modify AST nodes. This visitor is meant to be used with traverse.visitors.merge.

collect-await-expression.plugin.js
module.exports = (api) => {
const { types: t, traverse } = api;
return {
name: "collect-await",
visitor: {
Function(path) {
if (path.node.async) {
const awaitExpressions = [];
// Get a list of related await expressions within the async function body
path.traverse(traverse.visitors.merge([
environmentVisitor,
{
AwaitExpression(path) {
awaitExpressions.push(path);
},
ArrowFunctionExpression(path) {
path.skip();
},
}
]))
}
}
}
}
}

requeueComputedKeyAndDecorators

requeueComputedKeyAndDecorators(path: NodePath): void

Requeue the computed key and decorators of a class member path so that they will be revisited after current traversal queue is drained. See the example section for more usage.

my-babel-plugin.js
if (path.isMethod()) {
requeueComputedKeyAndDecorators(path)
}

Example

Replace top level this

Suppose we are migrating from vanilla JavaScript to ES Modules. Now that the this keyword is equivalent to undefined at the top level of an ESModule (spec), we want to replace all top-level this to globalThis:

input.js
// replace this expression to `globalThis.foo = "top"`
this.foo = "top";

() => {
// replace
this.foo = "top"
}

We can draft a code mod plugin, here is our first revision:

Revision 1: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
}
}
}

The first revision works for examples so far. However, it does not really capture the idea of top-level: For example, we should not replace this within a non-arrow function: e.g. function declaration, object methods and class methods:

input.js
function Foo() {
// don't replace
this.foo = "inner";
}

class Bar {
method() {
// don't replace
this.foo = "inner";
}
}

We can skip traversing if we encounter such non-arrow functions. Here we combine multiple AST types with | in the visitor selector.

Revision 2: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
"FunctionDeclaration|FunctionExpression|ObjectMethod|ClassMethod|ClassPrivateMethod"(path) {
path.skip();
}
}
}
}

"FunctionDeclaration|..." is a really long string and can be difficult to maintain. We can shorten it by using the FunctionParent alias:

Revision 3: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
FunctionParent(path) {
if (!path.isArrowFunctionExpression()) {
path.skip();
}
}
}
}
}

The plugin works generally. However, it can not handle an edge case where top-level this is used within computed class elements:

input.js
class Bar {
// replace
[this.foo = "outer"]() {
// don't replace
this.foo = "inner";
}
}

Here is a simplified syntax tree of the highlighted section above:

{
"type": "ClassMethod", // skipped
"key": { "type": "AssignmentExpression" }, // [this.foo = "outer"]
"body": { "type": "BlockStatement" }, // { this.foo = "inner"; }
"params": [], // should visit too if there are any
"computed": true
}

If the entire ClassMethod node is skipped, then we won't be able to visit the this.foo under the key property. However, we must visit it as it could be any expression. To achieve this, we need to tell Babel to skip only the ClassMethod node, but not its computed key. This is where requeueComputedKeyAndDecorators comes in handy:

Revision 4: replace-top-level-this-plugin.js
import {
requeueComputedKeyAndDecorators
} from "@babel/helper-environment-visitor";

module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
FunctionParent(path) {
if (!path.isArrowFunctionExpression()) {
path.skip();
}
if (path.isMethod()) {
requeueComputedKeyAndDecorators(path);
}
}
}
}
}

There is still one missing edge case: this can be used within computed keys of a class property:

input.js
class Bar {
// replace
[this.foo = "outer"] =
// don't replace
this.foo
}

Although requeueComputedKeyAndDecorators can handle this edge case as well, the plugin has become quite complex at this point, with a significant amount of time spent on handling the this context. In fact, the focus on dealing with this has detracted from the actual requirement, which is to replace top-level this with globalThis.

The environmentVisitor is created to simplify the code by extracting the error-prone this-handling logic into a helper function, so that you no longer have to deal with it directly.

Revision 5: replace-top-level-this-plugin.js
import environmentVisitor from "@babel/helper-environment-visitor";

module.exports = (api) => {
const { types: t, traverse } = api;
return {
name: "replace-top-level-this",
visitor: traverse.visitors.merge([
{
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
},
environmentVisitor
]);
}
}

You can try out the final revision on the AST Explorer.

As its name implies, requeueComputedKeyAndDecorators supports ES decorators as well:

input.js
class Foo {
// replaced to `@globalThis.log`
@(this.log) foo = 1;
}

Since the spec continues to evolve, using environmentVisitor can be easier than implementing your own this context visitor.

Find all super() calls

This is a code snippet from @babel/helper-create-class-features-plugin.

src/misc.ts
const findBareSupers = traverse.visitors.merge<NodePath<t.CallExpression>[]>([
{
Super(path) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
}
},
},
environmentVisitor,
]);