From 3df3afc2847b6653802c96e0dc5cc840886815cf Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 19 Apr 2024 16:29:08 +0200 Subject: [PATCH] module: detect ESM syntax by trying to recompile as SourceTextModule Instead of using an async function wrapper, just try compiling code with unknown module format as SourceTextModule when it cannot be compiled as CJS and the error message indicates that it's worth a retry. If it can be parsed as SourceTextModule then it's considered ESM. Also, move shouldRetryAsESM() to C++ completely so that we can reuse it in the CJS module loader for require(esm). Drive-by: move methods that don't belong to ContextifyContext out as static methods and move GetHostDefinedOptions to ModuleWrap. PR-URL: https://github.com/nodejs/node/pull/52413 Reviewed-By: Geoffrey Booth Reviewed-By: Jacob Smith --- lib/internal/modules/esm/get_format.js | 4 +- lib/internal/modules/helpers.js | 34 -- lib/internal/modules/run_main.js | 26 +- src/module_wrap.cc | 152 +++++---- src/module_wrap.h | 21 +- src/node_contextify.cc | 411 ++++++++++--------------- src/node_contextify.h | 15 - 7 files changed, 306 insertions(+), 357 deletions(-) diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 5e653c81c6e30d..d79e24c4188d4a 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -114,7 +114,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE // but this gets called again from `defaultLoad`/`defaultLoadSync`. if (getOptionValue('--experimental-detect-module')) { const format = source ? - (containsModuleSyntax(`${source}`, fileURLToPath(url)) ? 'module' : 'commonjs') : + (containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs') : null; if (format === 'module') { // This module has a .js extension, a package.json with no `type` field, and ESM syntax. @@ -158,7 +158,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE if (!source) { return null; } const format = getFormatOfExtensionlessFile(url); if (format === 'module') { - return containsModuleSyntax(`${source}`, fileURLToPath(url)) ? 'module' : 'commonjs'; + return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs'; } return format; } diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index b39dc5e6638671..f7ad4cd1be6c8d 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -19,15 +19,6 @@ const { } = require('internal/errors').codes; const { BuiltinModule } = require('internal/bootstrap/realm'); -const { - shouldRetryAsESM: contextifyShouldRetryAsESM, - constants: { - syntaxDetectionErrors: { - esmSyntaxErrorMessages, - throwsOnlyInCommonJSErrorMessages, - }, - }, -} = internalBinding('contextify'); const { validateString } = require('internal/validators'); const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched. const internalFS = require('internal/fs/utils'); @@ -329,30 +320,6 @@ function normalizeReferrerURL(referrerName) { } -let esmSyntaxErrorMessagesSet; // Declared lazily in shouldRetryAsESM -let throwsOnlyInCommonJSErrorMessagesSet; // Declared lazily in shouldRetryAsESM -/** - * After an attempt to parse a module as CommonJS throws an error, should we try again as ESM? - * We only want to try again as ESM if the error is due to syntax that is only valid in ESM; and if the CommonJS parse - * throws on an error that would not have been a syntax error in ESM (like via top-level `await` or a lexical - * redeclaration of one of the CommonJS variables) then we need to parse again to see if it would have thrown in ESM. - * @param {string} errorMessage The string message thrown by V8 when attempting to parse as CommonJS - * @param {string} source Module contents - */ -function shouldRetryAsESM(errorMessage, source) { - esmSyntaxErrorMessagesSet ??= new SafeSet(esmSyntaxErrorMessages); - if (esmSyntaxErrorMessagesSet.has(errorMessage)) { - return true; - } - - throwsOnlyInCommonJSErrorMessagesSet ??= new SafeSet(throwsOnlyInCommonJSErrorMessages); - if (throwsOnlyInCommonJSErrorMessagesSet.has(errorMessage)) { - return /** @type {boolean} */(contextifyShouldRetryAsESM(source)); - } - - return false; -} - /** * @param {string|undefined} url URL to convert to filename */ @@ -382,7 +349,6 @@ module.exports = { loadBuiltinModule, makeRequireFunction, normalizeReferrerURL, - shouldRetryAsESM, stripBOM, toRealPath, hasStartedUserCJSExecution() { diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index c06c8cc72cc9f4..fd592f45ffd3e9 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -1,7 +1,9 @@ 'use strict'; const { + ObjectGetPrototypeOf, StringPrototypeEndsWith, + SyntaxErrorPrototype, globalThis, } = primordials; @@ -159,7 +161,7 @@ function runEntryPointWithESMLoader(callback) { function executeUserEntryPoint(main = process.argv[1]) { const resolvedMain = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(resolvedMain); - + let mainURL; // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM. let retryAsESM = false; @@ -167,19 +169,21 @@ function executeUserEntryPoint(main = process.argv[1]) { const cjsLoader = require('internal/modules/cjs/loader'); const { Module } = cjsLoader; if (getOptionValue('--experimental-detect-module')) { + // TODO(joyeecheung): handle this in the CJS loader. Don't try-catch here. try { // Module._load is the monkey-patchable CJS module loader. Module._load(main, null, true); } catch (error) { - const source = cjsLoader.entryPointSource; - const { shouldRetryAsESM } = require('internal/modules/helpers'); - retryAsESM = shouldRetryAsESM(error.message, source); - // In case the entry point is a large file, such as a bundle, - // ensure no further references can prevent it being garbage-collected. - cjsLoader.entryPointSource = undefined; + if (error != null && ObjectGetPrototypeOf(error) === SyntaxErrorPrototype) { + const { shouldRetryAsESM } = internalBinding('contextify'); + const mainPath = resolvedMain || main; + mainURL = pathToFileURL(mainPath).href; + retryAsESM = shouldRetryAsESM(error.message, cjsLoader.entryPointSource, mainURL); + // In case the entry point is a large file, such as a bundle, + // ensure no further references can prevent it being garbage-collected. + cjsLoader.entryPointSource = undefined; + } if (!retryAsESM) { - const { enrichCJSError } = require('internal/modules/esm/translators'); - enrichCJSError(error, source, resolvedMain); throw error; } } @@ -190,7 +194,9 @@ function executeUserEntryPoint(main = process.argv[1]) { if (useESMLoader || retryAsESM) { const mainPath = resolvedMain || main; - const mainURL = pathToFileURL(mainPath).href; + if (mainURL === undefined) { + mainURL = pathToFileURL(mainPath).href; + } runEntryPointWithESMLoader((cascadedLoader) => { // Note that if the graph contains unsettled TLA, this may never resolve diff --git a/src/module_wrap.cc b/src/module_wrap.cc index c80ef95b7a78f1..fd3d9c0ceaf317 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -142,9 +142,17 @@ v8::Maybe ModuleWrap::CheckUnsettledTopLevelAwait() { return v8::Just(false); } -// new ModuleWrap(url, context, source, lineOffset, columnOffset, cachedData) +Local ModuleWrap::GetHostDefinedOptions( + Isolate* isolate, Local id_symbol) { + Local host_defined_options = + PrimitiveArray::New(isolate, HostDefinedOptions::kLength); + host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol); + return host_defined_options; +} + +// new ModuleWrap(url, context, source, lineOffset, columnOffset[, cachedData]); // new ModuleWrap(url, context, source, lineOffset, columOffset, -// hostDefinedOption) +// idSymbol); // new ModuleWrap(url, context, exportNames, evaluationCallback[, cjsModule]) void ModuleWrap::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); @@ -183,9 +191,10 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { // cjsModule]) CHECK(args[3]->IsFunction()); } else { - // new ModuleWrap(url, context, source, lineOffset, columOffset, cachedData) + // new ModuleWrap(url, context, source, lineOffset, columOffset[, + // cachedData]); // new ModuleWrap(url, context, source, lineOffset, columOffset, - // hostDefinedOption) + // idSymbol); CHECK(args[2]->IsString()); CHECK(args[3]->IsNumber()); line_offset = args[3].As()->Value(); @@ -199,7 +208,7 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { } else { id_symbol = Symbol::New(isolate, url); } - host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol); + host_defined_options = GetHostDefinedOptions(isolate, id_symbol); if (that->SetPrivate(context, realm->isolate_data()->host_defined_option_symbol(), @@ -234,50 +243,34 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { module = Module::CreateSyntheticModule( isolate, url, span, SyntheticModuleEvaluationStepsCallback); } else { - ScriptCompiler::CachedData* cached_data = nullptr; - bool used_cache_from_user = false; + // When we are compiling for the default loader, this will be + // std::nullopt, and CompileSourceTextModule() should use + // on-disk cache. + std::optional user_cached_data; + if (id_symbol != + realm->isolate_data()->source_text_module_default_hdo()) { + user_cached_data = nullptr; + } if (args[5]->IsArrayBufferView()) { - DCHECK(!can_use_builtin_cache); // We don't use this option internally. - used_cache_from_user = true; + CHECK(!can_use_builtin_cache); // We don't use this option internally. Local cached_data_buf = args[5].As(); uint8_t* data = static_cast(cached_data_buf->Buffer()->Data()); - cached_data = + user_cached_data = new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); } - Local source_text = args[2].As(); - ScriptOrigin origin(isolate, - url, - line_offset, - column_offset, - true, // is cross origin - -1, // script id - Local(), // source map URL - false, // is opaque (?) - false, // is WASM - true, // is ES Module - host_defined_options); - - CompileCacheEntry* cache_entry = nullptr; - if (can_use_builtin_cache && realm->env()->use_compile_cache()) { - cache_entry = realm->env()->compile_cache_handler()->GetOrInsert( - source_text, url, CachedCodeType::kESM); - } - if (cache_entry != nullptr && cache_entry->cache != nullptr) { - // source will take ownership of cached_data. - cached_data = cache_entry->CopyCache(); - } - ScriptCompiler::Source source(source_text, origin, cached_data); - ScriptCompiler::CompileOptions options; - if (source.GetCachedData() == nullptr) { - options = ScriptCompiler::kNoCompileOptions; - } else { - options = ScriptCompiler::kConsumeCodeCache; - } - if (!ScriptCompiler::CompileModule(isolate, &source, options) + bool cache_rejected = false; + if (!CompileSourceTextModule(realm, + source_text, + url, + line_offset, + column_offset, + host_defined_options, + user_cached_data, + &cache_rejected) .ToLocal(&module)) { if (try_catch.HasCaught() && !try_catch.HasTerminated()) { CHECK(!try_catch.Message().IsEmpty()); @@ -291,18 +284,8 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { return; } - bool cache_rejected = false; - if (options == ScriptCompiler::kConsumeCodeCache) { - cache_rejected = source.GetCachedData()->rejected; - } - - if (cache_entry != nullptr) { - realm->env()->compile_cache_handler()->MaybeSave( - cache_entry, module, cache_rejected); - } - - // If the cache comes from builtin compile cache, fail silently. - if (cache_rejected && used_cache_from_user) { + if (user_cached_data.has_value() && user_cached_data.value() != nullptr && + cache_rejected) { THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED( realm, "cachedData buffer was rejected"); try_catch.ReThrow(); @@ -345,6 +328,71 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(that); } +MaybeLocal ModuleWrap::CompileSourceTextModule( + Realm* realm, + Local source_text, + Local url, + int line_offset, + int column_offset, + Local host_defined_options, + std::optional user_cached_data, + bool* cache_rejected) { + Isolate* isolate = realm->isolate(); + EscapableHandleScope scope(isolate); + ScriptOrigin origin(isolate, + url, + line_offset, + column_offset, + true, // is cross origin + -1, // script id + Local(), // source map URL + false, // is opaque (?) + false, // is WASM + true, // is ES Module + host_defined_options); + ScriptCompiler::CachedData* cached_data = nullptr; + CompileCacheEntry* cache_entry = nullptr; + // When compiling for the default loader, user_cached_data is std::nullptr. + // When compiling for vm.Module, it's either nullptr or a pointer to the + // cached data. + if (user_cached_data.has_value()) { + cached_data = user_cached_data.value(); + } else if (realm->env()->use_compile_cache()) { + cache_entry = realm->env()->compile_cache_handler()->GetOrInsert( + source_text, url, CachedCodeType::kESM); + } + + if (cache_entry != nullptr && cache_entry->cache != nullptr) { + // source will take ownership of cached_data. + cached_data = cache_entry->CopyCache(); + } + + ScriptCompiler::Source source(source_text, origin, cached_data); + ScriptCompiler::CompileOptions options; + if (cached_data == nullptr) { + options = ScriptCompiler::kNoCompileOptions; + } else { + options = ScriptCompiler::kConsumeCodeCache; + } + + Local module; + if (!ScriptCompiler::CompileModule(isolate, &source, options) + .ToLocal(&module)) { + return scope.EscapeMaybe(MaybeLocal()); + } + + if (options == ScriptCompiler::kConsumeCodeCache) { + *cache_rejected = source.GetCachedData()->rejected; + } + + if (cache_entry != nullptr) { + realm->env()->compile_cache_handler()->MaybeSave( + cache_entry, module, *cache_rejected); + } + + return scope.Escape(module); +} + static Local createImportAttributesContainer( Realm* realm, Isolate* isolate, diff --git a/src/module_wrap.h b/src/module_wrap.h index 7c40ef08ea00f8..bd50bce8ad2add 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -3,10 +3,12 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include +#include #include +#include #include #include "base_object.h" +#include "v8-script.h" namespace node { @@ -69,6 +71,23 @@ class ModuleWrap : public BaseObject { return true; } + static v8::Local GetHostDefinedOptions( + v8::Isolate* isolate, v8::Local symbol); + + // When user_cached_data is not std::nullopt, use the code cache if it's not + // nullptr, otherwise don't use code cache. + // TODO(joyeecheung): when it is std::nullopt, use on-disk cache + // See: https://github.com/nodejs/node/issues/47472 + static v8::MaybeLocal CompileSourceTextModule( + Realm* realm, + v8::Local source_text, + v8::Local url, + int line_offset, + int column_offset, + v8::Local host_defined_options, + std::optional user_cached_data, + bool* cache_rejected); + private: ModuleWrap(Realm* realm, v8::Local object, diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 280607617edcd7..733cee2cae2815 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -344,16 +344,12 @@ void ContextifyContext::CreatePerIsolateProperties( Isolate* isolate = isolate_data->isolate(); SetMethod(isolate, target, "makeContext", MakeContext); SetMethod(isolate, target, "compileFunction", CompileFunction); - SetMethod(isolate, target, "containsModuleSyntax", ContainsModuleSyntax); - SetMethod(isolate, target, "shouldRetryAsESM", ShouldRetryAsESM); } void ContextifyContext::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(MakeContext); registry->Register(CompileFunction); - registry->Register(ContainsModuleSyntax); - registry->Register(ShouldRetryAsESM); registry->Register(PropertyGetterCallback); registry->Register(PropertySetterCallback); registry->Register(PropertyDescriptorCallback); @@ -1159,15 +1155,6 @@ ContextifyScript::ContextifyScript(Environment* env, Local object) ContextifyScript::~ContextifyScript() {} -static Local GetHostDefinedOptions(Isolate* isolate, - Local id_symbol) { - Local host_defined_options = - PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength); - host_defined_options->Set( - isolate, loader::HostDefinedOptions::kID, id_symbol); - return host_defined_options; -} - void ContextifyContext::CompileFunction( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1241,16 +1228,27 @@ void ContextifyContext::CompileFunction( } Local host_defined_options = - GetHostDefinedOptions(isolate, id_symbol); - ScriptCompiler::Source source = - GetCommonJSSourceInstance(isolate, - code, - filename, - line_offset, - column_offset, - host_defined_options, - cached_data); - ScriptCompiler::CompileOptions options = GetCompileOptions(source); + loader::ModuleWrap::GetHostDefinedOptions(isolate, id_symbol); + + ScriptOrigin origin(isolate, + filename, + line_offset, // line offset + column_offset, // column offset + true, // is cross origin + -1, // script id + Local(), // source map URL + false, // is opaque (?) + false, // is WASM + false, // is ES Module + host_defined_options); + ScriptCompiler::Source source(code, origin, cached_data); + + ScriptCompiler::CompileOptions options; + if (source.GetCachedData() != nullptr) { + options = ScriptCompiler::kConsumeCodeCache; + } else { + options = ScriptCompiler::kNoCompileOptions; + } Context::Scope scope(parsing_context); @@ -1298,39 +1296,6 @@ void ContextifyContext::CompileFunction( args.GetReturnValue().Set(result); } -ScriptCompiler::Source ContextifyContext::GetCommonJSSourceInstance( - Isolate* isolate, - Local code, - Local filename, - int line_offset, - int column_offset, - Local host_defined_options, - ScriptCompiler::CachedData* cached_data) { - ScriptOrigin origin(isolate, - filename, - line_offset, // line offset - column_offset, // column offset - true, // is cross origin - -1, // script id - Local(), // source map URL - false, // is opaque (?) - false, // is WASM - false, // is ES Module - host_defined_options); - return ScriptCompiler::Source(code, origin, cached_data); -} - -ScriptCompiler::CompileOptions ContextifyContext::GetCompileOptions( - const ScriptCompiler::Source& source) { - ScriptCompiler::CompileOptions options; - if (source.GetCachedData() != nullptr) { - options = ScriptCompiler::kConsumeCodeCache; - } else { - options = ScriptCompiler::kNoCompileOptions; - } - return options; -} - static std::vector> GetCJSParameters(IsolateData* data) { return { data->exports_string(), @@ -1436,160 +1401,17 @@ static std::vector throws_only_in_cjs_error_messages = { "await is only valid in async functions and " "the top level bodies of modules"}; -void ContextifyContext::ContainsModuleSyntax( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - Local context = env->context(); - - if (args.Length() == 0) { - return THROW_ERR_MISSING_ARGS( - env, "containsModuleSyntax needs at least 1 argument"); - } - - // Argument 1: source code - CHECK(args[0]->IsString()); - auto code = args[0].As(); - - // Argument 2: filename; if undefined, use empty string - Local filename = String::Empty(isolate); - if (!args[1]->IsUndefined()) { - CHECK(args[1]->IsString()); - filename = args[1].As(); - } - - // TODO(geoffreybooth): Centralize this rather than matching the logic in - // cjs/loader.js and translators.js - Local script_id = String::Concat( - isolate, String::NewFromUtf8(isolate, "cjs:").ToLocalChecked(), filename); - Local id_symbol = Symbol::New(isolate, script_id); - - Local host_defined_options = - GetHostDefinedOptions(isolate, id_symbol); - ScriptCompiler::Source source = GetCommonJSSourceInstance( - isolate, code, filename, 0, 0, host_defined_options, nullptr); - ScriptCompiler::CompileOptions options = GetCompileOptions(source); - - std::vector> params = GetCJSParameters(env->isolate_data()); - - TryCatchScope try_catch(env); - ShouldNotAbortOnUncaughtScope no_abort_scope(env); - - ContextifyContext::CompileFunctionAndCacheResult(env, - context, - &source, - params, - std::vector>(), - options, - true, - id_symbol, - try_catch); - - bool should_retry_as_esm = false; - if (try_catch.HasCaught() && !try_catch.HasTerminated()) { - should_retry_as_esm = - ContextifyContext::ShouldRetryAsESMInternal(env, code); - } - args.GetReturnValue().Set(should_retry_as_esm); -} - -void ContextifyContext::ShouldRetryAsESM( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - CHECK_EQ(args.Length(), 1); // code - - // Argument 1: source code - Local code; - CHECK(args[0]->IsString()); - code = args[0].As(); - - bool should_retry_as_esm = - ContextifyContext::ShouldRetryAsESMInternal(env, code); - - args.GetReturnValue().Set(should_retry_as_esm); -} - -bool ContextifyContext::ShouldRetryAsESMInternal(Environment* env, - Local code) { - Isolate* isolate = env->isolate(); - - Local script_id = - FIXED_ONE_BYTE_STRING(isolate, "[retry_as_esm_check]"); - Local id_symbol = Symbol::New(isolate, script_id); - - Local host_defined_options = - GetHostDefinedOptions(isolate, id_symbol); - ScriptCompiler::Source source = - GetCommonJSSourceInstance(isolate, - code, - script_id, // filename - 0, // line offset - 0, // column offset - host_defined_options, - nullptr); // cached_data - - TryCatchScope try_catch(env); - ShouldNotAbortOnUncaughtScope no_abort_scope(env); - - // Try parsing where instead of the CommonJS wrapper we use an async function - // wrapper. If the parse succeeds, then any CommonJS parse error for this - // module was caused by either a top-level declaration of one of the CommonJS - // module variables, or a top-level `await`. - code = String::Concat( - isolate, FIXED_ONE_BYTE_STRING(isolate, "(async function() {"), code); - code = String::Concat(isolate, code, FIXED_ONE_BYTE_STRING(isolate, "})();")); - - ScriptCompiler::Source wrapped_source = GetCommonJSSourceInstance( - isolate, code, script_id, 0, 0, host_defined_options, nullptr); - - Local context = env->context(); - std::vector> params = GetCJSParameters(env->isolate_data()); - USE(ScriptCompiler::CompileFunction( - context, - &wrapped_source, - params.size(), - params.data(), - 0, - nullptr, - ScriptCompiler::kNoCompileOptions, - v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason)); - - if (!try_catch.HasTerminated()) { - if (try_catch.HasCaught()) { - // If on the second parse an error is thrown by ESM syntax, then - // what happened was that the user had top-level `await` or a - // top-level declaration of one of the CommonJS module variables - // above the first `import` or `export`. - Utf8Value message_value(env->isolate(), try_catch.Message()->Get()); - auto message_view = message_value.ToStringView(); - for (const auto& error_message : esm_syntax_error_messages) { - if (message_view.find(error_message) != std::string_view::npos) { - return true; - } - } - } else { - // No errors thrown in the second parse, so most likely the error - // was caused by a top-level `await` or a top-level declaration of - // one of the CommonJS module variables. - return true; - } - } - return false; -} - -static void CompileFunctionForCJSLoader( - const FunctionCallbackInfo& args) { - CHECK(args[0]->IsString()); - CHECK(args[1]->IsString()); - Local code = args[0].As(); - Local filename = args[1].As(); - Isolate* isolate = args.GetIsolate(); - Local context = isolate->GetCurrentContext(); - Environment* env = Environment::GetCurrent(context); +static MaybeLocal CompileFunctionForCJSLoader(Environment* env, + Local context, + Local code, + Local filename, + bool* cache_rejected) { + Isolate* isolate = context->GetIsolate(); + EscapableHandleScope scope(isolate); Local symbol = env->vm_dynamic_import_default_internal(); - Local hdo = GetHostDefinedOptions(isolate, symbol); + Local hdo = + loader::ModuleWrap::GetHostDefinedOptions(isolate, symbol); ScriptOrigin origin(isolate, filename, 0, // line offset @@ -1636,10 +1458,7 @@ static void CompileFunctionForCJSLoader( options = ScriptCompiler::kConsumeCodeCache; } - TryCatchScope try_catch(env); - std::vector> params = GetCJSParameters(env->isolate_data()); - MaybeLocal maybe_fn = ScriptCompiler::CompileFunction( context, &source, @@ -1648,26 +1467,45 @@ static void CompileFunctionForCJSLoader( 0, /* context extensions size */ nullptr, /* context extensions data */ // TODO(joyeecheung): allow optional eager compilation. - options, - v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason); + options); Local fn; if (!maybe_fn.ToLocal(&fn)) { - if (try_catch.HasCaught() && !try_catch.HasTerminated()) { - errors::DecorateErrorStack(env, try_catch); - if (!try_catch.HasTerminated()) { - try_catch.ReThrow(); - } - return; - } + return scope.EscapeMaybe(MaybeLocal()); } - bool cache_rejected = false; if (options == ScriptCompiler::kConsumeCodeCache) { - cache_rejected = source.GetCachedData()->rejected; + *cache_rejected = source.GetCachedData()->rejected; } if (cache_entry != nullptr) { - env->compile_cache_handler()->MaybeSave(cache_entry, fn, cache_rejected); + env->compile_cache_handler()->MaybeSave(cache_entry, fn, *cache_rejected); + } + return scope.Escape(fn); +} + +static void CompileFunctionForCJSLoader( + const FunctionCallbackInfo& args) { + CHECK(args[0]->IsString()); + CHECK(args[1]->IsString()); + Local code = args[0].As(); + Local filename = args[1].As(); + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + + bool cache_rejected = false; + Local fn; + { + TryCatchScope try_catch(env); + if (!CompileFunctionForCJSLoader( + env, context, code, filename, &cache_rejected) + .ToLocal(&fn)) { + CHECK(try_catch.HasCaught()); + CHECK(!try_catch.HasTerminated()); + errors::DecorateErrorStack(env, try_catch); + try_catch.ReThrow(); + return; + } } std::vector> names = { @@ -1685,6 +1523,108 @@ static void CompileFunctionForCJSLoader( args.GetReturnValue().Set(result); } +static bool ShouldRetryAsESM(Realm* realm, + Local message, + Local code, + Local resource_name) { + Isolate* isolate = realm->isolate(); + + Utf8Value message_value(isolate, message); + auto message_view = message_value.ToStringView(); + + // These indicates that the file contains syntaxes that are only valid in + // ESM. So it must be true. + for (const auto& error_message : esm_syntax_error_messages) { + if (message_view.find(error_message) != std::string_view::npos) { + return true; + } + } + + // Check if the error message is allowed in ESM but not in CommonJS. If it + // is the case, let's check if file can be compiled as ESM. + bool maybe_valid_in_esm = false; + for (const auto& error_message : throws_only_in_cjs_error_messages) { + if (message_view.find(error_message) != std::string_view::npos) { + maybe_valid_in_esm = true; + break; + } + } + if (!maybe_valid_in_esm) { + return false; + } + + bool cache_rejected = false; + TryCatchScope try_catch(realm->env()); + ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env()); + Local module; + Local hdo = loader::ModuleWrap::GetHostDefinedOptions( + isolate, realm->isolate_data()->source_text_module_default_hdo()); + if (loader::ModuleWrap::CompileSourceTextModule( + realm, code, resource_name, 0, 0, hdo, nullptr, &cache_rejected) + .ToLocal(&module)) { + return true; + } + + return false; +} + +static void ShouldRetryAsESM(const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + + CHECK_EQ(args.Length(), 3); // message, code, resource_name + CHECK(args[0]->IsString()); + Local message = args[0].As(); + CHECK(args[1]->IsString()); + Local code = args[1].As(); + CHECK(args[2]->IsString()); + Local resource_name = args[2].As(); + + args.GetReturnValue().Set( + ShouldRetryAsESM(realm, message, code, resource_name)); +} + +static void ContainsModuleSyntax(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Realm* realm = Realm::GetCurrent(context); + Environment* env = realm->env(); + + CHECK_GE(args.Length(), 2); + + // Argument 1: source code + CHECK(args[0]->IsString()); + Local code = args[0].As(); + + // Argument 2: filename + CHECK(args[1]->IsString()); + Local filename = args[1].As(); + + // Argument 2: resource name (URL for ES module). + Local resource_name = filename; + if (args[2]->IsString()) { + resource_name = args[2].As(); + } + + bool cache_rejected = false; + Local message; + { + Local fn; + TryCatchScope try_catch(env); + ShouldNotAbortOnUncaughtScope no_abort_scope(env); + if (CompileFunctionForCJSLoader( + env, context, code, filename, &cache_rejected) + .ToLocal(&fn)) { + args.GetReturnValue().Set(false); + return; + } + CHECK(try_catch.HasCaught()); + message = try_catch.Message()->Get(); + } + + bool result = ShouldRetryAsESM(realm, message, code, resource_name); + args.GetReturnValue().Set(result); +} + static void StartSigintWatchdog(const FunctionCallbackInfo& args) { int ret = SigintWatchdogHelper::GetInstance()->Start(); args.GetReturnValue().Set(ret == 0); @@ -1741,6 +1681,9 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, target, "compileFunctionForCJSLoader", CompileFunctionForCJSLoader); + + SetMethod(isolate, target, "containsModuleSyntax", ContainsModuleSyntax); + SetMethod(isolate, target, "shouldRetryAsESM", ShouldRetryAsESM); } static void CreatePerContextProperties(Local target, @@ -1753,7 +1696,6 @@ static void CreatePerContextProperties(Local target, Local constants = Object::New(env->isolate()); Local measure_memory = Object::New(env->isolate()); Local memory_execution = Object::New(env->isolate()); - Local syntax_detection_errors = Object::New(env->isolate()); { Local memory_mode = Object::New(env->isolate()); @@ -1774,25 +1716,6 @@ static void CreatePerContextProperties(Local target, READONLY_PROPERTY(constants, "measureMemory", measure_memory); - { - Local esm_syntax_error_messages_array = - ToV8Value(context, esm_syntax_error_messages).ToLocalChecked(); - READONLY_PROPERTY(syntax_detection_errors, - "esmSyntaxErrorMessages", - esm_syntax_error_messages_array); - } - - { - Local throws_only_in_cjs_error_messages_array = - ToV8Value(context, throws_only_in_cjs_error_messages).ToLocalChecked(); - READONLY_PROPERTY(syntax_detection_errors, - "throwsOnlyInCommonJSErrorMessages", - throws_only_in_cjs_error_messages_array); - } - - READONLY_PROPERTY( - constants, "syntaxDetectionErrors", syntax_detection_errors); - target->Set(context, env->constants_string(), constants).Check(); } @@ -1805,6 +1728,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(StopSigintWatchdog); registry->Register(WatchdogHasPendingSigint); registry->Register(MeasureMemory); + registry->Register(ContainsModuleSyntax); + registry->Register(ShouldRetryAsESM); } } // namespace contextify } // namespace node diff --git a/src/node_contextify.h b/src/node_contextify.h index 66a9f765fe9212..b5cc4e546f4ab7 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -94,21 +94,6 @@ class ContextifyContext : public BaseObject { bool produce_cached_data, v8::Local id_symbol, const errors::TryCatchScope& try_catch); - static v8::ScriptCompiler::Source GetCommonJSSourceInstance( - v8::Isolate* isolate, - v8::Local code, - v8::Local filename, - int line_offset, - int column_offset, - v8::Local host_defined_options, - v8::ScriptCompiler::CachedData* cached_data); - static v8::ScriptCompiler::CompileOptions GetCompileOptions( - const v8::ScriptCompiler::Source& source); - static void ContainsModuleSyntax( - const v8::FunctionCallbackInfo& args); - static void ShouldRetryAsESM(const v8::FunctionCallbackInfo& args); - static bool ShouldRetryAsESMInternal(Environment* env, - v8::Local code); static void WeakCallback( const v8::WeakCallbackInfo& data); static void PropertyGetterCallback(