メインコンテンツまでスキップ

デュアルパッケージ開発者のためのtsconfig (Dual Package)

フロントエンドでもバックエンドでもTypeScriptこれ一本!Universal JSという考えがあります。確かにフロントエンドを動的にしたいのであればほぼ避けて通れないJavaScriptと、バックエンドでも使えるようになったJavaScriptで同じコードを使いまわせれば保守の観点でも異なる言語を触る必要がなくなり、統一言語としての価値が大いにあります。

しかしながらフロントエンドとバックエンドではJavaScriptのモジュール解決の方法が異なります。この差異のために同じTypeScriptのコードを別々に分けなければいけないかというとそうではありません。ひとつのモジュールをcommonjs, esmoduleの両方に対応した出力をするDual Packageという考えがあります。

Dual Packageことはじめ

名前が仰々しいですが、やることはcommonjs用のJavaScriptとesmodule用のJavaScriptを出力することです。つまり出力するmoduleの分だけtsconfig.jsonを用意します。

プロジェクトはおおよそ次のような構成になります。

text
./
├── tsconfig.base.json
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── tsconfig.json
text
./
├── tsconfig.base.json
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── tsconfig.json
  • tsconfig.base.json
    • 基本となるtsconfig.jsonです
  • tsconfig.cjs.json
    • tsconfig.base.jsonを継承したcommonjs用のtsconfig.jsonです
  • tsconfig.esm.json
    • tsconfig.base.jsonを継承したesmodule用のtsconfig.jsonです
  • tsconfig.json
    • IDEはこの名前を優先して探すので、そのためのtsconfig.jsonです

tsconfig.base.jsonとtsconfig.jsonを分けるかどうかについては好みの範疇です。まとめてしまっても問題はありません。

tsconfig.jsonの継承

tsconfig.jsonは他のtsconfig.jsonを継承する機能があります。上記はtsconfig.cjs.json, tsconfig.esm.jsonは次のようにしてtsconfig.base.jsonを継承しています。

json
// tsconfig.cjs.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs"
// ...
}
}
json
// tsconfig.cjs.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs"
// ...
}
}
json
// tsconfig.esm.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/esm"
// ...
}
}
json
// tsconfig.esm.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/esm"
// ...
}
}

outDirはコンパイルしたjsと、型定義ファイルを出力していれば(後述)それを出力するディレクトリを変更するオプションです。

このようなtsconfig.xxx.jsonができていれば、あとは次のようにファイル指定してコンパイルをするだけです。

bash
tsc -p tsconfig.cjs.json
tsc -p tsconfig.esm.json
bash
tsc -p tsconfig.cjs.json
tsc -p tsconfig.esm.json

Dual Packageのためのpackage.json

package.jsonもDual Packageのための設定が必要です。

main

package.jsonにあるそのパッケージのエントリーポイントとなるファイルを指定する項目です。Dual Packageのときはここにcommonjsのエントリーポイントとなるjsファイルを設定します。

module

Dual Packageのときはここにesmoduleのエントリーポイントとなるjsファイルを設定します。

types

型定義ファイルのエントリーポイントとなるtsファイルを設定します。型定義ファイルを出力するようにしていればcommonjs, esmoduleのどちらのtsconfig.jsonで出力したものでも問題ありません。

package.jsonはこのようになっているでしょう。

json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}
json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

コンパイル後のjsのファイルの出力先はあくまでも例です。tsconfig.jsonのoutDirを変更すれば出力先を変更できるのでそちらを設定後、package.jsonでエントリーポイントとなるjsファイルの設定をしてください。

Tree Shaking

module bundlerの登場により、フロントエンドは今までのような<script>でいろいろなjsファイルを読み込む方式に加えてを全部載せjsにしてしまうという選択肢が増えました。この全部載せjsは開発者としては自分ができるすべてをそのまま実行環境であるブラウザに持っていけるので楽になる一方、ひとつのjsファイルの容量が大きくなりすぎるという欠点があります。特にそれがSPA(Single Page Application)だと問題です。SPAは読み込みが完了してから動作するのでユーザーにしばらく何もない画面を見せることになってしまいます。

この事態を避けるためにmodule bundlerは容量削減のための涙ぐましい努力を続けています。その機能のひとつとして題名のTree Shakingを紹介するとともに、開発者にできるTree Shaking対応パッケージの作り方を紹介します。

Tree Shakingとは

Tree Shakingとは使われていない関数、クラスを最終的なjsファイルに含めない機能のことです。使っていないのであれば入れる必要はない。というのは至極当然の結論ですがこのTree Shakingを使うための条件があります。

  • esmoduleで書かれている
  • 副作用(side effects)のないコードである

各条件の詳細を見ていきましょう。

esmoduleで書かれている

commonjsesmoduleでは外部ファイルの解決方法が異なります。

commonjsrequire()を使用します。require()はファイルのどの行でも使用ができますがesmoduleimportはファイルの先頭でやらなければならないという決定的な違いがあります。

require()はあるときはこのjsを、それ以外のときはあのjsを、と読み込むファイルをコードで切り替えることができます。つまり、次のようなことができます。

ts
let police = null;
let firefighter = null;
 
if (shouldCallPolice()) {
police = require("./police");
} else {
firefighter = require("./firefighter");
}
ts
let police = null;
let firefighter = null;
 
if (shouldCallPolice()) {
police = require("./police");
} else {
firefighter = require("./firefighter");
}

一方、先述のとおりesmoduleはコードに読み込みロジックを混ぜることはできません。

上記例でshouldCallPolice()が常にtrueを返すように作られていたとしてもmodule bundlerはそれを検知できない可能性があります。本来なら必要のないfirefighterを読み込まないという選択を取ることは難しいでしょう。

最近ではcommonjsでもTree Shakingができるmodule bundlerも登場しています。

副作用のないコードである

ここで言及している副作用とは以下が挙げられます。

  • exportするだけで効果がある
  • プロトタイプ汚染のような、既存のものに対して影響を及ぼす

これらが含まれているかもしれないとmodule bundlerが判断するとTree Shakingの効率が落ちます。

副作用がないことを伝える

module bundlerに制作したパッケージに副作用がないことを伝える方法があります。package.jsonにひとつ加えるだけで完了します。

sideEffects

このプロパティをpackage.jsonに加えて、値をfalseとすればそのパッケージには副作用がないことを伝えられます。

json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"sideEffects": false,
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}
json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"sideEffects": false,
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

副作用があり、そのファイルが判明しているときはそのファイルを指定します。

json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"sideEffects": ["./xxx.js", "./yyy.js"],
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}
json
{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.0",
"sideEffects": ["./xxx.js", "./yyy.js"],
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}