Skip to content

module: async hooks stop require.extensions from loading CommonJS resolved by registerHooks() #66998

Open

Description

@sebdanielsson

Version

v26.2.0 and later (tested up to v26.12.1). v26.0.0 and v26.1.0 work. v24.22.1 behaves like v26.2.0.

Platform

Darwin 27.0.0 arm64

Subsystem

module

What steps will reproduce the bug?

setup.cjs:

const Module = require('node:module');
// A require hook for .ts files, like the one pirates installs
Module._extensions['.ts'] = (mod, filename) => {
  const src = require('node:fs').readFileSync(filename, 'utf8').replace('TYPESCRIPT', 'transformed');
  mod._compile(src, filename);
};
if (process.env.SYNC) {
  Module.registerHooks({
    resolve(s, c, n) {
      const r = n(s, c);
      return r.url.endsWith('.ts') ? { ...r, format: 'commonjs' } : r;
    },
    load: (u, c, n) => n(u, c),
  });
}
if (process.env.ASYNC) Module.register('./async-hooks.mjs', require('node:url').pathToFileURL(__filename));

async-hooks.mjs:

export async function resolve(specifier, context, nextResolve) {
  return nextResolve(specifier, context);
}
export async function load(url, context, nextLoad) {
  return nextLoad(url, context);
}

main.cjs:

import('./x.ts').then(m => console.log(m.default), e => console.log('ERR', e.message));

x.ts:

module.exports = "TYPESCRIPT";

package.json:

{ "name": "m" }
$ SYNC=1 node --require ./setup.cjs main.cjs
transformed
$ SYNC=1 ASYNC=1 node --require ./setup.cjs main.cjs
TYPESCRIPT

How often does it reproduce? Is there a required condition?

Always, when a module.registerHooks() resolve hook returns the commonjs format for a file, and any module.register() hooks are also registered, even pass-through ones.

What is the expected behavior? Why is that the expected behavior?

transformed in both cases, as in v26.0.0 and v26.1.0. Adding a pass-through hook shouldn't change how the module is loaded. Without it, the CommonJS module is loaded through require.extensions, so the .ts handler runs.

What do you see instead?

With the pass-through hook, the source is compiled as it is, and the .ts handler in require.extensions is never called. With real TypeScript in the file, the import fails with a SyntaxError.

Additional information

The behavior changed in v26.2.0. That release includes #62920 ("module: fix sync hook short-circuit in require() in imported CJS"), which may be related.

I hit this while moving the Backstage CLI's TypeScript transform from module.register() to module.registerHooks() because of DEP0205. Tools such as import-in-the-middle, which OpenTelemetry uses for ESM, still register their hooks with module.register(), so both kinds of hooks end up registered in the same process.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions