node-esm-mock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # esm-mock
2
+
3
+ Mock ES modules in Node.js using `registerHooks` (Node >=22.7.0).
4
+
5
+ No loader flags, no `--experimental-loader`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install esm-mock
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { mock } from 'esm-mock';
17
+ import assert from 'node:assert';
18
+ import sinon from 'sinon';
19
+
20
+ const handler = sinon.stub().returns('mocked!');
21
+
22
+ const mod = await mock({
23
+ './module.ts': { greet: handler },
24
+ }).for<typeof import('./module.ts')>('./module.ts');
25
+
26
+ mod.greet();
27
+ assert.equal(handler.callCount, 1);
28
+ ```
29
+
30
+ Without mocks, `for()` imports the module cleanly (cache-busted):
31
+
32
+ ```ts
33
+ const mod = await mock().for('./module.ts');
34
+ ```
35
+
36
+ ### API
37
+
38
+ #### `mock(mocks?)`
39
+
40
+ | Param | Type | Default | Description |
41
+ |---|---|---|---|
42
+ | `mocks` | `Record<string, Record<string, unknown>>` | `{}` | A map of module specifiers to their replacement exports |
43
+
44
+ Returns an object with a `for()` method.
45
+
46
+ #### `mock(mocks).for<T>(specifier): Promise<T>`
47
+
48
+ | Param | Type | Description |
49
+ |---|---|---|
50
+ | `specifier` | `string` | Module specifier to import (cache-busted with a timestamp query) |
51
+ | `T` (generic) | — | Type of the imported module (optional) |
52
+
53
+ Imports the given specifier with the configured mocks active. After the import resolves, mock registrations are automatically cleaned up.
54
+
55
+ ### Examples
56
+
57
+ **Mock a built-in module:**
58
+
59
+ ```ts
60
+ const { Worker } = await mock({
61
+ 'node:worker_threads': { Worker: FakeWorker },
62
+ }).for<typeof import('../worker.js')>('../worker.js');
63
+ ```
64
+
65
+ **No mocks (plain import with cache busting):**
66
+
67
+ ```ts
68
+ const { Worker } = await mock().for('../worker.js');
69
+ assert.ok(Worker instanceof worker_threads.Worker);
70
+ ```
71
+
72
+ ## How it works
73
+
74
+ `esm-mock` registers a [`registerHooks`](https://nodejs.org/api/module.html#moduleregisterhooksoptions) instance at module scope that intercepts `resolve` and `load` for any module URL registered via `add()`. When a mocked module is requested, resolution short-circuits to a synthetic `mock-facade:` URL and `load` serves auto-generated ES module source that re-exports the replacement values.
75
+
76
+ Each `mock(mocks).for(specifier)` call:
77
+ 1. Registers each mock entry via the internal `add()` function.
78
+ 2. Dynamically imports the specifier with a cache-busting query parameter.
79
+ 3. Cleans up all mock registrations after the import resolves.
80
+
81
+ ## Requirements
82
+
83
+ - **Node.js >= 22.7.0** (for `module.registerHooks`)
@@ -0,0 +1,4 @@
1
+ export declare const mockedModules: Map<any, any>;
2
+ export declare function mock(mocks?: Record<string, Record<string, unknown>>): {
3
+ for<T = any>(specifier: string): Promise<T>;
4
+ };
package/dist/index.js ADDED
@@ -0,0 +1,94 @@
1
+ import { registerHooks } from 'node:module';
2
+ const mockedModuleExports = new Map();
3
+ let mainImportURL = import.meta.url;
4
+ registerHooks({
5
+ resolve(specifier, context, nextResolve) {
6
+ var _a;
7
+ const def = nextResolve(specifier, context);
8
+ if (!((_a = context.parentURL) === null || _a === void 0 ? void 0 : _a.startsWith('mock-facade:'))) {
9
+ if (mockedModuleExports.has(def.url)) {
10
+ return {
11
+ shortCircuit: true, url: `mock-facade:${encodeURIComponent(def.url)}`,
12
+ };
13
+ }
14
+ }
15
+ return def;
16
+ }, load(url, context, nextLoad) {
17
+ if (url.startsWith('mock-facade:')) {
18
+ const encodedTargetURL = url.slice(url.lastIndexOf(':') + 1);
19
+ return {
20
+ shortCircuit: true, source: generateModule(encodedTargetURL), format: 'module',
21
+ };
22
+ }
23
+ return nextLoad(url, context);
24
+ }
25
+ });
26
+ function generateModule(encodedTargetURL) {
27
+ const exports = mockedModuleExports.get(decodeURIComponent(encodedTargetURL));
28
+ const body = [
29
+ `import { mockedModules } from ${JSON.stringify(mainImportURL)};`,
30
+ 'export {};',
31
+ 'let mapping = {__proto__: null};',
32
+ `const mock = mockedModules.get(${JSON.stringify(encodedTargetURL)});`,
33
+ ];
34
+ for (const [i, name] of Object.entries(exports)) {
35
+ const key = JSON.stringify(name);
36
+ body.push(`var _${i} = mock.namespace[${key}];`);
37
+ body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`);
38
+ body.push(`export {_${i} as ${name}};`);
39
+ }
40
+ body.push(`mock.listeners.push(() => {
41
+ for (var k in mapping) {
42
+ mapping[k] = mock.namespace[k];
43
+ }
44
+ });`);
45
+ return body.join('\n');
46
+ }
47
+ export const mockedModules = new Map();
48
+ function add(resolved, replacementProperties) {
49
+ const exportNames = Object.keys(replacementProperties);
50
+ const namespace = { __proto__: null };
51
+ const listeners = [];
52
+ for (const name of exportNames) {
53
+ let currentValueForPropertyName = replacementProperties[name];
54
+ Object.defineProperty(namespace, name, {
55
+ // @ts-ignore
56
+ __proto__: null,
57
+ enumerable: true,
58
+ get() {
59
+ return currentValueForPropertyName;
60
+ }, set(v) {
61
+ currentValueForPropertyName = v;
62
+ for (const fn of listeners) {
63
+ try {
64
+ fn(name);
65
+ }
66
+ catch (_a) {
67
+ /* noop */
68
+ }
69
+ }
70
+ },
71
+ });
72
+ }
73
+ mockedModules.set(encodeURIComponent(resolved), {
74
+ namespace, listeners,
75
+ });
76
+ mockedModuleExports.set(resolved, exportNames);
77
+ return namespace;
78
+ }
79
+ export function mock(mocks = {}) {
80
+ const mockedModules = new Map();
81
+ return {
82
+ for(specifier) {
83
+ try {
84
+ for (const spec in mocks) {
85
+ mockedModules.set(spec, add(spec, mocks[spec]));
86
+ }
87
+ return import(`${specifier}?${+new Date()}`);
88
+ }
89
+ finally {
90
+ mockedModules.forEach((_, mockedModule) => mockedModuleExports.delete(mockedModule));
91
+ }
92
+ }
93
+ };
94
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "node-esm-mock",
3
+ "version": "0.1.0",
4
+ "description": "Mock ES modules in Node.js using registerHooks",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "mock",
19
+ "module",
20
+ "esm",
21
+ "test",
22
+ "node",
23
+ "registerHooks"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=22.7.0"
28
+ },
29
+ "dependencies": {
30
+ "typescript": "^5.5.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.9.3",
34
+ "@types/sinon": "^21.0.1",
35
+ "sinon": "^22.0.0",
36
+ "ts-node": "^10.9.2"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "test": "node --test test/index.ts"
41
+ }
42
+ }