@babel/helper-environment-visitor
@babel/helper-environment-visitor 是一个实用工具包,提供当前的 this 上下文访问器。
安装
🌐 Installation
- npm
- Yarn
- pnpm
- Bun
npm install @babel/helper-environment-visitor
yarn add @babel/helper-environment-visitor
pnpm add @babel/helper-environment-visitor
bun add @babel/helper-environment-visitor
用法
🌐 Usage
要在你的 Babel 插件中使用这个包,从 @babel/helper-environment-visitor 导入所需的函数即可:
🌐 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
它会访问位于根遍历节点相同 this 上下文中的所有 AST 节点。单独运行这个访问器不会有任何效果,因为它不会修改 AST 节点。这个访问器是用来配合 traverse.visitors.merge 使用的。
🌐 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
将类成员 path 的计算键和装饰器重新入队,以便在当前遍历队列清空后再次处理。更多用法请参见 示例 部分。
🌐 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
替换顶层 this
🌐 Replace top level this
假设我们正在从原生 JavaScript 迁移到 ES 模块。现在 this 关键字在 ES 模块的顶层相当于 undefined(规范),我们想把所有顶层的 this 替换为 globalThis:
🌐 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"));
}
}
}
}
到目前为止,第一次修订的示例是有效的。不过,它并没有真正抓住顶层的概念:例如,我们不应该替换非箭头函数中的 this:比如函数声明、对象方法和类方法:
🌐 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";
}
}
如果遇到这样的非箭头函数,我们可以跳过遍历。在这里,我们在访问器选择器中用 | 组合了多种 AST 类型。
🌐 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|..." 是一个非常长的字符串,维护起来可能很困难。我们可以通过使用 FunctionParent 别名来缩短它:
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();
}
}
}
}
}
这个插件大体上能用。不过,它无法处理一个边缘情况,就是在计算的类元素中使用顶层的 this 时:
🌐 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);
}
}
}
}
}
还有一个遗失的边缘情况:this 可以在类属性的计算键中使用:
🌐 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
}
虽然 requeueComputedKeyAndDecorators 也能处理这个特殊情况,但这个插件到现在已经相当复杂了,而且花了不少时间在处理 this 上。实际上,过多关注 this 的处理反而偏离了真正的需求——就是用 globalThis 替换顶层的 this。
🌐 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.
environmentVisitor 的创建是为了通过将容易出错的 this 处理逻辑提取到一个辅助函数中来简化代码,这样你就不必再直接处理它了。
🌐 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
]);
}
}
你可以在 AST Explorer 上试用最终修订版。
🌐 You can try out the final revision on the AST Explorer.
正如其名字所示,requeueComputedKeyAndDecorators 同样支持 ES 装饰器 :
🌐 As its name implies, requeueComputedKeyAndDecorators supports ES decorators as well:
class Foo {
// replaced to `@globalThis.log`
@(this.log) foo = 1;
}
由于规范还在不断发展,使用 environmentVisitor 可能比自己实现 this 上下文访问器更容易。
🌐 Since the spec continues to evolve, using environmentVisitor can be easier than implementing your own this context visitor.
查找所有 super() 调用
🌐 Find all super() calls
这是来自 @babel/helper-create-class-features-plugin 的一个 代码片段。
🌐 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,
]);