As many front-end developers probably know, more than a year since sindresorhus published the esm only manifesto last year, many projects have started to move to esm only, i.e., esm only and not cjs, in order to force the whole ecosystem to migrate to esm only faster.
Some popular projects already do this
- thousands of npm packages maintained by sindresorhus
- remark series
They claim: you can still use the existing version without upgrading to the latest version, and that major version updates will not affect you. What are the facts?
I’ve had a few problems before with not being able to use esm only packages, whenever I tried to try esm only, there were always a few more problems, the most painful being that some packages were esm only and others were cjs only, always having to choose to drop one side. fuck esm only. the main problems were some cjs only packages, and packages that had to be compatible with typescript/jest/ts-jest/wallaby does not support esm properly. of course, I can choose to look for alternatives to esm only packages, such as globby => fast-glob, remark => markdown-it, node-fetch => node-fetch@2, and lodash-es => lodash, but this is not a permanent option after all, not to mention that some packages are hard to actually find replacements for, such as the remark family.
So, what are the problems with using older versions of packages? The main problem is that it is hard to find the correct version. Of course, if you are using a relatively independent package, such as node-fetch, you can just use the v2 version. But if you are using a project like vuepress/remark where monorepo contains many small packages, you will have a hard time finding the correct version of each subproject.
I recently had to do some conversions from markdown and manipulate ast to html when I was working on the epub generator, so I used remark again and decided to really try using esm. here are some of the steps I tried.
There are several issues that must be addressed to use esm, otherwise it is not possible to use it in a production environment
- typescript support - basically all web projects use ts and it is unacceptable not to support it
- jest support - also a heavily used testing tool
- wallaby support - a paid WYSIWYG testing tool
- Allow references to cjs modules - requires support for existing packages
- Dual module packages can still support both esm/cjs projects - requires support for cjs project references
- Support for unpackaged modules - some private modules in monorepo will not bundle
- esbuild support - esbuild is becoming a lib bundle standard
Modify package declaration
The first step is to modify the module type of the package, modify
"type": "module" to declare the package as esm, all js code will run as esm module by default.
NodeNext is supported since ts4.7, so you need to change tsconfig.json.
Also, importing other ts files within ts files must use the .js suffix.
This is a strange restriction, see ts 4.7 release documentation.
Does it seem strange, but it’s the only way to write it now, and typescript will even prompt for it.
For example, run the following code using the
pnpm jest src/lodash.test.ts command.
An error occurred
Experimental esm support is supported from jest 28 onwards, and wallaby/ts-jest are also supported by configuration, which can be handled by following the steps below.
change the command to
node --experimental-vm-modules node_modules/jest/bin/jest.js src/lodash.test.ts
Configure wallaby (This is where you can run the test files in the
__tests__directory, oddly enough.)
Since the esm import is static, you also need to uninstall
@types/jestand use the
@jest/globalspackage to import functions needed for testing, such as
it/expect/describe/beforeEachand so on.
nodejs has been supporting esm since 14, but the migration has not been smooth until now at 18, mainly due to the following issues.
Importing cjs only modules
Unfortunately, a large number of existing packages are for cjs only modules and it is not possible to migrate them anytime soon, and the interoperability between esm and cjs in nodejs is not very good, so it needs to be handled. Here is an example of fs-extra.
This is how it would normally be written before.
An error occurs when running with tsx
SyntaxError: The requested module 'fs-extra' does not provide an export named 'readdir', this seems to be a known error, refer to: https://github.com/esbuild-kit/tsx/issues/38
Now it needs to be modified as follows.
Or modify it to the following code. Run with
ts-node --esm <file> (this is not supported by tsx).
Yes, you read that right, under the esm module
__dirname is no longer available, it is replaced by
import.meta.url, in short, it is now used in the following way.
Refer to the article https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/, and later, when we talk about esbuild, we will talk about how to handle
import.meta.url in the cjs bundle (which is not supported in cjs, again, it’s a choice between (which is not supported in cjs and is again an option).
lib maintenance and usage
New esm and cjs dual package support configuration
Previously, we differentiated modules by the main/module field.
However, an exception occurs when referencing in an esm project.
The esm project does not recognize this, it has a new exports field, so you need to add (note that the main field still needs to remain compatible with older node versions) the exports field.
Refer to this answer: https://stackoverflow.com/a/7002098
I thought esbuild would be easy because it inherently supports esm, but I actually ran into quite a few problems.
Binding the following code to cjs gives an error.
Here, because cjs cannot contain top-level await, it is modified as follows.
There is no problem with the bundle, but it runs with errors.
First is the first mistake.
It says this is an esm package and the default code is the esm module, if you want it to be executed as a cjs module, you need to change it to the cjs suffix.
Modify the command.
Then there was a second error.
According to the author’s answer in this issue, modify the command as follows.
Unfortunately, this is not in effect and the bundle’s code is as follows.
You can clearly see that the variable name of the injected script is changed from
import_meta_url2, which is oddly problematic.
This takes effect.
What about running the esm bundle?
It also gives an error.
Modify the command according to the solution here.
Now, the code after the bundle can finally run.
Maybe esm only looks good, and tree shaking looks like a great idea, but right now, it’s not even really available in production. This includes a number of important projects that have not been migrated, including react/vscode/electron/vite and so on. In fact, many people (including me) have used esm modules to write code before this, except that the final bundle product might be cjs, e.g. iife in the browser, cjs in nodejs, but the vast majority of application-level developers don’t care about that, only the lib maintainers do, and esm only shifts the complexity of using packages The complexity of using packages is also transferred to the user, and there is no real available solution for referencing esm only packages in cjs. The esm only movement is more of an orgy in the web front-end community than a project like esbuild/vite that solves a real problem.