Skip to content

Commit 2c2bb6c

Browse files
esm: add deregister method
Suggestion from @GeoffreyBooth. Adds `deregister` method on `node:module` that looks like this: ```ts type Deregister = (id: string) => boolean; ``` Modifies the initialize hook to look like this: ```ts type Initialize = (data: any, meta: {id: opaque}) => Promise<any>; ``` Internally registered instances of hooks are now tracked. This is so they can be removed later. The id of the registered instance is now passed to the `initialize` hook which can then be passed back to the caller of `register`. ```js // Loader export const initialize = (_data, meta) => { return meta.id; } ``` ```js // Caller import {register, deregister} from "node:module"; const id = register(...); // ... deregister(id); ```
1 parent de4553f commit 2c2bb6c

File tree

5 files changed

+120
-2
lines changed

5 files changed

+120
-2
lines changed

lib/internal/modules/esm/hooks.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeFindIndex,
45
ArrayPrototypePush,
56
ArrayPrototypePushApply,
67
FunctionPrototypeCall,
@@ -121,6 +122,9 @@ class Hooks {
121122
],
122123
};
123124

125+
#lastInstanceId = 0;
126+
#loaderInstances = [];
127+
124128
// Cache URLs we've already validated to avoid repeated validation
125129
#validatedUrls = new SafeSet();
126130

@@ -143,6 +147,10 @@ class Hooks {
143147
return this.addCustomLoader(urlOrSpecifier, keyedExports, data);
144148
}
145149

150+
deregister(id) {
151+
return this.removeCustomLoader(id);
152+
}
153+
146154
/**
147155
* Collect custom/user-defined module loader hook(s).
148156
* After all hooks have been collected, the global preload hook(s) must be initialized.
@@ -160,6 +168,15 @@ class Hooks {
160168
load,
161169
} = pluckHooks(exports);
162170

171+
const instance = {
172+
__proto__: null,
173+
id: this.#lastInstanceId++,
174+
globalPreload,
175+
initialize,
176+
resolve,
177+
load,
178+
};
179+
163180
if (globalPreload && !initialize) {
164181
emitExperimentalWarning(
165182
'`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`',
@@ -174,7 +191,46 @@ class Hooks {
174191
const next = this.#chains.load[this.#chains.load.length - 1];
175192
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
176193
}
177-
return initialize?.(data);
194+
195+
ArrayPrototypePush(this.#loaderInstances, instance);
196+
return initialize?.(data, { __proto__: null, id: instance.id });
197+
}
198+
199+
#removeFromChain(chain, target) {
200+
for (let i = 0; i < chain.length; ++i) {
201+
if (target === chain[i+1]?.fn) {
202+
chain.splice(i+1, 1);
203+
}
204+
if (chain[i].next) {
205+
chain[i].next = chain[i+1];
206+
}
207+
}
208+
}
209+
210+
removeCustomLoader(id) {
211+
const index = ArrayPrototypeFindIndex(this.#loaderInstances, (target) => {
212+
return target.id === id;
213+
});
214+
if (index < 0) {
215+
return false;
216+
}
217+
const instance = this.#loaderInstances[index];
218+
if (instance.globalPreload) {
219+
const index = ArrayPrototypeFindIndex(this.#chains.globalPreload, (x) => {
220+
return x.fn === instance.globalPreload;
221+
});
222+
if (index >= 0) {
223+
this.#chains.globalPreload.splice(index, 1);
224+
}
225+
}
226+
if (instance.resolve) {
227+
this.#removeFromChain(this.#chains.resolve, instance.resolve);
228+
}
229+
if (instance.load) {
230+
this.#removeFromChain(this.#chains.load, instance.load);
231+
}
232+
this.#loaderInstances.splice(index, 1);
233+
return true;
178234
}
179235

180236
/**

lib/internal/modules/esm/loader.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,17 @@ class ModuleLoader {
323323
return this.#customizations.register(specifier, parentURL, data, transferList);
324324
}
325325

326+
deregister(id) {
327+
if (!this.#customizations) {
328+
// `CustomizedModuleLoader` is defined at the bottom of this file and
329+
// available well before this line is ever invoked. This is here in
330+
// order to preserve the git diff instead of moving the class.
331+
// eslint-disable-next-line no-use-before-define
332+
this.setCustomizations(new CustomizedModuleLoader());
333+
}
334+
return this.#customizations.deregister(id);
335+
}
336+
326337
/**
327338
* Resolve the location of the module.
328339
* @param {string} originalSpecifier The specified URL path of the module to
@@ -458,6 +469,10 @@ class CustomizedModuleLoader {
458469
return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
459470
}
460471

472+
deregister(id) {
473+
return hooksProxy.makeSyncRequest('deregister', undefined, id);
474+
}
475+
461476
/**
462477
* Resolve the location of the module.
463478
* @param {string} originalSpecifier The specified URL path of the module to
@@ -582,8 +597,14 @@ function register(specifier, parentURL = undefined, options) {
582597
);
583598
}
584599

600+
function deregister(id) {
601+
const moduleLoader = require('internal/process/esm_loader').esmLoader;
602+
return moduleLoader.deregister(id);
603+
}
604+
585605
module.exports = {
586606
createModuleLoader,
587607
getHooksProxy,
588608
register,
609+
deregister,
589610
};

lib/module.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
const { findSourceMap } = require('internal/source_map/source_map_cache');
44
const { Module } = require('internal/modules/cjs/loader');
5-
const { register } = require('internal/modules/esm/loader');
5+
const { register, deregister } = require('internal/modules/esm/loader');
66
const { SourceMap } = require('internal/source_map/source_map');
77

88
Module.findSourceMap = findSourceMap;
99
Module.register = register;
10+
Module.deregister = deregister;
1011
Module.SourceMap = SourceMap;
1112
module.exports = Module;

test/es-module/test-esm-loader-hooks.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,4 +766,38 @@ describe('Loader hooks', { concurrency: true }, () => {
766766
assert.strictEqual(code, 0);
767767
assert.strictEqual(signal, null);
768768
});
769+
770+
it('should `deregister` properly', async () => {
771+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
772+
'--no-warnings',
773+
'--input-type=module',
774+
'--eval',
775+
`
776+
import {register, deregister} from 'node:module';
777+
const id = register(
778+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-meta.mjs'))}
779+
);
780+
781+
await import('node:os');
782+
await import('node:os');
783+
784+
console.log('deregister', deregister(id));
785+
786+
await import('node:os');
787+
`,
788+
]);
789+
790+
const lines = stdout.split('\n');
791+
792+
assert.strictEqual(lines.length, 5);
793+
assert.strictEqual(lines[0], 'hooks initialize');
794+
assert.strictEqual(lines[1], 'resolve passthru');
795+
assert.strictEqual(lines[2], 'resolve passthru');
796+
assert.strictEqual(lines[3], 'deregister true');
797+
assert.strictEqual(lines[4], '');
798+
799+
assert.strictEqual(stderr, '');
800+
assert.strictEqual(code, 0);
801+
assert.strictEqual(signal, null);
802+
});
769803
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export {resolve} from './loader-resolve-passthru.mjs';
2+
3+
export async function initialize(_data, meta) {
4+
console.log('hooks initialize');
5+
return meta?.id;
6+
}

0 commit comments

Comments
 (0)