Open9

WyW-in-JS のコードリーディング

KotaroKotaro

babelTransform を読み進めていくと、以下の workflow に行き着く。
こちらが WyW-in-JS によるゼロランタイム処理のエントリーとなる重要なファイル。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/workflow.ts

大まかに以下のステップで処理が実行されるっぽい。

  1. processEntrypoint
    • コードの事前変換処理を行う
      • cssstyled API が使用されている場合は、CSS クラス名(文字列)に変換する
      • ホイスト[1]する
      • CommonJS に変換する
    • エントリーポイント[2]に元のコードや、上の処理によって変換されたコード等を保持する
  2. evalFile
    • インターポレーション[3]によって埋め込まれた変数や式を計算する
    • インターポレーションによって埋め込まれた変数や式が外部ファイルで定義されている場合でも、依存関係を解決した上で計算する
    • これらの計算、及び、依存関係の解決を Node.js の VM でホイストされたコードを実行することで実現している
  3. collect
    • cssstyled API で定義されたスタイルを収集して保持しておく
  4. extract
    • 収集したスタイルを抽出して CSS テキストに変換する
脚注
  1. cssstyled API が関数内で使用され、インターポレーションによって埋め込まれた処理がトップレベルで実行されるように変換 ↩︎

  2. JS ファイル毎に保持する WyW-in-JS での変換に必要な情報 ↩︎

  3. cssstyled API でスタイルを定義する際に、変数や式を埋め込むこと ↩︎

KotaroKotaro

1. processEntrypoint

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/processEntrypoint.ts

1.1. explodeReexports

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/processEntrypoint.ts#L20

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/explodeReexports.ts

1.2. transform

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/processEntrypoint.ts#L21-L26

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L201-L208

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L140-L200

1.2.1. prepareCode

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L155-L159

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L80-L138

1.2.1.1. runPreevalStage

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L95-L104

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L30-L68

1.2.1.1.1. preeval

この babel プラグインがかなり重要。
ホイストもここで行われる。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L46

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/plugins/preeval.ts

以下のようなコードを

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

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L123-L133

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/shaker.ts

1.2.2. resolveImports

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L177-L183

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/resolveImports.ts

1.2.3. processImports

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/transform.ts#L186-L192

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/transform/generators/processImports.ts

KotaroKotaro
KotaroKotaro

(コード読んだ感じ恐らく)Node.js の VM を利用して、ファイルの依存関係を解決した上で計算する

WyW-in-JS の凄いところは、別ファイルで定義された定数を css API で利用しても問題なく変換できること。

以下のようなコードが、

index.ts
import { css } from "@wyw-in-js/template-tag-syntax";
import { color } from "./color";

const classA = css`
  color: ${color};
  background: green;
`;
export { classA };
color.ts
export const color = "red";

以下のように変換される。

index.js
const classA = "c2tf5um";
export { classA };
index.css
.c2tf5um{font-size:14px;color:red;background:green}

ここら辺のファイルの依存関係を Node.js の VM を利用して解決していそうなので、詳しく見てみる。

KotaroKotaro

ファイルの依存関係を解決するための重要なクラスが以下の Module クラス。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/module.ts

コードをちゃんと読んでみたところ、やはり Node.js の VM を活用して依存関係を解決しているようだった。
VM について簡単に説明すると、V8 の仮想マシンのコンテキスト内で JavaScript コードを実行するためのもの。VM で呼び出されたコードと呼び出し元のコードとは異なるグローバルオブジェクトとなる = 独立したコンテキストでコードが実行される。そのため、VM コンテキスト内では requireprocess__dirname 等の一部のグローバルオブジェクトやモジュールを直接参照することができない。

WyW-in-JS では、VM コンテキスト内で参照できないグローバルオブジェクトやモジュールを参照可能にするための独自のコンテキストを作成している。例えば、 window オブジェクトは happy-domHappyWindow インスタンスが参照されるようにしたり、requireModule クラスの require メソッド(this.require)が、exportsEntrypoint クラス[1]の exports オブジェクト(entrypoint.exports)が参照されるようされるようになっている。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/vm/createVmContext.ts#L73-L101

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/module.ts#L281-L292

このコンテキストを使用して以下のように VM でコードを実行している。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/module.ts#L295-L302

上の返信コメントのコードが「1. processEntrypoint」によって、以下のように CommonJS 形式で変換されたコードが Entrypoint クラスのインスタンス内で保持されている。

index.ts
"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 テキストを生成する(はず)。

entrypoint.exports
{
  __wywPreval: {
    _exp: _exp,
  };
}

続いて、依存関係の解決について。
require("./color")Module クラスの require が参照される。

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/module.ts#L127-L189

color.tsindex.ts 同様、以下のように CommonJS 形式で変換されたコードが Entrypoint クラスのインスタンス内で保持されている。

color.ts
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
const red = exports.color = "red";

Module クラスの require ではこの保持されたコードを Module クラスの evaluate を実行(つまり、index.ts の時と同様 VM で実行)し、entrypoint.exports に以下のようなエキスポートされた値を抽出し、それを返すような実装となっている。

entrypoint.exports
{ color: "red" }

https://github.com/Anber/wyw-in-js/blob/7cc243bb5c083d1fff0a44c5e0597374289d0684/packages/transform/src/module.ts#L180-L183

つまり、依存関係を解決する際は、依存元だけでなく依存先も VM で実行し、コード内でエキスポートされた値を抽出することで、依存関係を解決している。

脚注
  1. Entrypoint クラスは JS ファイルの情報を管理しているクラスです。変換前のオリジナルのコードや、「1. processEntrypoint」によって変換されたコード、そのファイルでエキスポートされた値など保持しています。 ↩︎