WyW-in-JS のコードリーディング
WyW-in-JS のコードリーディングをしているのでメモを残しておく。
コードリーディングでの理解に誤りがある場合はコメントいただけると助かります。
コアロジックは @wyw-in-js/transform
にある。
Vite や rollup 等の各種バンドラーからは以下の Babel トランスフォームが呼び出されるため、こちらから読み進めていけば OK。
babelTransform
を読み進めていくと、以下の workflow
に行き着く。
こちらが WyW-in-JS によるゼロランタイム処理のエントリーとなる重要なファイル。
大まかに以下のステップで処理が実行されるっぽい。
- processEntrypoint
- evalFile
- インターポレーション[3]によって埋め込まれた変数や式を計算する
- インターポレーションによって埋め込まれた変数や式が外部ファイルで定義されている場合でも、依存関係を解決した上で計算する
- これらの計算、及び、依存関係の解決を Node.js の VM でホイストされたコードを実行することで実現している
- collect
-
css
やstyled
API で定義されたスタイルを収集して保持しておく
-
- extract
- 収集したスタイルを抽出して CSS テキストに変換する
1. processEntrypoint
1.1. explodeReexports
1.2. transform
1.2.1. prepareCode
1.2.1.1. runPreevalStage
1.2.1.1.1. preeval
この babel プラグインがかなり重要。
ホイストもここで行われる。
以下のようなコードを
import { css } from "@wyw-in-js/template-tag-syntax";
import { color } from "./color";
const classA = css`
color: ${color};
background: green;
`;
export { classA };
以下のように変換する。
import { color } from "./color";
const _exp = /*#__PURE__*/() => color;
const classA = "c2tf5um";
export { classA };
export const __wywPreval = {
_exp: _exp,
};
この時点ではモジュールの解決はしていないはずなのに、なぜ css
API をクラス名に変換できているのが不思議。スタイルのテキストからハッシュ化しているわけではなく、何かしらの方法で衝突しないクラス名を生成しているのかな?全く同じスタイルだった場合でもクラス名が一緒にならない感じ・・・?後で試してみる。
1.2.1.2. evaluator
1.2.2. resolveImports
1.2.3. processImports
2. evalFile
2.1. evaluate
2.1.1. Module
2.1.1.1. createVmContext
2.1.1.2. script.runInContext
(コード読んだ感じ恐らく)Node.js の VM を利用して、ファイルの依存関係を解決した上で計算する
WyW-in-JS の凄いところは、別ファイルで定義された定数を css
API で利用しても問題なく変換できること。
以下のようなコードが、
import { css } from "@wyw-in-js/template-tag-syntax";
import { color } from "./color";
const classA = css`
color: ${color};
background: green;
`;
export { classA };
export const color = "red";
以下のように変換される。
const classA = "c2tf5um";
export { classA };
.c2tf5um{font-size:14px;color:red;background:green}
ここら辺のファイルの依存関係を Node.js の VM を利用して解決していそうなので、詳しく見てみる。
ファイルの依存関係を解決するための重要なクラスが以下の Module
クラス。
コードをちゃんと読んでみたところ、やはり Node.js の VM を活用して依存関係を解決しているようだった。
VM について簡単に説明すると、V8 の仮想マシンのコンテキスト内で JavaScript コードを実行するためのもの。VM で呼び出されたコードと呼び出し元のコードとは異なるグローバルオブジェクトとなる = 独立したコンテキストでコードが実行される。そのため、VM コンテキスト内では require
や process
、__dirname
等の一部のグローバルオブジェクトやモジュールを直接参照することができない。
WyW-in-JS では、VM コンテキスト内で参照できないグローバルオブジェクトやモジュールを参照可能にするための独自のコンテキストを作成している。例えば、 window
オブジェクトは happy-dom の HappyWindow
インスタンスが参照されるようにしたり、require
は Module
クラスの require
メソッド(this.require
)が、exports
は Entrypoint クラス[1]の exports オブジェクト(entrypoint.exports
)が参照されるようされるようになっている。
このコンテキストを使用して以下のように VM でコードを実行している。
上の返信コメントのコードが「1. processEntrypoint」によって、以下のように CommonJS 形式で変換されたコードが Entrypoint
クラスのインスタンス内で保持されている。
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.__wywPreval = void 0;
var _color = require("./color");
const _exp = /*#__PURE__*/() => _color.color;
const __wywPreval = exports.__wywPreval = {
_exp: _exp,
};
この保持されたコードを VM で実行することで、VM コンテキストで定義した exports
の参照である entrypoint.exports
に以下のようなエキスポートされた値を抽出できる。このエキスポートされた値と css
API のスタイルを突合して css テキストを生成する(はず)。
{
__wywPreval: {
_exp: _exp,
};
}
続いて、依存関係の解決について。
require("./color")
で Module
クラスの require
が参照される。
color.ts
は index.ts
同様、以下のように CommonJS 形式で変換されたコードが Entrypoint
クラスのインスタンス内で保持されている。
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
const red = exports.color = "red";
Module
クラスの require
ではこの保持されたコードを Module
クラスの evaluate
を実行(つまり、index.ts
の時と同様 VM で実行)し、entrypoint.exports
に以下のようなエキスポートされた値を抽出し、それを返すような実装となっている。
{ color: "red" }
つまり、依存関係を解決する際は、依存元だけでなく依存先も VM で実行し、コード内でエキスポートされた値を抽出することで、依存関係を解決している。
-
Entrypoint クラスは JS ファイルの情報を管理しているクラスです。変換前のオリジナルのコードや、「1. processEntrypoint」によって変換されたコード、そのファイルでエキスポートされた値など保持しています。 ↩︎
3. collect
TODO
4. extract
TODO