背景
npm 包的自定义导出路径的类型推断的兼容性不是太好,Typescript需要4.7+。可以用这个网站进行类型兼容性测试:Are The Types Wrong? - Tool for analyzing TypeScript types of npm packages
实现
- 使用
typesVersion
字段,参考 巧用 exports 和 typeVersions 提升 npm 包用户使用体验 - 使用 tsup + 构建后脚本。tsup 使用 esbuild 对源代码进行构建,同时,在 7.1 版本,tsup 支持了按照模块类型生成对于类型定义文件
配置tsup
在输出 esm
和 cjs
时:
- 若
package.json
的type
为commonjs
或者缺省,cjs
输出类型文件的后缀为.d.ts
,esm
输出类型文件的后缀为.d.mts
- 若
package.json
的type
为module
,cjs
输出类型文件的后缀为.d.cts
,esm
输出类型文件的后缀为.d.ts
配置文件
tsup 挺开箱即用的
// tsup.config.ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'src/index.ts',
'src/another.ts',
],
clean: true,
format: ['cjs', 'esm'],
shims: true,
dts: true,
treeshake: true,
})
配置 package.json
这部分是花的时间和问题最多的。在 package.json
中,exports
字段是用于配置导出路径的,第一层用于指定路径,第二层用于指定类型为 esm(import)/cjs(require)
时导入的文件
types
可以根据 require
和 import
的路径自动读取同目录下同名的类型文件。
// package.json
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
//"types": "./dist/index.d.ts"
},
"./renderer": {
"import": "./dist/another.mjs",
"require": "./dist/another.js"
},
},
}
如果需要手动指定,可以使用以下代码。
{
"exports": {
".": {
"import": {
"default": "./dist/index.mjs",
"types": "./dist/index.d.mts"
},
"require": {
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
},
},
},
}
但是 typescript
在 4.7 版本后才支持读取和自动提示,而且 tsconfig.json
的 moduleResolution
必须设置为 Node16
或者 NodeNext
。这个设置导致 package.json
的 type
必须是 module
,否则 IDE 会报错,十分膈应人。
最终,选择了自动生成真实的路径映射,用于兼容低版本 node 的路径问题,于是有了构建后脚本
配置构建后脚本
低版本 node 会按照路径在 npm 包中寻找文件,因此只需要将真实文件路径和 package.json
的 exports
字段一一对应,就可以在导入时获得类型提示了
const { relative, join } = require('node:path/posix')
const { writeFileSync, mkdirSync, rmSync } = require('node:fs')
const { exports: exp } = require('./package.json')
const ROOT_PATH = __dirname
const exportMap = exp
for (const ex of Object.keys(exportMap)) {
if (ex === '.' || !exportMap[ex].require) {
continue
}
const [, ...folders] = ex.split('/')
const fileName = folders.pop()
const [, ...targetFolders] = exportMap[ex].require.split('/')
const targetFileName = targetFolders.pop()
const target = relative(
join(ROOT_PATH, ...folders),
join(ROOT_PATH, ...targetFolders, targetFileName),
)
mkdirSync(join(ROOT_PATH, ...folders), {
recursive: true,
})
writeFileSync(
join(ROOT_PATH, ...folders, `${fileName}.js`),
`module.exports = require('./${target}')`,
)
writeFileSync(
join(ROOT_PATH, ...folders, `${fileName}.d.ts`),
`export * from './${target.split('.')[0]}'`,
)
}
按照前面的 package.json
的配置,最终会在根目录增加 4 个文件,两个类型定义文件、两个代码文件,都是利用 tsup
生成的文件自动进行映射的。
其他配置
将生成的文件路径添加进 files
字段和 .gitignore 文件中