Joshua's Docs - ES Modules in NodeJS - Troubleshooting Resources

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 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 './src to import {foo} from ./src/index.js
      • Change import {foo} from ./alpha to import {foo} from ./alpha.js
  • 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 index file without specifying the full path
      • Change import {thing} from './utils to import {thing} from ./utils/index.js
  • 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
    • 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.
  • Using NYC / Istanbul for code coverage shows zero percent (0%) coverage, in an ESM code base
  • 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_THING is actually the module, with something like {default: [class THING]}, so you want to use .default to pick off the actual thing itself from the import
  • 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 require and module.exports) to be treated as an ESM file
      • For example, I got this to trigger when I had a ts-node config with moduleTypes: {"**/*.js": "esm"} set, which forced all JS files to be treated as ESM, including one that was really CJS

Augmenting Module Behavior Through CLI Options

  • node flags, node options
    • -r or --require
    • --experimental-specifier-resolution
      • Example: --experimental-specifier-resolution=node
    • --loader
  • ts-node
    • TS_NODE_PROJECT
      • Point to a different tsconfig file
    • TS_NODE_COMPILER_OPTIONS
      • Inline augmentation of TS compiler options
      • Example: TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}'
    • File module-type overrides
  • Mocha

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:

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:

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.ts

Use:

node --loader ts-node/esm ./my-file.ts

Or,

NODE_OPTIONS="--loader ts-node/esm" node ./my-file.ts

If 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.ts

The ts-node package 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 module field
    • If you are getting this with an indirect consumer (e.g., a library that uses ts-node under the hood, such as knex), 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
  • 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.
  • 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"
      	}
      }
Markdown Source Last Updated:
Wed Jul 27 2022 18:26:41 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Sun Nov 14 2021 00:56:35 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback