最近,把 rc-modal 做成了一个库。写代码却是不难,无非就是把本来的代码封装抽象一下。却是发包,和作为运用者怎么运用这个包难住了。
首要,咱们知道现在一个 npm 包假如不做 Pure ESM 的话,你需求考虑兼容 CJS/ESM,node 老版别和新版别(吃不吃 package.json 的 exports 字段),和 TypeScript 针对这些状况推出 bundler
敞开了特征和没有敞开 bundler
的项目。那么,咱们需求确保运用方不论是哪个版别 node 在 module resolution 没有问题,不论你的项目有没有敞开 type: module
都没有问题,不论你的 TypeScript 有没有敞开 "moduleResolution": "Bundler"
也都没有问题。
另外,还需求确保,打包出来的产品,不能丢掉源代码中存在的 "use client"
directives。
所以,下面的例子咱们来写一个这样的库,需求全面兼容上面的一切场景。
初始化
写库首要挑选打包东西,目前常见的有:rollup, esbuild。
rollup 在兼容上最好,同时生态也比较完善。这儿咱们挑选 vite 作为打包东西,相比直接运用 rollup,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
中,简略的写两个组件进行导出。这儿咱们界说一个为 Client Component 另一个为 Shared Component。假如你不知道这是什么也没有联系,你只需求知道 “use client” 是什么就行。
<Tabs>
<Tab label="index.ts">
```ts
export * from './components/client'
export * from './components/shared'
```
</Tab>
<Tab label="components/client.tsx">
```tsx
'use client'
export const Client = () => null
```
</Tab>
<Tab label="components/shared.tsx">
```tsx
export const Shared = () => null
```
</Tab>
</Tabs>
OK,那么这个简略库的代码就写好了。接下来咱们需求考虑怎么兼容上面提到的一切场景。
保留 “use client” directive
按照上面的代码进行打包之后,产品中并不会存在 “use client” directive。而且一切的模块都被打包成一个文件。
运用 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', // [!code --]
entry: ['src/index.ts'], // [!code ++]
formats: ['cjs', 'es'],
},
rollupOptions: {
external: [
'react',
'react-dom',
'lodash',
'lodash-es',
'react/jsx-runtime',
...Object.keys(globals),
],
output: {
preserveModules: true, // [!code ++]
},
plugins: [preserveDirectives({})], // [!code ++]
},
},
})
现在 build 之后,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” directive。
生成 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({})], // [!code highlight]
})
现在咱们就有了 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) // [!code ++]
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"
}
现在咱们 build 一下,然后 pack 之后去 arethetypeswrong 验证。
npm run build
npm pack
上传 taz
之后,得到了这样的成果。
Environment | rc-lib |
rc-lib/client |
rc-lib/shared |
---|---|---|---|
node10 | ✅ | Resolution failed | Resolution failed |
node16 (from CJS) | ✅ | ✅ (CJS) | ✅ (CJS) |
node16 (from ESM) | Internal resolution error (2) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
可以看到,在 node10
也就是不支持 exports 字段环境中,后边的非 index 导出都是无法被 resolve 到的。
其次,在 node16 中,tsconfig 敞开了 moduleResolution: "node16"
并且项目是 type: "module"
时候,index 反而是无法正确类型推导的。
咱们先来处理后者的问题。
起一个上面出现问题的 demo。
{
"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 关于这个 module 的 resolve 是没有问题的。但是问题出在了 TypeScript 的类型推导。
进入类型后,发现这样的报错。
问题找到了,导出项没有添加后缀。
现在咱们修正包的 index.ts
。
export * from './components/client' // [!code --]
export * from './components/shared' // [!code --]
export * from './components/client.js' // [!code ++]
export * from './components/shared.js' // [!code ++]
再次打包,在网站上验证。
Environment | rc-lib |
rc-lib/client |
rc-lib/shared |
---|---|---|---|
node10 | ✅ | Resolution failed | Resolution failed |
node16 (from CJS) | ✅ (CJS) | ✅ (CJS) | ✅ (CJS) |
node16 (from ESM) | ✅ (ESM) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
那么现在除了 node10 不支持 exports 之外,咱们已经悉数处理了。
处理旧版别 node 的非 index 导出问题
要处理这个问题,咱们首要需求知道 typesVersions
。
typesVersions
是 TypeScript 4.1 引入的一个字段,用于处理不同版别的 TypeScript 关于不同版别的 node 的类型推导问题。
在 node10 中,咱们可以通过这个字段来处理 exports 导出项的推导问题。
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./dist/components/*", "./*"]
}
}
}
由于产品导出项存在于 dist
和 dist/components
中,所以咱们需求界说两层目录,最后一个根目录通过测验也是有必要的。
现在 pack 之后重新验证兼容性。
Environment | rc-lib |
rc-lib/client |
rc-lib/shared |
---|---|---|---|
node10 | ✅ | ✅ | ✅ |
node16 (from CJS) | ✅ (CJS) | ✅ (CJS) | ✅ (CJS) |
node16 (from ESM) | ✅ (ESM) | ✅ (ESM) | ✅ (ESM) |
bundler | ✅ | ✅ | ✅ |
现在咱们已经彻底兼容了一切的环境,功德圆满。
上面代码的完整模板位于: