ローカルで動作するNode.jsのライブラリ(node_modules)欲しくないですか?欲しいですよね?という訳で作ってみました。
要件としてはTypeScriptで実装出来、Jestでテスト可能で、ESLintでLintが可能というところです。
| Env |
Ver |
| @swc/cli |
0.1.62 |
| @swc/core |
1.3.93 |
| @swc/jest |
0.2.29 |
| @types/jest |
29.5.5 |
| @types/node |
20.8.6 |
| @typescript-eslint/eslint-plugin |
6.7.5 |
| @typescript-eslint/parser |
6.7.5 |
| eslint |
8.51.0 |
| eslint-config-prettier |
9.0.0 |
| eslint-plugin-jest |
27.4.2 |
| jest |
29.7.0 |
| jest-watch-typeahead |
2.2.2 |
| prettier |
3.0.3 |
| ts-jest |
29.1.1 |
| typescript |
5.2.2 |
| Node.js |
20.8.0 |
| npm |
10.1.0 |
https://github.com/Lycolia/ts-library-example
monorepoでmainからlibrary/singleやlibrary/multi/hoge, piyoを参照するような構成です。
root
└─packages
├─library // ライブラリ側
│ └─packages
│ ├─multi // 複数ファイルのライブラリ
│ │ └─src
│ │ └─utils
│ │ ├─hoge
│ │ └─piyo
│ └─single // 単一ファイルのライブラリ
│ └─src
└─main // ライブラリを使う側
└─src
└─libs
これが全てというわけではないと思いますが、一旦今回作ったもののポイントを解説していきます。利用側がtscを利用しない場合、ライブラリ側と利用側で構成は別々になります。
またこの実装はエディタにVSCodeを利用し、importするパスが相対パスであることを前提に説明しています。
まずビルドに関してはtscでやります。
これはTypeScriptで開発するためにはビルド成果物として.d.tsファイルが必要になるのと、Jestを通すためにビルド成果物がCommonJS形式(以下CJS)である必要があるためです。CJSが吐けるなら何でもいいとは思いますが、.d.tsも必要になるので、tscを使うのが無難な選択肢だと思います。
テストファイルを出力したくないので、tsconfig.jsonはビルドと開発で分けます。
またtsconfig.jsonは各ワークスペースのルートに置いて置く必要があります。これはTypeScriptがtsconfig.jsonのパスを起点として動作するためです。
ビルド用の設定ポイントとしては以下の通りです。
"compilerOptions"
"module": "NodeNext"
"moduleResolution": "NodeNext"
"declaration": true,
"outDir": "./dist",
"exclude": ["src/**/*.spec.ts"]
"include": ["src/**/*"]
ビルドにtscを使うため、Jestのローダーとしてもts-jestを利用します。公式ではbabelが推奨されているようですが、構成方法が不明だったので諦めました。BabelはJest公式の案内通りにやっても多分上手く行きません。
jest.config.jsには以下の設定を追加します。
preset: 'ts-jest'
開発とビルドでtsconfig.jsonを分けるので、開発用のも必要です。これはビルド用から"exclude": ["src/**/*.spec.ts"]を抜くだけです。
基本的にはpackage.jsonに外部参照させるための定義を書くことによって行います。
この辺りの仕様はdocs.npmjs.comにはなく、nodejs.orgにあります。
単一ファイルを外部参照(import出来るように)するときに使う手法です。
importをimport { hoge } from '@my-lib/example'みたいな書き方をしたい時に必要になるやつです。
以下の様にビルド成果物の.jsのパスをpackage.jsonに追加してやると出来るようになります。
"main": "./dist/index.js",
以下の書き方でも同様に可能です。
"exports": {
".": {
"default": "./dist/index.js"
}
},
上記には他にtypesというフィールドがあり、本来ここに.d.tsを追加するのですが、TypeScriptが解決してくれるので、なくても動きます。一応ない場合はファイル探索を行うようなので、あった方が少しだけパフォーマンスが上がるかもしれません。
複数ファイルを外部参照(import出来るように)するときに使う手法です。
基本的に何もしなくてよいですが、import { hoge } from '@my-lib/example'みたいな書き方もしたい場合は以下の記述が必要です。
"main": "./dist/core/index.js"
ここで指定していないものはimport { hoge } from '@my-lic/example/dist/hoge'みたいにして参照します。distがダサくて嫌な場合は適当な名前に変えます。
参考までに@actions/githubはバージョン6.0.0時点でdistに相当する部分をlibにしており、import { Context } from '@actions/github/lib/context';の様にして参照するようになっています。
exportsを使ってdistを隠すことも出来るとは思うのですが面倒なので試してません。
今回利用する側はビルドにswcを使う想定ですが、たぶんなんでも動くと思います。参考までにswc-loader + webpackでも動きました。
tsconfig.jsonに以下の設定があれば恐らく最低限大丈夫だと思います。
"compilerOptions"
"module": "NodeNext"
"moduleResolution": "NodeNext"
"include": ["src/**/*"]
以下のように相対パスを指定するとインストールできます。
npm i ../package/library/package/single
消すときはパッケージ名を指定すれば消せます。
npm un @lycolia/library-example-single
色々してる過程で試行錯誤した名残。
以下の二通りの設定は試しましたが、どっちもダメだったので諦めました。何よりtscを使うならts-jestの方が楽なのは確定的に明らかですし…。
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
presets: [
['@babel/preset-env', {targets: {node: 'current'}, modules: 'commonjs'}],
'@babel/preset-typescript',
],
.swcrcを以下の設定にしても
"module": {
"type": "commonjs"
},
以下の出力がされるため、jest.spyOn()が上手く動かない(getが邪魔でhelloが見れない
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "hello", {
enumerable: true,
get: function() {
return hello;
}
});
const hello = (param)=>{
console.log(param);
};
どの道これでは.d.tsが出せないので、あんま意味ないなと…。
実装方法は@actions/githubが一番単純で参考になると思います。
実装はビルド成果物とビルド前のコード、package.jsonやtsconfig.json辺りがどうなっているのかを参考にしました。