重新发布 fs-extra 用以正确支持 esm/cjs 使用
本文最后更新于:2022年12月25日 早上
动机
自从更新 nodejs@18 并切换到 esm only 以来,许多库都已经被替换成支持 esm 导入,但其中 fs-extra 却一直没有正确的支持 esm 使用,也没有找到合适的替代品。在吾辈之前提出的 一个 PR 被否定之后,决定重新发布一个正确支持 esm 使用的 fs-extra-unified 模块。
如果你还不知道 fs-extra 是什么,这里可以简单介绍一下:它是一个 nodejs 文件操作相关的工具库,用以完全替代 fs 模块,在 fs/promises 存在之前,它就已经将所有 fs 中的异步 callback 函数转换为了 Promise。同时提供了另外一些非常有用的工具函数以供使用,例如 pathExists
、remove
、mkdirp
、copy
。
例如删除一个临时目录之后然后重建它
1 |
|
但在 esm 模块中,它目前并不能正确支持 ts 使用,例如上面那段代码只能在 cjs 模块中可以正常使用。在 esm 模块中,必须使用以下导入
1 |
|
即便 fs-extra@11 宣称已经支持 esm 了,但却是以另一个 entry fs-extra/esm
支持的,而且 ts 类型定义还尚未更新导致 ts 中实际上无法使用。例如上面的导入可以转换为以下导入
1 |
|
另外它还有另一个麻烦的问题,即不支持 fs 导出的函数,例如以下代码会报错
1 |
|
官方声称只会 fs-extra/esm
只会导出独有的一些函数,fs 原本导出的函数需要使用 fs/promises
模块,需要修改为以下导入
1 |
|
好的,看起来 esm/ts 支持就是二等公民,让吾辈总结一下已知的问题
- 默认的
fs-extra
entry 不支持 esm 命名导入 fs-extra/esm
不支持 fs 的原有函数fs-extra/esm
没有正确声明 ts 类型定义- cjs/esm 使用的行为不同
正是因为它是常用的工具库,所以吾辈才重新发布它。
重新发布
基本思路很简单,通过脚本扫描 fs-extra
导出的模块,然后生成一个 esm 的 entry,最终在 package.json 的 exports
中正确声明,这样 esm/cjs 便在使用层面不再有差异。
期望的结果
esm 支持命名导入和默认导入
1
2
3
4
5
6
7
8
9import { readdir } from 'fs-extra'
import fsExtra from 'fs-extra'
import { fileURLToPath } from 'url'
import path from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
console.log(await readdir(__dirname))
console.log(await fsExtra.readdir(__dirname))cjs 支持命名导入和默认导入
1
2
3
4
5
6
7
8
9
10import { readdir } from 'fs-extra'
import fsExtra from 'fs-extra'
const { readdir: readdirCjs } = require('fs-extra')
const fsExtraCjs = require('fs-extra')
;(async () => {
console.log(await readdir(__dirname))
console.log(await readdirCjs(__dirname))
console.log(await fsExtra.readdir(__dirname))
console.log(await fsExtraCjs.readdir(__dirname))
})()正确支持 ts 使用,esm 不再使用单独的 entry
最终的实现方法
使用生成脚本生成 esm entry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35const fsExtra = require('./lib/index')
const path = require('path')
const { difference } = require('lodash')
function scan() {
const excludes = [
'FileReadStream',
'FileWriteStream',
'_toUnixTimestamp',
'F_OK',
'R_OK',
'W_OK',
'X_OK',
'gracefulify',
]
return difference(Object.keys(fsExtra), excludes)
}
function generate(list) {
return (
"import fsExtra from './index'\n" +
list.map((item) => `export const ${item} = fsExtra.${item}\n`).join('') +
`export default {${list
.map((item) => `${item}: fsExtra.${item},`)
.join('')}}`
)
}
async function build() {
const list = scan()
const code = generate(list)
await fsExtra.writeFile(path.resolve(__dirname, 'lib/esm.mjs'), code)
}
build()然后添加
@types/fs-extra
的依赖,在index.d.ts
中重新导出1
export * from 'fs-extra'
在 package.json 中声明正确的
exports/types
字段1
2
3
4
5
6
7
8
9{
"exports": {
".": {
"import": "./lib/esm.mjs",
"require": "./lib/index.js"
}
},
"types": "./index.d.ts"
}
结语
如果 fs-extra 最终正确支持 esm/ts 使用,吾辈也将会删除这个模块,避免造成麻烦,不过在此之前,吾辈还是只能先使用这个模块。