DEV Community

rxliuli
rxliuli

Posted on

Re-release fs-extra to properly support esm/cjs usage

motivation

Since updating nodejs@18 and switching to esm only, many libraries have been replaced to support esm import, but fs-extra has not been The use of esm is correctly supported, and no suitable replacement has been found. After the a PR proposed by us was rejected, I decided to re-release a 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

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)
Enter fullscreen mode Exit fullscreen mode

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

import fsExtra from 'fs-extra'
const { remove, mkdirp } = fsExtra
Enter fullscreen mode Exit fullscreen mode

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

import { remove, mkdirp } from 'fs-extra/esm'
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

    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

    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

    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

    export * from 'fs-extra'
    
  3. Declare the correct exports/types field in package.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

Latest comments (0)