Babelのプラグインを書く

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









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



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





訪問者はvisitor



オブジェクトのメソッドであり、その名前は、ノードへのパスが渡されるFunctionDeclaration



StringLiteral



完全なリスト )などの抽象構文ツリー(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に出会い、デバイスの原理とプラグインの操作を検証しました。 作成したプラグインは、正しく機能しない簡易バージョンです。 しかし、得られた知識があれば、 プラグインのコード全体を簡単に読むことができます 。 この記事がおもしろく、経験が役に立つことを願っています。 みんなありがとう!



All Articles