GeezerVison is showing github.com — module: detect ESM syntax by trying to recompile as SourceTextModule · nodejs/node@b07ad39
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/get_format.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -114,7 +114,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE | 114 | 114 | // but this gets called again from `defaultLoad`/`defaultLoadSync`. | 115 | 115 | if (getOptionValue('--experimental-detect-module')) { | 116 | 116 | const format = source ? | 117 | | - (containsModuleSyntax(`${source}`, fileURLToPath(url)) ? 'module' : 'commonjs') : | | 117 | + (containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs') : | 118 | 118 | null; | 119 | 119 | if (format === 'module') { | 120 | 120 | // 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 | 158 | 158 | if (!source) { return null; } | 159 | 159 | const format = getFormatOfExtensionlessFile(url); | 160 | 160 | if (format === 'module') { | 161 | | - return containsModuleSyntax(`${source}`, fileURLToPath(url)) ? 'module' : 'commonjs'; | | 161 | + return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs'; | 162 | 162 | } | 163 | 163 | return format; | 164 | 164 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/helpers.js -34Lines changed: 0 additions & 34 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -19,15 +19,6 @@ const { | 19 | 19 | } = require('internal/errors').codes; | 20 | 20 | const { BuiltinModule } = require('internal/bootstrap/realm'); | 21 | 21 |
| 22 | | -const { | 23 | | - shouldRetryAsESM: contextifyShouldRetryAsESM, | 24 | | - constants: { | 25 | | - syntaxDetectionErrors: { | 26 | | - esmSyntaxErrorMessages, | 27 | | - throwsOnlyInCommonJSErrorMessages, | 28 | | - }, | 29 | | - }, | 30 | | -} = internalBinding('contextify'); | 31 | 22 | const { validateString } = require('internal/validators'); | 32 | 23 | const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched. | 33 | 24 | const internalFS = require('internal/fs/utils'); | @@ -338,30 +329,6 @@ function urlToFilename(url) { | 338 | 329 | return url; | 339 | 330 | } | 340 | 331 |
| 341 | | -let esmSyntaxErrorMessagesSet; // Declared lazily in shouldRetryAsESM | 342 | | -let throwsOnlyInCommonJSErrorMessagesSet; // Declared lazily in shouldRetryAsESM | 343 | | -/** | 344 | | - * After an attempt to parse a module as CommonJS throws an error, should we try again as ESM? | 345 | | - * 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 | 346 | | - * throws on an error that would not have been a syntax error in ESM (like via top-level `await` or a lexical | 347 | | - * redeclaration of one of the CommonJS variables) then we need to parse again to see if it would have thrown in ESM. | 348 | | - * @param {string} errorMessage The string message thrown by V8 when attempting to parse as CommonJS | 349 | | - * @param {string} source Module contents | 350 | | - */ | 351 | | -function shouldRetryAsESM(errorMessage, source) { | 352 | | - esmSyntaxErrorMessagesSet ??= new SafeSet(esmSyntaxErrorMessages); | 353 | | - if (esmSyntaxErrorMessagesSet.has(errorMessage)) { | 354 | | - return true; | 355 | | - } | 356 | | - | 357 | | - throwsOnlyInCommonJSErrorMessagesSet ??= new SafeSet(throwsOnlyInCommonJSErrorMessages); | 358 | | - if (throwsOnlyInCommonJSErrorMessagesSet.has(errorMessage)) { | 359 | | - return /** @type {boolean} */(contextifyShouldRetryAsESM(source)); | 360 | | - } | 361 | | - | 362 | | - return false; | 363 | | -} | 364 | | - | 365 | 332 |
| 366 | 333 | // Whether we have started executing any user-provided CJS code. | 367 | 334 | // This is set right before we call the wrapped CJS code (not after, | @@ -396,7 +363,6 @@ module.exports = { | 396 | 363 | loadBuiltinModule, | 397 | 364 | makeRequireFunction, | 398 | 365 | normalizeReferrerURL, | 399 | | - shouldRetryAsESM, | 400 | 366 | stripBOM, | 401 | 367 | toRealPath, | 402 | 368 | hasStartedUserCJSExecution() { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/run_main.js +16-10Lines changed: 16 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| 3 | 3 | const { | | 4 | + ObjectGetPrototypeOf, | 4 | 5 | StringPrototypeEndsWith, | | 6 | + SyntaxErrorPrototype, | 5 | 7 | } = primordials; | 6 | 8 |
| 7 | 9 | const { getOptionValue } = require('internal/options'); | @@ -155,27 +157,29 @@ function runEntryPointWithESMLoader(callback) { | 155 | 157 | function executeUserEntryPoint(main = process.argv[1]) { | 156 | 158 | const resolvedMain = resolveMainPath(main); | 157 | 159 | const useESMLoader = shouldUseESMLoader(resolvedMain); | 158 | | - | | 160 | + let mainURL; | 159 | 161 | // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first | 160 | 162 | // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM. | 161 | 163 | let retryAsESM = false; | 162 | 164 | if (!useESMLoader) { | 163 | 165 | const cjsLoader = require('internal/modules/cjs/loader'); | 164 | 166 | const { Module } = cjsLoader; | 165 | 167 | if (getOptionValue('--experimental-detect-module')) { | | 168 | + // TODO(joyeecheung): handle this in the CJS loader. Don't try-catch here. | 166 | 169 | try { | 167 | 170 | // Module._load is the monkey-patchable CJS module loader. | 168 | 171 | Module._load(main, null, true); | 169 | 172 | } catch (error) { | 170 | | - const source = cjsLoader.entryPointSource; | 171 | | - const { shouldRetryAsESM } = require('internal/modules/helpers'); | 172 | | - retryAsESM = shouldRetryAsESM(error.message, source); | 173 | | - // In case the entry point is a large file, such as a bundle, | 174 | | - // ensure no further references can prevent it being garbage-collected. | 175 | | - cjsLoader.entryPointSource = undefined; | | 173 | + if (error != null && ObjectGetPrototypeOf(error) === SyntaxErrorPrototype) { | | 174 | + const { shouldRetryAsESM } = internalBinding('contextify'); | | 175 | + const mainPath = resolvedMain || main; | | 176 | + mainURL = pathToFileURL(mainPath).href; | | 177 | + retryAsESM = shouldRetryAsESM(error.message, cjsLoader.entryPointSource, mainURL); | | 178 | + // In case the entry point is a large file, such as a bundle, | | 179 | + // ensure no further references can prevent it being garbage-collected. | | 180 | + cjsLoader.entryPointSource = undefined; | | 181 | + } | 176 | 182 | if (!retryAsESM) { | 177 | | - const { enrichCJSError } = require('internal/modules/esm/translators'); | 178 | | - enrichCJSError(error, source, resolvedMain); | 179 | 183 | throw error; | 180 | 184 | } | 181 | 185 | } | @@ -186,7 +190,9 @@ function executeUserEntryPoint(main = process.argv[1]) { | 186 | 190 |
| 187 | 191 | if (useESMLoader || retryAsESM) { | 188 | 192 | const mainPath = resolvedMain || main; | 189 | | - const mainURL = pathToFileURL(mainPath).href; | | 193 | + if (mainURL === undefined) { | | 194 | + mainURL = pathToFileURL(mainPath).href; | | 195 | + } | 190 | 196 |
| 191 | 197 | runEntryPointWithESMLoader((cascadedLoader) => { | 192 | 198 | // Note that if the graph contains unfinished TLA, this may never resolve | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.cc +92-30Lines changed: 92 additions & 30 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -102,9 +102,17 @@ ModuleWrap* ModuleWrap::GetFromModule(Environment* env, | 102 | 102 | return nullptr; | 103 | 103 | } | 104 | 104 |
| 105 | | -// new ModuleWrap(url, context, source, lineOffset, columnOffset, cachedData) | | 105 | +Local<PrimitiveArray> ModuleWrap::GetHostDefinedOptions( | | 106 | + Isolate* isolate, Local<Symbol> id_symbol) { | | 107 | + Local<PrimitiveArray> host_defined_options = | | 108 | + PrimitiveArray::New(isolate, HostDefinedOptions::kLength); | | 109 | + host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol); | | 110 | + return host_defined_options; | | 111 | +} | | 112 | + | | 113 | +// new ModuleWrap(url, context, source, lineOffset, columnOffset[, cachedData]); | 106 | 114 | // new ModuleWrap(url, context, source, lineOffset, columOffset, | 107 | | -// hostDefinedOption) | | 115 | +// idSymbol); | 108 | 116 | // new ModuleWrap(url, context, exportNames, evaluationCallback[, cjsModule]) | 109 | 117 | void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 110 | 118 | CHECK(args.IsConstructCall()); | @@ -134,7 +142,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 134 | 142 | int column_offset = 0; | 135 | 143 |
| 136 | 144 | bool synthetic = args[2]->IsArray(); | 137 | | - | | 145 | + bool can_use_builtin_cache = false; | 138 | 146 | Local<PrimitiveArray> host_defined_options = | 139 | 147 | PrimitiveArray::New(isolate, HostDefinedOptions::kLength); | 140 | 148 | Local<Symbol> id_symbol; | @@ -143,20 +151,24 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 143 | 151 | // cjsModule]) | 144 | 152 | CHECK(args[3]->IsFunction()); | 145 | 153 | } else { | 146 | | - // new ModuleWrap(url, context, source, lineOffset, columOffset, cachedData) | | 154 | + // new ModuleWrap(url, context, source, lineOffset, columOffset[, | | 155 | + // cachedData]); | 147 | 156 | // new ModuleWrap(url, context, source, lineOffset, columOffset, | 148 | | - // hostDefinedOption) | | 157 | + // idSymbol); | 149 | 158 | CHECK(args[2]->IsString()); | 150 | 159 | CHECK(args[3]->IsNumber()); | 151 | 160 | line_offset = args[3].As<Int32>()->Value(); | 152 | 161 | CHECK(args[4]->IsNumber()); | 153 | 162 | column_offset = args[4].As<Int32>()->Value(); | 154 | 163 | if (args[5]->IsSymbol()) { | 155 | 164 | id_symbol = args[5].As<Symbol>(); | | 165 | + can_use_builtin_cache = | | 166 | + (id_symbol == | | 167 | + realm->isolate_data()->source_text_module_default_hdo()); | 156 | 168 | } else { | 157 | 169 | id_symbol = Symbol::New(isolate, url); | 158 | 170 | } | 159 | | - host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol); | | 171 | + host_defined_options = GetHostDefinedOptions(isolate, id_symbol); | 160 | 172 |
| 161 | 173 | if (that->SetPrivate(context, | 162 | 174 | realm->isolate_data()->host_defined_option_symbol(), | @@ -189,36 +201,34 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 189 | 201 | module = Module::CreateSyntheticModule(isolate, url, export_names, | 190 | 202 | SyntheticModuleEvaluationStepsCallback); | 191 | 203 | } else { | 192 | | - ScriptCompiler::CachedData* cached_data = nullptr; | | 204 | + // When we are compiling for the default loader, this will be | | 205 | + // std::nullopt, and CompileSourceTextModule() should use | | 206 | + // on-disk cache (not present on v20.x). | | 207 | + std::optional<v8::ScriptCompiler::CachedData*> user_cached_data; | | 208 | + if (id_symbol != | | 209 | + realm->isolate_data()->source_text_module_default_hdo()) { | | 210 | + user_cached_data = nullptr; | | 211 | + } | 193 | 212 | if (args[5]->IsArrayBufferView()) { | | 213 | + CHECK(!can_use_builtin_cache); // We don't use this option internally. | 194 | 214 | Local<ArrayBufferView> cached_data_buf = args[5].As<ArrayBufferView>(); | 195 | 215 | uint8_t* data = | 196 | 216 | static_cast<uint8_t*>(cached_data_buf->Buffer()->Data()); | 197 | | - cached_data = | | 217 | + user_cached_data = | 198 | 218 | new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(), | 199 | 219 | cached_data_buf->ByteLength()); | 200 | 220 | } | 201 | | - | 202 | 221 | Local<String> source_text = args[2].As<String>(); | 203 | | - ScriptOrigin origin(isolate, | 204 | | - url, | 205 | | - line_offset, | 206 | | - column_offset, | 207 | | - true, // is cross origin | 208 | | - -1, // script id | 209 | | - Local<Value>(), // source map URL | 210 | | - false, // is opaque (?) | 211 | | - false, // is WASM | 212 | | - true, // is ES Module | 213 | | - host_defined_options); | 214 | | - ScriptCompiler::Source source(source_text, origin, cached_data); | 215 | | - ScriptCompiler::CompileOptions options; | 216 | | - if (source.GetCachedData() == nullptr) { | 217 | | - options = ScriptCompiler::kNoCompileOptions; | 218 | | - } else { | 219 | | - options = ScriptCompiler::kConsumeCodeCache; | 220 | | - } | 221 | | - if (!ScriptCompiler::CompileModule(isolate, &source, options) | | 222 | + | | 223 | + bool cache_rejected = false; | | 224 | + if (!CompileSourceTextModule(realm, | | 225 | + source_text, | | 226 | + url, | | 227 | + line_offset, | | 228 | + column_offset, | | 229 | + host_defined_options, | | 230 | + user_cached_data, | | 231 | + &cache_rejected) | 222 | 232 | .ToLocal(&module)) { | 223 | 233 | if (try_catch.HasCaught() && !try_catch.HasTerminated()) { | 224 | 234 | CHECK(!try_catch.Message().IsEmpty()); | @@ -231,8 +241,9 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 231 | 241 | } | 232 | 242 | return; | 233 | 243 | } | 234 | | - if (options == ScriptCompiler::kConsumeCodeCache && | 235 | | - source.GetCachedData()->rejected) { | | 244 | + | | 245 | + if (user_cached_data.has_value() && user_cached_data.value() != nullptr && | | 246 | + cache_rejected) { | 236 | 247 | THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED( | 237 | 248 | realm, "cachedData buffer was rejected"); | 238 | 249 | try_catch.ReThrow(); | @@ -275,6 +286,57 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 275 | 286 | args.GetReturnValue().Set(that); | 276 | 287 | } | 277 | 288 |
| | 289 | +MaybeLocal<Module> ModuleWrap::CompileSourceTextModule( | | 290 | + Realm* realm, | | 291 | + Local<String> source_text, | | 292 | + Local<String> url, | | 293 | + int line_offset, | | 294 | + int column_offset, | | 295 | + Local<PrimitiveArray> host_defined_options, | | 296 | + std::optional<ScriptCompiler::CachedData*> user_cached_data, | | 297 | + bool* cache_rejected) { | | 298 | + Isolate* isolate = realm->isolate(); | | 299 | + EscapableHandleScope scope(isolate); | | 300 | + ScriptOrigin origin(isolate, | | 301 | + url, | | 302 | + line_offset, | | 303 | + column_offset, | | 304 | + true, // is cross origin | | 305 | + -1, // script id | | 306 | + Local<Value>(), // source map URL | | 307 | + false, // is opaque (?) | | 308 | + false, // is WASM | | 309 | + true, // is ES Module | | 310 | + host_defined_options); | | 311 | + ScriptCompiler::CachedData* cached_data = nullptr; | | 312 | + // When compiling for the default loader, user_cached_data is std::nullptr. | | 313 | + // When compiling for vm.Module, it's either nullptr or a pointer to the | | 314 | + // cached data. | | 315 | + if (user_cached_data.has_value()) { | | 316 | + cached_data = user_cached_data.value(); | | 317 | + } | | 318 | + | | 319 | + ScriptCompiler::Source source(source_text, origin, cached_data); | | 320 | + ScriptCompiler::CompileOptions options; | | 321 | + if (cached_data == nullptr) { | | 322 | + options = ScriptCompiler::kNoCompileOptions; | | 323 | + } else { | | 324 | + options = ScriptCompiler::kConsumeCodeCache; | | 325 | + } | | 326 | + | | 327 | + Local<Module> module; | | 328 | + if (!ScriptCompiler::CompileModule(isolate, &source, options) | | 329 | + .ToLocal(&module)) { | | 330 | + return scope.EscapeMaybe(MaybeLocal<Module>()); | | 331 | + } | | 332 | + | | 333 | + if (options == ScriptCompiler::kConsumeCodeCache) { | | 334 | + *cache_rejected = source.GetCachedData()->rejected; | | 335 | + } | | 336 | + | | 337 | + return scope.Escape(module); | | 338 | +} | | 339 | + | 278 | 340 | static Local<Object> createImportAttributesContainer( | 279 | 341 | Realm* realm, | 280 | 342 | Isolate* isolate, | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.h +20-1Lines changed: 20 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 |
| 4 | 4 | #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS | 5 | 5 |
| 6 | | -#include <unordered_map> | | 6 | +#include <optional> | 7 | 7 | #include <string> | | 8 | +#include <unordered_map> | 8 | 9 | #include <vector> | 9 | 10 | #include "base_object.h" | | 11 | +#include "v8-script.h" | 10 | 12 |
| 11 | 13 | namespace node { | 12 | 14 |
| @@ -68,6 +70,23 @@ class ModuleWrap : public BaseObject { | 68 | 70 | return true; | 69 | 71 | } | 70 | 72 |
| | 73 | + static v8::Local<v8::PrimitiveArray> GetHostDefinedOptions( | | 74 | + v8::Isolate* isolate, v8::Local<v8::Symbol> symbol); | | 75 | + | | 76 | + // When user_cached_data is not std::nullopt, use the code cache if it's not | | 77 | + // nullptr, otherwise don't use code cache. | | 78 | + // TODO(joyeecheung): when it is std::nullopt, use on-disk cache | | 79 | + // See: https://github.com/nodejs/node/issues/47472 | | 80 | + static v8::MaybeLocal<v8::Module> CompileSourceTextModule( | | 81 | + Realm* realm, | | 82 | + v8::Local<v8::String> source_text, | | 83 | + v8::Local<v8::String> url, | | 84 | + int line_offset, | | 85 | + int column_offset, | | 86 | + v8::Local<v8::PrimitiveArray> host_defined_options, | | 87 | + std::optional<v8::ScriptCompiler::CachedData*> user_cached_data, | | 88 | + bool* cache_rejected); | | 89 | + | 71 | 90 | private: | 72 | 91 | ModuleWrap(Realm* realm, | 73 | 92 | v8::Local<v8::Object> object, | |
|