「スケルトン」から始めましょう。 プラグインは、訪問者とともにオブジェクトを返す関数です。 引数により、 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に出会い、デバイスの原理とプラグインの操作を検証しました。 作成したプラグインは、正しく機能しない簡易バージョンです。 しかし、得られた知識があれば、 プラグインのコード全体を簡単に読むことができます 。 この記事がおもしろく、経験が役に立つことを願っています。 みんなありがとう!