Repository: nodejs/node
Pull request: module: runtime deprecate `module.register()`

Building on #62395, this runtime-deprecates `module.register()`.

I’m not sure when we want to land and release this; I think the options are basically either:

1. When `module.registerHooks()` goes stable, hopefully sometime before 26 goes LTS; https://github.com/nodejs/node/pull/62395#issuecomment-4107156571
2. In 26.0.0; the doc-deprecation is backported to the 25.x line, and the runtime deprecation is 26+; https://github.com/nodejs/node/pull/62395#issuecomment-4107486442

I’m opening this now in case the team wants to do the second option. cc @nodejs/loaders @nodejs/userland-migrations 

Changed files:
filename: doc/api/deprecations.md
patch: @@ -4527,16 +4527,27 @@ deprecated and will throw an error in a future version.
 
 <!-- YAML
 changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/62401
+    description: Runtime deprecation.
   - version: REPLACEME
     pr-url: https://github.com/nodejs/node/pull/62395
     description: Documentation-only deprecation.
 -->
 
-Type: Documentation-only
+Type: Runtime
 
 [`module.register()`][] is deprecated. Use [`module.registerHooks()`][]
 instead.
 
+The `module.register()` API provides off-thread async hooks for customizing ES modules;
+the `module.registerHooks()` API provides similar hooks that are synchronous, in-thread, and
+work for all types of modules.
+Supporting async hooks has proven to be complex, involving worker threads orchestration, and there are issues
+that have proven unresolveable. See [caveats of asynchronous customization hooks][]. Please migrate to
+`module.registerHooks()` as soon as possible as `module.register()` will be
+removed in a future version of Node.js.
+
 [DEP0142]: #dep0142-repl_builtinlibs
 [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
 [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -4696,6 +4707,7 @@ instead.
 [`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
 [alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
 [alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
+[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
 [from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
 [from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding
 [legacy URL API]: url.md#legacy-url-api

filename: doc/api/module.md
patch: @@ -180,6 +180,9 @@ added:
   - v18.19.0
 deprecated: REPLACEME
 changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/62401
+    description: Runtime deprecation (DEP0205).
   - version: REPLACEME
     pr-url: https://github.com/nodejs/node/pull/62395
     description: Documentation-only deprecation (DEP0205). Use

filename: lib/internal/modules/esm/loader.js
patch: @@ -31,7 +31,10 @@ const {
 } = require('internal/errors').codes;
 const { getOptionValue } = require('internal/options');
 const { isURL, pathToFileURL } = require('internal/url');
-const { kEmptyObject } = require('internal/util');
+const {
+  getDeprecationWarningEmitter,
+  kEmptyObject,
+} = require('internal/util');
 const {
   compileSourceTextModule,
   SourceTextModuleTypes: { kUser },
@@ -955,7 +958,16 @@ function isCascadedLoaderInitialized() {
  * });
  * ```
  */
+const emitRegisterDeprecation = getDeprecationWarningEmitter(
+  'DEP0205',
+  '`module.register()` is deprecated. Use `module.registerHooks()` instead.',
+  undefined,
+  false,
+);
+
 function register(specifier, parentURL = undefined, options) {
+  emitRegisterDeprecation();
+
   if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) {
     options = parentURL;
     parentURL = options.parentURL;

filename: test/es-module/test-esm-register-deprecation.mjs
patch: @@ -0,0 +1,60 @@
+import { spawnPromisified } from '../common/index.mjs';
+import * as fixtures from '../common/fixtures.mjs';
+
+import assert from 'node:assert';
+import { execPath } from 'node:process';
+import { describe, it } from 'node:test';
+
+const urlToRegister = fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs');
+const urlToRegisterEscaped = JSON.stringify(urlToRegister.href);
+
+
+describe('module.register() deprecation (DEP0205)', { concurrency: !process.env.TEST_PARALLEL }, () => {
+  it('emits DEP0205 when module.register() is called', async () => {
+    const { code, stderr } = await spawnPromisified(execPath, [
+      '--input-type=module',
+      '--eval',
+      `import { register } from 'node:module'; register(${urlToRegisterEscaped});`,
+    ]);
+
+    assert.match(stderr, /\[DEP0205\]/);
+    assert.strictEqual(code, 0);
+  });
+
+  it('only emits the warning once per process', async () => {
+    const { code, stderr } = await spawnPromisified(execPath, [
+      '--input-type=module',
+      '--eval',
+      `import { register } from 'node:module';
+       register(${urlToRegisterEscaped});
+       register(${urlToRegisterEscaped});`,
+    ]);
+
+    assert.strictEqual(stderr.split('[DEP0205]').length - 1, 1);
+    assert.strictEqual(code, 0);
+  });
+
+  it('does not emit when --no-deprecation is set', async () => {
+    const { code, stderr } = await spawnPromisified(execPath, [
+      '--no-deprecation',
+      '--input-type=module',
+      '--eval',
+      `import { register } from 'node:module'; register(${urlToRegisterEscaped});`,
+    ]);
+
+    assert.doesNotMatch(stderr, /DEP0205/);
+    assert.strictEqual(code, 0);
+  });
+
+  it('throws when --throw-deprecation is set', async () => {
+    const { code, stderr } = await spawnPromisified(execPath, [
+      '--throw-deprecation',
+      '--input-type=module',
+      '--eval',
+      `import { register } from 'node:module'; register(${urlToRegisterEscaped});`,
+    ]);
+
+    assert.match(stderr, /DEP0205/);
+    assert.notStrictEqual(code, 0);
+  });
+});

filename: test/module-hooks/test-async-loader-hooks-use-hooks-require-esm.mjs
patch: @@ -7,6 +7,7 @@ import { spawnSyncAndAssert } from '../common/child_process.js';
 spawnSyncAndAssert(
   execPath,
   [
+    '--no-deprecation',
     '--no-experimental-require-module',
     '--import',
     fixtures.fileURL('es-module-loaders/builtin-named-exports.mjs'),