Vite と VitePress でドキュメントまで含めた NPM Packageを作成する(esm, cjs対応)
投稿日: 2024/07/30
僕は最近JS(TS)を一番書いていますが、最も好きな言語はRubyです。
それは言語自体が高機能であることに加えて、エコシステムが成熟していることが大きいです(枯れているとも言える)。
例えばRubyで以前、yaml_structure_checkerというGem(NpmでいうPackage)を作成したのですが、非常に体験が良いです。
Gemを作る際、用意されているコマンドでひな形を作ると、すぐに機能の開発に取り掛かれます。Railsと同じように、どこにどんなファイルを配置するか決まっているので悩むことがありません。
対して、JSはどうでしょうか。Package制作にかかわらず、非常に多くの選択肢から最適と思うのもを選択しなければいけません。TSかJSか、ViteかWebpackか、Pure ESM packageにするのか、フォルダ構成はどうするのか、tsconfigどうするか。実行エンジンですら選択肢があります。ベストプラクティスが無いというよりは、非常に流れの速い中で常にベストプラクティスが変わり続けているという印象です。
前置きが長くなりましたが、そんな中でViteやVitePress を用いてPackage作成を行ったら非常に体験が良かったです。少ない設定ですぐに開発に取り掛かれ、CJSとESMに対応した出力が可能で、ドキュメントも同じconfigでビルドできます。
今回はVite と VitePress で NPM Packageを作成する方法を解説していきます。
各ツールの簡単な紹介
Viteとは
AstroやHonoなどの新しめのフレームワークによく使われているフロントエンドツール(開発サーバーやビルドツール)です。高速かつ少ない設定で動かせるので非常に好きなツールです。
VitePressとは
コンテンツ中心の静的サイトを構築するジェネレーター。基本的にはマークダウンを追加していくことで、ほぼコードを書かずにドキュメントサイトなどを構築することができます。マークダウン内にVueやReactのコードを埋め込んで、ドキュメント内でデモプログラムを動かすこともできます(こんな感じ)。ViteやVitePress自体のドキュメントもVitePressを使って作られてそうです。
NPM Packageを作成する方法
今回作成するプログラム例
この記事はreact-dropzone-vvというパッケージを作った時の知見をまとめた記事になってます。細かいファイル構成や設定は上記リポジトリも参考にしてください。
作成する構成全体は以下のような感じです。
. ├── Dockerfile ├── LICENSE ├── README.md ├── dist │ ├── index.js │ ├── index.umd.cjs │ ├── lib │ └── tsconfig.package.tsbuildinfo ├── docker-compose.yml ├── docs │ ├── examples │ ├── index.md │ ├── introduction │ └── public ├── index.html ├── lib │ ├── ReactDropzoneVV.tsx │ ├── index.ts │ ├── types.ts │ ├── useReactDropzoneVV.ts │ ├── utils.test.ts │ └── utils.ts ├── package-lock.json ├── package.json ├── src │ ├── App.tsx │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.package.json └── vite.config.ts
lib/ はPackageとして公開するコードを入れるフォルダです。
src/ はlibに書いたコードを実際に動かすコードを入れるフォルダです。
docs/ はVitePressに関するファイルが含まれています。
dist/ はlib/ をビルドしたファイルが出力されるフォルダです。
僕はDockerを使って環境を作るのが好きなので関連ファイルがありますが、使わずに直接NodeをインストールしてももちろんOKです(この記事ではDocker使わない説明を書きます)。
Viteプロジェクトを新規作成
create vite
コマンドを使って作成します。私の場合は「React」「TypeScript + SWC」を選択しましたが、自身の使いたいものを選んでください。
npm create vite@latest
通常のViteプロジェクトができたのでnpm run dev
コマンドを実行すると開発サーバーが立ち上がります。
ViteプロジェクトをPackage作成できるように変更する
まずはlib/フォルダを作成して好きなコンポーネントや関数を追加しましょう。lib/index.tsでexportしていればOKです。以下のようなイメージです。
lib/index.ts
export * from "./ReactDropzoneVV" export * from "./useReactDropzoneVV" export * from "./utils" export * from "./types"
lib/utils.ts
... export const isExtensionMatch = (file: File, extensions: string[]) => { const fileName = file.name.toLowerCase() return extensions.some((extension) => fileName.endsWith(extension.toLowerCase()) ) } export const isMimeTypeMatch = (file: File, mimeTypes: string[]) => { const fileType = file.type return mimeTypes.some((mimeType) => { if (mimeType.includes("/*")) { const baseType = mimeType.split("/")[0] return fileType.startsWith(baseType + "/") } return fileType === mimeType }) } ...
次にlib/に追加したコードをビルドするようにvite.config.tsを変更します。
vite.config.ts
import { defineConfig } from "vite" import react from "@vitejs/plugin-react-swc" // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { return { plugins: [react()], resolve: { alias: [{ find: "@lib", replacement: "/lib" }], }, build: { outDir: "dist", lib: { entry: "lib/index.ts", name: "react-dropzone-vv", fileName: "index", formats: ["es", "umd"], }, rollupOptions: { external: ["react", "react-dom"], output: { globals: { react: "React", "react-dom": "ReactDOM", }, }, }, }, } })
解説します。
formatsのデフォルトは ["es", "umd"]
なので書かなくてもよいです。これでESMとCJS向けにビルドできます。
Reactを使ってなければrollupOptionsとかは丸々いらないです。
以下の設定でsrc/からlib/のコードにエイリアスを使ってimportできるようにしています。
vite.config.ts
resolve: { alias: [{ find: "@lib", replacement: "/lib" }], },
tsconfigも変更しましょう。
tsconfig.json
{ "compilerOptions": { "composite": true, "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "moduleDetection": "force", "jsx": "react-jsx", "declaration": true, "emitDeclarationOnly": true, "declarationMap": true, "outDir": "./dist", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@lib/*": ["./lib/*"] } }, "include": ["./lib", "./src"] }
解説します。
以下の設定で型定義ファイルを出力するようにしています。tscコマンド実行で生成されるようになります。
tsconfig.json
"declaration": true, "emitDeclarationOnly": true, "declarationMap": true, "outDir": "./dist",
先ほどvite.config.tsの設定を変えてエイリアスインポートできるようにしたと思いますが、vite.config.tsの設定だけだとTSがそれを理解できずVSCodeなどで補完できなかったり問題があります。
そこでtsconfig.jsonにもエイリアスの設定を追加しています。
tsconfig.json
"baseUrl": ".", "paths": { "@lib/*": ["./lib/*"] }
最後に"include": ["./lib", "./src"]
でsrc/だけじゃなくてlib/もビルドの対象にしています。
src/はPackageのビルド結果に必要ないですが、VSCodeの補完とかのためにこの設定は必要です。
とはいえ、パッケージのビルド結果には必要ないので、パッケージビルド時は別のtsconfigを使います。
tsconfig.package.json
{ "extends": "./tsconfig", "include": ["./lib"], "exclude": ["./lib/**/*.test.ts"] }
extends
で指定することでtsconfig.json
をベースの設定にしつつ、include
を上書きしてビルド対象をlib/だけに絞っています。
package.jsonのビルドコマンドを更新しましょう。
package.json
"scripts": { "dev": "vite", "build": "vite build && tsc -p tsconfig.package.json",
vite build
は実行時にdist/内のファイルを一度消してしまうので、tsc && vite build
の順番で実行するとtscコマンドで生成された型定義ファイルが消えてしまいます。そこでtscコマンドを後に実行しています。
余談ですが、tsconfigにincremental: true
やtsBuildInfoFile
の指定があると、型定義ファイル生成が最適かされて変更差分があったコード分しか型定義ファイルが生成されなくなります。vite build
と同じdist/に生成している関係上、tsc実行のたびに型定義ファイル全体を生成する必要があります。
https://noh.ink/articles/cuQq3iC5angmpOseGRlA
ここまで出来たらnpm run build
コマンドを実行すると、パッケージに関するコードと型定義ファイルがdist/に生成されます。
# npm run build > [email protected] build > vite build && tsc -p tsconfig.package.json vite v5.3.5 building for production... ✓ 13 modules transformed. dist/index.js 26.73 kB │ gzip: 7.92 kB dist/index.umd.cjs 17.85 kB │ gzip: 6.80 kB ✓ built in 135ms
長くなったのでCI/CDの導入やリポジトリの設定、パッケージをNPMJSで公開する話は別記事になりました。
次は