Skip to content

Commit 3e846c4

Browse files
GeoffreyBoothaduh95
authored andcommitted
esm: refactor mocking test
1 parent 0add7a8 commit 3e846c4

File tree

2 files changed

+87
-164
lines changed

2 files changed

+87
-164
lines changed

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

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,73 @@
1-
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
1+
// Flags: --no-warnings
22
import '../common/index.mjs';
3-
import assert from 'assert/strict';
3+
import * as fixtures from '../common/fixtures.mjs';
4+
import assert from 'node:assert/strict';
5+
import { register } from 'node:module';
6+
import { MessageChannel } from 'node:worker_threads';
47

5-
// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
6-
import mock from 'node:mock';
8+
const { port1, port2 } = new MessageChannel();
9+
10+
register(fixtures.fileURL('es-module-loaders/mock-loader.mjs'), {
11+
parentURL: import.meta.url,
12+
data: { port: port2 },
13+
transferList: [port2],
14+
});
15+
16+
/**
17+
* This is the Map that saves *all* the mocked URL -> replacement Module
18+
* mappings
19+
* @type {Map<string, {namespace, listeners}>}
20+
*/
21+
globalThis.mockedModules = new Map();
22+
let mockVersion = 0;
23+
24+
/**
25+
* @param {string} resolved an absolute URL HREF string
26+
* @param {object} replacementProperties an object to pick properties from
27+
* to act as a module namespace
28+
* @returns {object} a mutator object that can update the module namespace
29+
* since we can't do something like old Object.observe
30+
*/
31+
function mock(resolved, replacementProperties) {
32+
const exportNames = Object.keys(replacementProperties);
33+
const namespace = { __proto__: null };
34+
/**
35+
* @type {Array<(name: string)=>void>} functions to call whenever an
36+
* export name is updated
37+
*/
38+
const listeners = [];
39+
for (const name of exportNames) {
40+
let currentValueForPropertyName = replacementProperties[name];
41+
Object.defineProperty(namespace, name, {
42+
enumerable: true,
43+
get() {
44+
return currentValueForPropertyName;
45+
},
46+
set(v) {
47+
currentValueForPropertyName = v;
48+
for (const fn of listeners) {
49+
try {
50+
fn(name);
51+
} catch {
52+
/* noop */
53+
}
54+
}
55+
}
56+
});
57+
}
58+
globalThis.mockedModules.set(encodeURIComponent(resolved), {
59+
namespace,
60+
listeners
61+
});
62+
mockVersion++;
63+
// Inform the loader that the `resolved` URL should now use the specific
64+
// `mockVersion` and has export names of `exportNames`
65+
//
66+
// This allows the loader to generate a fake module for that version
67+
// and names the next time it resolves a specifier to equal `resolved`
68+
port1.postMessage({ mockVersion, resolved, exports: exportNames });
69+
return namespace;
70+
}
771

872
mock('node:events', {
973
EventEmitter: 'This is mocked!'
@@ -43,3 +107,5 @@ assert.deepStrictEqual(mockedV2, Object.defineProperty({
43107
enumerable: false,
44108
value: 'Module'
45109
}));
110+
111+
delete globalThis.mockedModules;

test/fixtures/es-module-loaders/mock-loader.mjs

Lines changed: 17 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { receiveMessageOnPort } from 'node:worker_threads';
21
const mockedModuleExports = new Map();
32
let currentMockVersion = 0;
43

5-
// This loader causes a new module `node:mock` to become available as a way to
4+
// This loader enables code running on the application thread to
65
// swap module resolution results for mocking purposes. It uses this instead
76
// of import.meta so that CommonJS can still use the functionality.
87
//
@@ -22,7 +21,7 @@ let currentMockVersion = 0;
2221
// it cannot be changed. So things like the following DO NOT WORK:
2322
//
2423
// ```mjs
25-
// import mock from 'node:mock';
24+
// import mock from 'test-esm-loader-mock'; // See test-esm-loader-mock.mjs
2625
// mock('file:///app.js', {x:1});
2726
// const namespace1 = await import('file:///app.js');
2827
// namespace1.x; // 1
@@ -34,148 +33,16 @@ let currentMockVersion = 0;
3433
// assert(namespace1 === namespace2);
3534
// ```
3635

37-
/**
38-
* FIXME: this is a hack to workaround loaders being
39-
* single threaded for now, just ensures that the MessagePort drains
40-
*/
41-
function doDrainPort() {
42-
let msg;
43-
while (msg = receiveMessageOnPort(preloadPort)) {
44-
onPreloadPortMessage(msg.message);
45-
}
46-
}
4736

48-
/**
49-
* @param param0 message from the application context
50-
*/
51-
function onPreloadPortMessage({
52-
mockVersion, resolved, exports
53-
}) {
54-
currentMockVersion = mockVersion;
55-
mockedModuleExports.set(resolved, exports);
37+
export async function initialize({ port }) {
38+
port.on('message', ({ mockVersion, resolved, exports }) => {
39+
currentMockVersion = mockVersion;
40+
mockedModuleExports.set(resolved, exports);
41+
});
5642
}
57-
let preloadPort;
58-
export function globalPreload({port}) {
59-
// Save the communication port to the application context to send messages
60-
// to it later
61-
preloadPort = port;
62-
// Every time the application context sends a message over the port
63-
port.on('message', onPreloadPortMessage);
64-
// This prevents the port that the Loader/application talk over
65-
// from keeping the process alive, without this, an application would be kept
66-
// alive just because a loader is waiting for messages
67-
port.unref();
68-
69-
const insideAppContext = (getBuiltin, port, setImportMetaCallback) => {
70-
/**
71-
* This is the Map that saves *all* the mocked URL -> replacement Module
72-
* mappings
73-
* @type {Map<string, {namespace, listeners}>}
74-
*/
75-
let mockedModules = new Map();
76-
let mockVersion = 0;
77-
/**
78-
* This is the value that is placed into the `node:mock` default export
79-
*
80-
* @example
81-
* ```mjs
82-
* import mock from 'node:mock';
83-
* const mutator = mock('file:///app.js', {x:1});
84-
* const namespace = await import('file:///app.js');
85-
* namespace.x; // 1;
86-
* mutator.x = 2;
87-
* namespace.x; // 2;
88-
* ```
89-
*
90-
* @param {string} resolved an absolute URL HREF string
91-
* @param {object} replacementProperties an object to pick properties from
92-
* to act as a module namespace
93-
* @returns {object} a mutator object that can update the module namespace
94-
* since we can't do something like old Object.observe
95-
*/
96-
const doMock = (resolved, replacementProperties) => {
97-
let exportNames = Object.keys(replacementProperties);
98-
let namespace = Object.create(null);
99-
/**
100-
* @type {Array<(name: string)=>void>} functions to call whenever an
101-
* export name is updated
102-
*/
103-
let listeners = [];
104-
for (const name of exportNames) {
105-
let currentValueForPropertyName = replacementProperties[name];
106-
Object.defineProperty(namespace, name, {
107-
enumerable: true,
108-
get() {
109-
return currentValueForPropertyName;
110-
},
111-
set(v) {
112-
currentValueForPropertyName = v;
113-
for (let fn of listeners) {
114-
try {
115-
fn(name);
116-
} catch {
117-
}
118-
}
119-
}
120-
});
121-
}
122-
mockedModules.set(resolved, {
123-
namespace,
124-
listeners
125-
});
126-
mockVersion++;
127-
// Inform the loader that the `resolved` URL should now use the specific
128-
// `mockVersion` and has export names of `exportNames`
129-
//
130-
// This allows the loader to generate a fake module for that version
131-
// and names the next time it resolves a specifier to equal `resolved`
132-
port.postMessage({ mockVersion, resolved, exports: exportNames });
133-
return namespace;
134-
}
135-
// Sets the import.meta properties up
136-
// has the normal chaining workflow with `defaultImportMetaInitializer`
137-
setImportMetaCallback((meta, context, defaultImportMetaInitializer) => {
138-
/**
139-
* 'node:mock' creates its default export by plucking off of import.meta
140-
* and must do so in order to get the communications channel from inside
141-
* preloadCode
142-
*/
143-
if (context.url === 'node:mock') {
144-
meta.doMock = doMock;
145-
return;
146-
}
147-
/**
148-
* Fake modules created by `node:mock` get their meta.mock utility set
149-
* to the corresponding value keyed off `mockedModules` and use this
150-
* to setup their exports/listeners properly
151-
*/
152-
if (context.url.startsWith('mock-facade:')) {
153-
let [proto, version, encodedTargetURL] = context.url.split(':');
154-
let decodedTargetURL = decodeURIComponent(encodedTargetURL);
155-
if (mockedModules.has(decodedTargetURL)) {
156-
meta.mock = mockedModules.get(decodedTargetURL);
157-
return;
158-
}
159-
}
160-
/**
161-
* Ensure we still get things like `import.meta.url`
162-
*/
163-
defaultImportMetaInitializer(meta, context);
164-
});
165-
};
166-
return `(${insideAppContext})(getBuiltin, port, setImportMetaCallback)`
167-
}
168-
16943

17044
// Rewrites node: loading to mock-facade: so that it can be intercepted
17145
export async function resolve(specifier, context, defaultResolve) {
172-
if (specifier === 'node:mock') {
173-
return {
174-
shortCircuit: true,
175-
url: specifier
176-
};
177-
}
178-
doDrainPort();
17946
const def = await defaultResolve(specifier, context);
18047
if (context.parentURL?.startsWith('mock-facade:')) {
18148
// Do nothing, let it get the "real" module
@@ -192,48 +59,38 @@ export async function resolve(specifier, context, defaultResolve) {
19259
}
19360

19461
export async function load(url, context, defaultLoad) {
195-
doDrainPort();
196-
if (url === 'node:mock') {
197-
/**
198-
* Simply grab the import.meta.doMock to establish the communication
199-
* channel with preloadCode
200-
*/
201-
return {
202-
shortCircuit: true,
203-
source: 'export default import.meta.doMock',
204-
format: 'module'
205-
};
206-
}
20762
/**
20863
* Mocked fake module, not going to be handled in default way so it
20964
* generates the source text, then short circuits
21065
*/
21166
if (url.startsWith('mock-facade:')) {
212-
let [proto, version, encodedTargetURL] = url.split(':');
213-
let ret = generateModule(mockedModuleExports.get(
214-
decodeURIComponent(encodedTargetURL)
215-
));
67+
let [_proto, _version, encodedTargetURL] = url.split(':');
68+
let source = generateModule(encodedTargetURL);
21669
return {
21770
shortCircuit: true,
218-
source: ret,
71+
source,
21972
format: 'module'
22073
};
22174
}
22275
return defaultLoad(url, context);
22376
}
22477

22578
/**
226-
*
227-
* @param {Array<string>} exports name of the exports of the module
79+
* Generate the source code for a mocked module.
80+
* @param {string} encodedTargetURL the module being mocked
22881
* @returns {string}
22982
*/
230-
function generateModule(exports) {
83+
function generateModule(encodedTargetURL) {
84+
const exports = mockedModuleExports.get(
85+
decodeURIComponent(encodedTargetURL)
86+
);
23187
let body = [
23288
'export {};',
23389
'let mapping = {__proto__: null};'
23490
];
23591
for (const [i, name] of Object.entries(exports)) {
23692
let key = JSON.stringify(name);
93+
body.push(`import.meta.mock = globalThis.mockedModules.get('${encodedTargetURL}');`);
23794
body.push(`var _${i} = import.meta.mock.namespace[${key}];`);
23895
body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`);
23996
body.push(`export {_${i} as ${name}};`);

0 commit comments

Comments
 (0)