調査:良いDXをライブラリユーザーに提供するために、TypeScriptライブラリのtsconfig設定はどうあるべきか?
このスクラップでは、TypeScriptでライブラリを開発し、それをNPMを通じて提供する際、ライブラリユーザーにより良い開発体験(DX)を提供するには、tsconfigの設定はどうあるべきかを調査するものです。
このスクラップが役立つかも知れない人
- TypeScriptでライブラリを開発する人
アプリ開発者にはたぶん役に立ちません
良いDXとは?
良いDXは次のように定義する。
TypeScriptソースコードの閲覧可能
- GOOD: IDEでライブラリのシンボル(関数名など)へジャンプしたときに、その実装がTypeScriptで読める
- BAD: 型定義ファイルにジャンプするので、実装を知ろうと思ったら、対応するバージョンのソースコードをGitHubで調べないといけない
- SUPER BAD: 型定義ファイルすら無く、コンパイル後のJavaScriptにジャンプする
ブラウザでソースコードが閲覧可能
- GOOD: 例外などのスタックトレースから、TypeScriptソースコードにジャンプできる
- BAD: 例外などのスタックトレースから、コンパイル後のJavaScriptにジャンプしてしまう
二次コンパイルの対応
- GOOD: Babelやwebpackでライブラリを二次コンパイルしたとき、ソースマップが引き継がれる
- BAD: 二次コンパイルすると、ソースマップが引き継がれない
Node.jsでのスタックトレースのみやすさ
node -r source-map-support/register compiled.js
- GOOD:
source-map-support
を使って実行したとき、スタックトレースに現れるのがライブラリのソースコードになる
01: 残念なライブラリ
まずは理想とはほど遠いライブラリ。
残念なライブラリの特徴
- 型定義ファイルはあるものの、
- TypeScriptのソースコードが同梱されていない
.
├── node_modules
│ └── your-package ... 残念なライブラリ
│ ├── index.d.ts … 型定義ファイル
│ ├── index.js … コンパイル後のJS
│ └── package.json
├── package.json
└── main.js ... 残念なライブラリのユーザー
ライブラリの型定義ファイル
declare const _default: () => Promise<never>;
export default _default;
ライブラリの実装。コンパイル後のコードなので、可読性はそんなに良くない。
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default () => __awaiter(void 0, void 0, void 0, function* () {
throw new Error("this is dummy error");
});
ライブラリユーザーのコード
import func from "your-package";
func();
Node.jsで実行したときの様子。
❯❯❯ node main.js
file:///app/node_modules/your-package/index.js:11
throw new Error("this is dummy error");
^
Error: this is dummy error
at file:///app/node_modules/your-package/index.js:11:11
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at default (file:///app/node_modules/your-package/index.js:10:22)
at file:///app/main.js:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
WebStormのコードジャンプは型定義ファイルに行く。
VS Codeのコードジャンプも型定義ファイルに行く。
完全なコード
02: TypeScriptのソースコードも同梱したライブラリ
上に加えて、TypeScriptのソースコードを同梱した場合
ファイル構造
.
├── main.ts
├── node_modules
│ └── your-package
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts … 追加
│ └── package.json
└── package.json
ファイルの中身
export default async () => {
throw new Error("this is dummy error");
};
各ツールの動き
WebStorm → ✅TypeScriptソースコードにジャンプする
VS Code → 😕型定義ファイルにジャンプする
ts-node → 😕スタックトレースはJavaScript
ソースマップがないため当然
❯❯❯ node --loader ts-node/esm main.ts
(node:26945) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Error: this is dummy error
at file:///app/node_modules/your-package/index.js:11:11
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at default (file:///app/node_modules/your-package/index.js:10:22)
at file:///app/main.ts:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
esbuildでバンドル+Chromeのコンソール → 😕スタックトレースはコンパイル後のJavaScript
ソースマップがないため当然。
npx esbuild main.ts --bundle --minify --sourcemap --outfile=esbuild.js
tsc→node --enable-source-maps → 😕スタックトレースはコンパイル後のJS
ソースマップがないため当然。
$ npx tsc && node --enable-source-maps main.js
Error: this is dummy error
at file:///app/node_modules/your-package/index.js:11:11
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at default (file:///app/node_modules/your-package/index.js:10:22)
at null.<anonymous> (/Volumes/v/suinplayground/research-the-best-source-map-settings-for-libraries/02-with-source-code/main.ts:3:1)
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
完全なコード
03: ソースマップは同梱、ソースコードは同梱されていない場合
ファイル構造
.
├── main.ts
├── node_modules
│ └── your-package
│ ├── index.d.ts … 型定義ファイル
│ ├── index.js … コンパイル後JS+ソースマップコメント
│ ├── index.js.map … ソースマップファイル
│ └── package.json
├── package.json
└── tsconfig.json
ファイルの内容
ライブラリのコンパイラオプション
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"declaration": true,
"sourceMap": true, // これがオン
"esModuleInterop": true,
"moduleResolution": "node"
}
}
型定義ファイル
型定義ファイルにはソースマップコメントがない。
declare const _default: () => Promise<never>;
export default _default;
コンパイル後のJS
コンパイルオプションsourceMap
を有効にしたことで、ファイル末尾にソースマップファイルへの参照//# sourceMappingURL=index.js.map
が追加された。
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default () => __awaiter(void 0, void 0, void 0, function* () {
throw new Error("this is dummy error");
});
//# sourceMappingURL=index.js.map
ソースマップファイル
sources
では"index.ts"を指しているが、index.tsは同梱されていない……。
{
"version": 3,
"file": "index.js",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": ";;;;;;;;;AAAA,eAAe,GAAS,EAAE;IACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC,CAAA,CAAC"
}
各ツールの動き
WebStorm → 😕型定義ファイルへのジャンプになる
VS Code → 😕型定義ファイルへのジャンプになる
esbuild→Chrome閲覧 → 😕スタックトレースはTSになるものの、TSソースコードは見れない
npx esbuild main.ts --bundle --minify --sourcemap --outfile=esbuild.js
✅スタックトレースには、ソースコードのファイル名index.ts
と行数が現れる
😕しかし、スタックトレースをクリックしてソースコードを閲覧しようとすると「Could not load content for http://localhost:63342/03-with-source-map-without-source-code/node_modules/your-package/index.ts (HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE)」というエラーになる。
ts-node → ✅スタックトレースはTSソースコードになる
ただし、ソースコードの当該箇所を見ようとしたら、GitHubなりを見に行かないといけないので、DX的には良くない。
$ node --loader ts-node/esm main.ts
(node:30700) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Error: this is dummy error
at file:///app/node_modules/your-package/index.ts:2:9
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at default (file:///app/node_modules/your-package/index.ts:1:27)
at file:///app/main.ts:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
node --enable-source maps → ✅スタックトレースはTSソースコードになる
ただし、ソースコードの当該箇所を見ようとしたら、GitHubなりを見に行かないといけないので、DX的には良くない。
$ npx tsc && node --enable-source-maps main.js
Error: this is dummy error
at null.<anonymous> (/app/node_modules/your-package/index.ts:2:9)
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at null.default (/app/node_modules/your-package/index.ts:1:27)
at null.<anonymous> (/app/main.ts:3:1)
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
完全なコード
中間まとめ
型定義ファイルだけ
- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: -
- esbuildでバンドル+Chromeコンソール: -
- node --enable-source-maps: -
ソースコード同梱・ソースマップなし
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: ❌
- esbuildでバンドル+Chromeコンソール: ❌
- node --enable-source-maps: ❌
"sourceMap": true
)
ソースコードなし・ソースマップ同梱(- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- esbuildでバンドル+Chromeコンソール: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- node --enable-source-maps: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
"sourceMap": true
)
04: ソースコード同梱・ソースマップ同梱(ソースコードとソースマップどちらもライブラリに同梱した場合。
ファイル構成
.
├── main.ts
├── node_modules
│ └── your-package
│ ├── index.d.ts … 型定義ファイル
│ ├── index.js … コンパイル後のJS
│ ├── index.js.map … ソースマップ
│ ├── index.ts … ソースコード
│ └── package.json
├── package.json
└── tsconfig.json
ファイルの内容
ライブラリのtsconfig
ライブラリに同梱する必要はない
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"declaration": true,
"sourceMap": true, // これがオン
"esModuleInterop": true,
"moduleResolution": "node"
}
}
ソースコード
TypeScriptのソースコードが同梱されている。
export default async () => {
throw new Error("this is dummy error");
};
型定義ファイル
これもある
ソースマップファイル
03のケースとは異なり、今度はindex.tsがちゃんと参照できそうだ。
{
"version": 3,
"file": "index.js",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": ";;;;;;;;;AAAA,eAAe,GAAS,EAAE;IACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC,CAAA,CAAC"
}
各ツールの動き
WebStorm → ✅ソースコードにコードジャンプできる
VS Code→❌型定義ファイルへのジャンプになる
VS CodeはソースコードとソースマップだけではTypeScriptソースコードへはジャンプできないようだ。
ts-nodeのスタックトレース→✅スタックトレースがTSになる
😆ソースコードも付随しているので、スタックトレースから当該行を見るのが容易
$ node --loader ts-node/esm main.ts
(node:33075) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Error: this is dummy error
at file:///app/node_modules/your-package/index.ts:2:9
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at default (file:///app/node_modules/your-package/index.ts:1:27)
at file:///app/main.ts:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
esbuildでバンドル+Chromeコンソール → ✅スタックトレースがTS、ソースコードも追える
npx esbuild main.ts --bundle --minify --sourcemap --outfile=esbuild.js
ただし、バンドル後JSのソースマップファイルを見ると、"node_modules/your-package/index.ts"のsourcesContent
はnull
なので、HTTP越しにGET /node_modules/your-package/index.ts
できない環境になっているとブラウザ側ではソースコードが追えなさそうだ。
{
"version": 3,
"sources": ["node_modules/your-package/index.ts", "main.ts"],
"sourcesContent": [null, "import func from \"your-package\";\n\nfunc();\n"],
"mappings": "0TAAA,EAAe,IAAW,EAAA,OAAA,OAAA,OAAA,WAAA,CACxB,KAAM,IAAI,OAAM,yBCClB",
"names": []
}
esbuildで--sourcemap=inline
にするとどうなるか?
npx esbuild main.ts --bundle --minify --sourcemap=inline --outfile=esbuild-inline.js
生成されるJS
(()=>{var p=function(u,d,i,o){function a(t){return t instanceof i?t:new i(function(c){c(t)})}return new(i||(i=Promise))(function(t,c){function h(n){try{f(o.next(n))}catch(e){c(e)}}function w(n){try{f(o.throw(n))}catch(e){c(e)}}function f(n){n.done?t(n.value):a(n.value).then(h,w)}f((o=o.apply(u,d||[])).next())})},r=()=>p(void 0,void 0,void 0,function*(){throw new Error("this is dummy error")});r();})();
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsibm9kZV9tb2R1bGVzL3lvdXItcGFja2FnZS9pbmRleC50cyIsICJtYWluLnRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogW251bGwsICJpbXBvcnQgZnVuYyBmcm9tIFwieW91ci1wYWNrYWdlXCI7XG5cbmZ1bmMoKTtcbiJdLAogICJtYXBwaW5ncyI6ICIwVEFBQSxFQUFlLElBQVcsRUFBQSxPQUFBLE9BQUEsT0FBQSxXQUFBLENBQ3hCLEtBQU0sSUFBSSxPQUFNLHlCQ0NsQiIsCiAgIm5hbWVzIjogW10KfQo=
このsourceMappingURL
をデコードする↓
{
"version": 3,
"sources": ["node_modules/your-package/index.ts", "main.ts"],
"sourcesContent": [null, "import func from \"your-package\";\n\nfunc();\n"],
"mappings": "0TAAA,EAAe,IAAW,EAAA,OAAA,OAAA,OAAA,WAAA,CACxB,KAAM,IAAI,OAAM,yBCClB",
"names": []
}
結局、node_modules/your-package/index.ts
に対応するsourcesContent
はnull
なので、ブラウザがGET /node_modules/your-package/index.ts
できる必要はありそうだ。
node --enable-source-maps → ✅スタックトレースがTSになっており、ソースコードもローカルで追える
$ npx tsc && node --enable-source-maps main.js
/app/node_modules/your-package/index.ts:2
throw new Error("this is dummy error");
^
Error: this is dummy error
at null.<anonymous> (/app/node_modules/your-package/index.ts:2:9)
at Generator.next (<anonymous>)
at file:///app/node_modules/your-package/index.js:7:71
at new Promise (<anonymous>)
at __awaiter (file:///app/node_modules/your-package/index.js:3:12)
at null.default (/app/node_modules/your-package/index.ts:1:27)
at null.<anonymous> (/app/main.ts:3:1)
at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
完全なコード
Tips: webpackでライブラリのソースマップを利用する方法
webpackはesbuildと異なり、デフォルトではライブラリのソースマップファイルを利用しない。そのため、ソースマップファイルを提供しているライブラリであっても、webpackでビルドするとそのバンドルのソースマップはコンパイル後のJSを指すようになってしまう。
- source-map-loaderをインストールする
- webpack.config.jsにsource-map-loaderを組み込む
export default (env, argv) => {
return {
entry: "./main.ts",
devtool: "source-map", // ここ
module: {
rules: [
{ test: /\.js$/, enforce: "pre", use: ["source-map-loader"] }, // ここ
{ test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ },
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename:
argv.mode === "production"
? "webpack-production.js"
: "webpack-development.js",
path: process.cwd(),
},
};
};
この設定でビルドすると、ライブラリのソースマップが使われるようになる。
--mode=production
{
"version": 3,
"file": "webpack-production.js",
"mappings": "6BAA0B,YACxB,MAAM,IAAIA,MAAM,wB,YADQ,K,kPAAA,E",
"sources": ["webpack://01-poor-dx/./node_modules/your-package/index.ts"], // 🔴ここ
"sourcesContent": [
"export default async () => {\n throw new Error(\"this is dummy error\");\n};\n" // 🔴ここ
],
"names": ["Error"],
"sourceRoot": ""
}
--mode=development
{
"version": 3,
"file": "webpack-development.js",
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,iEAAe,GAAS,EAAE;IACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC,GAAC;;;;;;;UCFF;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;ACNgC;AAEhC,wDAAI,EAAE,CAAC",
"sources": [
"webpack://01-poor-dx/./node_modules/your-package/index.ts", // 🔴ここ
"webpack://01-poor-dx/webpack/bootstrap",
"webpack://01-poor-dx/webpack/runtime/define property getters",
"webpack://01-poor-dx/webpack/runtime/hasOwnProperty shorthand",
"webpack://01-poor-dx/webpack/runtime/make namespace object",
"webpack://01-poor-dx/./main.ts"
],
"sourcesContent": [
"export default async () => {\n throw new Error(\"this is dummy error\");\n};\n", // 🔴ここ
"// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n",
"// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};",
"__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))",
"// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};",
"import func from \"your-package\";\n\nfunc();\n"
],
"names": [],
"sourceRoot": ""
}
中間まとめ
01. 型定義ファイルだけ
- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: -
- esbuildでバンドル+Chromeコンソール: -
- node --enable-source-maps: -
02. ソースコード同梱・ソースマップなし
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: ❌
- esbuildでバンドル+Chromeコンソール: ❌
- node --enable-source-maps: ❌
"sourceMap": true
)
03. ソースコードなし・ソースマップ同梱(- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- esbuildでバンドル+Chromeコンソール: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- node --enable-source-maps: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
04: ソースコード同梱・ソースマップ同梱("sourceMap": true)
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: ✅
- esbuildでバンドル+Chromeコンソール: ✅ (ただしHTTP越しにGET /node_modules/**/*.tsできる必要あり)
- node --enable-source-maps: ✅
あとはVS Codeだけなんとかなればいい
"sourceMap": true
)・型定義ソースマップ同梱("declarationMap": true
)
05: ソースコード同梱・ソースマップ同梱(ファイル構成
.
├── main.ts
├── node_modules
│ └── your-package
│ ├── index.d.ts … 型定義ファイル
│ ├── index.d.ts.map … 型定義のソースマップ
│ ├── index.js … コンパイル後JS
│ ├── index.js.map … ソースマップ
│ ├── index.ts … TypeScriptソースコード
│ └── package.json
└── package.json
ファイルの内容
ライブラリのtsconfig
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"declaration": true, // 型定義ファイルを生成する
"declarationMap": true, // 型定義ソースマップを生成する
"sourceMap": true, // ソースマップを生成する
"esModuleInterop": true,
"moduleResolution": "node"
}
}
型定義ファイル
declarationMap
を有効にしたことで、sourceMappingURL
がコメントで追加された。
declare const _default: () => Promise<never>;
export default _default;
//# sourceMappingURL=index.d.ts.map
型定義ソースマップファイル
index.ts
を参照する形でソースマップが生成された。
{
"version": 3,
"file": "index.d.ts",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": ";AAAA,wBAEE"
}
コンパイル後JS
上のケースと変わらない。
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default () => __awaiter(void 0, void 0, void 0, function* () {
throw new Error("this is dummy error");
});
//# sourceMappingURL=index.js.map
ソースマップファイル
これも上のケースと変わらない。
{
"version": 3,
"file": "index.js",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": ";;;;;;;;;AAAA,eAAe,GAAS,EAAE;IACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC,CAAA,CAAC"
}
各ツールの動き
WebStormのコードジャンプ → ✅TypeScriptソースコードにジャンプできる
VS Codeのコードジャンプ → ✅TypeScriptソースコードにジャンプできる
VS CodeはWebStormと違い、型定義ソースマップがあると、TSソースコードにジャンプする仕様のようだ。
ts-nodeのスタックトレース → ✅スタックトレースにはTSのファイル名・行が現れる
04のケースと同じ。
esbuildでバンドル+Chromeコンソール → ✅スタックトレースはTSのファイル名・行が現れる
04のケースと同じ。
node --enable-source-maps → ✅スタックトレースはTSのファイル名・行が現れる
04のケースと同じ。
完全なコード
中間まとめ
01. 型定義ファイルだけ
- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: -
- esbuildでバンドル+Chromeコンソール: -
- node --enable-source-maps: -
02. ソースコード同梱・ソースマップなし
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: ❌
- esbuildでバンドル+Chromeコンソール: ❌
- node --enable-source-maps: ❌
"sourceMap": true
)
03. ソースコードなし・ソースマップ同梱(- WebStormのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- esbuildでバンドル+Chromeコンソール: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
- node --enable-source-maps: 😕 (スタックトレースはTSになるが、ソースコードの参照はできない)
04: ソースコード同梱・ソースマップ同梱("sourceMap": true)
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ❌ (型定義ファイルへのジャンプ)
- ts-nodeのスタックトレース: ✅
- esbuildでバンドル+Chromeコンソール: ✅ (ただしHTTP越しにGET /node_modules/**/*.tsできる必要あり)
- node --enable-source-maps: ✅
05: ソースコード同梱・ソースマップ同梱("sourceMap": true)・型定義ソースマップ同梱("declarationMap": true)
- WebStormのコードジャンプ: ✅
- VS Codeのコードジャンプ: ✅
- ts-nodeのスタックトレース: ✅
- esbuildでバンドル+Chromeコンソール: ✅ (ただしHTTP越しにGET /node_modules/**/*.tsできる必要あり)
- node --enable-source-maps: ✅
結論
良いDXをライブラリユーザーに提供するために、TypeScriptライブラリのtsconfig設定はどうあるべきか?
sourceMap
はtrue
にする。
コンパイラオプション- スタックトレースがTypeScriptのファイル名・行になる
declarationMap
はtrue
にする。
コンパイラオプション- VS Codeでコードジャンプができるようになる
ソースコードのTypeScriptも同梱する。
- WebStormでコードジャンプができるようになる
- Google ChromeのインスペクタでスタックトレースからTypeScriptソースコードが見れるようになる
inlineSourceMap
ってどうなん?
そういえば、inlineSourceMapとは
-
.js.map
を生成する代わりに、.js
にコメントでTypeScriptソースコードを埋め込むオプション -
inlineSourceMap
を有効にしても型定義ファイルの.d.ts.map
はインライン化されない- 型定義ソースマップもinline化できるようにしてほしいというリクエストは上がってはいる。
- ファイルが散らからなくて良いという主張はありそう。
ファイル構成
.
├── main.ts
├── node_modules
│ └── your-package
│ ├── index.d.ts
│ ├── index.d.ts.map
│ ├── index.js
│ ├── index.ts
│ └── package.json
└── package.json
基本的には05と同じ。違いは、index.js.mapが生成されないこと。
ファイルの内容
基本的には05と同じ。違いは以下。
ライブラリ側のtsconfig
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"declaration": true,
"declarationMap": true,
// "sourceMap": true,
"inlineSourceMap": true, // これを有効化
"esModuleInterop": true,
"moduleResolution": "node"
}
}
sourceMap
とinlineSourceMap
は併用できない。
コンパイル後JavaScript
ソースマップコメントがBase64になっている。
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default () => __awaiter(void 0, void 0, void 0, function* () {
throw new Error("this is dummy error");
});
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFBQSxlQUFlLEdBQVMsRUFBRTtJQUN4QixNQUFNLElBQUksS0FBSyxDQUFDLHFCQUFxQixDQUFDLENBQUM7QUFDekMsQ0FBQyxDQUFBLENBQUMifQ==
このBase64をデコードすると次の内容になる
{
"version": 3,
"file": "index.js",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": ";;;;;;;;;AAAA,eAAe,GAAS,EAAE;IACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC,CAAA,CAAC"
}
これは05のindex.js.mapの内容と同じ。
inlineSourceMap
をtrue
にしても、TypeScriptソースコードがコンパイル後JSにインライン化されるわけではない
なので、inlineSourceMap
にせよsourceMap
にせよ、ライブラリ作者はソースコードとなるTypeScriptを同梱する必要がある。
結論
良いDXをライブラリユーザーに提供するために、TypeScriptライブラリのtsconfig設定はどうあるべきか?
sourceMap
またはinlineSourceMap
をtrue
にする。
コンパイラオプション- スタックトレースがTypeScriptのファイル名・行になるので、コードが追いやすくなる。
- 以下のツールが出力するスタックトレースに対応できる
- Google Chromeのコンソールなど
- node --enable-source-maps
- ts-node
- 以下のツールが出力するスタックトレースに対応できる
declarationMap
はtrue
にする。
コンパイラオプション- WebStormとVS Codeの両方でコードジャンプができるようになる
ソースコードのTypeScriptも同梱する。
- WebStormでコードジャンプができるようになる
- Google ChromeのインスペクタなどでスタックトレースからTypeScriptソースコードが見れるようになる