Skip to content

Commit bd945b6

Browse files
dygaboGeoffreyBooth
andcommitted
module: resolve format for all situations with module detection on
Co-authored-by: Geoffrey Booth <[email protected]>
1 parent c681f57 commit bd945b6

File tree

6 files changed

+134
-16
lines changed

6 files changed

+134
-16
lines changed

lib/internal/modules/esm/get_format.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ function underNodeModules(url) {
8282
return StringPrototypeIncludes(url.pathname, '/node_modules/');
8383
}
8484

85+
/**
86+
* Determine whether the given source contains CJS or ESM module syntax.
87+
* @param {string | Buffer | undefined} source
88+
* @param {URL} url
89+
*/
90+
function detectModuleFormat(source, url) {
91+
const moduleSource = source ? `${source}` : undefined;
92+
const modulePath = fileURLToPath(url);
93+
return containsModuleSyntax(moduleSource, modulePath, url) ? 'module' : 'commonjs';
94+
}
95+
8596
let typelessPackageJsonFilesWarnedAbout;
8697
/**
8798
* @param {URL} url
@@ -113,9 +124,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
113124
// `source` is undefined when this is called from `defaultResolve`;
114125
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
115126
if (getOptionValue('--experimental-detect-module')) {
116-
const format = source ?
117-
(containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs') :
118-
null;
127+
const format = detectModuleFormat(source, url);
119128
if (format === 'module') {
120129
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
121130
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
@@ -155,12 +164,8 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
155164
}
156165
default: { // The user did not pass `--experimental-default-type`.
157166
if (getOptionValue('--experimental-detect-module')) {
158-
if (!source) { return null; }
159167
const format = getFormatOfExtensionlessFile(url);
160-
if (format === 'module') {
161-
return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs';
162-
}
163-
return format;
168+
return (format === 'module') ? detectModuleFormat(source, url) : format;
164169
}
165170
return 'commonjs';
166171
}

lib/internal/modules/esm/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ class ModuleLoader {
329329
* @returns {ModuleJobBase}
330330
*/
331331
getModuleJobForRequire(specifier, parentURL, importAttributes) {
332-
assert(getOptionValue('--experimental-require-module'));
332+
assert(getOptionValue('--experimental-require-module') || getOptionValue('--experimental-detect-module'));
333333

334334
const parsed = URLParse(specifier);
335335
if (parsed != null) {

src/node_contextify.cc

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,14 +1705,32 @@ static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
17051705

17061706
CHECK_GE(args.Length(), 2);
17071707

1708-
// Argument 1: source code
1709-
CHECK(args[0]->IsString());
1710-
Local<String> code = args[0].As<String>();
1711-
17121708
// Argument 2: filename
17131709
CHECK(args[1]->IsString());
17141710
Local<String> filename = args[1].As<String>();
17151711

1712+
// Argument 1: source code; if undefined, read from filename in argument 2
1713+
Local<String> code;
1714+
if (args[0]->IsUndefined()) {
1715+
CHECK(!filename.IsEmpty());
1716+
const char* filename_str = Utf8Value(isolate, filename).out();
1717+
std::string contents;
1718+
int result = ReadFileSync(&contents, filename_str);
1719+
if (result != 0) {
1720+
isolate->ThrowException(
1721+
ERR_MODULE_NOT_FOUND(isolate, "Cannot read file %s", filename_str));
1722+
return;
1723+
}
1724+
code = String::NewFromUtf8(isolate,
1725+
contents.c_str(),
1726+
v8::NewStringType::kNormal,
1727+
contents.length())
1728+
.ToLocalChecked();
1729+
} else {
1730+
CHECK(args[0]->IsString());
1731+
code = args[0].As<String>();
1732+
}
1733+
17161734
// Argument 3: resource name (URL for ES module).
17171735
Local<String> resource_name = filename;
17181736
if (args[2]->IsString()) {
@@ -1729,6 +1747,7 @@ static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
17291747
Local<Function> fn;
17301748
TryCatchScope try_catch(env);
17311749
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
1750+
17321751
if (CompileFunctionForCJSLoader(
17331752
env, context, code, filename, &cache_rejected, cjs_var)
17341753
.ToLocal(&fn)) {
@@ -1740,7 +1759,13 @@ static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
17401759
}
17411760

17421761
bool result = ShouldRetryAsESM(realm, message, code, resource_name);
1743-
args.GetReturnValue().Set(result);
1762+
if (result) {
1763+
// successfully parsed as ESM after failing to parse as CJS => ESM syntax
1764+
args.GetReturnValue().Set(result);
1765+
return;
1766+
}
1767+
1768+
args.GetReturnValue().SetUndefined();
17441769
}
17451770

17461771
static void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe('--experimental-detect-module', { concurrency: !process.env.TEST_PARALL
168168
});
169169
}
170170

171-
it('should not hint wrong format in resolve hook', async () => {
171+
it('should hint format correctly for the resolve hook for extensionless modules', async () => {
172172
let writeSync;
173173
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
174174
'--experimental-detect-module',
@@ -185,7 +185,7 @@ describe('--experimental-detect-module', { concurrency: !process.env.TEST_PARALL
185185
]);
186186

187187
strictEqual(stderr, '');
188-
strictEqual(stdout, 'null\nexecuted\n');
188+
strictEqual(stdout, 'module\nexecuted\n');
189189
strictEqual(code, 0);
190190
strictEqual(signal, null);
191191

@@ -385,6 +385,65 @@ describe('--experimental-detect-module', { concurrency: !process.env.TEST_PARALL
385385
strictEqual(signal, null);
386386
});
387387
});
388+
389+
describe('should work with module customization hooks', { concurrency: true }, () => {
390+
it('should not break basic hooks functionality of substituting a module', async () => {
391+
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
392+
'--experimental-detect-module',
393+
'--import',
394+
fixtures.fileURL('es-module-loaders/builtin-named-exports.mjs'),
395+
fixtures.path('es-modules/require-esm-throws-with-loaders.js'),
396+
]);
397+
398+
strictEqual(stderr, '');
399+
strictEqual(stdout, '');
400+
strictEqual(code, 0);
401+
strictEqual(signal, null);
402+
});
403+
404+
it('should detect the syntax of the source as returned by a custom load hook', async () => {
405+
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
406+
'--no-warnings',
407+
'--experimental-detect-module',
408+
'--import',
409+
`data:text/javascript,${encodeURIComponent(
410+
'import { register } from "node:module";' +
411+
'import { pathToFileURL } from "node:url";' +
412+
'register("./transpile-esm-to-cjs.mjs", pathToFileURL("./"));'
413+
)}`,
414+
fixtures.path('es-modules/package-without-type/module.js'),
415+
], { cwd: fixtures.fileURL('es-module-loaders/') });
416+
417+
strictEqual(stderr, '');
418+
strictEqual(stdout, `
419+
Resolved format: module
420+
Loaded original format: module
421+
executed
422+
Evaluated format: commonjs
423+
`.replace(/^\s+/gm, ''));
424+
strictEqual(code, 0);
425+
strictEqual(signal, null);
426+
});
427+
428+
it('should throw the usual error for a missing file', async () => {
429+
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
430+
'--no-warnings',
431+
'--experimental-detect-module',
432+
'--import',
433+
`data:text/javascript,${encodeURIComponent(
434+
'import { register } from "node:module";' +
435+
'import { pathToFileURL } from "node:url";' +
436+
'register("./transpile-esm-to-cjs.mjs", pathToFileURL("./"));'
437+
)}`,
438+
fixtures.path('es-modules/package-without-type/imports-nonexistent.js'),
439+
], { cwd: fixtures.fileURL('es-module-loaders/') });
440+
441+
match(stderr, /ERR_MODULE_NOT_FOUND.+does-not-exist\.js/);
442+
strictEqual(stdout, 'Resolved format: module\nLoaded original format: module\n');
443+
strictEqual(code, 1);
444+
strictEqual(signal, null);
445+
});
446+
});
388447
});
389448

390449
// Validate temporarily disabling `--abort-on-uncaught-exception`
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { writeSync } from "node:fs";
2+
3+
export async function resolve(specifier, context, next) {
4+
const result = await next(specifier, context);
5+
if (specifier.startsWith("file://")) {
6+
writeSync(1, `Resolved format: ${result.format}\n`);
7+
}
8+
return result;
9+
}
10+
11+
export async function load(url, context, next) {
12+
const output = await next(url, context);
13+
writeSync(1, `Loaded original format: ${output.format}\n`);
14+
15+
let source = `${output.source}`
16+
17+
// This is a very incomplete and naively done implementation for testing purposes only
18+
if (source?.includes('export default')) {
19+
source = source.replace('export default', 'module.exports =');
20+
21+
source += '\nconsole.log(`Evaluated format: ${this === undefined ? "module" : "commonjs"}`);';
22+
23+
output.source = source;
24+
output.format = 'commonjs';
25+
}
26+
27+
return output;
28+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "./does-not-exist.js";

0 commit comments

Comments
 (0)