-
Notifications
You must be signed in to change notification settings - Fork 30.2k
/
mock-loader.mjs
133 lines (125 loc) Β· 4.47 KB
/
mock-loader.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { receiveMessageOnPort } from 'node:worker_threads';
const mockedModuleExports = new Map();
let currentMockVersion = 0;
// These hooks enable code running on the application thread to
// swap module resolution results for mocking purposes. It uses this instead
// of import.meta so that CommonJS can still use the functionality.
//
// It does so by allowing non-mocked modules to live in normal URL cache
// locations but creates 'mock-facade:' URL cache location for every time a
// module location is mocked. Since a single URL can be mocked multiple
// times but it cannot be removed from the cache, `mock-facade:` URLs have a
// form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL
// percent encoded every time a module is resolved. So if a module for
// 'file:///app.js' is mocked it might look like
// 'mock-facade:12:file%3A%2F%2F%2Fapp.js'. This encoding is done to prevent
// problems like mocking URLs with special URL characters like '#' or '?' from
// accidentally being picked up as part of the 'mock-facade:' URL containing
// the mocked URL.
//
// NOTE: due to ESM spec, once a specifier has been resolved in a source text
// it cannot be changed. So things like the following DO NOT WORK:
//
// ```mjs
// import mock from 'test-esm-loader-mock'; // See test-esm-loader-mock.mjs
// mock('file:///app.js', {x:1});
// const namespace1 = await import('file:///app.js');
// namespace1.x; // 1
// mock('file:///app.js', {x:2});
// const namespace2 = await import('file:///app.js');
// namespace2.x; // STILL 1, because this source text already set the specifier
// // for 'file:///app.js', a different specifier that resolves
// // to that could still get a new namespace though
// assert(namespace1 === namespace2);
// ```
/**
* @param param0 message from the application context
*/
function onPreloadPortMessage({
mockVersion, resolved, exports
}) {
currentMockVersion = mockVersion;
mockedModuleExports.set(resolved, exports);
}
/** @type {URL['href']} */
let mainImportURL;
/** @type {MessagePort} */
let preloadPort;
export async function initialize(data) {
({ mainImportURL, port: preloadPort } = data);
data.port.on('message', onPreloadPortMessage);
}
/**
* Because Node.js internals use a separate MessagePort for cross-thread
* communication, there could be some messages pending that we should handle
* before continuing.
*/
function doDrainPort() {
let msg;
while (msg = receiveMessageOnPort(preloadPort)) {
onPreloadPortMessage(msg.message);
}
}
// Rewrites node: loading to mock-facade: so that it can be intercepted
export async function resolve(specifier, context, defaultResolve) {
doDrainPort();
const def = await defaultResolve(specifier, context);
if (context.parentURL?.startsWith('mock-facade:')) {
// Do nothing, let it get the "real" module
} else if (mockedModuleExports.has(def.url)) {
return {
shortCircuit: true,
url: `mock-facade:${currentMockVersion}:${encodeURIComponent(def.url)}`
};
};
return {
shortCircuit: true,
url: def.url,
};
}
export async function load(url, context, defaultLoad) {
doDrainPort();
/**
* Mocked fake module, not going to be handled in default way so it
* generates the source text, then short circuits
*/
if (url.startsWith('mock-facade:')) {
const encodedTargetURL = url.slice(url.lastIndexOf(':') + 1);
return {
shortCircuit: true,
source: generateModule(encodedTargetURL),
format: 'module',
};
}
return defaultLoad(url, context);
}
/**
* Generate the source code for a mocked module.
* @param {string} encodedTargetURL the module being mocked
* @returns {string}
*/
function generateModule(encodedTargetURL) {
const exports = mockedModuleExports.get(
decodeURIComponent(encodedTargetURL)
);
let body = [
`import { mockedModules } from ${JSON.stringify(mainImportURL)};`,
'export {};',
'let mapping = {__proto__: null};',
`const mock = mockedModules.get(${JSON.stringify(encodedTargetURL)});`,
];
for (const [i, name] of Object.entries(exports)) {
let key = JSON.stringify(name);
body.push(`var _${i} = mock.namespace[${key}];`);
body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`);
body.push(`export {_${i} as ${name}};`);
}
body.push(`mock.listeners.push(${
() => {
for (var k in mapping) {
mapping[k] = mock.namespace[k];
}
}
});`);
return body.join('\n');
}