From b07ad39bdacf6a518726e9de476bb204e0261690 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 Backport-PR-URL: https://github.com/nodejs/node/pull/56927 Reviewed-By: Geoffrey Booth Reviewed-By: Jacob Smith Refs: https://github.com/nodejs/node/issues/52697 --- 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 | 122 +++++-- src/module_wrap.h | 21 +- src/node_contextify.cc | 420 ++++++++++--------------- src/node_contextify.h | 15 - 7 files changed, 303 insertions(+), 339 deletions(-) diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 1fe5564545dbc8..f07f2a42b260a3 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 dd4c3924a47037..d560c0d8089e19 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'); @@ -338,30 +329,6 @@ function urlToFilename(url) { return url; } -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; -} - // Whether we have started executing any user-provided CJS code. // This is set right before we call the wrapped CJS code (not after, @@ -396,7 +363,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 cfe00865d1f22a..7a99c386fe6ded 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, } = primordials; const { getOptionValue } = require('internal/options'); @@ -155,7 +157,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; @@ -163,19 +165,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; } } @@ -186,7 +190,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 unfinished TLA, this may never resolve diff --git a/src/module_wrap.cc b/src/module_wrap.cc index eea74bed4bb8a9..4cd75447b77526 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -102,9 +102,17 @@ ModuleWrap* ModuleWrap::GetFromModule(Environment* env, return nullptr; } -// 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()); @@ -134,7 +142,7 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { int column_offset = 0; bool synthetic = args[2]->IsArray(); - + bool can_use_builtin_cache = false; Local host_defined_options = PrimitiveArray::New(isolate, HostDefinedOptions::kLength); Local id_symbol; @@ -143,9 +151,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(); @@ -153,10 +162,13 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { column_offset = args[4].As()->Value(); if (args[5]->IsSymbol()) { id_symbol = args[5].As(); + can_use_builtin_cache = + (id_symbol == + realm->isolate_data()->source_text_module_default_hdo()); } 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(), @@ -189,36 +201,34 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { module = Module::CreateSyntheticModule(isolate, url, export_names, SyntheticModuleEvaluationStepsCallback); } else { - ScriptCompiler::CachedData* cached_data = nullptr; + // When we are compiling for the default loader, this will be + // std::nullopt, and CompileSourceTextModule() should use + // on-disk cache (not present on v20.x). + std::optional user_cached_data; + if (id_symbol != + realm->isolate_data()->source_text_module_default_hdo()) { + user_cached_data = nullptr; + } if (args[5]->IsArrayBufferView()) { + 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); - 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()); @@ -231,8 +241,9 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { } return; } - if (options == ScriptCompiler::kConsumeCodeCache && - source.GetCachedData()->rejected) { + + 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(); @@ -275,6 +286,57 @@ 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; + // 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(); + } + + 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; + } + + return scope.Escape(module); +} + static Local createImportAttributesContainer( Realm* realm, Isolate* isolate, diff --git a/src/module_wrap.h b/src/module_wrap.h index 45a338b38e01c8..09da8ee43fa8b4 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 { @@ -68,6 +70,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 0a409c5086f713..7eb7335975e219 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -363,16 +363,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); @@ -1196,15 +1192,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); @@ -1278,16 +1265,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); @@ -1335,39 +1333,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(), @@ -1473,160 +1438,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 @@ -1641,7 +1463,6 @@ static void CompileFunctionForCJSLoader( ScriptCompiler::CachedData* cached_data = nullptr; #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - bool used_cache_from_sea = false; if (sea::IsSingleExecutable()) { sea::SeaResource sea = sea::FindSingleExecutableResource(); if (sea.use_code_cache()) { @@ -1650,16 +1471,18 @@ static void CompileFunctionForCJSLoader( reinterpret_cast(data.data()), static_cast(data.size()), v8::ScriptCompiler::CachedData::BufferNotOwned); - used_cache_from_sea = true; } } #endif ScriptCompiler::Source source(code, origin, cached_data); - - TryCatchScope try_catch(env); + ScriptCompiler::CompileOptions options; + if (cached_data == nullptr) { + options = ScriptCompiler::kNoCompileOptions; + } else { + options = ScriptCompiler::kConsumeCodeCache; + } std::vector> params = GetCJSParameters(env->isolate_data()); - MaybeLocal maybe_fn = ScriptCompiler::CompileFunction( context, &source, @@ -1668,27 +1491,43 @@ static void CompileFunctionForCJSLoader( 0, /* context extensions size */ nullptr, /* context extensions data */ // TODO(joyeecheung): allow optional eager compilation. - cached_data == nullptr ? ScriptCompiler::kNoCompileOptions - : ScriptCompiler::kConsumeCodeCache, - 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()); } + if (options == ScriptCompiler::kConsumeCodeCache) { + *cache_rejected = source.GetCachedData()->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; -#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (used_cache_from_sea) { - cache_rejected = source.GetCachedData()->rejected; + 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; + } } -#endif std::vector> names = { env->cached_data_rejected_string(), @@ -1705,6 +1544,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); @@ -1761,6 +1702,9 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, target, "compileFunctionForCJSLoader", CompileFunctionForCJSLoader); + + SetMethod(isolate, target, "containsModuleSyntax", ContainsModuleSyntax); + SetMethod(isolate, target, "shouldRetryAsESM", ShouldRetryAsESM); } static void CreatePerContextProperties(Local target, @@ -1773,7 +1717,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()); @@ -1794,25 +1737,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(); } @@ -1825,6 +1749,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 ff975c8cf135e4..88b5684844b915 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -98,21 +98,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(