Original link: https://blog.rxliuli.com/p/73331967c1814df480811eee598e714b/
foreword
Many front-end developers may know that since sindresorhus issued the esm only declaration for more than a year last year, many projects have turned to esm only, that is, only supporting esm but not cjs, in order to force the entire ecosystem to migrate to esm faster only.
Some popular projects already do this
- Thousands of npm packages maintained by sindresorhus
- node-fetch
- remark series
- More. . .
They claim: You can still use the existing version without upgrading to the latest version, major version updates will not affect you. How are the facts?
I have encountered the problem of not being able to use the esm only package several times before. Whenever I want to try esm only, there are always some problems. The most painful thing is that some packages are esm only, while others are cjs only. To choose to give up one side, fuck esm only. Some of the main problems are that the cjs only packages, and the must-compatible packages typescript/jest/ts-jest/wallaby do not properly support esm. Of course, we can choose to look for alternatives to esm only packages, such as globby => fast-glob, remark => markdown-it, node-fetch => node-fetch@2, lodash-es => lodash, but this is not A long-term option, not to mention that some bags are hard to really find a replacement for, such as the remark series.
So, what’s wrong with using an older version of the package?
The main problem is that it is difficult to find the correct version. Of course, if you are using a relatively independent package, such as node-fetch, you can use the v2 version directly. But if you’re using a project like vuepress/remark that contains many small packages in a monorepo, it’s hard to find the correct version for each subproject.
When I was working on an epub generator recently, I needed to do some conversions from markdown and ast, and finally convert it to html, so I used remark again, and decided to really try to use esm. The following are some attempts.
Target
Using esm must solve the following problems, otherwise it is impossible to use in production environment
- Typescript support – basically all web projects use ts, not supporting it is unacceptable
- jest support – also heavily used testing tool
- wallaby support – a paid WYSIWYG testing tool
- Allow references to cjs modules – need to support existing packages
- The dual module package can still support both esm/cjs projects – need to support cjs project references
- Support for unbundled modules – some private modules in monorepo will not be bundled
- esbuild support – esbuild is becoming the lib bundle standard
Modify the package declaration
The first step is to modify the module type of the package. Modify "type": "module"
to declare the package as esm, and all js code will run as esm module by default.
{ "type" : "module" }
TypeScript support
NodeNext is supported since ts4.7, so need to change tsconfig.json
{ "compilerOptions" : { "module" : "ESNext" , "moduleResolution" : "NodeNext" } }
In addition, importing other ts files in the ts file must use the .js suffix
This is an odd limitation, refer to the ts 4.7 release documentation
import { helper } from "./foo.js" ; // works in ESM & CJS helper ( ) ;
Does it seem strange, but now it can only be written like this, typescript even prompts like this
jest/wallaby support
For example run the following code using the pnpm jest src/lodash.test.ts
command
import { uniq } from "lodash-es" ; it ( "uniq" , ( ) => { console . log ( uniq ( [ 1 , 2 , 1 ] ) ) ; } ) ;
An error occurred
Jest encountered an unexpected token
From jest 28, experimental esm support is supported, and wallaby/ts-jest can also be supported by configuration. Follow the steps below to handle it
-
Configure ts-jest
{ "jest" : { "preset" : "ts-jest/presets/default-esm" , "globals" : { "ts-jest" : { "useESM" : true } } , "moduleNameMapper" : { "^(\\.{1,2}/.*)\\.js$" : "$1" } , "testMatch" : [ "<rootDir>/src/**/__tests__/*.test.ts" ] } }
-
Modify the command to
node --experimental-vm-modules node_modules/jest/bin/jest.js src/lodash.test.ts
-
Configure wallaby (you can run the test files in the
__tests__
directory here, strange…){ "wallaby" : { "env" : { "params" : { "runner" : "--experimental-vm-modules" } } } }
-
Since the import of esm is static, you also need to uninstall
@types/jest
use the@jest/globals
package to import the functions required for testing, such asit/expect/describe/beforeEach
, etc.import { it , expect } from "@jest/globals" ; it ( "basic" , ( ) => { expect ( 1 + 2 ) . toBe ( 3 ) ; } ) ;
nodejs support
Nodejs has supported esm since 14, but until now 18, the migration is still not smooth, mainly encountered the following problems.
import cjs only module
Unfortunately, a large number of existing packages are cjs only modules, it is impossible to migrate in a short time, and the interoperability of esm and cjs in nodejs is not very good, so it needs to be dealt with. Take fs-extra as an example below:
I used to write like this
import { readdir } from "fs-extra" ; import path from "path" ; console . log ( await readdir ( path . resolve ( ) ) ) ;
The error SyntaxError: The requested module 'fs-extra' does not provide an export named 'readdir'
when running with tsx appears to be a known bug, reference: https://github.com/esbuild-kit/tsx /issues/38
Now it needs to be changed to
import fsExtra from "fs-extra" ; import path from "path" ; console . log ( await fsExtra . readdir ( path . resolve ( ) ) ) ;
Or modify the following code to run with ts-node --esm <file>
(tsx does not support this method)
import fsExtra = require ( "fs-extra" ) ; import path from "path" ; console . log ( await fsExtra . readdir ( path . resolve ( ) ) ) ;
use __dirname
Yes, you read that right, __dirname
is not available under the esm module, instead import.meta.url
is used. All in all, the way it is used now is
import path from "path" ; import { fileURLToPath } from "url" ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; console . log ( __dirname ) ;
Refer to the article https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/ , and then talk about how to package cjs bundles to deal with import.meta.url
when talking about esbuild (not supported in cjs, and is a choice of two).
lib maintenance and use
New esm and cjs dual package support configuration
Previously, we differentiated modules by the main/module field
{ "main" : "dist/index.js" , "module" : "dist/index.esm.js" , "types" : "dist/index.d.ts" }
But referencing in the esm project will report an error
SyntaxError: The requested module 'cjs-and-esm-lib' does not provide an export named 'hello'
The esm project does not recognize this, it newly defines the exports field, so it needs to be added (note that the main field still needs to remain compatible with the old version of node) the exports field
{ "exports" : { "." : { "import" : "./dist/index.esm.js" , "require" : "./dist/index.js" , "types" : "./dist/index.d.ts" } } }
Refer to this answer: https://stackoverflow.com/a/70020984
esbuild support
I thought that esbuild naturally supports esm so it should be very simple, but in fact, it also encountered quite a few problems.
bundling the following code as cjs will give an error
import path from "path" ; import { fileURLToPath } from "url" ; import fsExtra from "fs-extra" ; const { readdir } = fsExtra ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; console . log ( __dirname ) ; console . log ( await readdir ( __dirname ) ) ;
Order
esbuild src/bin.ts --platform=node --outfile=dist/bin.esm.js --bundle --sourcemap --format=esmesbuild src/bin.ts --platform=node --outfile=dist/bin.js --bundle --sourcemap --format=cjs
mistake
[ERROR] Top-level await is currently not supported with the "cjs" output format
This is because cjs cannot contain top-level await, which is modified to
import path from "path" ; import { fileURLToPath } from "url" ; import fsExtra from "fs-extra" ; const { readdir } = fsExtra ; ( async ( ) => { const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; console . log ( __dirname ) ; console . log ( await readdir ( __dirname ) ) ; } ) ( ) ;
There is no problem with bundling, but running will give errors
node dist/bin.js
first error first
var import_path = __toESM(require("path"), 1); ^ReferenceError: require is not defined in ES module scope, you can use import insteadThis file is being treated as an ES module because it has a '.js' file extension and 'esm-demo\packages\esm-include-cjs-lib\package.json' contains "type": "module". Totreat it as a CommonJS script, rename it to use the '.cjs' file extension.
It says that this is an esm package, the default code is esm module, if you want to execute in cjs module, you need to modify it to cjs suffix.
Modify command
esbuild src/bin.ts --platform=node --outfile=dist/bin.cjs --bundle --sourcemap --format=cjs
Then came the second error
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string or an instance of URL. Received undefined
Related code
// src/bin.ts var import_path = __toESM ( require ( "path" ) , 1 ) ; var import_url = require ( "url" ) ; var import_fs_extra = __toESM ( require_lib ( ) , 1 ) ; var import_meta = { } ; var { readdir } = import_fs_extra . default ; ( async ( ) => { const __filename = ( 0 , import_url . fileURLToPath ) ( import_meta . url ) ; // 这里是关键,因为import.meta.url 在cjs 代码中是空的const __dirname = import_path . default . dirname ( __filename ) ; console . log ( __dirname ) ; console . log ( await readdir ( __dirname ) ) ; } ) ( ) ;
Based on the author’s answer in this issue , modify the command
esbuild src/bin.ts --platform=node --outfile=dist/bin.cjs --inject:./import-meta-url.js --define:import.meta.url=import_meta_url --bundle --sourcemap --format=cjs
Unfortunately, this is no longer in effect, the code for the bundle is as follows
// import-meta-url.js var import_meta_url2 = require ( "url" ) . pathToFileURL ( __filename ) ; console . log ( import_meta_url2 ) ; // src/bin.ts var import_path = __toESM ( require ( "path" ) , 1 ) ; var import_url = require ( "url" ) ; ( async ( ) => { const __filename2 = ( 0 , import_url . fileURLToPath ) ( import_meta_url ) ; const __dirname = import_path . default . dirname ( __filename2 ) ; console . log ( __dirname ) ; } ) ( ) ;
It is obvious that the variable name of the injected script has been modified, from import_meta_url
=> import_meta_url2
, which is strange problem. . .
Maybe replace --inject
=> --banner
esbuild src/bin.ts --platform=node --outfile=dist/bin.cjs --define:import.meta.url=import_meta_url --bundle --sourcemap --banner:js="var import_meta_url = require('url').pathToFileURL(__filename)" --format=cjs
This will take effect
So, what about running esm bundle?
error also occurs
throw new Error ( 'Dynamic require of "' + x + '" is not supported' ) Error : Dynamic require of "fs" is not supported
Modify the command as per the workaround here
esbuild src/bin.ts --platform=node --outfile=dist/bin.esm.js --bundle --sourcemap --banner:js="import { createRequire } from 'module';const require = createRequire(import.meta.url);" --format=esm
Now, the bundled code can finally run.
Epilogue
Maybe esm only looks good, and tree shaking looks great, but right now, it’s not really available in production. Including a series of important projects are not migrated, including react/vscode/electron/vite and so on. In fact, before this, many people (and my generation) also used esm modules to write code, but the final bundle product may be cjs, such as iife in browsers, cjs in nodejs, but never Most application layer developers do not care about these, only lib maintainers will care, esm only transfers the complexity of using packages to users, and esm only packages referenced in cjs are not really available ‘s plan. Compared to projects like esbuild/vite that solve practical problems, the esm only movement is more like a carnival in the web front-end circle.
This article is reproduced from: https://blog.rxliuli.com/p/73331967c1814df480811eee598e714b/
This site is for inclusion only, and the copyright belongs to the original author.