重新发布 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。同时提供了另外一些非常有用的工具函数以供使用,例如 pathExistsremovemkdirpcopy

例如删除一个临时目录之后然后重建它

1
2
3
4
5
6
7
8
9
import { remove, mkdirp } from 'fs-extra'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const tempPath = path.resolve(__dirname, '.temp')
await remove(tempPath)
await mkdirp(tempPath)

但在 esm 模块中,它目前并不能正确支持 ts 使用,例如上面那段代码只能在 cjs 模块中可以正常使用。在 esm 模块中,必须使用以下导入

1
2
import fsExtra from 'fs-extra'
const { remove, mkdirp } = fsExtra

即便 fs-extra@11 宣称已经支持 esm 了,但却是以另一个 entry fs-extra/esm 支持的,而且 ts 类型定义还尚未更新导致 ts 中实际上无法使用。例如上面的导入可以转换为以下导入

1
import { remove, mkdirp } from 'fs-extra/esm'

另外它还有另一个麻烦的问题,即不支持 fs 导出的函数,例如以下代码会报错

1
2
3
import { readFile } from 'fs-extra/esm'
import { fileURLToPath } from 'node:url'
console.log(await readFile(fileURLToPath(import.meta.url), 'utf-8'))

官方声称只会 fs-extra/esm 只会导出独有的一些函数,fs 原本导出的函数需要使用 fs/promises 模块,需要修改为以下导入

1
2
3
import { readFile } from 'fs/promises'
import { fileURLToPath } from 'node:url'
console.log(await readFile(fileURLToPath(import.meta.url), 'utf-8'))

好的,看起来 esm/ts 支持就是二等公民,让吾辈总结一下已知的问题

  1. 默认的 fs-extra entry 不支持 esm 命名导入
  2. fs-extra/esm 不支持 fs 的原有函数
  3. fs-extra/esm 没有正确声明 ts 类型定义
  4. cjs/esm 使用的行为不同

正是因为它是常用的工具库,所以吾辈才重新发布它。

重新发布

基本思路很简单,通过脚本扫描 fs-extra 导出的模块,然后生成一个 esm 的 entry,最终在 package.json 的 exports 中正确声明,这样 esm/cjs 便在使用层面不再有差异。

期望的结果

  1. esm 支持命名导入和默认导入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { 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))
  2. cjs 支持命名导入和默认导入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { 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))
    })()
  3. 正确支持 ts 使用,esm 不再使用单独的 entry

最终的实现方法

  1. 使用生成脚本生成 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
    35
    const 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()
  2. 然后添加 @types/fs-extra 的依赖,在 index.d.ts 中重新导出

    1
    export * from 'fs-extra'
  3. 在 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 使用,吾辈也将会删除这个模块,避免造成麻烦,不过在此之前,吾辈还是只能先使用这个模块。

GitHub: https://github.com/rxliuli/node-fs-extra


重新发布 fs-extra 用以正确支持 esm/cjs 使用
https://blog.rxliuli.com/p/0e1fad3617514ced89c07959c8d8f52b/
作者
rxliuli
发布于
2022年12月25日
许可协议