Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,32 @@ function loadCJSModuleWithSpecialRequire(module, source, url, filename, isMain,
let importAttributes = kEmptyObject;
if (!StringPrototypeStartsWith(specifier, 'node:') && !BuiltinModule.normalizeRequirableId(specifier)) {
// TODO: do not depend on the monkey-patchable CJS loader here.
const path = CJSModule._resolveFilename(specifier, module);
switch (extname(path)) {
case '.json':
importAttributes = { __proto__: null, type: 'json' };
break;
case '.node':
// If it gets here in the translators, the hooks must have already been invoked
// in the loader. Skip them in the synthetic module evaluation step.
return wrapModuleLoad(specifier, module, false, kShouldSkipModuleHooks);
default:
// fall through
// The CJS resolver works on file paths, so it cannot resolve a specifier whose
// referrer is not a file - that happens when the source of this module comes from
// a load hook that gave it a non-file URL. Leave the specifier alone in that case
// and let the ESM resolver below run the hooks on it.
let path;
try {
path = CJSModule._resolveFilename(specifier, module);
} catch (err) {
if (err?.code !== 'MODULE_NOT_FOUND' || StringPrototypeStartsWith(url, 'file:')) {
throw err;
}
}
if (path !== undefined) {
switch (extname(path)) {
case '.json':
importAttributes = { __proto__: null, type: 'json' };
break;
case '.node':
// If it gets here in the translators, the hooks must have already been invoked
// in the loader. Skip them in the synthetic module evaluation step.
return wrapModuleLoad(specifier, module, false, kShouldSkipModuleHooks);
default:
// fall through
}
specifier = `${pathToFileURL(path)}`;
}
specifier = `${pathToFileURL(path)}`;
}

// NOTE: This re-invented require() is only used on the loader-hook worker thread.
Expand Down
17 changes: 17 additions & 0 deletions test/es-module/test-esm-loader-cjs-source-relative-require.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/non-file-cjs-source.mjs
import '../common/index.mjs';
import assert from 'node:assert';

// When a load hook provides the source of a CommonJS module, the `require` calls
// that source makes are resolved by the ESM loader, so registered hooks apply to
// them. That has to keep working when the module has a URL that is not a file:
// the CJS resolver cannot resolve a relative specifier against such a referrer.

const { default: fromHookedRequire } = await import('custom:entry');
assert.strictEqual(fromHookedRequire, 'loaded through the hooks');

// When the hooks do not claim the specifier either, the failure has to come from
// the ESM resolver rather than from the CJS one.
await assert.rejects(import('custom:missing-dep'), {
code: 'ERR_UNSUPPORTED_RESOLVE_REQUEST',
});
25 changes: 25 additions & 0 deletions test/fixtures/es-module-loaders/non-file-cjs-source.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Serves CommonJS sources under a non-file URL scheme, so that the `require`
// calls they contain have a referrer that the CJS resolver cannot handle.

const sources = {
'custom:entry': 'module.exports = require("./dep");',
'custom:dep': 'module.exports = "loaded through the hooks";',
'custom:missing-dep': 'module.exports = require("./no-such-dep");',
};

export function resolve(specifier, context, next) {
if (specifier in sources) {
return { shortCircuit: true, url: specifier };
}
if (specifier === './dep') {
return { shortCircuit: true, url: 'custom:dep' };
}
return next(specifier, context);
}

export function load(url, context, next) {
if (url in sources) {
return { shortCircuit: true, format: 'commonjs', source: sources[url] };
}
return next(url, context);
}
Loading