diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index c258524196..466bc8c7a8 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -823,6 +823,29 @@ ThrowCompletionOr VM::link_and_eval_module(Module& module) return {}; } +static String resolve_module_filename(StringView filename, StringView module_type) +{ + auto extensions = Vector { "js"sv, "mjs"sv }; + if (module_type == "json"sv) + extensions = { "json"sv }; + if (!Core::File::exists(filename)) { + for (auto extension : extensions) { + // import "./foo" -> import "./foo.ext" + auto resolved_filepath = String::formatted("{}.{}", filename, extension); + if (Core::File::exists(resolved_filepath)) + return resolved_filepath; + } + } else if (Core::File::is_directory(filename)) { + for (auto extension : extensions) { + // import "./foo" -> import "./foo/index.ext" + auto resolved_filepath = LexicalPath::join(filename, String::formatted("index.{}", extension)).string(); + if (Core::File::exists(resolved_filepath)) + return resolved_filepath; + } + } + return filename; +} + // 16.2.1.7 HostResolveImportedModule ( referencingScriptOrModule, specifier ), https://tc39.es/ecma262/#sec-hostresolveimportedmodule ThrowCompletionOr> VM::resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& module_request) { @@ -840,6 +863,12 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod // The actual mapping semantic is host-defined but typically a normalization process is applied to specifier as part of the mapping process. // A typical normalization process would include actions such as alphabetic case folding and expansion of relative and abbreviated path specifiers. + // We only allow "type" as a supported assertion so it is the only valid key that should ever arrive here. + VERIFY(module_request.assertions.is_empty() || (module_request.assertions.size() == 1 && module_request.assertions.first().key == "type")); + auto module_type = module_request.assertions.is_empty() ? String {} : module_request.assertions.first().value; + + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] module at {} has type {} [is_null={}]", module_request.module_specifier, module_type, module_type.is_null()); + StringView base_filename = referencing_script_or_module.visit( [&](Empty) { return "."sv; @@ -851,6 +880,13 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod LexicalPath base_path { base_filename }; auto filename = LexicalPath::absolute_path(base_path.dirname(), module_request.module_specifier); + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] base path: '{}'", base_path); + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] initial filename: '{}'", filename); + + filename = resolve_module_filename(filename, module_type); + + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolved filename: '{}'", filename); + #if JS_MODULE_DEBUG String referencing_module_string = referencing_script_or_module.visit( [&](Empty) -> String { @@ -867,12 +903,6 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolved {} + {} -> {}", base_path, module_request.module_specifier, filename); #endif - // We only allow "type" as a supported assertion so it is the only valid key that should ever arrive here. - VERIFY(module_request.assertions.is_empty() || (module_request.assertions.size() == 1 && module_request.assertions.first().key == "type")); - auto module_type = module_request.assertions.is_empty() ? String {} : module_request.assertions.first().value; - - dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] module at {} has type {} [is_null={}]", module_request.module_specifier, module_type, module_type.is_null()); - auto* loaded_module_or_end = get_stored_module(referencing_script_or_module, filename, module_type); if (loaded_module_or_end != nullptr) { dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module({}) already loaded at {}", filename, loaded_module_or_end->module.ptr()); diff --git a/Userland/Libraries/LibJS/Tests/modules/path-resolution.mjs b/Userland/Libraries/LibJS/Tests/modules/path-resolution.mjs new file mode 100644 index 0000000000..18c20f43ef --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/path-resolution.mjs @@ -0,0 +1,5 @@ +// If all of these succeed, path resolution with implicit extensions is working. + +import "./module-with-default"; +import "./submodule"; +import "./json-module" assert { type: "json" }; diff --git a/Userland/Libraries/LibJS/Tests/modules/submodule/index.js b/Userland/Libraries/LibJS/Tests/modules/submodule/index.js new file mode 100644 index 0000000000..ac0751e06f --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/submodule/index.js @@ -0,0 +1 @@ +// Submodule, `import "./submodule"` should resolve to `submodule/index.js`.