I'm going to just say it - the transition to ESM (ES Modules) in NodeJS has not been easy or pain-free for most developers, and continues to be a sore spot for many areas of NodeJS development, at least for now.
This page is my attempt at consolidating a lot of the issues that I have personally ran into trying to build and consume ESM code in NodeJS (particular with libraries / packages).
Required Reading / Resources
- NodeJS Docs: ESM API
- Sindre Sorhus:
- "Pure ESM Package" (good cheatsheet / FAQ for lib authors)
- "Get Ready for ESM"
- Github Discussion - "The ESM move"
- Gil Tayar: "Using ES Modules in Node.js" (part 1, part 2, and part 3)
- The Guild Blog: "What Does It Take to Support Node.js ESM"
- David Herron: ESM to CJS (guide on building ESM package that is consumable in CJS land)
Related Tooling and Libraries
standard-things/esmpackage- This is an ESM loader, which lives outside of Node, and lets you back-port support to older versions, as well as do some other things
esmo(underantfu/esno)- Alias to
tsx. Node runtime powered by esbuild, supports TS and ESM
- Alias to
- See my tooling section in my TS notes, and my NodeJS notes
NodeJS Built-Ins and Global Objects
Switching from CJS to ESM actually changes how some NodeJS built-ins function: __dirname and __filename. This is because these are not true globals, but rather injected variables provided by the module wrapper enclosure.
These will no longer be available when switching to ESM, but there are replacements you can use:
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));Detecting if a file was run via the CLI, with require.main === module also no longer works with ESM, for the same reason. An alternative (comparing process.argv[1] with the filename) is outlined here, or you can use the es-main package they link to.
Assorted Errors and Issues
Some of these might be specific to
ts-node.
node-esm-resolve-implementation.... Error: ERR_MODULE_NOT_FOUND- Make sure all imports include file and extension
- Change
import {foo} from './srctoimport {foo} from ./src/index.js - Change
import {foo} from ./alphatoimport {foo} from ./alpha.js
- Change
- Make sure all imports include file and extension
Error: Cannot find module '.../MyFile' imported by [...]- If you are using the ESM loader, you need to specify the file extension (see error above this one)
- This includes imports from third party libraries under
node_modules, if you are drilling down into the file structure
ERR_UNSUPPORTED_DIR_IMPORT- Same as above - make sure all imports include file and extension.
- This throws specifically when trying to import via an
indexfile without specifying the full path- Change
import {thing} from './utilstoimport {thing} from ./utils/index.js
- Change
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module- This can be caused be a few different things; a common cause being a dependency that is an ESM-only module (aka a Pure ESM Package), such as
node-fetch- A pure ESM package kind of poisons your project
- The main fix is to convert your project to ESM. This guide covers how to do that (for both regular JS and TS), as well as some other options.
- This can be caused be a few different things; a common cause being a dependency that is an ESM-only module (aka a Pure ESM Package), such as
- Using NYC / Istanbul for code coverage shows zero percent (0%) coverage, in an ESM code base
- ESM is currently not supported with NYC, I would recommend using
c8instead
- ESM is currently not supported with NYC, I would recommend using
Error: ERR_INVALID_MODULE_SPECIFIER ___ is not a valid package name- If you are using TypeScript, this is probably a tsconfig path alias + ESM issue: reference the TypeScript section below for troubleshooting ideas
TypeError: IMPORTED_THING is not a constructor- Can be hard to pin down, but often a case where
IMPORTED_THINGis actually the module, with something like{default: [class THING]}, so you want to use.defaultto pick off the actual thing itself from the import
- Can be hard to pin down, but often a case where
SyntaxError: The requested module '../my-file.js' does not provide an export named 'default'- Check to make sure that you aren't accidentally forcing a CommonJS/CJS file (with
requireandmodule.exports) to be treated as an ESM file- For example, I got this to trigger when I had a
ts-nodeconfig withmoduleTypes: {"**/*.js": "esm"}set, which forced all JS files to be treated as ESM, including one that was really CJS
- For example, I got this to trigger when I had a
- Check to make sure that you aren't accidentally forcing a CommonJS/CJS file (with
Augmenting Module Behavior Through CLI Options
- node flags, node options
-ror--require--experimental-specifier-resolution- Example:
--experimental-specifier-resolution=node
- Example:
--loader
- ts-node
TS_NODE_PROJECT- Point to a different
tsconfigfile
- Point to a different
TS_NODE_COMPILER_OPTIONS- Inline augmentation of TS compiler options
- Example:
TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}'
- File module-type overrides
- Mocha
- You can put node options as top-level key-pairs in your
.mocharc.jsonconfig file, like"loader": "ts-node/esm"
- You can put node options as top-level key-pairs in your
Next.js ESM
Next.js now mostly supports ESM, including the config file (in v12+). To use, rename your config file to next.config.mjs, and set "type": "module" in your package.json.
If you get an error like SyntaxError: Named export '___' not found. The requested module './___.js' is a CommonJS module [...], check to make sure that you have set "type": "module" in your package.json file.
TypeScript and ESM
TypeScript and ESM - Resources
General resources for TS + ESM:
- @ddprrt: TypeScript and ECMAScript Modules
- Rauschmayer: TypeScript and Native ESM on Node.js
- My TypeScript cheatsheet has a few sections that overlap with this area
resolve-typescript-plugin: Webpack plugin that fixes.jsimports in TS files in an ESM project
TypeScript - Fully Resolved Import Filepaths with Extensions
Full import filepaths are a sore spot within the ESM ecosystem, and especially with TS tooling.
Both NodeJS and the browser make it MANDATORY that file imports include the .js extension:
// ESM - FAIL!
import { MyComponent } from './MyComponent';
// ESM - OK, this works
import { MyComponent } from './MyComponent.js'The biggest issue with TypeScript and this requirement is that TypeScript will not automatically add the extension for your, even if you tell TSC that you are targeting ESM output. Furthermore, the TypeScript team has made it clear that this is by design, and not something they are willing to consider (see #16577 and #42151).
Unfortunately, even though fully resolved import paths are required for ESM, they can also break some different systems:
- Next.js
- CJS compatibility (TypeScript)
- If you have a set of files that you are trying to write as both CJS and ESM, you might run into issues here, especially with TypeScript
- As a side-note, if you are trying to author a hybrid / dual package (library that is released with both CJS and ESM exports) what you typically want to do is write your code using only one module pattern (ideally ESM), and then use build tooling to transpile to both module types
- For more details, see my "Building an NPM Package" page.
- Trying to import with CJS or MJS extension: You might run into the
cannot find module '../my-file.cjs' or its corresponding type declarationsfor a CJS or MJS import in a TS file (or JS file, if you have checkJS on)- This has been fixed in a new (as of writing this) release of TS,
v4.5, but requires the use ofnode12ornodenextfor themoduleResolutionoption, which is further restricted to just thebetaornightlyreleases (for now)- See TS Issue #38784, closed by PR #45884
- This has been fixed in a new (as of writing this) release of TS,
TS-Node and ESM
ESM support is currently a WIP with
ts-node. Main progress tracker is issue #1007 - please check that for most up-to-date reference.
TS-Node: ESM Loader
Many issues with ESM and ts-node can be fixed by using the correct loader.
If you have the newest version of TS-Node (>= v10.7.0), you can now use any of these three consolidated approaches to load ESM:
Run
ts-node-esm,ts-node --esm, or add"ts-node": {"esm": true}to your tsconfig.json.[source]
If you are using an older version, try these approaches:
Instead of using
ts-node ./my-file.tsUse:
node --loader ts-node/esm ./my-file.tsOr,
NODE_OPTIONS="--loader ts-node/esm" node ./my-file.tsIf you want to pass options to ts-node, you can do so via env variables (see docs for options and env strings to use). However, there is one exception; for --transpile-only, there is an ESM loader available to use directly:
node --loader ts-node/esm/transpile-only ./my-file.tsThe
ts-nodepackage does not need to be installed globally for the above loader commands to work.
TS-Node and General TypeScript + ESM Troubleshooting
- TSConfig path aliases don't seem to work with ESM
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for {FILEPATH}.ts- If you are using ESM, use the specific ESM loader, as outlined above
- If you are trying to use CJS, make sure that your TSConfig is set appropriately for the
modulefield - If you are getting this with an indirect consumer (e.g., a library that uses
ts-nodeunder the hood, such asknex), you might need to force it to use the ESM loader via environmental variables. E.gl:NODE_OPTIONS="--loader ts-node/esm" node my-cmd ...(bash syntax)
TypeError: ERR_UNKNOWN_FILE_EXTENSION is not a constructor- This is probably an error caused by importing JSON. Normally, this works fine in TS with
resolveJsonModule: true, but there are currently issues with combining this with ESM features - see this comment and TS Issue #46362- This should be fixed very soon
- This is probably an error caused by importing JSON. Normally, this works fine in TS with
TypeError: ERR_INVALID_MODULE_SPECIFIER: Invalid Module "file://[...].ts"- Check if you are using
experimental-specifier-resolution, but with CommonJS / CJS instead of ESM. This flag will cause an override.
- Check if you are using
ReferenceError: exports is not defined in ES module scope- You probably have the TS module type set to
commonjs. - Try using:
{ "compilerOptions": { "module": "ES2020", // Recommended "moduleResolution": "node" } }
- You probably have the TS module type set to