Skip to content

Commit 3a37bc3

Browse files
committed
File input; improve string input tests
1 parent 0fa17a6 commit 3a37bc3

File tree

15 files changed

+236
-29
lines changed

15 files changed

+236
-29
lines changed

β€Žlib/internal/modules/esm/translators.jsβ€Ž

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,21 @@ const {
3636
hasEsmSyntax,
3737
loadBuiltinModule,
3838
stripBOM,
39+
isModuleSyntaxError,
3940
} = require('internal/modules/helpers');
4041
const {
4142
Module: CJSModule,
4243
cjsParseCache,
4344
} = require('internal/modules/cjs/loader');
45+
const { getPackageType } = require('internal/modules/esm/resolve');
4446
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
4547
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
4648
debug = fn;
4749
});
50+
const { getOptionValue } = require('internal/options');
51+
const inputTypeFlagInputValue = getOptionValue('--input-type'); // Empty string if the flag is unset.
52+
const defaultTypeFlagInputValue = getOptionValue('--experimental-default-type'); // Empty string if the flag is unset.
53+
const detectModule = getOptionValue('--experimental-detect-module');
4854
const { emitExperimentalWarning, kEmptyObject, setOwnProperty } = require('internal/util');
4955
const {
5056
ERR_UNKNOWN_BUILTIN_MODULE,
@@ -146,7 +152,7 @@ async function importModuleDynamically(specifier, { url }, assertions) {
146152
}
147153

148154
// Strategy for loading a standard JavaScript module.
149-
translators.set('module', async function moduleStrategy(url, source, isMain) {
155+
async function moduleStrategy(url, source, isMain) {
150156
assertBufferSource(source, true, 'load');
151157
source = stringify(source);
152158
maybeCacheSourceMap(url, source);
@@ -159,16 +165,18 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
159165
importModuleDynamically,
160166
});
161167
return module;
162-
});
168+
}
169+
translators.set('module', moduleStrategy);
163170

164171
/**
165-
* Provide a more informative error for CommonJS imports.
166-
* @param {Error | any} err
172+
* Provide a more informative error for CommonJS erroring because of ESM syntax,
173+
* when we aren't retrying CommonJS modules as ES modules.
174+
* @param {Error | unknown} err
167175
* @param {string} [content] Content of the file, if known.
168176
* @param {string} [filename] Useful only if `content` is unknown.
169177
*/
170178
function enrichCJSError(err, content, filename) {
171-
if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype &&
179+
if (!detectModule && err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype &&
172180
hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) {
173181
// Emit the warning synchronously because we are in the middle of handling
174182
// a SyntaxError that will throw and likely terminate the process before an
@@ -264,7 +272,8 @@ const cjsCache = new SafeMap();
264272
function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
265273
debug(`Translating CJSModule ${url}`);
266274

267-
const filename = StringPrototypeStartsWith(url, 'file://') ? fileURLToPath(url) : url;
275+
const isFileURL = StringPrototypeStartsWith(url, 'file://');
276+
const filename = isFileURL ? fileURLToPath(url) : url;
268277
source = stringify(source);
269278

270279
const { exportNames, module } = cjsPreparseModuleExports(filename, source);
@@ -276,11 +285,37 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
276285
setOwnProperty(process, 'mainModule', module);
277286
}
278287

279-
return new ModuleWrap(url, undefined, namesWithDefault, function() {
280-
debug(`Loading CJSModule ${url}`);
281-
282-
if (!module.loaded) {
288+
let moduleLoadingError;
289+
debug(`Loading CJSModule ${url}`);
290+
if (!module.loaded) {
291+
try {
283292
loadCJS(module, source, url, filename);
293+
} catch (err) {
294+
/**
295+
* If this module contained ESM syntax, we will retry loading it as an ES module under the following conditions:
296+
* - For file input:
297+
* - It is in a package scope with no defined `type`: neither `module` nor `commonjs`.
298+
* - It does not have an explicit .cjs or .mjs extension.
299+
* - For string input:
300+
* - `--input-type` is unset.
301+
* - `--experimental-default-type` is unset.
302+
*/
303+
if (detectModule && isModuleSyntaxError(err) && (
304+
(isFileURL && getPackageType(url) === 'none' && !filename.endsWith('.mjs') && !filename.endsWith('.cjs')) ||
305+
(!isFileURL && inputTypeFlagInputValue === '' && defaultTypeFlagInputValue === '')
306+
)) {
307+
// We need to catch this error here, rather than during the ModuleWrap callback, in order to bubble this up so
308+
// that we can retry loading the module as an ES module.
309+
throw err;
310+
}
311+
moduleLoadingError = err;
312+
}
313+
}
314+
315+
return new ModuleWrap(url, undefined, namesWithDefault, function() {
316+
// The non-detection flow expects module loading errors to be thrown here.
317+
if (moduleLoadingError) {
318+
throw moduleLoadingError;
284319
}
285320

286321
let exports;
@@ -344,8 +379,15 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
344379
} catch {
345380
// Continue regardless of error.
346381
}
347-
return createCJSModuleWrap(url, source, isMain, cjsLoader);
348382

383+
try {
384+
return createCJSModuleWrap(url, source, isMain, cjsLoader);
385+
} catch (err) {
386+
if (detectModule && isModuleSyntaxError(err)) {
387+
debug(`Retrying evaluating module ${url} as ESM`);
388+
return moduleStrategy(url, source, isMain);
389+
}
390+
}
349391
});
350392

351393
/**

β€Žlib/internal/modules/helpers.jsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ function isModuleSyntaxError(err) {
337337
err.message === 'Cannot use import statement outside a module' ||
338338
err.message === "Unexpected token 'export'" ||
339339
err.message === "Cannot use 'import.meta' outside a module"
340-
);
340+
);
341341
}
342342

343343
module.exports = {

β€Žlib/internal/modules/run_main.jsβ€Ž

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,19 @@ function executeUserEntryPoint(main = process.argv[1]) {
119119
} else {
120120
// Module._load is the monkey-patchable CJS module loader.
121121
const { Module } = require('internal/modules/cjs/loader');
122-
Module._load(main, null, true);
122+
try {
123+
Module._load(main, null, true);
124+
} catch (err) {
125+
if (getOptionValue('--experimental-detect-module')) {
126+
const { isModuleSyntaxError } = require('internal/modules/helpers');
127+
if (isModuleSyntaxError(err)) {
128+
// If the error is a module syntax error, we should use the ESM loader instead.
129+
runMainESM(resolvedMain || main);
130+
return;
131+
}
132+
}
133+
throw err;
134+
}
123135
}
124136
}
125137

Lines changed: 157 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,160 @@
11
import { spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { spawn } from 'node:child_process';
24
import { describe, it } from 'node:test';
3-
import { strictEqual } from 'node:assert';
4-
5-
describe('--experimental-detect-module', () => {
6-
it('permits ESM syntax in --eval input without requiring --input-type=module', async () => {
7-
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
8-
'--experimental-detect-module',
9-
'--eval',
10-
'import { version } from "node:process"; console.log(version);',
11-
]);
12-
13-
strictEqual(stderr, '');
14-
strictEqual(stdout, `${process.version}\n`);
15-
strictEqual(code, 0);
16-
strictEqual(signal, null);
5+
import { strictEqual, match } from 'node:assert';
6+
7+
describe('--experimental-detect-module', { concurrency: true }, () => {
8+
describe('string input', { concurrency: true }, () => {
9+
it('permits ESM syntax in --eval input without requiring --input-type=module', async () => {
10+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
11+
'--experimental-detect-module',
12+
'--eval',
13+
'import { version } from "node:process"; console.log(version);',
14+
]);
15+
16+
strictEqual(stderr, '');
17+
strictEqual(stdout, `${process.version}\n`);
18+
strictEqual(code, 0);
19+
strictEqual(signal, null);
20+
});
21+
22+
// ESM is unsupported for --print via --input-type=module
23+
24+
it('permits ESM syntax in STDIN input without requiring --input-type=module', async () => {
25+
const child = spawn(process.execPath, [
26+
'--experimental-detect-module',
27+
]);
28+
child.stdin.end('console.log(typeof import.meta.resolve)');
29+
30+
match((await child.stdout.toArray()).toString(), /^function\r?\n$/);
31+
});
32+
33+
it('should be overridden by --input-type', async () => {
34+
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
35+
'--experimental-detect-module',
36+
'--input-type=commonjs',
37+
'--eval',
38+
'import.meta.url',
39+
]);
40+
41+
match(stderr, /SyntaxError: Cannot use 'import\.meta' outside a module/);
42+
strictEqual(stdout, '');
43+
strictEqual(code, 1);
44+
strictEqual(signal, null);
45+
});
46+
47+
it('should be overridden by --experimental-default-type', async () => {
48+
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
49+
'--experimental-detect-module',
50+
'--experimental-default-type=commonjs',
51+
'--eval',
52+
'import.meta.url',
53+
]);
54+
55+
match(stderr, /SyntaxError: Cannot use 'import\.meta' outside a module/);
56+
strictEqual(stdout, '');
57+
strictEqual(code, 1);
58+
strictEqual(signal, null);
59+
});
60+
});
61+
62+
describe('file input in a typeless package', { concurrency: true }, () => {
63+
for (const { testName, entryPath } of [
64+
{
65+
testName: 'permits CommonJS syntax in a .js entry point',
66+
entryPath: fixtures.path('es-modules/package-without-type/commonjs.js'),
67+
},
68+
{
69+
testName: 'permits ESM syntax in a .js entry point',
70+
entryPath: fixtures.path('es-modules/package-without-type/module.js'),
71+
},
72+
{
73+
testName: 'permits CommonJS syntax in a .js file imported by a CommonJS entry point',
74+
entryPath: fixtures.path('es-modules/package-without-type/imports-commonjs.cjs'),
75+
},
76+
{
77+
testName: 'permits ESM syntax in a .js file imported by a CommonJS entry point',
78+
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.js'),
79+
},
80+
{
81+
testName: 'permits CommonJS syntax in a .js file imported by an ESM entry point',
82+
entryPath: fixtures.path('es-modules/package-without-type/imports-commonjs.mjs'),
83+
},
84+
{
85+
testName: 'permits ESM syntax in a .js file imported by an ESM entry point',
86+
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.mjs'),
87+
},
88+
]) {
89+
it(testName, async () => {
90+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
91+
'--experimental-detect-module',
92+
entryPath,
93+
]);
94+
95+
strictEqual(stderr, '');
96+
strictEqual(stdout, 'executed\n');
97+
strictEqual(code, 0);
98+
strictEqual(signal, null);
99+
});
100+
}
101+
});
102+
103+
describe('file input in a "type": "commonjs" package', { concurrency: true }, () => {
104+
for (const { testName, entryPath } of [
105+
{
106+
testName: 'disallows ESM syntax in a .js entry point',
107+
entryPath: fixtures.path('es-modules/package-type-commonjs/module.js'),
108+
},
109+
{
110+
testName: 'disallows ESM syntax in a .js file imported by a CommonJS entry point',
111+
entryPath: fixtures.path('es-modules/package-type-commonjs/imports-esm.js'),
112+
},
113+
{
114+
testName: 'disallows ESM syntax in a .js file imported by an ESM entry point',
115+
entryPath: fixtures.path('es-modules/package-type-commonjs/imports-esm.mjs'),
116+
},
117+
]) {
118+
it(testName, async () => {
119+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
120+
'--experimental-detect-module',
121+
entryPath,
122+
]);
123+
124+
match(stderr, /SyntaxError: Unexpected token 'export'/);
125+
strictEqual(stdout, '');
126+
strictEqual(code, 1);
127+
strictEqual(signal, null);
128+
});
129+
}
130+
});
131+
132+
describe('file input in a "type": "module" package', { concurrency: true }, () => {
133+
for (const { testName, entryPath } of [
134+
{
135+
testName: 'disallows CommonJS syntax in a .js entry point',
136+
entryPath: fixtures.path('es-modules/package-type-module/cjs.js'),
137+
},
138+
{
139+
testName: 'disallows CommonJS syntax in a .js file imported by a CommonJS entry point',
140+
entryPath: fixtures.path('es-modules/package-type-module/imports-commonjs.cjs'),
141+
},
142+
{
143+
testName: 'disallows CommonJS syntax in a .js file imported by an ESM entry point',
144+
entryPath: fixtures.path('es-modules/package-type-module/imports-commonjs.mjs'),
145+
},
146+
]) {
147+
it(testName, async () => {
148+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
149+
'--experimental-detect-module',
150+
entryPath,
151+
]);
152+
153+
match(stderr, /ReferenceError: module is not defined in ES module scope/);
154+
strictEqual(stdout, '');
155+
strictEqual(code, 1);
156+
strictEqual(signal, null);
157+
});
158+
}
17159
});
18-
})
160+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import('./module.js');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './module.js';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export default 'module';
2+
console.log('executed');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import('./cjs.js');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './cjs.js';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module.exports = 'cjs';
2+
console.log('executed');

0 commit comments

Comments
Β (0)