@babel/helper-environment-visitor
@babel/helper-environment-visitor
is a utility package that provides a current this
context visitor.
Installation
- npm
- Yarn
- pnpm
npm install @babel/helper-environment-visitor
yarn add @babel/helper-environment-visitor
pnpm add @babel/helper-environment-visitor
Usage
To use the package in your Babel plugin, import the required functions from @babel/helper-environment-visitor
:
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
.
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.
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
:
// 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:
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:
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.
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:
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:
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:
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:
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.
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:
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
.
const findBareSupers = traverse.visitors.merge<NodePath<t.CallExpression>[]>([
{
Super(path) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
}
},
},
environmentVisitor,
]);