🗽

Typescript+ESMでnpmパッケージ開発した備忘録

に公開

Typescript+ESMでnpmパッケージを作る方法が案外まとまってなかったので残しておく。マサカリ歓迎。ほぼ確実に間違い/非効率な点がある。jestでテストもやってる。基本yarnを使う。

モジュール化するときにCommonJS(requireのやつ)とESM(import/exportのやつ)とかがある。調べたところESMが業界標準で、nodejs>13.2.0とか モダンな ブラウザではすでに対応してるらしいので、ESMで行く。Tree Shakeがしやすかったりするらしい。

プロジェクトの初期化

YOUR_PACKAGE_NAME="<名前>"
mkdir $YOUR_PACKAGE_NAME && cd $YOUR_PACKAGE_NAME
yarn init -y

ESMはpackage.jsonに "type": "module"を指定しないといけないので追加しておく(nameversionと同じ階層)。参考

Typescriptの導入

yarn add -D typescript
yarn tsc --init --target es6 --module esnext --declaration true --outDir ./build --rootDir ./src

オプションを少し解説する。--module esnextでESMにコンパイルするよう指定してる。--declaration true.d.tsを生成。--target es6でes6にコンパイルする。nodejsでは6あたりから?、es6の機能が使えるようになったぽいので心配ない。ブラウザでもIE以外は対応してるし、そもそもバンドルするので問題ない。...とおもう。

パッケージの中身を作成

src/index.ts
export { IamExported } from "./module.js";
export const IamIndex = () => {
  console.log("I am exported in index.ts");
};
src/module.ts
export const IamExported = (name: string) => {
  return `Hello, ${name}!! I am exported in module.ts!`;
};

index.tsに注意してほしい。from "./module.js"のように.jsを付けている。これは間違いではない。

.jsの理由

import {hoge} from "./module"をTypescriptがコンパイルすると、手を加えずにそのまま出力される。しかしこれは正しいESMの記法ではない。本来のESMは拡張子が必要で、理想的にはTSにimport {hoge} from "./module.js"とコンパイルしてほしい。しかしこれを実現するCompilerOptionはないので次善の策として.jsを付けている。.jsを付けても同じように動くようだ。以下参照

https://github.com/microsoft/TypeScript/issues/33588

ここまできたらyarn tscすると、build/にコンパイルされた.jsと.d.tsが出てくる。以下のような構成になるはず。

src/
  - index.ts
  - module.ts
  - module.test.ts

build/
  - index.js
  - index.d.ts
  - module.ts
  - module.d.ts

publishの準備

以上でモジュール開発は終わりだが、npmにpublishするために追加で設定することがある。

まずpackage.json。最低でもこれは欲しい。

package.json
{
  "main": "./build/index.js", // JSのエントリーポイント
  "types": "./build/index.d.ts", // TSのエントリーポイント
  "publishConfig": {
    "access": "public" // デフォルトはrestricted。無料アカウントならpublicが必須。
  },
  "type": "module" // ESMを使う(再掲)
}

ほかにはlicense、repositoryとかkeywordsも指定したほうが良い。以下参照。

https://docs.npmjs.com/cli/v7/configuring-npm/package-json
https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html

ここでnpm publish --dry-runを実行すると、パッケージの概要が見れる。Tarball Contentsにはパッケージに同梱されるファイルが一覧で出てくる(多分)。

npm notice === Tarball Contents === 
npm notice 133B  babel.config.cjs  
npm notice 122B  build/index.js    
npm notice 101B  build/module.js   
npm notice 503B  package.json      
npm notice 900B  tsconfig.json     
npm notice 6.5kB jest.config.mjs   
npm notice 83B   build/index.d.ts  
npm notice 119B  src/index.ts      
npm notice 60B   build/module.d.ts 
npm notice 149B  src/module.test.ts
npm notice 107B  src/module.ts      

babel.config.cjsやjest.config.mjs, src/以下のファイルは同梱しても意味ないので省きたい。そういう時は.npmignoreを使う[1]

.npmignore
src
*config.*

これを作ってもう一度npm publish --dry-runをする。

npm notice === Tarball Contents === 
npm notice 122B build/index.js   
npm notice 101B build/module.js  
npm notice 503B package.json     
npm notice 83B  build/index.d.ts 
npm notice 60B  build/module.d.ts

必要なファイルだけ同梱していることがわかる。

動作確認

yarn linkを使うとローカルでテストできる。パッケージとは別のディレクトリで動作確認をする。

yarn link
cd ..
mkdir package-test && cd package-test
yarn init -y # "type": "module"を追加しておく
yarn link $YOUR_PACKAGE_NAME

package-test/node_modulesをみると、作ったパッケージへシンボリックリンクが貼られているのがわかる。だから次のように簡単にモジュールの動作確認ができる。package.jsonに"type":"module"を忘れないように。

import { IamExported, IamIndex } from "$YOUR_PACKAGE_NAME"
console.log(IamExported("arark")); // Hello, arark!! I am exported in module.ts!
IamIndex(); // I am exported in index.ts

npmへpublish

npmのアカウントを作る。メール認証しないとpublishが403ではじかれるので注意。

yarn login
yarn publish

ここは良い感じにやって(飽きた)。

package.jsonにこう書いておくとpublishとかの前にビルドしてくれる。

"scripts": {
  "prepare": "tsc"
 },

(付録)jestの導入

テストスクリプトを書く。

src/module.test.ts
import { IamExported } from "./module";
test("IamExported returns greeting", () => {
  expect(IamExported("arark")).toContain("Hello, arark!!");
});

jestの設定をする。

yarn add jest @types/jest -D
yarn run jest --init
 Would you like to use Jest when running "test" script in "package.json"? yes
 Would you like to use Typescript for the configuration file? no
 Choose the test environment that will be used for testing node

これでyarn testやるとimportがわかんねえよ!と怒られる(jestがESMにネイティブ対応してないからbabelを咬ませなきゃいけないっぽい?)。
よくわかんないけどドキュメントに沿うと解決できる。具体的には:

yarn add -D babel-jest @babel/core @babel/preset-env  @babel/preset-typescript
echo 'module.exports = { presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript", ],};' > babel.config.cjs

でbabelを設定する。わざわざ.cjsにしたのは、configだけCommonJSを使いたいから。.jsではpackage.jsonの"type": "module"からESMと解釈されるのでだめ。

これでjestが動くはず。

$ yarn test
yarn run v1.22.5
$ jest
 PASS  src/module.test.ts
 IamExported returns greeting (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.8 s
Ran all test suites.
Done in 1.35s.
脚注
  1. もしくは、package.jsonのfilesで指定することもできる。https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files ↩︎

Discussion