TypeScript

require() of ES Module string-width: fix yarn.lock in Docker

Why yarn install in a node:18 container dies in node-gyp with ERR_REQUIRE_ESM from wide-align, how to spot the broken yarn.lock entry, and two fixes reproduced in Docker.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

The require() that fails is not in your code. wide-align (pulled in by gauge, npmlog and a project-local node-gyp) does require('string-width') and accepts versions 1 to 4. string-width 5 is an ES module. The broken install puts version 5 at the top of node_modules, with no copy of version 4 for wide-align to find, The trigger documented in yarnpkg/yarn#8994 is a Yarn 1 yarn.lock in which the alias string-width-cjs@npm:string-width@^4.2.0 shares one entry with the ordinary string-width 4 ranges. Installing from a lockfile hand-edited into that shape reproduced the error here: Yarn skips unpacking string-width 4 and hoists 5. In the test, Yarn never wrote that merged entry by itself. Regenerate yarn.lock and commit it, or add "resolutions": { "string-width": "4.2.3" } to package.json. The asker tried other Node versions and base images without success.

The error#

The Stack Overflow question builds an Angular project with a three-line Dockerfile, FROM node:18.18, COPY ./ ./, then RUN yarn install, and the install stops while @parcel/watcher runs its install script:

text
error /node_modules/@parcel/watcher: Command failed.
Exit code: 1
Command: node-gyp-build
Arguments:
Directory: /node_modules/@parcel/watcher
Output:
/node_modules/wide-align/align.js:2
var stringWidth = require('string-width')
                  ^
 
Error [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/string-width/index.js from /node_modules/wide-align/align.js not supported.
Instead change the require of index.js in /node_modules/wide-align/align.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/node_modules/wide-align/align.js:2:19)
    at Object.<anonymous> (/node_modules/gauge/lib/render-template.js:2:13)
    at Object.<anonymous> (/node_modules/gauge/lib/plumbing.js:3:22)
    at Object.<anonymous> (/node_modules/gauge/lib/index.js:2:16)
    at Object.<anonymous> (/node_modules/npmlog/lib/log.js:3:13)
    at Object.<anonymous> (/node_modules/node-gyp/lib/node-gyp.js:5:13)
    at Object.<anonymous> (/node_modules/node-gyp/bin/node-gyp.js:8:13) {
  code: 'ERR_REQUIRE_ESM'
}

Read the stack from the bottom up: node-gyp loads npmlog, which loads gauge, which loads wide-align, which calls require('string-width'). The message tells you to switch that call to import(), but the file is inside node_modules and belongs to a package you do not control, so that advice does not apply. The asker also tried other Node versions and the bitnami/node image, and the error stayed.

Why it happens#

Three facts combine.

wide-align is CommonJS and wants string-width 4 or older. Its source starts with var stringWidth = require('string-width'), and its latest release on the npm registry, 1.1.5, declares the range ^1.0.2 || 2 || 3 || 4.

string-width 5 is an ES module. Its package.json at 5.1.2 sets "type": "module", and its entry point uses export default. On Node 18, require() refuses such a file and throws ERR_REQUIRE_ESM.

Some packages want both majors. @isaacs/cliui 8.0.2 depends on string-width ^5.1.2 and also on string-width-cjs, an npm alias for string-width@^4.2.0. When the tree also contains wide-align, Yarn has to place version 4 next to wide-align and version 5 where @isaacs/cliui can reach it.

The open issue yarnpkg/yarn#8994 shows the lockfile shape that breaks this: one entry starting "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", ... that serves the alias and the plain ranges together. Fed that lockfile, Yarn 1 decides the plain string-width patterns would unpack into the same cache folder as the alias, skips them, and leaves version 5 as the only string-width it hoists. The same failure is reported against wide-align in iarna/wide-align#63, which is where the accepted answer points. Both issues are still open.

Why node-gyp runs at all: @parcel/watcher has node-gyp-build as its install script. The current node-gyp-build source tests for a usable prebuilt binary first. If there is none, it falls back to node-gyp rebuild, and it prefers a node-gyp it can resolve from the project. That project-local node-gyp is what loads npmlog, gauge and wide-align in the trace.

Reproduce it in a container#

This was reproduced on 21 October 2026 in the official node:18-slim image, which ships Node 18.20.8 and Yarn 1.22.22 (the asker used node:18.18). The project had two dependencies, @isaacs/cliui 8.0.2 and wide-align 1.1.5. A fresh install, and a yarn add on top of an existing lockfile, both wrote the alias as a separate entry and worked. The reproduction never saw Yarn write the merged entry by itself, and the issue does not say how it gets there. So the lockfile was edited by hand into the shape quoted in #8994:

text
71,80c71
< "string-width-cjs@npm:string-width@^4.2.0":
<   version "4.2.3"
<   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
<   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
<   dependencies:
<     emoji-regex "^8.0.0"
<     is-fullwidth-code-point "^3.0.0"
<     strip-ansi "^6.0.1"
< 
< "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0:
---
> "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0:

yarn install against that lockfile succeeds, but prints two warnings:

text
warning Pattern ["string-width@^4.1.0"] is trying to unpack in the same destination "/usr/local/share/.cache/yarn/v6/npm-string-width-cjs-4.2.3-269c7117d27b05ad2e536830a8ec895ef9c6d010-integrity/node_modules/string-width-cjs" as pattern ["string-width-cjs@npm:string-width@^4.2.0"]. This could result in non-deterministic behavior, skipping.
warning Pattern ["string-width@^1.0.2 || 2 || 3 || 4"] is trying to unpack in the same destination "/usr/local/share/.cache/yarn/v6/npm-string-width-cjs-4.2.3-269c7117d27b05ad2e536830a8ec895ef9c6d010-integrity/node_modules/string-width-cjs" as pattern ["string-width-cjs@npm:string-width@^4.2.0"]. This could result in non-deterministic behavior, skipping.

Loading wide-align afterwards gives the asker's error, with /app as the project root:

text
/app/node_modules/wide-align/align.js:2
var stringWidth = require('string-width')
                  ^
 
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/string-width/index.js from /app/node_modules/wide-align/align.js not supported.

node_modules/wide-align has no nested node_modules folder, so the only string-width it can reach is the hoisted version 5.

Confirm your lockfile has the merged entry#

Two commands tell you whether this is your case. First, look at the alias line in yarn.lock. In the broken file it shares a line with the plain ranges:

bash
grep -n 'string-width-cjs@npm' yarn.lock
text
71:"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0:

In a healthy lockfile the same command prints the alias alone:

text
71:"string-width-cjs@npm:string-width@^4.2.0":

Second, ask Yarn which string-width it installed. In the broken install it finds only version 5, and nothing mentions wide-align:

bash
yarn why string-width
text
info => Found "string-width@5.1.2"
info Reasons this module exists
   - "@isaacs#cliui" depends on it
   - Hoisted from "@isaacs#cliui#string-width"
   - Hoisted from "@isaacs#cliui#wrap-ansi#string-width"

Check the version with grep '"version"' node_modules/string-width/package.json, not with node -p "require('string-width/package.json').version". Version 5 has an exports map that does not list ./package.json, so that one-liner fails with a second error, ERR_PACKAGE_PATH_NOT_EXPORTED. If you hit it, the ERR_PACKAGE_PATH_NOT_EXPORTED page explains the exports map.

Fix#

1. Regenerate yarn.lock and commit it#

The accepted answer's workaround is to remove yarn.lock. The durable version is to regenerate it once and commit the result, not to keep the file out of the image. Run this on your machine or in a throwaway container with the same Yarn 1:

bash
rm yarn.lock
rm -rf node_modules
yarn install
grep -n 'string-width-cjs@npm' yarn.lock

From the broken lockfile above, the regenerated file wrote the alias as its own entry again, string-width 4.2.3 came back to the top of node_modules, and node -e "require('wide-align')" loaded. The cost is that every transitive dependency moves to the newest version its range allows, so run your lint and tests before you commit. In yarnpkg/yarn#8994, one user reports that deleting the lockfile helped but the error came back later, so keep the grep check above in mind after future upgrades.

2. Pin string-width with resolutions#

If you cannot regenerate the whole lockfile now, force a single version, as the accepted answer suggests. Yarn 1 reads selective version resolutions from package.json:

json
{
  "resolutions": {
    "string-width": "4.2.3"
  }
}

Merge that key into your existing package.json, then run yarn install. From the same broken lockfile it printed:

text
warning Resolution field "string-width@4.2.3" is incompatible with requested version "string-width@^5.1.2"
warning Resolution field "string-width@4.2.3" is incompatible with requested version "string-width@^5.0.1"
success Saved lockfile.

Those warnings are expected. The pin applies to every string-width in the tree, including the ones that ask for 5, and the Yarn docs say a resolution that is not compatible with the original range produces a warning. In the reproduction @isaacs/cliui still rendered text with 4.2.3, but that is one small check, not a guarantee for every package in your tree. Treat the pin as temporary and remove it once the lockfile is clean.

The other answers on the question are untested here. One moves the project to Yarn 3.6.4. Another pins jackspeak to 2.1.1. On the npm registry, jackspeak 2.1.1 depends on cliui, while the 2.2.0 release and later depend on @isaacs/cliui, the package that brings the alias. The accepted answer gives "overrides" as the npm equivalent of resolutions.

Verify the fix#

Inside the image, check that wide-align can load and which string-width sits at the top:

bash
grep '"version"' node_modules/string-width/package.json
node -e "require('wide-align'); console.log('wide-align loaded')"
text
	"version": "4.2.3",
wide-align loaded

Then rebuild the image without cache so the install step actually runs again:

bash
docker build --no-cache -t angular-lint .

For comparison, npm does not trip over this project. With no lockfile, npm install hoisted string-width 5.1.2 to the top and nested 4.2.3 under wide-align:

text
node_modules/string-width/package.json:	"version": "5.1.2",
node_modules/wide-align/node_modules/string-width/package.json:	"version": "4.2.3",

That is the layout Yarn should have produced. It confirms the fault is in the lockfile, not in wide-align or Node 18.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

On Node 22 the error changes to a TypeError#

Upgrading the base image looks like an escape route, because the Node versions released after the question was asked can require() an ES module. The Node.js modules documentation says this no longer needs a flag from v22.12.0, and adds: "The result returned by require() is the module namespace object, which places the default export in the .default property". string-width 5 exposes its function only as the default export, so wide-align gets an object where it expects a function.

With the same broken lockfile on node:22-slim (Node 22.23.3, Yarn 1.22.22), require('wide-align') loads without complaint, and the first call fails:

text
/app/node_modules/wide-align/align.js:56
  var strWidth = stringWidth(trimmed)
                 ^
 
TypeError: stringWidth is not a function
    at Object.alignCenter [as center] (/app/node_modules/wide-align/align.js:56:18)

The failure moves from the require() to the first call. Whether the node-gyp install path ever reaches that call was not tested, so a newer image may or may not get the install through, and it does not repair the tree. If a Yarn 1 project on Node 22 or later reports stringWidth is not a function from wide-align, it is likely the same hoisting problem: check with the grep above, then apply the same two fixes.

Check your own module error#

If your ERR_REQUIRE_ESM comes from your own code rather than from node_modules, put the full message and its first at lines into the explainer below to see which file Node loaded as CommonJS and which as an ES module. It runs in your browser, and nothing is sent anywhere.

Runs entirely in your browser — nothing you paste is sent anywhere. Tokens and passwords that appear in paths or URLs are masked in the output.

Optional: package.json, file extension, Node.js version (sharper advice)
Try:

For an ESM-only package that your own require() loads, the gulp ERR_REQUIRE_ESM page walks through the pin-or-migrate choice with gulp-imagemin. If the error is the reverse, ES module code that meets a CommonJS global, see exports is not defined in ES module scope. For other installs that work on the host and fail in a container, the esbuild-linux-64 page covers a host node_modules leaking into a Linux image through a bind mount, and Next.js builds locally, fails in Docker covers next build crashing in the container on a statically imported image until sharp is added.

The #8994 thread is also the best evidence on the "works locally, fails in the container" part of the question: one user reports that yarn upgrade fixed the problem locally while their CI machine kept failing, and another fixed a NestJS Docker build by not copying yarn.lock into the image. Neither the thread nor the question shows exactly why the host install survived. What the reproduction does show is the end state to aim for: once yarn.lock has separate entries for string-width-cjs and string-width, a clean yarn install puts string-width 4.2.3 where wide-align can load it. The reproduction project had no @parcel/watcher, no node-gyp build and no lint step, so whether your full docker build then gets through yarn lint is not tested here.

Further reading: Fix print "%s.%s.%s" invalid syntax in yarn install. Also see Fix RangeError: Invalid time zone specified in Node.js.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote