Babelのプラグインを書く

モジュール性は、javascriptの世界でしっかりと確立されています。 ただし、すべての利点があるため、各ファイルに同じインポートを書き込むのは面倒です。 しかし、コレクターで頻繁に使用されるモジュールの接続を削除し、コードでグローバル変数として使用するとどうなりますか? babelプラグインのタスクのように見えます。 さて、babelの仕組みを理解しながら、このようなプラグインを一緒に作成しましょう。


「スケルトン」から始めましょう。 プラグインは、訪問者とともにオブジェクトを返す関数です。 引数により、 babel-coreからのモジュールを持つオブジェクトがそれに渡されます。 将来的には、 babel-typesモジュールが必要になります。

export default function({types: t}) { return { visitor: {} }; } 

訪問者はvisitorオブジェクトのメソッドであり、その名前は、ノードへのパスが渡されるFunctionDeclarationStringLiteral完全なリスト )などの抽象構文ツリー(ASD)のノードタイプに対応しています。 タイプIdentifierノードに興味があります。

 export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { } } }; } 

また、訪問者は2番目の引数の.optsプロパティのプラグイン設定にアクセスできます。 それらを通して、変数の名前と、インポートが作成されるモジュールへのパスを渡します。 次のようになります。

.babelrc
 { plugins: [[ "babel-plugin-auto-import", { declarations: [{name: "React", path: "react"}] } ]] } 

ASDをバイパスします。 方法。 結び目


Babelは、いくつかのコード(文字列形式)を受け入れます。このコードはトークンに分割され、そこからASDが構築されます。 次に、プラグインはASDを変更し、そこから新しいコードが生成され、出力に送られます。 ASDを操作するために、プラグインはパスを使用します。 また、このパスが表すノードのタイプをパスで確認することもできます。 このためのフォーマットメソッドがあり.["is" + ]() 。 たとえば、 path.isIdentifier() 。 パスは、 .find(callback)メソッドを使用して子パス間で検索でき、 .find(callback)メソッドを使用して親パス間で検索できます。 .parentPathプロパティは、親パスへの参照を保持します。

プラグイン自体の作成を始めましょう。 そしてまず、識別子をフィルタリングする必要があります。 Identifierタイプは、さまざまなタイプのノードで広く使用されています。 それらのいくつかだけが必要です。 次のようなコードがあるとします:

 React.Component 

このコードのSDAは次のようになります。

 { type: "MemberExpression", object: { type: "Identifier", name: "React" }, property: { type: "Identifier", name: "Component" }, computed: false } 

ノードは、 .typeプロパティと、各タイプに固有の他のプロパティを持つオブジェクトです。 ルートノードMemberExpressionについて考えます。 3つのプロパティがあります。 Objectは、ポイントの左側の式です。 この場合、これは識別子です。 computedプロパティは、識別子または式が右側にあるかどうかを示します(例: x["a" + b]Property -実際には、ポイントの右側にあります。

ここでプラグインフレームワークを実行すると、 React識別子とComponent識別子に対してそれぞれIdentifierメソッドが2回呼び出されます。 プラグインはReact IDを処理する必要がありますが、 Component IDはスキップしComponent 。 これを行うには、識別子パスが親パスを取得し、 MemberExpression型のノードである場合、識別子が.objectプロパティであるかどうかを確認する.objectます。 別の関数で検証を実行します。

 export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { if (!isCorrectIdentifier(path)) return; } } }; function isCorrectIdentifier(path) { let {parentPath} = path; if (parentPath.isMemberExpression() && parentPath.get("object") == path) return true; } } 

最終バージョンには多くのこのようなチェックがあります-各ケースには独自のチェックがあります。 しかし、それらはすべて同じ原理で機能します。

全リスト
 function isCorrectIdentifier(path) { let {parentPath} = path; if (parentPath.isArrayExpression()) return true; else if (parentPath.isArrowFunctionExpression()) return true; else if (parentPath.isAssignmentExpression() && parentPath.get("right") == path) return true; else if (parentPath.isAwaitExpression()) return true; else if (parentPath.isBinaryExpression()) return true; else if (parentPath.bindExpression && parentPath.bindExpression()) return true; else if (parentPath.isCallExpression()) return true; else if (parentPath.isClassDeclaration() && parentPath.get("superClass") == path) return true; else if (parentPath.isClassExpression() && parentPath.get("superClass") == path) return true; else if (parentPath.isConditionalExpression()) return true; else if (parentPath.isDecorator()) return true; else if (parentPath.isDoWhileStatement()) return true; else if (parentPath.isExpressionStatement()) return true; else if (parentPath.isExportDefaultDeclaration()) return true; else if (parentPath.isForInStatement()) return true; else if (parentPath.isForStatement()) return true; else if (parentPath.isIfStatement()) return true; else if (parentPath.isLogicalExpression()) return true; else if (parentPath.isMemberExpression() && parentPath.get("object") == path) return true; else if (parentPath.isNewExpression()) return true; else if (parentPath.isObjectProperty() && parentPath.get("value") == path) return !parentPath.node.shorthand; else if (parentPath.isReturnStatement()) return true; else if (parentPath.isSpreadElement()) return true; else if (parentPath.isSwitchStatement()) return true; else if (parentPath.isTaggedTemplateExpression()) return true; else if (parentPath.isThrowStatement()) return true; else if (parentPath.isUnaryExpression()) return true; else if (parentPath.isVariableDeclarator() && parentPath.get("init") == path) return true; return false; } 


可変範囲


次のステップでは、識別子がローカル変数として宣言されているか、グローバルであるかを確認します。 this- scopeパスには1つの便利なプロパティがあります。 これを使用して、現在の領域から始めて、可視性のすべての領域を反復処理します。 現在のスコープの変数は.bindingsプロパティにあります。 親スコープへのリンクは.parentプロパティにあります。 すべてのスコープのすべての変数を再帰的に調べて、そこに識別子が見つかったかどうかを確認することが残っています。

 export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { if (!isCorrectIdentifier(path)) return; let {node: identifier, scope} = path; if (isDefined(identifier, scope)) return; } } }; // ... function isDefined(identifier, {bindings, parent}) { let variables = Object.keys(bindings); if (variables.some(has, identifier)) return true; return parent ? isDefined(identifier, parent) : false; } function has(identifier) { let {name} = this; return identifier == name; } } 

いいね! これで、識別子を操作できるようになりました。 「グローバル」変数のoptions宣言から取得して処理します。

 let {declarations} = options; declarations.some(declaration => { if (declaration.name == identifier.name) { let program = path.findParent(path => path.isProgram()); insertImport(program, declaration); return true; } }); 

ASDの変更


そして、ASDの変更に着手しました。 ただし、新しいインポートの挿入を開始する前に、既存のインポートをすべて取得します。 これを行うには、 .reduceメソッドを使用して、タイプImportDeclarationパスを持つ配列を取得します。

 function insertImport(program, { name, path }) { let programBody = program.get("body"); let currentImportDeclarations = programBody.reduce(currentPath => { if (currentPath.isImportDeclaration()) list.push(currentPath); return list; }, []); } 

次に、識別子が既に接続されているかどうかを確認しましょう。

 let importDidAppend = currentImportDeclarations.some(({node: importDeclaration}) => { if (importDeclaration.source.value == path) { return importDeclaration.specifiers.some(specifier => specifier.local.name == name); } }); 

モジュールが接続されていない場合は、新しいインポートノードを作成し、プログラムに貼り付けます。

ノードを作成するには、 babel-typesモジュールを使用します。 それへの参照は変数tます。 各ノードには独自のメソッドがあります。 importDeclarationを作成する必要があります。 ドキュメントを見て、インポートを作成するには、修飾子(つまり、インポートされた変数の名前)とモジュールへのパスが必要であることがわかります。

まず、修飾子を作成します。 プラグインは、デフォルトでエクスポートされたモジュールを接続します( export default ... )。 次に、モジュールへのパスを持つノードを作成します。 これは、 StringLiteral型の単純な文字列です。

 let specifier = t.importDefaultSpecifier(t.identifier(name)); let pathToModule = t.stringLiteral(path); 

さて、インポートを作成するためのすべてがあります:

 let importDeclaration = t.importDeclaration([specifier], pathToModule); 

ASDにノードを挿入するために残ります。 このためには、方法が必要です。 パスは、 .replaceWith(node)メソッドを使用してノードに置き換えるか、 .replaceWithMultiple([...nodes])メソッドを使用してノードの配列に置き換えることができます。 .remove()メソッドを使用して削除できます。 挿入するには、 .insertBefore(node)および.insertAfter(node)メソッドを使用して、それぞれパスの前または後にノードを挿入します。

この場合、インポートはいわゆるコンテナに挿入する必要があります。 programノードには、 programを表す式の配列を含む.bodyプロパティがあります。 このような「コンテナ」配列にノードを挿入するために、パスには特別なpushContainerおよびunshiftContainerます。 最後のものを使用します。

 program.unshiftContainer("body", importNode); 

プラグインの準備ができました。 基本的なBabel APIに出会い、デバイスの原理とプラグインの操作を検証しました。 作成したプラグインは、正しく機能しない簡易バージョンです。 しかし、得られた知識があれば、 プラグインのコード全体を簡単に読むことができます 。 この記事がおもしろく、経験が役に立つことを願っています。 みんなありがとう!

Source: https://habr.com/ru/post/J330018/


All Articles