The issue is that Node.js has its own punycode module, which is deprecated starting with Node.js 22. The recommendation is to use a replacement punycode module from npm instead, which is not maintained by the Node.js crew. The most prominent replacement punycode package on npm is called punycode as well, and it will be pulled into a Node.js project, if packages.json contains a dependency on punycode. This mean any package that depends on punycode tries do the right thing. This does not mean, every package that depends on punycode actually does the right thing.
npm ls punycode, as suggested by some answers, lists packages with a declared dependency on "the userland-provided Punycode.js module", so it will find all packages that already comply with the migration path for the deprecation of the punycode module as supplied with Node.js. On the other hand, packages that just rely on a module named punycode being unconditionally available, because that module is part of the core Node.js runtime environment (which is the punycode module that is deprecated) are not found by this command.
The next key point is the behaviour of require("punycode"). This statements imports the "first module named punycode the JavaScript runtime stumbles upon". Most prominently, as the deprecated punycode module provided with Node.js still is present, it will be found first, even if a punycode package from npm is installed as well. So depending on punycode in pacakges.json only does half of what you need to do, because that dependency will just cause the punycode package to be downloaded, but not to be used instead of the deprecated punycode module. The require statement needs to be re-written in a way that it is unable to pick up the integrated punycode module and thus needs to resort to the punycode package inside node_modules. The recommended way of doing so is replacing require("punycode") by require("punycode/").
In my case, the issue was caused indirectly by jest-environment-jsdom which depends on jsdom: "^20.0.0" in the latest "stable" version 29.7.0. This dependency resolves to the latest 20.x version of jsdom, which is 20.0.3. Its packages.json file references whatwg-url: "^11.0.0", which will resolve to 11.0.0, as this is the only 11.x version of whatwg-url ever released. That package then references tr46: "^3.0.0", which resolves to version 3.0.0 of that package, which is a known offender that has already been mentioned in other answers.
Now let's look deeper into this package, though: In packages.json, the required dependency to download the "userland" aka "3rd party" punycode package is present, which means this package tries to not use the built-in punycode support. Nevertheless, it uses the require statement that will pick up the integrated punycode support, as long as that one is shipped with Node.js. The require statement has been fixed with version 4 of tr46, and seems to be the only significant change between version 3.0.0 and 4.0.0 except for bumping dependency versions.
So what does it mean for readers of this question? For me, the take-aways are:
- We do not need to get rid of dependencies on
punycode.
- We do not get clear indications on the root cause of the issue by running
npm ls punycode.
- Bumping dependencies helps, but this requires that bumped dependencies exist. For good reason, dependencies are locked to not apply to newer major versions. If any one package in the chain
jest-environment-jsdom@latest (29.7.0 at the time of writing this post) -> jsdom@20.* (20.0.3 at the time of writing this post) -> whatwg-url@11.* (11.0.0 at the time of writing this post) would issue a patch or minor release that slightly bumps dependencies, we would get [email protected] or newer which fixes the issue (although for interop reasons, bumping the dependencies to a new major version is likely not allowed in a minor or patch version according to semver). Having a [email protected] with the punycode fix would help as well, and most likely be the smartest solution possible. Makes me wonder whether you can npm link a "private fork" of tr46 at a self-deployed 3.0.1.
npm --node-options=--trace-deprecation run test did help me find the issue. (see below this list)
- Searching for
require("punycode") or require('punycode') inside node_modules is also a way to find violators.
- Maybe praying also helps, because if a Node.js version comes along that no longer ships a
punycode module, require("punycode") will no longer use that module instead of a "userland" module.
- Finally, the easiest short term solution is adding overrides for modules that contain the deprecated
require("punycode") statement. For example, to work around the issue in [email protected], I added "overrides": { "[email protected]": "4.0.0"} to packages.json. As I understand it, although the major version has been bumped, it should work just fine in dependency chains created for major version 3.
(node:....) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead,
at node:punycode:3:9
at BuiltinModule.compileForInteralLoader (node:internal/bootstrap/ream:399:7)
....
at require (node:internal/modules/helpers:141:16)
at Object.<anonymous> (<workdir>/node_modules/tr46/index.js:3:18)
Note that the whole stack trace is just the internal logic in Node.js that processes the require statement in tr46/index.js and doesn't provide any value in diagnosis, just the lowest line points to the issue.
const punycode = require('punycode/');?import punycode from "punycode/";for example.