Skip to content

Commit b7ec307

Browse files
committed
module: resolve format for all situations with module detection on
1 parent d4442a9 commit b7ec307

File tree

5 files changed

+72
-36
lines changed

5 files changed

+72
-36
lines changed

lib/internal/modules/esm/get_format.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const experimentalNetworkImports =
2222
const { containsModuleSyntax } = internalBinding('contextify');
2323
const { getPackageScopeConfig, getPackageType } = require('internal/modules/package_json_reader');
2424
const { fileURLToPath } = require('internal/url');
25+
const { readFileSync } = require('fs');
26+
const { Buffer } = require('buffer');
2527
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
2628

2729
const protocolHandlers = {
@@ -82,6 +84,24 @@ function underNodeModules(url) {
8284
return StringPrototypeIncludes(url.pathname, '/node_modules/');
8385
}
8486

87+
/**
88+
* Determine whether the given source contains CJS or ESM module syntax.
89+
* @param {string} source
90+
* @param {URL} url
91+
*/
92+
function detectModuleFormat(source, url) {
93+
try {
94+
let realSource = source ?? readFileSync(url, 'utf8');
95+
if (Buffer.isBuffer(realSource)) {
96+
// `containsModuleSyntax` requires source to be passed in as string
97+
realSource = realSource.toString();
98+
}
99+
return containsModuleSyntax(realSource, fileURLToPath(url), url) ? 'module' : 'commonjs';
100+
} catch {
101+
return 'commonjs';
102+
}
103+
}
104+
85105
let typelessPackageJsonFilesWarnedAbout;
86106
/**
87107
* @param {URL} url
@@ -113,9 +133,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
113133
// `source` is undefined when this is called from `defaultResolve`;
114134
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
115135
if (getOptionValue('--experimental-detect-module')) {
116-
const format = source ?
117-
(containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs') :
118-
null;
136+
const format = detectModuleFormat(source, url);
119137
if (format === 'module') {
120138
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
121139
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
@@ -155,12 +173,8 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
155173
}
156174
default: { // The user did not pass `--experimental-default-type`.
157175
if (getOptionValue('--experimental-detect-module')) {
158-
if (!source) { return null; }
159176
const format = getFormatOfExtensionlessFile(url);
160-
if (format === 'module') {
161-
return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs';
162-
}
163-
return format;
177+
return (format === 'module') ? detectModuleFormat(source, url) : format;
164178
}
165179
return 'commonjs';
166180
}

lib/internal/modules/esm/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ class ModuleLoader {
321321
* @returns {ModuleJobBase}
322322
*/
323323
getModuleJobForRequire(specifier, parentURL, importAttributes) {
324-
assert(getOptionValue('--experimental-require-module'));
324+
assert(getOptionValue('--experimental-require-module') || getOptionValue('--experimental-detect-module'));
325325

326326
if (canParse(specifier)) {
327327
const protocol = new URL(specifier).protocol;

test/es-module/test-esm-detect-ambiguous.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describe('--experimental-detect-module', { concurrency: !process.env.TEST_PARALL
155155
});
156156
}
157157

158-
it('should not hint wrong format in resolve hook', async () => {
158+
it('should hint format correctly for extensionles modules resolve hook', async () => {
159159
let writeSync;
160160
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
161161
'--experimental-detect-module',
@@ -172,7 +172,7 @@ describe('--experimental-detect-module', { concurrency: !process.env.TEST_PARALL
172172
]);
173173

174174
strictEqual(stderr, '');
175-
strictEqual(stdout, 'null\nexecuted\n');
175+
strictEqual(stdout, 'module\nexecuted\n');
176176
strictEqual(code, 0);
177177
strictEqual(signal, null);
178178

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

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -744,32 +744,41 @@ describe('Loader hooks', { concurrency: !process.env.TEST_PARALLEL }, () => {
744744
assert.strictEqual(signal, null);
745745
});
746746

747-
it('should use ESM loader to respond to require.resolve calls when opting in', async () => {
748-
const readFile = async () => {};
749-
const fileURLToPath = () => {};
750-
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
751-
'--no-warnings',
752-
'--experimental-loader',
753-
`data:text/javascript,import{readFile}from"node:fs/promises";import{fileURLToPath}from"node:url";export ${
754-
async function load(u, c, n) {
755-
const r = await n(u, c);
756-
if (u.endsWith('/common/index.js')) {
757-
r.source = '"use strict";module.exports=require("node:module").createRequire(' +
758-
`${JSON.stringify(u)})(${JSON.stringify(fileURLToPath(u))});\n`;
759-
} else if (c.format === 'commonjs') {
760-
r.source = await readFile(new URL(u));
761-
}
762-
return r;
763-
}}`,
764-
'--experimental-loader',
765-
fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'),
766-
fixtures.path('require-resolve.js'),
767-
]);
768747

769-
assert.strictEqual(stderr, '');
770-
assert.strictEqual(stdout, 'resolve passthru\n'.repeat(10));
771-
assert.strictEqual(code, 0);
772-
assert.strictEqual(signal, null);
748+
describe('should use ESM loader to respond to require.resolve calls when opting in', () => {
749+
for (const { testConfigName, additionalOptions } of [
750+
{ testConfigName: 'without --experimental-detect-module', additionalOptions: [] },
751+
{ testConfigName: 'with --experimental-detect-module', additionalOptions: ['--experimental-detect-module'] },
752+
]) {
753+
it(testConfigName, async () => {
754+
const readFile = async () => {};
755+
const fileURLToPath = () => {};
756+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
757+
...additionalOptions,
758+
'--no-warnings',
759+
'--experimental-loader',
760+
`data:text/javascript,import{readFile}from"node:fs/promises";import{fileURLToPath}from"node:url";export ${
761+
async function load(u, c, n) {
762+
const r = await n(u, c);
763+
if (u.endsWith('/common/index.js')) {
764+
r.source = '"use strict";module.exports=require("node:module").createRequire(' +
765+
`${JSON.stringify(u)})(${JSON.stringify(fileURLToPath(u))});\n`;
766+
} else if (c.format === 'commonjs') {
767+
r.source = await readFile(new URL(u));
768+
}
769+
return r;
770+
}}`,
771+
'--experimental-loader',
772+
fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'),
773+
fixtures.path('require-resolve.js'),
774+
]);
775+
776+
assert.strictEqual(stderr, '');
777+
assert.strictEqual(stdout, 'resolve passthru\n'.repeat(10));
778+
assert.strictEqual(code, 0);
779+
assert.strictEqual(signal, null);
780+
});
781+
}
773782
});
774783

775784
it('should support source maps in commonjs translator', async () => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Flags: --experimental-detect-module --import ./test/fixtures/es-module-loaders/builtin-named-exports.mjs
2+
'use strict';
3+
4+
const common = require('../common');
5+
common.skipIfWorker();
6+
7+
const { readFile, __fromLoader } = require('fs');
8+
const assert = require('assert');
9+
10+
assert.throws(() => require('../fixtures/es-modules/test-esm-ok.mjs'), { code: 'ERR_REQUIRE_ESM' });
11+
12+
assert(readFile);
13+
assert(__fromLoader);

0 commit comments

Comments
 (0)