なぜWebpackの設定はTypeScriptで書けるのか?
この記事について
webpack の設定ファイルであるwebpack.config.js
は、TypeScript で書いて Node.js 上で実行できます。しかし、本来であれば TypeScript のソースコードは Node.js では実行できないはずです。 この事が気になった私は、今回その仕組みを調べてみたので、この場を借りてその調査結果を共有したいと思います 💪
参照
記事の概要
概要のみ知りたい人に向けて、以下にこの記事で解説する内容をまとめておきます 👇
-
webpack-cli では、 rechoir を使って TypeScript を
require()
できるようにしているよ - rechoir は、 ts-node などを使って require.extensions を拡張しているよ
- ちなみに、 require.extensions は非推奨だよ
- webpack-cli が対応している言語は、 node-interpret から確認できるよ
- この記事の後半では、 require.extensions を使って YAML を
require()
できるようにするよ
webpack-cli の実装を見る
まずは、webpack-cliの実装を見てみましょう。
以下に、設定ファイルを読み込む部分のソースコードを引用します。
const loadConfig = async (configPath) => {
const { interpret } = this.utils;
const ext = path.extname(configPath);
const interpreted = Object.keys(interpret.jsVariants).find(
(variant) => variant === ext
);
if (interpreted) {
const { rechoir } = this.utils;
try {
rechoir.prepare(interpret.extensions, configPath);
} catch (error) {
if (error.failures) {
this.logger.error(`Unable load '${configPath}'`);
this.logger.error(error.message);
error.failures.forEach((failure) => {
this.logger.error(failure.error.message);
});
this.logger.error("Please install one of them");
process.exit(2);
}
this.logger.error(error);
process.exit(2);
}
}
let options;
try {
options = await this.tryRequireThenImport(configPath, false);
} catch (error) {
this.logger.error(`Failed to load '${configPath}' config`);
if (this.isValidationError(error)) {
this.logger.error(error.message);
} else {
this.logger.error(error);
}
process.exit(2);
}
return { options, path: configPath };
};
なにやら色々な事をしていますが、上記のソースコードがやっている事をまとめると、
- 設定ファイルをロードする(
require()
する )為の前処理 - 設定ファイルのロード処理
に分けられます。
「 2. 設定ファイルのロード処理 」の方は、ファイルパスや拡張子を見てrequire()
やimport()
を実行するだけなので、この記事では「1. 設定ファイルの前処理」のみ解説します。
以下に、そのソースコードを抜粋します 👇
const { interpret } = this.utils;
const ext = path.extname(configPath);
const interpreted = Object.keys(interpret.jsVariants).find(
(variant) => variant === ext
);
if (interpreted) {
const { rechoir } = this.utils;
try {
rechoir.prepare(interpret.extensions, configPath);
} catch (error) {
if (error.failures) {
this.logger.error(`Unable load '${configPath}'`);
this.logger.error(error.message);
error.failures.forEach((failure) => {
this.logger.error(failure.error.message);
});
this.logger.error("Please install one of them");
process.exit(2);
}
this.logger.error(error);
process.exit(2);
}
}
まずは一行目を見てみましょう 👀
const { interpret } = this.utils;
いきなり良く分からないモノが出てきましたが、これは interpret というライブラリになります。
このライブラリは、ファイル拡張子とそれに紐づいているローダーの情報を持ったオブジェクトを提供します。例えば、TypeScript の場合は、
{
'.ts': [
'ts-node/register',
'typescript-node/register',
'typescript-register',
'typescript-require',
'sucrase/register/ts',
{
module: '@babel/register',
register: function(hook) {
hook({
extensions: '.ts',
rootMode: 'upward-optional',
ignore: [ignoreNonBabelAndNodeModules],
});
},
},
],
}
といった具合です。
2 行目以降の部分では設定ファイル( webpack.config.*
)の拡張子を取得して、 その拡張子が interpret に定義されているかを判定しています。[1]
const ext = path.extname(configPath);
const interpreted = Object.keys(interpret.jsVariants).find(
(variant) => variant === ext
);
そして、拡張子が対応しているモノであれば ローダー情報を使って処理をしています。
以下の部分です 👇
if (interpreted) {
const { rechoir } = this.utils;
try {
rechoir.prepare(interpret.extensions, configPath);
} catch (error) {
/* -- 今回は関係ないので省略 -- */
}
}
ソースコードを見ると、また rechoir
と言う良く分からないモノが出てきましたね。しかし、これも interpret と同様にライブラリです。
この rechoir は、渡されたローダー情報( interpret.extensions
)を元に require.extensions にローダーを設定します。
これによって、.ts
ファイルなどの通常対応していない拡張子のファイルに対してローダーを適用することが出来るので、webpack.config.ts
のようなファイルをrequire()
することが可能となっています。
まとめ
ここまでをまとめると、
- interpret を使ってローダー情報を取得する
- 設定ファイルの拡張子に interpret が対応しているか確認
- 対応していれば、 rechoir を使ってローダーを require.extensions に登録する
- 設定ファイル(
webpack.config.*
)をロードする
と言う流れになります。
これで、「 webpack の設定ファイルがなぜ TypeScript で書けるのか? 」という謎は解決しました!
しかし、その謎を解決しているrequire.extensions
について解説していませんので、次の節でその詳細を見て行きましょう 👉
require.extensions とは何か?
require.extensions
は、特定の拡張子の処理方法を定義するためのモノです。この API を用いることで、通常では対応していない拡張子のファイルを Node.js 上でrequire()
することができます。
今回の場合のように、TypeScript のソースコードをrequire()
したい場合は、ts-node などのライブラリを使うことで簡単に実現できるようになります 👇
require('ts-node/register');
// TypeScriptで書かれた設定情報を読み込む
const config = require('./config.ts');
結構便利な API ですが、実はこれは非推奨となっています。
理由が、Node.js のドキュメントに書いてありましたので引用させてもらいます。
要約すると、
「 処理が遅くなる可能性があるから、事前にコンパイルするとか他の方法で対応して! 」
とのことです。まあ、わざわざランタイム上でコンパイルするなら、実行前にコンパイルしたほうが良いですよね。
公式が非推奨にしているモノなので、あまり多用しない方が良いと思いますが、こんな便利な API を知ったら、これを使って遊んでみたくなるのがプログラマーの性ってヤツですよね!
ということで、require.extensions
を使って.yaml
ファイルをrequire()
できるようにしたいと思います!
.yaml
ファイルをrequire()
できるようにする
まず必要なモジュールをインストールします。
$> npm i --save yaml
そしたら次に、require.extensions
の処理を書いていきます 🖊
const fs = require("fs")
const YAML = require('yaml')
// `.yaml`ファイルをrequire()できるようにする
require.extensions[".yaml"] = function (module, filename) {
const code = fs.readFileSync(filename, "utf8") // ファイルを読み込む
module.exports = YAML.parse(code) // ファイルを解析してJSONとして返す
}
上記のファイルができたら、次はrequire()
する.yaml
ファイルと、.yaml
ファイルを読み込むindex.js
ファイルを記述します。
Hello:
- World
const yamlData = require("./target.yaml") // 上記で定義したファイル
console.log(`出力結果:`, yamlData)
あとは、以下のように実行するだけです!
$> node -r ./yaml-setup.js ./index.js
出力結果: { Hello: [ 'World' ] }
無事、.yaml
ファイルの読み込みに成功しました 🎉
-r
オプションを使わない場合
また、以下のようにも書けます。
require("./yaml-setup") // `.yaml`ファイルを読み込めるようにする
const yamlData = require("./target.yaml") // 上記で定義したファイル
console.log(`出力結果:`, yamlData)
$> node ./index.js
出力結果: { Hello: [ 'World' ] }
参考リンク
あとがき
ここまで読んでくれてありがとうございます 🙏
「 webpack の設定ファイルって、TypeScript で書けるのに Node.js で実行できるのはなんでだろう 🤔 」という疑問から調べてみましたが、想像とは違った形で実装されていて少し驚きました。
Module Hack の事や、なんとなく使っていた-r
オプションの意味を理解できたり、色々なライブラリの事を知れたりと、小さな気づきから色々な事が学べる良い機会だったと思います。
みなさんも小さな気づきがあれば、調べてみると案外面白いことが分かるかもしれませんよ?
記事に間違いなどがあれば、コメントなどで教えて頂けると嬉しいです。
これが誰かの参考になれば幸いです。
それではまた 👋
-
具体的な情報は、interpret のソースコードを参照してください ↩︎
Discussion