最近、rc-modal をライブラリにしました。コードを書くのは難しくありませんが、元のコードをラップして抽象化するだけです。しかし、パッケージを配布することや、ユーザーがこのパッケージをどのように使用するかが難しい点でした。
まず、現在の npm パッケージは ESM Pure を行わない場合、CJS/ESM、古いバージョンと新しいバージョンの Node(package.json の exports フィールドを使用するかどうか)、および TypeScript がこれらの状況に対して bundler
を有効にしたプロジェクトと無効にしたプロジェクトを考慮する必要があります。したがって、使用者がどのバージョンの Node を使用してもモジュール解決に問題がないこと、プロジェクトが type: module
を有効にしているかどうかに関係なく問題がないこと、TypeScript が "moduleResolution": "Bundler"
を有効にしているかどうかに関係なく問題がないことを保証する必要があります。
さらに、パッケージから生成された成果物には、ソースコードに存在する "use client"
ディレクティブが失われないようにする必要があります。
したがって、以下の例では、このようなライブラリを作成し、上記のすべてのシナリオに完全に対応する必要があります。
初期化#
ライブラリを書くために、まずパッケージツールを選択します。現在一般的なものには、rollup、esbuild、swc があります。
rollup は互換性が最も優れており、エコシステムも比較的充実しています。ここでは、直接 rollup を使用する代わりに vite をパッケージツールとして選択します。vite の設定はよりシンプルです。
まず、プロジェクトを初期化します。
npm create vite@latest
# react-ts を選択
次に、vite の設定を調整します。
import { readFileSync } from 'fs'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
const packageJson = JSON.parse(
readFileSync('./package.json', { encoding: 'utf-8' }),
)
const globals = {
...(packageJson?.dependencies || {}),
}
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/index.ts',
formats: ['cjs', 'es'],
},
rollupOptions: {
external: [
'react',
'react-dom',
'lodash',
'lodash-es',
'react/jsx-runtime',
...Object.keys(globals),
],
},
},
})
次に、src
フォルダ内に、エクスポートするための 2 つのコンポーネントを簡単に作成します。ここでは、1 つを Client Component、もう 1 つを Shared Component と定義します。これが何であるか分からなくても、"use client" が何であるかを知っていれば問題ありません。
export const Client = () => null
```
OK、これでこのシンプルなライブラリのコードが書けました。次に、上記のすべてのシナリオにどのように対応するかを考える必要があります。
"use client" ディレクティブの保持#
上記のコードでパッケージを作成すると、成果物には "use client" ディレクティブが存在しません。また、すべてのモジュールが 1 つのファイルにパッケージ化されます。
この問題を解決するために、rollup プラグイン rollup-plugin-preserve-directives
を使用します。
npm i rollup-plugin-preserve-directives -D
vite の設定を少し変更します。
import { preserveDirectives } from 'rollup-plugin-preserve-directives'
export default defineConfig({
// ...
build: {
lib: {
entry: 'src/index.ts',
entry: ['src/index.ts'],
formats: ['cjs', 'es'],
},
rollupOptions: {
external: [
'react',
'react-dom',
'lodash',
'lodash-es',
'react/jsx-runtime',
...Object.keys(globals),
],
output: {
preserveModules: true,
},
plugins: [preserveDirectives({})],
},
},
})
現在、ビルド後の dist ディレクトリはこのようになります。
.
├── components
│ ├── client.cjs
│ ├── client.js
│ ├── shared.cjs
│ └── shared.js
├── index.cjs
└── index.js
そして、client.js
の成果物は次のようになります。
'use client'
const l = () => null
export { l as Client }
"use client" ディレクティブが保持されました。
d.ts
の生成#
このステップは非常に重要であり、古いバージョンの Node との互換性の基礎となります。
現在、私たちの成果物には型がまったくありません。d.ts
ファイルを生成する必要があります。ここでは、vite-plugin-dts
プラグインを使用できます。
npm i -D vite-plugin-dts
設定を変更します。
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [react(), dts({})],
})
これで、d.ts
ファイルが生成されました。各成果物には対応する d.ts
ファイルがあります。
次に、package.json
の成果物エクスポートフィールドをどのように定義するかを考えます。
exports
フィールドをサポートする Node バージョンでは、次のように定義する必要があります。現在の type: "module"
を仮定します。
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
},
"./client": {
"import": {
"types": "./dist/components/client.d.ts",
"default": "./dist/components/client.js"
},
"require": {
"types": "./dist/components/client.d.ts",
"default": "./dist/components/client.cjs"
}
},
"./shared": {
"import": {
"types": "./dist/components/shared.d.ts",
"default": "./dist/components/shared.js"
},
"require": {
"types": "./dist/components/shared.d.ts",
"default": "./dist/components/shared.cjs"
}
}
}
}
書くのは非常に面倒です。各エクスポート項目には import
と require
フィールドをそれぞれ書く必要があり、内部にはさらに types
フィールドを記述する必要があります。types
フィールドは最初に書かなければならず、間違って書いたり書き漏れたりすると大変です。
では、なぜ次のように書けないのでしょうか。
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
それは、types
は ESM にのみ有効で、CJS には無効だからです。CJS/ESM が分離されている以上、types
も分離する必要があります。
vite.config.ts
を変更します。
export default defineConfig({
plugins: [
react(),
dts({
beforeWriteFile: (filePath, content) => {
writeFileSync(filePath.replace('.d.ts', '.d.cts'), content)
return { filePath, content }
},
}),
],
})
これで、各成果物には対応する d.cts
と d.ts
ファイルが生成されました。
.
├── components
│ ├── client.cjs
│ ├── client.d.cts
│ ├── client.d.ts
│ ├── client.js
│ ├── shared.cjs
│ ├── shared.d.cts
│ ├── shared.d.ts
│ └── shared.js
├── index.cjs
├── index.d.cts
├── index.d.ts
└── index.js
現在、exports
フィールドは次のように変更する必要があります。
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./client": {
"import": "./components/client.js",
"require": "./dist/components/client.cjs"
},
"./shared": {
"import": "./dist/components/shared.js",
"require": "./dist/components/shared.cjs"
}
}
}
これで、types
フィールドはもはや必要ありません。
一般的な定義を追加します。
{
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts"
}
これでビルドして、パッケージを作成し、arethetypeswrong で検証します。
npm run build
npm pack
taz
をアップロードした後、次のような結果が得られました。
環境 | rc-lib | rc-lib/client | rc-lib/shared |
---|---|---|---|
node10 | ✅ | 💀 解決に失敗しました | 💀 解決に失敗しました |
node16 (CJS から) | ✅ | ✅ (CJS) | ✅ (CJS) |
node16 (ESM から) | 🥴 内部解決エラー (2) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
node10
では、exports
フィールドをサポートしていないため、後の非 index エクスポートは解決できないことがわかります。
次に、node16 では、tsconfig で moduleResolution: "node16"
を有効にし、プロジェクトが type: "module"
の場合、index が正しく型推論されないという問題があります。
まず、後者の問題を解決します。
上記の問題が発生したデモを作成します。
{
"type": "module",
"dependencies": {
"rc-lib": "workspace:*" // ここで pnpm workspace を使用してこのパッケージをリンクします。
}
}
{
"compilerOptions": {
"moduleResolution": "node16"
}
}
index.ts
を作成します。
import { Client } from 'rc-lib'
import { Share } from 'rc-lib/shared'
console.log(!!Client, !!Share)
この時点で tsx index.ts
を使用すると、プログラムは正常に実行され、true true
と出力されます。このことから、Node はこのモジュールの解決に問題がないことがわかります。しかし、問題は TypeScript の型推論にあります。
型に入ると、次のようなエラーが発生します。
問題が見つかりました。エクスポート項目にサフィックスが追加されていません。
現在、パッケージの index.ts
を修正します。
export * from './components/client'
export * from './components/shared'
export * from './components/client.js'
export * from './components/shared.js'
再度パッケージを作成し、ウェブサイトで検証します。
環境 | rc-lib | rc-lib/client | rc-lib/shared |
---|---|---|---|
node10 | ✅ | 💀 解決に失敗しました | 💀 解決に失敗しました |
node16 (CJS から) | ✅ (CJS) | ✅ (CJS) | ✅ (CJS) |
node16 (ESM から) | ✅ (ESM) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
これで、node10
が exports
をサポートしていないことを除いて、すべての問題が解決されました。
古いバージョンの Node の非 index エクスポート問題の解決#
この問題を解決するためには、typesVersions
を知っておく必要があります。
typesVersions
は TypeScript 4.1 で導入されたフィールドで、異なるバージョンの TypeScript が異なるバージョンの Node に対して型推論を行う問題を解決するために使用されます。
Node10 では、このフィールドを使用してエクスポート項目の推論問題を解決できます。
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./dist/components/*", "./*"]
}
}
}
成果物のエクスポート項目は dist
と dist/components
に存在するため、2 層のディレクトリを定義する必要があり、最後のルートディレクトリもテストの結果、必須です。
これでパッケージを作成し、再度互換性を検証します。
環境 | rc-lib | rc-lib/client | rc-lib/shared |
---|---|---|---|
node10 | ✅ | ✅ | ✅ |
node16 (CJS から) | ✅ (CJS) | ✅ (CJS) | ✅ (CJS) |
node16 (ESM から) | ✅ (ESM) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
これで、すべての環境に完全に対応できました。大成功です。
上記のコードの完全なテンプレートは次の場所にあります:
この記事は Mix Space によって xLog に同期更新されました。元のリンクは https://innei.in/posts/tech/write-a-universally-compatible-js-library-with-type-support