Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
279 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use strict'; | ||
|
||
const { getOptionValue } = require('internal/options'); | ||
const experimentalImportMetaResolve = | ||
getOptionValue('--experimental-import-meta-resolve'); | ||
const { PromisePrototypeThen, PromiseReject } = primordials; | ||
const asyncESM = require('internal/process/esm_loader'); | ||
|
||
function createImportMetaResolve(defaultParentUrl) { | ||
return async function resolve(specifier, parentUrl = defaultParentUrl) { | ||
return PromisePrototypeThen( | ||
asyncESM.esmLoader.resolve(specifier, parentUrl), | ||
({ url }) => url, | ||
(error) => ( | ||
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? | ||
error.url : PromiseReject(error)) | ||
); | ||
}; | ||
} | ||
|
||
function initializeImportMeta(meta, context) { | ||
const url = context.url; | ||
|
||
// Alphabetical | ||
if (experimentalImportMetaResolve) | ||
meta.resolve = createImportMetaResolve(url); | ||
meta.url = url; | ||
} | ||
|
||
module.exports = { | ||
initializeImportMeta | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs | ||
import '../common/index.mjs'; | ||
import assert from 'assert/strict'; | ||
import mock from 'node:mock'; | ||
|
||
mock('node:events', { | ||
EventEmitter: 'This is mocked!' | ||
}); | ||
|
||
// This resolves to node:events | ||
assert.deepStrictEqual(await import('events'), Object.defineProperty({ | ||
__proto__: null, | ||
EventEmitter: 'This is mocked!' | ||
}, Symbol.toStringTag, { | ||
enumerable: false, | ||
value: 'Module' | ||
})); | ||
|
||
const mutator = mock('node:events', { | ||
EventEmitter: 'This is mocked v2!' | ||
}); | ||
|
||
const mockedV2 = await import('node:events'); | ||
assert.deepStrictEqual(mockedV2, Object.defineProperty({ | ||
__proto__: null, | ||
EventEmitter: 'This is mocked v2!' | ||
}, Symbol.toStringTag, { | ||
enumerable: false, | ||
value: 'Module' | ||
})); | ||
|
||
mutator.EventEmitter = 'This is mocked v3!'; | ||
assert.deepStrictEqual(mockedV2, Object.defineProperty({ | ||
__proto__: null, | ||
EventEmitter: 'This is mocked v3!' | ||
}, Symbol.toStringTag, { | ||
enumerable: false, | ||
value: 'Module' | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import {receiveMessageOnPort} from 'worker_threads'; | ||
const mockedModuleExports = new Map(); | ||
let currentMockVersion = 0; | ||
|
||
/** | ||
* FIXME: this is a hack to workaround loaders being | ||
* single threaded for now | ||
*/ | ||
function doDrainPort() { | ||
let msg; | ||
while (msg = receiveMessageOnPort(preloadPort)) { | ||
onPreloadPortMessage(msg.message); | ||
} | ||
} | ||
function onPreloadPortMessage({ | ||
mockVersion, resolved, exports | ||
}) { | ||
currentMockVersion = mockVersion; | ||
mockedModuleExports.set(resolved, exports); | ||
} | ||
let preloadPort; | ||
export function globalPreload({port}) { | ||
preloadPort = port; | ||
port.on('message', onPreloadPortMessage); | ||
port.unref(); | ||
return `(${()=>{ | ||
let mockedModules = new Map(); | ||
let mockVersion = 0; | ||
const doMock = (resolved, replacementProperties) => { | ||
let exports = Object.keys(replacementProperties); | ||
let namespace = Object.create(null); | ||
let listeners = []; | ||
for (const name of exports) { | ||
let currentValue = replacementProperties[name]; | ||
Object.defineProperty(namespace, name, { | ||
enumerable: true, | ||
get() { | ||
return currentValue; | ||
}, | ||
set(v) { | ||
currentValue = v; | ||
for (let fn of listeners) { | ||
try { | ||
fn(name); | ||
} catch { | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
mockedModules.set(resolved, { | ||
namespace, | ||
listeners | ||
}); | ||
mockVersion++; | ||
port.postMessage({mockVersion, resolved, exports }); | ||
return namespace; | ||
} | ||
setImportMetaCallback((meta, context, parent) => { | ||
if (context.url === 'node:mock') { | ||
meta.doMock = doMock; | ||
return; | ||
} | ||
if (context.url.startsWith('mock:')) { | ||
let [proto, version, encodedTargetURL] = context.url.split(':'); | ||
let decodedTargetURL = decodeURIComponent(encodedTargetURL); | ||
if (mockedModules.has(decodedTargetURL)) { | ||
meta.mock = mockedModules.get(decodedTargetURL); | ||
return; | ||
} | ||
} | ||
parent(meta, context); | ||
}); | ||
}})()`; | ||
} | ||
|
||
|
||
// rewrites node: loading to mock: so that it can be intercepted | ||
export function resolve(specifier, context, defaultResolve) { | ||
if (specifier === 'node:mock') { | ||
return { | ||
url: specifier | ||
}; | ||
} | ||
doDrainPort(); | ||
const def = defaultResolve(specifier, context); | ||
if (context.parentURL?.startsWith('mock:')) { | ||
// do nothing, let it get the "real" module | ||
} else if (mockedModuleExports.has(def.url)) { | ||
return { | ||
url: `mock:${currentMockVersion}:${encodeURIComponent(def.url)}` | ||
}; | ||
}; | ||
return { | ||
url: `${def.url}` | ||
}; | ||
} | ||
|
||
export function load(url, context, defaultLoad) { | ||
doDrainPort(); | ||
if (url === 'node:mock') { | ||
return { | ||
source: 'export default import.meta.doMock', | ||
format: 'module' | ||
}; | ||
} | ||
if (url.startsWith('mock:')) { | ||
let [proto, version, encodedTargetURL] = url.split(':'); | ||
let ret = generateModule(mockedModuleExports.get( | ||
decodeURIComponent(encodedTargetURL) | ||
)); | ||
return { | ||
source: ret, | ||
format: 'module' | ||
}; | ||
} | ||
return defaultLoad(url, context); | ||
} | ||
|
||
function generateModule(exports) { | ||
let body = 'export {};let mapping = {__proto__: null};' | ||
for (const [i, name] of Object.entries(exports)) { | ||
let key = JSON.stringify(name); | ||
body += `var _${i} = import.meta.mock.namespace[${key}];` | ||
body += `Object.defineProperty(mapping, ${key}, {enumerable: true,set(v) {_${i} = v;}});` | ||
body += `export {_${i} as ${name}};`; | ||
} | ||
body += `import.meta.mock.listeners.push(${ | ||
() => { | ||
for (var k in mapping) { | ||
mapping[k] = import.meta.mock.namespace[k]; | ||
} | ||
} | ||
});` | ||
return body; | ||
} |
Oops, something went wrong.