From 399917c6d51ee268e0c989bb39d3a82b3dfa7849 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sun, 8 Oct 2023 13:09:08 -0700 Subject: [PATCH] Detect ESM syntax in --eval --- lib/internal/modules/helpers.js | 21 +++++++++++ lib/internal/process/execution.js | 37 +++++++++++++------- test/es-module/test-esm-detect-ambiguous.mjs | 18 ++++++++++ 3 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 test/es-module/test-esm-detect-ambiguous.mjs diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 7f2959cc469dc1..8f8344eaab204e 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -5,6 +5,7 @@ const { ArrayPrototypeJoin, ArrayPrototypeSome, ObjectDefineProperty, + ObjectGetPrototypeOf, ObjectPrototypeHasOwnProperty, SafeMap, SafeSet, @@ -12,6 +13,7 @@ const { StringPrototypeIncludes, StringPrototypeSlice, StringPrototypeStartsWith, + SyntaxErrorPrototype, } = primordials; const { ERR_INVALID_ARG_TYPE, @@ -320,10 +322,29 @@ function hasEsmSyntax(code) { stmt.type === 'ExportAllDeclaration'); } +/** + * Check if the error is a syntax error due to ESM syntax in CommonJS. + * - `import` statements return an error with a message `Cannot use import statement outside a module`. + * - `export` statements return an error with a message `Unexpected token 'export'`. + * - `import.meta` returns an error with a message `Cannot use 'import.meta' outside a module`. + * Top-level `await` currently returns the same error message as when `await` is used in a sync function, + * so we don't use it as a disambiguation. + * Dynamic `import()` is permitted in CommonJS, so we don't use it as a disambiguation. + * @param {Error} err + */ +function isModuleSyntaxError(err) { + return err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype && ( + err.message === 'Cannot use import statement outside a module' || + err.message === "Unexpected token 'export'" || + err.message === "Cannot use 'import.meta' outside a module" + ); +} + module.exports = { addBuiltinLibsToObject, getCjsConditions, initializeCjsConditions, + isModuleSyntaxError, hasEsmSyntax, loadBuiltinModule, makeRequireFunction, diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 4b77aa47c2cb35..443d038718ff32 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -79,19 +79,30 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { `; globalThis.__filename = name; RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. - const result = module._compile(script, `${name}-wrapper`)(() => - require('vm').runInThisContext(body, { - filename: name, - displayErrors: true, - [kVmBreakFirstLineSymbol]: !!breakFirstLine, - importModuleDynamically(specifier, _, importAssertions) { - const loader = asyncESM.esmLoader; - return loader.import(specifier, baseUrl, importAssertions); - }, - })); - if (print) { - const { log } = require('internal/console/global'); - log(result); + try { + const result = module._compile(script, `${name}-wrapper`)(() => + require('vm').runInThisContext(body, { + filename: name, + displayErrors: true, + [kVmBreakFirstLineSymbol]: !!breakFirstLine, + importModuleDynamically(specifier, _, importAssertions) { + const loader = asyncESM.esmLoader; + return loader.import(specifier, baseUrl, importAssertions); + }, + })); + if (print) { + const { log } = require('internal/console/global'); + log(result); + } + } catch (error) { + const { getOptionValue } = require('internal/options'); + if (getOptionValue('--experimental-detect-module') && getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '') { + const { isModuleSyntaxError } = require('internal/modules/helpers'); + if (isModuleSyntaxError(error)) { + return evalModule(body, print); + } + } + throw error; } if (origModule !== undefined) diff --git a/test/es-module/test-esm-detect-ambiguous.mjs b/test/es-module/test-esm-detect-ambiguous.mjs new file mode 100644 index 00000000000000..cf0805c7fe46ba --- /dev/null +++ b/test/es-module/test-esm-detect-ambiguous.mjs @@ -0,0 +1,18 @@ +import { spawnPromisified } from '../common/index.mjs'; +import { describe, it } from 'node:test'; +import { strictEqual } from 'node:assert'; + +describe('--experimental-detect-module', () => { + it('permits ESM syntax in --eval input without requiring --input-type=module', async () => { + const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [ + '--experimental-detect-module', + '--eval', + 'import { version } from "node:process"; console.log(version);', + ]); + + strictEqual(stderr, ''); + strictEqual(stdout, `${process.version}\n`); + strictEqual(code, 0); + strictEqual(signal, null); + }); +})