Re-release fs-extra to properly support esm/cjs usage
Published: 2022-12-25 06:22:09

## motivation

Since updating nodejs@18 and switching to esm only, many libraries have been replaced to support esm import, but [fs-extra](https://www.npmjs.com/package/fs-extra) has not been The use of esm is correctly supported, and no suitable replacement has been found. After the a [PR](https://github.com/jprichardson/node-fs-extra/pull/986) proposed by us was rejected, I decided to re-release a [fs-extra-unified](https://www.npmjs.com/package/fs-extra-unified) that correctly supports the use of esm module.

If you don't know what fs-extra is, here is a brief introduction: it is a tool library related to nodejs file operations, which is used to completely replace the fs module. Before the existence of fs/promises, it has all the fs Asynchronous callback functions are converted to Promises. At the same time, some other very useful utility functions are provided for use, such as `pathExists`, `remove`, `mkdirp`, `copy`.

For example after deleting a temporary directory and then rebuilding it

```ts
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)
```

But in the esm module, it currently does not support the use of ts correctly. For example, the above code can only be used normally in the cjs module. In the esm module, the following imports must be used

```ts
import fsExtra from 'fs-extra'
const { remove, mkdirp } = fsExtra
```

Even though fs-extra@11 claims to support esm, it is supported by another entry `fs-extra/esm`, and the ts type definition has not been updated yet, so it cannot actually be used in ts. For example the above import can be converted to the following import

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

In addition, it has another troublesome problem, that is, the functions exported by fs are not supported, for example, the following code will report an error

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

The official claims that only `fs-extra/esm` will only export some unique functions. The functions originally exported by fs need to use the `fs/promises` module, which needs to be modified to the following imports

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

Ok, looks like esm/ts support is second class citizen, let me summarize known issues

1. The default `fs-extra` entry does not support esm named imports
2. `fs-extra/esm` does not support the original functions of fs
3. `fs-extra/esm` does not correctly declare the ts type definition
4. cjs/esm use different behavior

It is precisely because it is a commonly used tool library that we republish it.

## Republish

The basic idea is very simple. Scan the modules exported by `fs-extra` through the script, then generate an esm entry, and finally declare it correctly in the `exports` of package.json, so that there is no difference between esm and cjs at the usage level.

### Desired result

1. esm supports named import and default import

    ```ts
    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 supports named import and default import

    ```ts
    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. Correctly support the use of ts, esm no longer uses a separate entry

### Final implementation method

1. Use the generate script to generate esm entry

    ```ts
    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',
        'graceful',
      ]
      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. Then add the dependency of `@types/fs-extra` and re-export it in `index.d.ts`

    ```ts
    export * from 'fs-extra'
    ```

3. Declare the correct `exports/types` field in package.json

    ```json
    {
      "exports": {
        ".": {
          "import": "./lib/esm.mjs",
          "require": "./lib/index.js"
        }
      },
      "types": "./index.d.ts"
    }
    ```

## Conclusion

If fs-extra finally supports esm/ts correctly, we will also delete this module to avoid trouble, but before that, we can only use this module first.

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