Skip to content

Commit

Permalink
esm: add deregister method
Browse files Browse the repository at this point in the history
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);
```
  • Loading branch information
izaakschroeder committed Aug 14, 2023
1 parent de4553f commit 2c2bb6c
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 2 deletions.
58 changes: 57 additions & 1 deletion lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayPrototypeFindIndex,
ArrayPrototypePush,
ArrayPrototypePushApply,
FunctionPrototypeCall,
Expand Down Expand Up @@ -121,6 +122,9 @@ class Hooks {
],
};

#lastInstanceId = 0;
#loaderInstances = [];

// Cache URLs we've already validated to avoid repeated validation
#validatedUrls = new SafeSet();

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

deregister(id) {
return this.removeCustomLoader(id);
}

/**
* Collect custom/user-defined module loader hook(s).
* After all hooks have been collected, the global preload hook(s) must be initialized.
Expand All @@ -160,6 +168,15 @@ class Hooks {
load,
} = pluckHooks(exports);

const instance = {
__proto__: null,
id: this.#lastInstanceId++,
globalPreload,
initialize,
resolve,
load,
};

if (globalPreload && !initialize) {
emitExperimentalWarning(
'`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`',
Expand All @@ -174,7 +191,46 @@ class Hooks {
const next = this.#chains.load[this.#chains.load.length - 1];
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
}
return initialize?.(data);

ArrayPrototypePush(this.#loaderInstances, instance);
return initialize?.(data, { __proto__: null, id: instance.id });
}

#removeFromChain(chain, target) {
for (let i = 0; i < chain.length; ++i) {
if (target === chain[i+1]?.fn) {
chain.splice(i+1, 1);
}
if (chain[i].next) {
chain[i].next = chain[i+1];
}
}
}

removeCustomLoader(id) {
const index = ArrayPrototypeFindIndex(this.#loaderInstances, (target) => {
return target.id === id;
});
if (index < 0) {
return false;
}
const instance = this.#loaderInstances[index];
if (instance.globalPreload) {
const index = ArrayPrototypeFindIndex(this.#chains.globalPreload, (x) => {
return x.fn === instance.globalPreload;
});
if (index >= 0) {
this.#chains.globalPreload.splice(index, 1);
}
}
if (instance.resolve) {
this.#removeFromChain(this.#chains.resolve, instance.resolve);
}
if (instance.load) {
this.#removeFromChain(this.#chains.load, instance.load);
}
this.#loaderInstances.splice(index, 1);
return true;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,17 @@ class ModuleLoader {
return this.#customizations.register(specifier, parentURL, data, transferList);
}

deregister(id) {
if (!this.#customizations) {
// `CustomizedModuleLoader` is defined at the bottom of this file and
// available well before this line is ever invoked. This is here in
// order to preserve the git diff instead of moving the class.
// eslint-disable-next-line no-use-before-define
this.setCustomizations(new CustomizedModuleLoader());
}
return this.#customizations.deregister(id);
}

/**
* Resolve the location of the module.
* @param {string} originalSpecifier The specified URL path of the module to
Expand Down Expand Up @@ -458,6 +469,10 @@ class CustomizedModuleLoader {
return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
}

deregister(id) {
return hooksProxy.makeSyncRequest('deregister', undefined, id);
}

/**
* Resolve the location of the module.
* @param {string} originalSpecifier The specified URL path of the module to
Expand Down Expand Up @@ -582,8 +597,14 @@ function register(specifier, parentURL = undefined, options) {
);
}

function deregister(id) {
const moduleLoader = require('internal/process/esm_loader').esmLoader;
return moduleLoader.deregister(id);
}

module.exports = {
createModuleLoader,
getHooksProxy,
register,
deregister,
};
3 changes: 2 additions & 1 deletion lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

const { findSourceMap } = require('internal/source_map/source_map_cache');
const { Module } = require('internal/modules/cjs/loader');
const { register } = require('internal/modules/esm/loader');
const { register, deregister } = require('internal/modules/esm/loader');
const { SourceMap } = require('internal/source_map/source_map');

Module.findSourceMap = findSourceMap;
Module.register = register;
Module.deregister = deregister;
Module.SourceMap = SourceMap;
module.exports = Module;
34 changes: 34 additions & 0 deletions test/es-module/test-esm-loader-hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -766,4 +766,38 @@ describe('Loader hooks', { concurrency: true }, () => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should `deregister` properly', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--input-type=module',
'--eval',
`
import {register, deregister} from 'node:module';
const id = register(
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-meta.mjs'))}
);
await import('node:os');
await import('node:os');
console.log('deregister', deregister(id));
await import('node:os');
`,
]);

const lines = stdout.split('\n');

assert.strictEqual(lines.length, 5);
assert.strictEqual(lines[0], 'hooks initialize');
assert.strictEqual(lines[1], 'resolve passthru');
assert.strictEqual(lines[2], 'resolve passthru');
assert.strictEqual(lines[3], 'deregister true');
assert.strictEqual(lines[4], '');

assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
});
6 changes: 6 additions & 0 deletions test/fixtures/es-module-loaders/hooks-meta.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {resolve} from './loader-resolve-passthru.mjs';

export async function initialize(_data, meta) {
console.log('hooks initialize');
return meta?.id;
}

0 comments on commit 2c2bb6c

Please sign in to comment.