From a45e21956a5a8eb11d05d301ad99782b41f6013b Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sat, 14 Oct 2023 15:35:39 -0700 Subject: [PATCH] Detect ambiguous files and string input --- lib/internal/modules/esm/get_format.js | 25 +++++++++++++++---- lib/internal/modules/esm/load.js | 34 +++++++++++++++----------- lib/internal/modules/run_main.js | 16 +++++++++++- lib/internal/process/execution.js | 8 ++++++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 56d002ca0883ad..d970de730df435 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -21,6 +21,7 @@ const experimentalNetworkImports = const defaultTypeFlag = getOptionValue('--experimental-default-type'); // The next line is where we flip the default to ES modules someday. const defaultType = defaultTypeFlag === 'module' ? 'module' : 'commonjs'; +const { containsModuleSyntax } = internalBinding('contextify'); const { getPackageType } = require('internal/modules/esm/resolve'); const { fileURLToPath } = require('internal/url'); const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; @@ -90,6 +91,7 @@ function underNodeModules(url) { * @returns {string} */ function getFileProtocolModuleFormat(url, context, ignoreErrors) { + const { source } = context; const ext = extname(url); if (ext === '.js') { @@ -97,13 +99,26 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors) { if (packageType !== 'none') { return packageType; } + // The controlling `package.json` file has no `type` field. - if (defaultType === 'module') { - // An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules` - // should retain the assumption that a lack of a `type` field means CommonJS. - return underNodeModules(url) ? 'commonjs' : 'module'; + switch (getOptionValue('--experimental-default-type')) { + case 'module': { // The user explicitly passed `--experimental-default-type=module`. + // An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules` + // should retain the assumption that a lack of a `type` field means CommonJS. + return underNodeModules(url) ? 'commonjs' : 'module'; + } + case 'commonjs': { // The user explicitly passed `--experimental-default-type=commonjs`. + return 'commonjs'; + } + default: { // The user did not pass `--experimental-default-type`. + // `source` is undefined when this is called from `defaultResolve`; + // but this gets called again from `defaultLoad`/`defaultLoadSync`. + if (source && getOptionValue('--experimental-detect-module')) { + return containsModuleSyntax(`${source}`, fileURLToPath(url)) ? 'module' : 'commonjs'; + } + return 'commonjs'; + } } - return 'commonjs'; } if (ext === '') { diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index 1881745a6d3134..a3ebd3e5eb9a25 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -33,7 +33,7 @@ const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/; /** * @param {URL} url URL to the module * @param {ESModuleContext} context used to decorate error messages - * @returns {{ responseURL: string, source: string | BufferView }} + * @returns {Promise<{ responseURL: string, source: string | BufferView }>} */ async function getSource(url, context) { const { protocol, href } = url; @@ -127,19 +127,24 @@ async function defaultLoad(url, context = kEmptyObject) { throwIfUnsupportedURLScheme(urlInstance, experimentalNetworkImports); - format ??= await defaultGetFormat(urlInstance, context); - - validateAttributes(url, format, importAttributes); - - if ( - format === 'builtin' || - format === 'commonjs' - ) { + if (urlInstance.protocol === 'node:') { source = null; } else if (source == null) { ({ responseURL, source } = await getSource(urlInstance, context)); + context.source = source; + } + + if (format == null || format === 'commonjs') { + // Now that we have the source for the module, run `defaultGetFormat` again in case we detect ESM syntax. + format = await defaultGetFormat(urlInstance, context); + } + + if (format === 'commonjs') { + source = null; // Let the CommonJS loader handle it (for now) } + validateAttributes(url, format, importAttributes); + return { __proto__: null, format, @@ -178,16 +183,17 @@ function defaultLoadSync(url, context = kEmptyObject) { throwIfUnsupportedURLScheme(urlInstance, false); - format ??= defaultGetFormat(urlInstance, context); - - validateAttributes(url, format, importAttributes); - - if (format === 'builtin') { + if (urlInstance.protocol === 'node:') { source = null; } else if (source == null) { ({ responseURL, source } = getSourceSync(urlInstance, context)); + context.source = source; } + format ??= defaultGetFormat(urlInstance, context); + + validateAttributes(url, format, importAttributes); + return { __proto__: null, format, diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index a9828286a9c0e0..3bd7393fd725d4 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -4,7 +4,9 @@ const { StringPrototypeEndsWith, } = primordials; +const { containsModuleSyntax } = internalBinding('contextify'); const { getOptionValue } = require('internal/options'); +const fs = require('fs'); const path = require('path'); /** @@ -70,7 +72,19 @@ function shouldUseESMLoader(mainPath) { const { readPackageScope } = require('internal/modules/package_json_reader'); const pkg = readPackageScope(mainPath); // No need to guard `pkg` as it can only be an object or `false`. - return pkg.data?.type === 'module' || getOptionValue('--experimental-default-type') === 'module'; + switch (pkg.data?.type) { + case 'module': + return true; + case 'commonjs': + return false; + default: { // No package.json or no `type` field. + if (getOptionValue('--experimental-detect-module')) { + const content = fs.readFileSync(mainPath, 'utf8'); + return containsModuleSyntax(content, mainPath); + } + return false; + } + } } /** diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 9575fc580af9c3..181d7cc2f669c5 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -26,6 +26,8 @@ const { emitAfter, popAsyncContext, } = require('internal/async_hooks'); +const { containsModuleSyntax } = internalBinding('contextify'); +const { getOptionValue } = require('internal/options'); const { makeContextifyScript, runScriptInThisContext, } = require('internal/vm'); @@ -70,6 +72,12 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { const baseUrl = pathToFileURL(module.filename).href; const { loadESM } = asyncESM; + if (getOptionValue('--experimental-detect-module') && + getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '' && + containsModuleSyntax(body, name)) { + return evalModule(body, print); + } + const runScript = () => { // Create wrapper for cache entry const script = `