Bläddra i källkod

LibJS: Infer file extensions when resolving module paths

This allows `import "./foo"` to succeed, even if the file is actually
called `foo.js`. IDEs commonly exclude file extensions in auto-imports.

Closes #14364.
Linus Groh 3 år sedan
förälder
incheckning
93b4c3bb82

+ 36 - 6
Userland/Libraries/LibJS/Runtime/VM.cpp

@@ -823,6 +823,29 @@ ThrowCompletionOr<void> VM::link_and_eval_module(Module& module)
     return {};
 }
 
+static String resolve_module_filename(StringView filename, StringView module_type)
+{
+    auto extensions = Vector<StringView, 2> { "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<NonnullRefPtr<Module>> VM::resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& module_request)
 {
@@ -840,6 +863,12 @@ ThrowCompletionOr<NonnullRefPtr<Module>> 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<NonnullRefPtr<Module>> 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<NonnullRefPtr<Module>> 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());

+ 5 - 0
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" };

+ 1 - 0
Userland/Libraries/LibJS/Tests/modules/submodule/index.js

@@ -0,0 +1 @@
+// Submodule, `import "./submodule"` should resolve to `submodule/index.js`.