Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,24 @@
const DATA_URL_PATTERN = /^data:application\/json(?:[^,]*?)(;base64)?,([\s\S]*)$/;
const JSON_URL_PATTERN = /^[^?]+\.json(\?[^#]*)?(#.*)?$/;
export async function resolve(specifier, context, next) {
const noAttributesSpecified = context.importAttributes.type == null;
// Mutation from resolve hook should be discarded.
context.importAttributes.type = 'whatever';
// This fixture assumes that no other resolve hooks in the chain will error on invalid import attributes
// (as defaultResolve doesn't).
const result = await next(specifier, context);
if (noAttributesSpecified &&
(DATA_URL_PATTERN.test(result.url) || JSON_URL_PATTERN.test(result.url))) {
// Clean new import attributes object to ensure that this test isn't passing due to mutation.
result.importAttributes = {
...(result.importAttributes ?? context.importAttributes),
type: 'json',
};
}
return result;
}

View File

@ -0,0 +1,59 @@
import module from 'node:module';
import { readFileSync } from 'node:fs';
/** @type {string} */
let GET_BUILTIN;
export function initialize(data) {
GET_BUILTIN = data.GET_BUILTIN;
}
export async function resolve(specifier, context, next) {
const def = await next(specifier, context);
if (def.url.startsWith('node:')) {
return {
shortCircuit: true,
url: `custom-${def.url}`,
importAttributes: context.importAttributes,
};
}
return def;
}
export function load(url, context, next) {
if (url.startsWith('custom-node:')) {
const urlObj = new URL(url);
return {
shortCircuit: true,
source: generateBuiltinModule(urlObj.pathname),
format: 'commonjs',
};
} else if (context.format === undefined || context.format === null || context.format === 'commonjs') {
return {
shortCircuit: true,
source: readFileSync(new URL(url)),
format: 'commonjs',
};
}
return next(url);
}
function generateBuiltinModule(builtinName) {
const builtinInstance = module._load(builtinName);
const builtinExports = [
...Object.keys(builtinInstance),
];
return `\
const $builtinInstance = ${GET_BUILTIN}(${JSON.stringify(builtinName)});
module.exports = $builtinInstance;
module.exports.__fromLoader = true;
// We need this for CJS-module-lexer can parse the exported names.
${
builtinExports
.map(name => `exports.${name} = $builtinInstance.${name};`)
.join('\n')
}
`;
}

View File

@ -0,0 +1,17 @@
import * as fixtures from '../../common/fixtures.mjs';
import { createRequire, register } from 'node:module';
const require = createRequire(import.meta.url);
const GET_BUILTIN = `$__get_builtin_hole_${Date.now()}`;
Object.defineProperty(globalThis, GET_BUILTIN, {
value: builtinName => require(builtinName),
enumerable: false,
configurable: false,
});
register(fixtures.fileURL('es-module-loaders/builtin-named-exports-loader.mjs'), {
data: {
GET_BUILTIN,
},
});

View File

@ -0,0 +1,30 @@
export function load(url, context, nextLoad) {
switch (url) {
case 'byop://1/index.mjs':
return {
source: 'console.log("index.mjs!")',
format: 'module',
shortCircuit: true,
};
case 'byop://1/index2.mjs':
return {
source: 'import c from "./sub.mjs"; console.log(c);',
format: 'module',
shortCircuit: true,
};
case 'byop://1/sub.mjs':
return {
source: 'export default 42',
format: 'module',
shortCircuit: true,
};
case 'byop://1/index.byoe':
return {
source: 'console.log("index.byoe!")',
format: 'module',
shortCircuit: true,
};
default:
return nextLoad(url, context);
}
}

View File

@ -0,0 +1,46 @@
import { URL } from 'url';
import path from 'path';
import process from 'process';
import { builtinModules } from 'module';
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
const baseURL = new URL('file://');
baseURL.pathname = process.cwd() + '/';
export function resolve(specifier, { parentURL = baseURL }, next) {
if (builtinModules.includes(specifier)) {
return {
shortCircuit: true,
url: 'node:' + specifier
};
}
if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
// return next(specifier);
throw new Error(
`imports must be URLs or begin with './', or '../'; '${specifier}' does not`);
}
const resolved = new URL(specifier, parentURL);
return {
shortCircuit: true,
url: resolved.href
};
}
export function getFormat(url, context, defaultGetFormat) {
if (url.startsWith('node:') && builtinModules.includes(url.slice(5))) {
return {
format: 'builtin'
};
}
const { pathname } = new URL(url);
const ext = path.extname(pathname);
if (!JS_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
return {
format: 'module'
};
}

View File

@ -0,0 +1,25 @@
/** @type {Uint8Array} */
let data;
/** @type {number} */
let ESM_MODULE_INDEX;
/** @type {number} */
let CJS_MODULE_INDEX;
export function initialize({ sab, ESM_MODULE_INDEX:e, CJS_MODULE_INDEX:c }) {
data = new Uint8Array(sab);
ESM_MODULE_INDEX = e;
CJS_MODULE_INDEX = c;
}
export async function resolve(specifier, context, next) {
const nextResult = await next(specifier, context);
const { format } = nextResult;
if (format === 'module' || specifier.endsWith('.mjs')) {
Atomics.add(data, ESM_MODULE_INDEX, 1);
} else if (format == null || format === 'commonjs') {
Atomics.add(data, CJS_MODULE_INDEX, 1);
}
return nextResult;
}

View File

@ -0,0 +1,18 @@
import * as fixtures from '../../common/fixtures.mjs';
import { register } from 'node:module';
const sab = new SharedArrayBuffer(2);
const data = new Uint8Array(sab);
const ESM_MODULE_INDEX = 0
const CJS_MODULE_INDEX = 1
export function getModuleTypeStats() {
const importedESM = Atomics.load(data, ESM_MODULE_INDEX);
const importedCJS = Atomics.load(data, CJS_MODULE_INDEX);
return { importedESM, importedCJS };
}
register(fixtures.fileURL('es-module-loaders/hook-resolve-type-loader.mjs'), {
data: { sab, ESM_MODULE_INDEX, CJS_MODULE_INDEX },
});

View File

@ -0,0 +1,116 @@
import { pathToFileURL } from 'node:url';
import count from '../es-modules/stateful.mjs';
// Arbitrary instance of manipulating a module's internal state
// used to assert node-land and user-land have different contexts
count();
export function resolve(specifier, { importAttributes }, next) {
let format = '';
if (specifier === 'esmHook/format.false') {
format = false;
}
if (specifier === 'esmHook/format.true') {
format = true;
}
if (specifier === 'esmHook/preknownFormat.pre') {
format = 'module';
}
if (specifier.startsWith('esmHook')) {
return {
format,
shortCircuit: true,
url: pathToFileURL(specifier).href,
importAttributes,
};
}
return next(specifier);
}
/**
* @param {string} url A fully resolved file url.
* @param {object} context Additional info.
* @param {function} next for now, next is defaultLoad a wrapper for
* defaultGetFormat + defaultGetSource
* @returns {{ format: string, source: (string|SharedArrayBuffer|Uint8Array) }}
*/
export function load(url, context, next) {
// Load all .js files as ESM, regardless of package scope
if (url.endsWith('.js')) {
return next(url, {
...context,
format: 'module',
});
}
if (url.endsWith('.ext')) {
return next(url, {
...context,
format: 'module',
});
}
if (url.endsWith('esmHook/badReturnVal.mjs')) {
return 'export function returnShouldBeObject() {}';
}
if (url.endsWith('esmHook/badReturnFormatVal.mjs')) {
return {
format: Array(0),
shortCircuit: true,
source: '',
};
}
if (url.endsWith('esmHook/unsupportedReturnFormatVal.mjs')) {
return {
format: 'foo', // Not one of the allowable inputs: no translator named 'foo'
shortCircuit: true,
source: '',
};
}
if (url.endsWith('esmHook/badReturnSourceVal.mjs')) {
return {
format: 'module',
shortCircuit: true,
source: Array(0),
};
}
if (url.endsWith('esmHook/preknownFormat.pre')) {
return {
format: context.format,
shortCircuit: true,
source: `const msg = 'hello world'; export default msg;`
};
}
if (url.endsWith('esmHook/virtual.mjs')) {
return {
format: 'module',
shortCircuit: true,
source: `export const message = 'Woohoo!'.toUpperCase();`,
};
}
if (url.endsWith('esmHook/commonJsNullSource.mjs')) {
return {
format: 'commonjs',
shortCircuit: true,
source: 1n,
};
}
if (url.endsWith('esmHook/maximumCallStack.mjs')) {
function recurse() {
recurse();
}
recurse();
}
return next(url);
}

View File

@ -0,0 +1,16 @@
let thePort = null;
export async function initialize(port) {
port.postMessage('initialize');
thePort = port;
}
export async function resolve(specifier, context, next) {
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
thePort.postMessage(`resolve ${specifier}`);
return next(specifier);
}

View File

@ -0,0 +1,7 @@
import { writeFileSync } from 'node:fs';
let counter = 0;
export async function initialize() {
writeFileSync(1, `hooks initialize ${++counter}\n`);
}

View File

@ -0,0 +1,92 @@
// This is expected to be used by test-esm-loader-hooks.mjs via:
// node --loader ./test/fixtures/es-module-loaders/hooks-input.mjs ./test/fixtures/es-modules/json-modules.mjs
import assert from 'assert';
import { writeSync } from 'fs';
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
let resolveCalls = 0;
let loadCalls = 0;
export async function resolve(specifier, context, next) {
resolveCalls++;
let url;
if (resolveCalls === 1) {
url = new URL(specifier).href;
assert.match(specifier, /json-modules\.mjs$/);
if (!(/\[eval\d*\]$/).test(context.parentURL)) {
assert.strictEqual(context.parentURL, undefined);
}
assert.deepStrictEqual(context.importAttributes, {});
} else if (resolveCalls === 2) {
url = new URL(specifier, context.parentURL).href;
assert.match(specifier, /experimental\.json$/);
assert.match(context.parentURL, /json-modules\.mjs$/);
assert.deepStrictEqual(context.importAttributes, {
type: 'json',
});
}
// Ensure `context` has all and only the properties it's supposed to
assert.deepStrictEqual(Reflect.ownKeys(context), [
'conditions',
'importAttributes',
'parentURL',
'importAssertions',
]);
assert.ok(Array.isArray(context.conditions));
assert.strictEqual(typeof next, 'function');
const returnValue = {
url,
format: 'test',
shortCircuit: true,
}
writeSync(1, JSON.stringify(returnValue) + '\n'); // For the test to validate when it parses stdout
return returnValue;
}
export async function load(url, context, next) {
loadCalls++;
const source = await readFile(fileURLToPath(url));
let format;
if (loadCalls === 1) {
assert.match(url, /json-modules\.mjs$/);
assert.deepStrictEqual(context.importAttributes, {});
format = 'module';
} else if (loadCalls === 2) {
assert.match(url, /experimental\.json$/);
assert.deepStrictEqual(context.importAttributes, {
type: 'json',
});
format = 'json';
}
assert.ok(new URL(url));
// Ensure `context` has all and only the properties it's supposed to
assert.deepStrictEqual(Reflect.ownKeys(context), [
'format',
'importAttributes',
'importAssertions',
]);
assert.strictEqual(context.format, 'test');
assert.strictEqual(typeof next, 'function');
const returnValue = {
source,
format,
shortCircuit: true,
};
writeSync(1, JSON.stringify(returnValue) + '\n'); // For the test to validate when it parses stdout
return returnValue;
}

View File

@ -0,0 +1,22 @@
import { get } from 'http';
export function load(url, context, nextLoad) {
if (url.startsWith('http://')) {
return new Promise((resolve, reject) => {
get(url, (rsp) => {
let data = '';
rsp.on('data', (chunk) => data += chunk);
rsp.on('end', () => {
resolve({
format: 'module',
shortCircuit: true,
source: data,
});
});
})
.on('error', reject);
});
}
return nextLoad(url);
}

View File

@ -0,0 +1 @@
export const namedExport = 'named-export';

View File

@ -0,0 +1 @@
exports.format = 'module';

View File

@ -0,0 +1,13 @@
import { strictEqual } from "node:assert";
import { isMainThread, workerData, parentPort } from "node:worker_threads";
strictEqual(isMainThread, false);
// We want to make sure that internals are not leaked on the public module:
strictEqual(workerData, null);
strictEqual(parentPort, null);
// We don't want `import.meta.resolve` being available from loaders
// as the sync implementation is not compatible with calling async
// functions on the same thread.
strictEqual(typeof import.meta.resolve, 'undefined');

View File

@ -0,0 +1,20 @@
export async function resolve(specifier, { parentURL, importAttributes }, next) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
shortCircuit: true,
url: 'file:///asdf',
};
}
return next(specifier);
}
export async function load(url, context, next) {
if (url === 'file:///asdf') {
return {
format: 'esm',
shortCircuit: true,
source: '',
}
}
return next(url);
}

View File

@ -0,0 +1,10 @@
export async function resolve(specifier, { parentURL, importAttributes }, next) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
shortCircuit: true,
url: specifier,
importAttributes,
};
}
return next(specifier);
}

View File

@ -0,0 +1,3 @@
export async function load(url, context, next) {
return next(url, []);
}

View File

@ -0,0 +1,3 @@
export async function load(url, context, next) {
return next([]);
}

View File

@ -0,0 +1,14 @@
import { writeSync } from 'node:fs';
export async function load(url, context, next) {
if (url === 'node:fs' || url.includes('loader')) {
return next(url);
}
// Here for asserting dynamic import
await import('xxx/loader-load-passthru.mjs');
writeSync(1, 'load dynamic import' + '\n'); // Signal that this specific hook ran
return next(url, context);
}

View File

@ -0,0 +1,19 @@
export async function load(url, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (url === 'node:fs' || url.includes('loader')) {
return next(url);
}
const val = url.includes('42')
? '42'
: '"foo"';
return {
format: 'module',
shortCircuit: true,
source: `export default ${val}`,
};
}

View File

@ -0,0 +1,3 @@
export async function load(url, context, next) {
return next('not/a/url');
}

View File

@ -0,0 +1,14 @@
export async function load(url, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (url === 'node:fs' || url.includes('loader')) {
return next(url);
}
return {
format: 'module',
source: 'export default 42',
};
}

View File

@ -0,0 +1,11 @@
export async function load(url, context, next) {
const {
format,
source,
} = await next(url);
return {
format,
source: source + 1,
};
}

View File

@ -0,0 +1,3 @@
export async function load(specifier, context, next) {
return null;
}

View File

@ -0,0 +1,6 @@
export async function load(url, context, next) {
return next(url, {
...context,
foo: 'bar',
});
}

View File

@ -0,0 +1,14 @@
import { writeSync } from 'node:fs';
export async function load(url, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (url === 'node:fs' || url.includes('loader')) {
return next(url);
}
writeSync(1, 'load passthru' + '\n'); // Signal that this specific hook ran
return next(url);
}

View File

@ -0,0 +1,7 @@
import { writeSync } from 'node:fs';
export async function load(url, context, next) {
writeSync(1, context.foo + '\n'); // Expose actual value the hook was called with
return next(url, context);
}

View File

@ -0,0 +1,31 @@
import { writeSync } from 'node:fs';
import { inspect } from 'node:util'
export async function resolve(...args) {
writeSync(1, `resolve arg count: ${args.length}\n`);
writeSync(1, inspect({
specifier: args[0],
context: args[1],
next: args[2],
}) + '\n');
return {
shortCircuit: true,
url: args[0],
};
}
export async function load(...args) {
writeSync(1, `load arg count: ${args.length}\n`);
writeSync(1, inspect({
url: args[0],
context: args[1],
next: args[2],
}) + '\n');
return {
format: 'module',
source: '',
shortCircuit: true,
};
}

View File

@ -0,0 +1,17 @@
import { writeSync } from 'node:fs';
export async function resolve(specifier, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
writeSync(1, 'resolve 42' + '\n'); // Signal that this specific hook ran
writeSync(1, `next<HookName>: ${next.name}\n`); // Expose actual value the hook was called with
return next('file:///42.mjs');
}

View File

@ -0,0 +1,3 @@
export async function resolve(specifier, context, next) {
return next(specifier, []);
}

View File

@ -0,0 +1,3 @@
export async function resolve(specifier, context, next) {
return next([]);
}

View File

@ -0,0 +1,14 @@
import { writeSync } from 'node:fs';
export async function resolve(specifier, context, next) {
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
// Here for asserting dynamic import
await import('xxx/loader-resolve-passthru.mjs');
writeSync(1, 'resolve dynamic import' + '\n'); // Signal that this specific hook ran
return next(specifier);
}

View File

@ -0,0 +1,15 @@
import { writeSync } from 'node:fs';
export async function resolve(specifier, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
writeSync(1, 'resolve foo' + '\n'); // Signal that this specific hook ran
return next('file:///foo.mjs');
}

View File

@ -0,0 +1,13 @@
export async function resolve(specifier, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
return {
url: 'file:///incomplete-resolve-chain.js',
};
}

View File

@ -0,0 +1,9 @@
export async function resolve(specifier, context, next) {
const { url: first } = await next(specifier);
const { url: second } = await next(specifier);
return {
format: 'module',
url: first,
};
}

View File

@ -0,0 +1,19 @@
export async function resolve(url, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (url === 'node:fs' || url.includes('loader')) {
return next(url);
}
const {
format,
url: nextUrl,
} = await next(url, context);
return {
format,
url: `${nextUrl}?foo`,
};
}

View File

@ -0,0 +1,3 @@
export async function resolve(specifier, context, next) {
return null;
}

View File

@ -0,0 +1,6 @@
export async function resolve(specifier, context, next) {
return next(specifier, {
...context,
foo: 'bar',
});
}

View File

@ -0,0 +1,14 @@
import { writeSync } from 'node:fs';
export async function resolve(specifier, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
writeSync(1, 'resolve passthru' + '\n'); // Signal that this specific hook ran
return next(specifier);
}

View File

@ -0,0 +1,7 @@
import { writeSync } from 'node:fs';
export async function resolve(specifier, context, next) {
writeSync(1, context.foo + '\n'); // Expose actual value the hook was called with
return next(specifier, context);
}

View File

@ -0,0 +1,14 @@
export async function resolve(specifier, context, next) {
// This check is needed to make sure that we don't prevent the
// resolution from follow-up loaders. It wouldn't be a problem
// in real life because loaders aren't supposed to break the
// resolution, but the ones used in our tests do, for convenience.
if (specifier === 'node:fs' || specifier.includes('loader')) {
return next(specifier);
}
return {
shortCircuit: true,
url: specifier,
}
}

View File

@ -0,0 +1,10 @@
import { writeSync } from 'node:fs';
import { inspect } from 'node:util';
export async function resolve(specifier, context, nextResolve) {
if (specifier.startsWith('node:')) {
return nextResolve(specifier);
}
writeSync(1, `loader-a ${inspect({specifier})}\n`);
return nextResolve(specifier.replace(/^xxx\//, `./`));
}

View File

@ -0,0 +1,7 @@
import { writeSync } from 'node:fs';
import { inspect } from 'node:util';
export async function resolve(specifier, context, nextResolve) {
writeSync(1, `loader-b ${inspect({specifier})}\n`);
return nextResolve(specifier.replace(/^yyy\//, `./`));
}

View File

@ -0,0 +1,11 @@
import assert from 'assert';
import { createRequire } from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
export function resolve(specifier, context, next) {
assert.strictEqual(dep.format, 'module');
return next(specifier);
}

View File

@ -0,0 +1,21 @@
export function initialize() {
if (this != null) {
throw new Error('hook function must not be bound to loader instance');
}
}
export function resolve(url, _, next) {
if (this != null) {
throw new Error('hook function must not be bound to loader instance');
}
return next(url);
}
export function load(url, _, next) {
if (this != null) {
throw new Error('hook function must not be bound to loader instance');
}
return next(url);
}

View File

@ -0,0 +1,7 @@
export function resolve(specifier, context, next) {
if (specifier === 'unknown-builtin-module') return {
url: 'node:unknown-builtin-module'
};
return next(specifier);
}

View File

@ -0,0 +1,17 @@
import { ok, deepStrictEqual } from 'assert';
export async function resolve(specifier, context, defaultResolve) {
ok(Array.isArray(context.conditions), 'loader receives conditions array');
deepStrictEqual([...context.conditions].sort(), [
'import',
'module-sync',
'node',
'node-addons',
]);
return defaultResolve(specifier, {
...context,
conditions: ['custom-condition', ...context.conditions],
});
}

View File

@ -0,0 +1,11 @@
import {createRequire} from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
export async function resolve(specifier, { parentURL, importAttributes }, defaultResolve) {
return {
url: (await defaultResolve(specifier, { parentURL, importAttributes }, defaultResolve)).url,
format: dep.format
};
}

View File

@ -0,0 +1,7 @@
export async function resolve(specifier, context, next) {
return next(specifier, context, 'resolve-extra-arg');
}
export async function load(url, context, next) {
return next(url, context, 'load-extra-arg');
}

View File

@ -0,0 +1,17 @@
export function resolve(specifier, context, next) {
if (specifier === 'test') {
return {
url: 'file://'
};
}
return next(specifier);
}
export function getFormat(url, context, defaultGetFormat) {
if (url === 'file://') {
return {
format: 'dynamic'
}
}
return defaultGetFormat(url, context, defaultGetFormat);
}

View File

@ -0,0 +1,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');
}

View File

@ -0,0 +1,70 @@
import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';
const { port1, port2 } = new MessageChannel();
register('./mock-loader.mjs', import.meta.url, {
data: {
port: port2,
mainImportURL: import.meta.url,
},
transferList: [port2],
});
/**
* This is the Map that saves *all* the mocked URL -> replacement Module
* mappings
* @type {Map<string, {namespace, listeners}>}
*/
export const mockedModules = new Map();
let mockVersion = 0;
/**
* @param {string} resolved an absolute URL HREF string
* @param {object} replacementProperties an object to pick properties from
* to act as a module namespace
* @returns {object} a mutator object that can update the module namespace
* since we can't do something like old Object.observe
*/
export function mock(resolved, replacementProperties) {
const exportNames = Object.keys(replacementProperties);
const namespace = { __proto__: null };
/**
* @type {Array<(name: string)=>void>} functions to call whenever an
* export name is updated
*/
const listeners = [];
for (const name of exportNames) {
let currentValueForPropertyName = replacementProperties[name];
Object.defineProperty(namespace, name, {
__proto__: null,
enumerable: true,
get() {
return currentValueForPropertyName;
},
set(v) {
currentValueForPropertyName = v;
for (const fn of listeners) {
try {
fn(name);
} catch {
/* noop */
}
}
},
});
}
mockedModules.set(encodeURIComponent(resolved), {
namespace,
listeners,
});
mockVersion++;
// Inform the loader that the `resolved` URL should now use the specific
// `mockVersion` and has export names of `exportNames`
//
// This allows the loader to generate a fake module for that version
// and names the next time it resolves a specifier to equal `resolved`
port1.postMessage({ mockVersion, resolved, exports: exportNames });
return namespace;
}

View File

@ -0,0 +1,2 @@
export const foo = 'foo';
export const bar = 'bar';

View File

@ -0,0 +1,5 @@
console.log('should be output');
import.meta.resolve('never-settle-resolve');
console.log('should not be output');

View File

@ -0,0 +1,10 @@
export function resolve(specifier, context, next) {
if (specifier === 'never-settle-resolve') return new Promise(() => {});
if (specifier === 'never-settle-load') return { __proto__: null, shortCircuit: true, url: 'never-settle:///' };
return next(specifier, context);
}
export function load(url, context, next) {
if (url === 'never-settle:///') return new Promise(() => {});
return next(url, context);
}

View File

@ -0,0 +1,7 @@
'use strict';
const neverSettlingDynamicImport = import('never-settle-load');
console.log('should be output');
neverSettlingDynamicImport.then(() => process.exit(1));

View File

@ -0,0 +1,5 @@
const neverSettlingDynamicImport = import('never-settle-load');
console.log('should be output');
await neverSettlingDynamicImport;

View File

@ -0,0 +1,7 @@
'use strict';
const neverSettlingDynamicImport = import('never-settle-resolve');
console.log('should be output');
neverSettlingDynamicImport.then(() => process.exit(1));

View File

@ -0,0 +1,5 @@
const neverSettlingDynamicImport = import('never-settle-resolve');
console.log('should be output');
await neverSettlingDynamicImport;

View File

@ -0,0 +1,7 @@
'use strict';
Promise.race([
import('never-settle-resolve'),
import('never-settle-load'),
import('node:process'),
]).then(result => console.log(result.default === process));

View File

@ -0,0 +1,7 @@
const result = await Promise.race([
import('never-settle-resolve'),
import('never-settle-load'),
import('node:process'),
]);
console.log(result.default === process);

View File

@ -0,0 +1 @@
export * from '../../loader-load-passthru.mjs'

View File

@ -0,0 +1,3 @@
{
"exports": "./index.mjs"
}

View File

@ -0,0 +1 @@
export * from '../../loader-resolve-passthru.mjs'

View File

@ -0,0 +1,3 @@
{
"exports": "./index.mjs"
}

View File

@ -0,0 +1,16 @@
import assert from 'node:assert';
// A loader that asserts that the defaultResolve will throw "not found"
// (skipping the top-level main of course, and the built-in ones needed for run-worker).
let mainLoad = true;
export async function resolve(specifier, { importAttributes }, next) {
if (mainLoad || specifier === 'path' || specifier === 'worker_threads') {
mainLoad = false;
return next(specifier);
}
await assert.rejects(next(specifier), { code: 'ERR_MODULE_NOT_FOUND' });
return {
url: 'node:fs',
importAttributes,
};
}

View File

@ -0,0 +1,21 @@
export function resolve(specifier, context, next) {
if (specifier.endsWith('/no-such-file.cjs')) {
// Shortcut to avoid ERR_MODULE_NOT_FOUND for non-existing file, but keep the url for the load hook.
return {
shortCircuit: true,
url: specifier,
};
}
return next(specifier);
}
export function load(href, context, next) {
if (href.endsWith('.cjs')) {
return {
format: 'commonjs',
shortCircuit: true,
source: 'module.exports = "no .cjs file was read to get this source";',
};
}
return next(href);
}

View File

@ -0,0 +1,4 @@
const { register } = require('node:module');
const fixtures = require('../../common/fixtures.js');
register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'));

View File

@ -0,0 +1,4 @@
import { register } from 'node:module';
import fixtures from '../../common/fixtures.js';
register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'));

View File

@ -0,0 +1,4 @@
import * as fixtures from '../../common/fixtures.mjs';
import { register } from 'node:module';
register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'));

View File

@ -0,0 +1,3 @@
import { register } from 'node:module';
register('./loader-resolve-passthru.mjs', import.meta.url);

View File

@ -0,0 +1,43 @@
const SOURCES = {
__proto__: null,
'test:Array': ['1', '2'], // both `1,2` and `12` are valid ESM
'test:ArrayBuffer': new ArrayBuffer(0),
'test:BigInt64Array': new BigInt64Array(0),
'test:BigUint64Array': new BigUint64Array(0),
'test:Float32Array': new Float32Array(0),
'test:Float64Array': new Float64Array(0),
'test:Int8Array': new Int8Array(0),
'test:Int16Array': new Int16Array(0),
'test:Int32Array': new Int32Array(0),
'test:null': null,
'test:Object': {},
'test:SharedArrayBuffer': new SharedArrayBuffer(0),
'test:string': '',
'test:String': new String(''),
'test:Uint8Array': new Uint8Array(0),
'test:Uint8ClampedArray': new Uint8ClampedArray(0),
'test:Uint16Array': new Uint16Array(0),
'test:Uint32Array': new Uint32Array(0),
'test:undefined': undefined,
}
export function resolve(specifier, context, next) {
if (specifier.startsWith('test:')) {
return {
importAttributes: context.importAttributes,
shortCircuit: true,
url: specifier,
};
}
return next(specifier);
}
export function load(href, context, next) {
if (href.startsWith('test:')) {
return {
format: 'module',
shortCircuit: true,
source: SOURCES[href],
};
}
return next(href);
}

View File

@ -0,0 +1,2 @@
'use strict';
await async () => 0;

View File

@ -0,0 +1,10 @@
export function load () {
let thenAlreadyAccessed = false;
return {
get then() {
if (thenAlreadyAccessed) throw new Error('must not call');
thenAlreadyAccessed = true;
return (_, reject) => reject();
}
};
}

View File

@ -0,0 +1,10 @@
export function load () {
let thenAlreadyAccessed = false;
return {
get then() {
if (thenAlreadyAccessed) throw new Error('must not call');
thenAlreadyAccessed = true;
return (_, reject) => reject(new Error('must crash the process'));
}
};
}

View File

@ -0,0 +1,10 @@
export function load(url, context, next) {
let thenAlreadyAccessed = false;
return {
get then() {
if (thenAlreadyAccessed) throw new Error('must not call');
thenAlreadyAccessed = true;
return (resolve) => resolve(next(url, context));
}
};
}

View File

@ -0,0 +1,3 @@
'use strict';
throw undefined;