本文へスキップ

@babel/helper-environment-visitor

@babel/helper-environment-visitor は、現在の `this` コンテキストビジターを提供するユーティリティパッケージです。

インストール

npm install @babel/helper-environment-visitor

使用方法

Babelプラグインでこのパッケージを使用するには、@babel/helper-environment-visitorから必要な関数をインポートします。

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

environmentVisitor

これは、ルートトラバースノードへの同じ`this`コンテキスト内のすべてのASTノードを訪問します。ASTノードを変更しないため、このビジターを単独で実行しても何も起こりません。このビジターは、`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

クラスメンバ`path`の計算されたキーとデコレーターを再キューイングします。これにより、現在のトラバーサルキューが空になった後に再訪されます。詳細な使用方法については、セクションを参照してください。

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

最上位レベルの`this`の置換

プレーンなJavaScriptからESモジュールへの移行を考えてみましょう。`this`キーワードはESモジュールの最上位レベルで`undefined`と同等であるため(仕様)、最上位レベルの`this`をglobalThisに置き換えたいとします。

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

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

コードモッドプラグインを作成できます。これが最初の改訂版です。

改訂版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"));
}
}
}
}

最初の改訂版は今のところ例では機能します。しかし、最上位レベルの概念を実際には捉えていません。たとえば、非矢印関数(例:関数宣言、オブジェクトメソッド、クラスメソッド)内では`this`を置き換えるべきではありません。

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

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

このような非矢印関数に遭遇した場合、トラバーサルをスキップできます。ここでは、ビジターセレクターで`|`を使用して複数のASTタイプを組み合わせます。

改訂版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|..."`は非常に長い文字列であり、保守が困難になる可能性があります。FunctionParentエイリアスを使用することで短縮できます。

改訂版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();
}
}
}
}
}

プラグインは一般的に機能します。ただし、最上位レベルの`this`が計算されたクラス要素内で使用されているというエッジケースを処理できません。

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

上記で強調表示されているセクションの簡略化された構文ツリーを次に示します。

{
"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
}

ClassMethodノード全体をスキップすると、`key`プロパティの下にある`this.foo`にアクセスできなくなります。ただし、これは任意の式である可能性があるため、アクセスする必要があります。これを実現するには、ClassMethodノードのみをスキップする必要があることをBabelに指示する必要がありますが、その計算されたキーはスキップする必要がありません。requeueComputedKeyAndDecoratorsが役立ちます。

改訂版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);
}
}
}
}
}

まだ1つのエッジケースが欠けています。`this`はクラスプロパティの計算されたキー内で使用される可能性があります。

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

requeueComputedKeyAndDecoratorsはこのエッジケースも処理できますが、プラグインはこの時点でかなり複雑になり、`this`コンテキストの処理にかなりの時間が費やされています。実際、`this`の処理に焦点を当てているため、実際の要件である最上位レベルの`this`を`globalThis`に置き換えるということが脇に追いやられています。

environmentVisitorは、エラーが発生しやすい`this`処理ロジックをヘルパー関数に抽出することによりコードを簡素化するために作成されたため、直接処理する必要がなくなります。

改訂版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
]);
}
}

AST Explorerで最終的な改訂版を試すことができます。

名前が示すように、requeueComputedKeyAndDecoratorsESデコレーターもサポートしています。

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

仕様は進化し続けているため、environmentVisitorを使用する方が、独自の`this`コンテキストビジターを実装するよりも簡単です。

すべての`super()`呼び出しの検索

これは、@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,
]);