summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xMeta/lint-prettier.sh6
-rw-r--r--Userland/Libraries/LibJS/AST.cpp34
-rw-r--r--Userland/Libraries/LibJS/Runtime/ErrorTypes.h2
-rw-r--r--Userland/Libraries/LibJS/Runtime/VM.cpp161
-rw-r--r--Userland/Libraries/LibJS/Runtime/VM.h5
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/basic-export-types.mjs13
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/basic-modules.js144
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/declarations-tests.mjs28
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/empty.mjs0
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/loop-a.mjs3
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/loop-b.mjs3
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/loop-entry.mjs3
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/loop-self.mjs5
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/module-with-default.mjs7
-rw-r--r--Userland/Libraries/LibJS/Tests/modules/single-const-export.mjs1
-rw-r--r--Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp4
-rw-r--r--Userland/Utilities/js.cpp2
17 files changed, 416 insertions, 5 deletions
diff --git a/Meta/lint-prettier.sh b/Meta/lint-prettier.sh
index 7a6701e8ee..00f0f70b4e 100755
--- a/Meta/lint-prettier.sh
+++ b/Meta/lint-prettier.sh
@@ -10,12 +10,12 @@ if [ "$#" -eq "0" ]; then
git ls-files \
--exclude-from .prettierignore \
-- \
- '*.js'
+ '*.js' '*.mjs'
)
else
files=()
for file in "$@"; do
- if [[ "${file}" == *".js" ]]; then
+ if [[ "${file}" == *".js" ]] || [[ "${file}" == *".mjs" ]]; then
files+=("${file}")
fi
done
@@ -34,5 +34,5 @@ if (( ${#files[@]} )); then
prettier --check "${files[@]}"
else
- echo "No .js files to check."
+ echo "No .js or .mjs files to check."
fi
diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp
index 5b036590ee..57a6d35fbb 100644
--- a/Userland/Libraries/LibJS/AST.cpp
+++ b/Userland/Libraries/LibJS/AST.cpp
@@ -3190,7 +3190,39 @@ void ImportCall::dump(int indent) const
Completion ImportCall::execute(Interpreter& interpreter, GlobalObject& global_object) const
{
InterpreterNodeScope node_scope { interpreter, *this };
- return interpreter.vm().throw_completion<InternalError>(global_object, ErrorType::NotImplemented, "'import(...)' in modules");
+ // 1. Let referencingScriptOrModule be ! GetActiveScriptOrModule().
+ auto referencing_script_or_module = interpreter.vm().get_active_script_or_module();
+
+ if (m_options)
+ return interpreter.vm().throw_completion<InternalError>(global_object, ErrorType::NotImplemented, "import call with assertions/options");
+
+ // 2. Let argRef be the result of evaluating AssignmentExpression.
+ // 3. Let specifier be ? GetValue(argRef).
+ auto specifier = TRY(m_specifier->execute(interpreter, global_object));
+
+ // 4. Let promiseCapability be ! NewPromiseCapability(%Promise%).
+ auto promise_capability = MUST(new_promise_capability(global_object, global_object.promise_constructor()));
+
+ VERIFY(!interpreter.exception());
+ // 5. Let specifierString be ToString(specifier).
+ auto specifier_string = specifier->to_string(global_object);
+
+ // 6. IfAbruptRejectPromise(specifierString, promiseCapability).
+ // Note: Since we have to use completions and not ThrowCompletionOr's in AST we have to do this manually.
+ if (specifier_string.is_throw_completion()) {
+ // FIXME: We shouldn't have to clear this exception
+ interpreter.vm().clear_exception();
+ (void)TRY(call(global_object, promise_capability.reject, js_undefined(), *specifier_string.throw_completion().value()));
+ return Value { promise_capability.promise };
+ }
+
+ ModuleRequest request { specifier_string.release_value() };
+
+ // 7. Perform ! HostImportModuleDynamically(referencingScriptOrModule, specifierString, promiseCapability).
+ interpreter.vm().host_import_module_dynamically(referencing_script_or_module, request, promise_capability);
+
+ // 8. Return promiseCapability.[[Promise]].
+ return Value { promise_capability.promise };
}
// 13.2.3.1 Runtime Semantics: Evaluation, https://tc39.es/ecma262/#sec-literals-runtime-semantics-evaluation
diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h
index b83c4bf899..bb63ac29be 100644
--- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h
+++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h
@@ -29,6 +29,7 @@
M(DescWriteNonWritable, "Cannot write to non-writable property '{}'") \
M(DetachedArrayBuffer, "ArrayBuffer is detached") \
M(DivisionByZero, "Division by zero") \
+ M(DynamicImportNotAllowed, "Dynamic Imports are not allowed") \
M(FinalizationRegistrySameTargetAndValue, "Target and held value must not be the same") \
M(GetCapabilitiesExecutorCalledMultipleTimes, "GetCapabilitiesExecutor was called multiple times") \
M(GlobalEnvironmentAlreadyHasBinding, "Global environment already has binding '{}'") \
@@ -68,6 +69,7 @@
M(MissingRequiredProperty, "Required property {} is missing or undefined") \
M(ModuleNoEnvironment, "Cannot find module environment for imported binding") \
M(ModuleNotFound, "Cannot find/open module: '{}'") \
+ M(ModuleNotFoundNoReferencingScript, "Cannot resolve module {} without any active script or module") \
M(NegativeExponent, "Exponent must be positive") \
M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \
M(NotAConstructor, "{} is not a constructor") \
diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp
index 4773c24459..cbbff28fe6 100644
--- a/Userland/Libraries/LibJS/Runtime/VM.cpp
+++ b/Userland/Libraries/LibJS/Runtime/VM.cpp
@@ -50,6 +50,37 @@ VM::VM(OwnPtr<CustomData> custom_data)
return resolve_imported_module(move(referencing_script_or_module), specifier);
};
+ host_import_module_dynamically = [&](ScriptOrModule, ModuleRequest const&, PromiseCapability promise_capability) {
+ // By default, we throw on dynamic imports this is to prevent arbitrary file access by scripts.
+ VERIFY(current_realm());
+ auto& global_object = current_realm()->global_object();
+ auto* promise = Promise::create(global_object);
+
+ // If you are here because you want to enable dynamic module importing make sure it won't be a security problem
+ // by checking the default implementation of HostImportModuleDynamically and creating your own hook or calling
+ // vm.enable_default_host_import_module_dynamically_hook().
+ promise->reject(Error::create(global_object, ErrorType::DynamicImportNotAllowed.message()));
+
+ promise->perform_then(
+ NativeFunction::create(global_object, "", [](auto&, auto&) -> ThrowCompletionOr<Value> {
+ VERIFY_NOT_REACHED();
+ }),
+ NativeFunction::create(global_object, "", [reject = make_handle(promise_capability.reject)](auto& vm, auto& global_object) -> ThrowCompletionOr<Value> {
+ auto error = vm.argument(0);
+
+ // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « error »).
+ MUST(JS::call(global_object, reject.cell(), js_undefined(), error));
+
+ // b. Return undefined.
+ return js_undefined();
+ }),
+ {});
+ };
+
+ host_finish_dynamic_import = [&](ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability, Promise* promise) {
+ return finish_dynamic_import(move(referencing_script_or_module), specifier, promise_capability, promise);
+ };
+
#define __JS_ENUMERATE(SymbolName, snake_name) \
m_well_known_symbol_##snake_name = js_symbol(*this, "Symbol." #SymbolName, false);
JS_ENUMERATE_WELL_KNOWN_SYMBOLS
@@ -60,6 +91,13 @@ VM::~VM()
{
}
+void VM::enable_default_host_import_module_dynamically_hook()
+{
+ host_import_module_dynamically = [&](ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability) {
+ return import_module_dynamically(move(referencing_script_or_module), specifier, promise_capability);
+ };
+}
+
Interpreter& VM::interpreter()
{
VERIFY(!m_interpreters.is_empty());
@@ -848,4 +886,127 @@ ThrowCompletionOr<NonnullRefPtr<Module>> VM::resolve_imported_module(ScriptOrMod
return module;
}
+// 16.2.1.8 HostImportModuleDynamically ( referencingScriptOrModule, specifier, promiseCapability ), https://tc39.es/ecma262/#sec-hostimportmoduledynamically
+void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability)
+{
+ auto& global_object = current_realm()->global_object();
+
+ // Success path:
+ // - At some future time, the host environment must perform FinishDynamicImport(referencingScriptOrModule, specifier, promiseCapability, promise),
+ // where promise is a Promise resolved with undefined.
+ // - Any subsequent call to HostResolveImportedModule after FinishDynamicImport has completed,
+ // given the arguments referencingScriptOrModule and specifier, must complete normally.
+ // - The completion value of any subsequent call to HostResolveImportedModule after FinishDynamicImport has completed,
+ // given the arguments referencingScriptOrModule and specifier, must be a module which has already been evaluated,
+ // i.e. whose Evaluate concrete method has already been called and returned a normal completion.
+ // Failure path:
+ // - At some future time, the host environment must perform
+ // FinishDynamicImport(referencingScriptOrModule, specifier, promiseCapability, promise),
+ // where promise is a Promise rejected with an error representing the cause of failure.
+
+ auto* promise = Promise::create(global_object);
+
+ ScopeGuard finish_dynamic_import = [&] {
+ host_finish_dynamic_import(referencing_script_or_module, specifier, promise_capability, promise);
+ };
+
+ // Generally within ECMA262 we always get a referencing_script_or_moulde. However, ShadowRealm gives an explicit null.
+ // To get around this is we attempt to get the active script_or_module otherwise we might start loading "random" files from the working directory.
+ if (referencing_script_or_module.has<Empty>()) {
+ referencing_script_or_module = get_active_script_or_module();
+
+ // If there is no ScriptOrModule in any of the execution contexts
+ if (referencing_script_or_module.has<Empty>()) {
+ // Throw an error for now
+ promise->reject(InternalError::create(global_object, String::formatted(ErrorType::ModuleNotFoundNoReferencingScript.message(), specifier.module_specifier)));
+ return;
+ }
+ }
+
+ VERIFY(!exception());
+ // Note: If host_resolve_imported_module returns a module it has been loaded successfully and the next call in finish_dynamic_import will retrieve it again.
+ auto module_or_error = host_resolve_imported_module(referencing_script_or_module, specifier);
+ dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] HostImportModuleDynamically(..., {}) -> {}", specifier.module_specifier, module_or_error.is_error() ? "failed" : "passed");
+ if (module_or_error.is_throw_completion()) {
+ // Note: We should not leak the exception thrown in host_resolve_imported_module.
+ clear_exception();
+ promise->reject(*module_or_error.throw_completion().value());
+ } else {
+ // Note: If you are here because this VERIFY is failing overwrite host_import_module_dynamically
+ // because this is LibJS internal logic which won't always work
+ auto module = module_or_error.release_value();
+ VERIFY(is<SourceTextModule>(*module));
+ auto& source_text_module = static_cast<SourceTextModule&>(*module);
+
+ auto evaluated_or_error = link_and_eval_module(source_text_module);
+
+ if (evaluated_or_error.is_throw_completion()) {
+ // Note: Again we don't want to leak the exception from link_and_eval_module.
+ clear_exception();
+ promise->reject(*evaluated_or_error.throw_completion().value());
+ } else {
+ VERIFY(!exception());
+ promise->fulfill(js_undefined());
+ }
+ }
+
+ // It must return NormalCompletion(undefined).
+ // Note: Just return void always since the resulting value cannot be accessed by user code.
+}
+
+// 16.2.1.9 FinishDynamicImport ( referencingScriptOrModule, specifier, promiseCapability, innerPromise ), https://tc39.es/ecma262/#sec-finishdynamicimport
+void VM::finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability, Promise* inner_promise)
+{
+ dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] finish_dynamic_import on {}", specifier.module_specifier);
+
+ // 1. Let fulfilledClosure be a new Abstract Closure with parameters (result) that captures referencingScriptOrModule, specifier, and promiseCapability and performs the following steps when called:
+ auto fulfilled_closure = [referencing_script_or_module, specifier, promise_capability](VM& vm, GlobalObject& global_object) -> ThrowCompletionOr<Value> {
+ auto result = vm.argument(0);
+ // a. Assert: result is undefined.
+ VERIFY(result.is_undefined());
+ // b. Let moduleRecord be ! HostResolveImportedModule(referencingScriptOrModule, specifier).
+ auto module_record = MUST(vm.host_resolve_imported_module(referencing_script_or_module, specifier));
+
+ // c. Assert: Evaluate has already been invoked on moduleRecord and successfully completed.
+ // Note: If HostResolveImportedModule returns a module evaluate will have been called on it.
+
+ // d. Let namespace be GetModuleNamespace(moduleRecord).
+ auto namespace_ = module_record->get_module_namespace(vm);
+
+ VERIFY(!vm.exception());
+ // e. If namespace is an abrupt completion, then
+ if (namespace_.is_throw_completion()) {
+ // i. Perform ! Call(promiseCapability.[[Reject]], undefined, « namespace.[[Value]] »).
+ MUST(JS::call(global_object, promise_capability.reject, js_undefined(), *namespace_.throw_completion().value()));
+ }
+ // f. Else,
+ else {
+ // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace.[[Value]] »).
+ MUST(JS::call(global_object, promise_capability.resolve, js_undefined(), namespace_.release_value()));
+ }
+ // g. Return undefined.
+ return js_undefined();
+ };
+
+ // 2. Let onFulfilled be ! CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
+ auto* on_fulfilled = NativeFunction::create(current_realm()->global_object(), "", move(fulfilled_closure));
+
+ // 3. Let rejectedClosure be a new Abstract Closure with parameters (error) that captures promiseCapability and performs the following steps when called:
+ auto rejected_closure = [promise_capability](VM& vm, GlobalObject& global_object) -> ThrowCompletionOr<Value> {
+ auto error = vm.argument(0);
+ // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « error »).
+ MUST(JS::call(global_object, promise_capability.reject, js_undefined(), error));
+ // b. Return undefined.
+ return js_undefined();
+ };
+
+ // 4. Let onRejected be ! CreateBuiltinFunction(rejectedClosure, 0, "", « »).
+ auto* on_rejected = NativeFunction::create(current_realm()->global_object(), "", move(rejected_closure));
+
+ // 5. Perform ! PerformPromiseThen(innerPromise, onFulfilled, onRejected).
+ inner_promise->perform_then(on_fulfilled, on_rejected, {});
+
+ VERIFY(!exception());
+}
+
}
diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h
index ba68592c4e..ddc590f44b 100644
--- a/Userland/Libraries/LibJS/Runtime/VM.h
+++ b/Userland/Libraries/LibJS/Runtime/VM.h
@@ -251,6 +251,8 @@ public:
Function<HashMap<PropertyKey, Value>(SourceTextModule const&)> host_get_import_meta_properties;
Function<void(Object*, SourceTextModule const&)> host_finalize_import_meta;
+ void enable_default_host_import_module_dynamically_hook();
+
private:
explicit VM(OwnPtr<CustomData>);
@@ -262,6 +264,9 @@ private:
ThrowCompletionOr<NonnullRefPtr<Module>> resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier);
ThrowCompletionOr<void> link_and_eval_module(SourceTextModule& module);
+ void import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability);
+ void finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability, Promise* inner_promise);
+
Exception* m_exception { nullptr };
HashMap<String, PrimitiveString*> m_string_cache;
diff --git a/Userland/Libraries/LibJS/Tests/modules/basic-export-types.mjs b/Userland/Libraries/LibJS/Tests/modules/basic-export-types.mjs
new file mode 100644
index 0000000000..68f45e2342
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/basic-export-types.mjs
@@ -0,0 +1,13 @@
+export const constValue = 1;
+
+export let letValue = 2;
+
+export var varValue = 3;
+
+const namedConstValue = 4;
+let namedLetValue = 5;
+var namedVarValue = 6;
+
+export { namedConstValue, namedLetValue, namedVarValue };
+
+export const passed = true;
diff --git a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js
new file mode 100644
index 0000000000..7693b1df14
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js
@@ -0,0 +1,144 @@
+// Because you can't easily load modules directly we load them via here and check
+// if they passed by checking the result
+
+function expectModulePassed(filename) {
+ if (!filename.endsWith(".mjs") || !filename.startsWith("./")) {
+ throw new ExpectationError(
+ "Expected module name to start with './' " +
+ "and end with '.mjs' but got '" +
+ filename +
+ "'"
+ );
+ }
+
+ async function getModule() {
+ return import(filename);
+ }
+
+ let moduleLoaded = false;
+ let moduleResult = null;
+ let thrownError = null;
+
+ getModule()
+ .then(result => {
+ moduleLoaded = true;
+ moduleResult = result;
+ expect(moduleResult).toHaveProperty("passed", true);
+ })
+ .catch(error => {
+ thrownError = error;
+ });
+
+ runQueuedPromiseJobs();
+
+ if (thrownError) {
+ throw thrownError;
+ }
+
+ expect(moduleLoaded).toBeTrue();
+
+ return moduleResult;
+}
+
+describe("testing behavior", () => {
+ // To ensure the other tests are interpreter correctly we first test the underlying
+ // mechanisms so these tests don't use expectModulePassed.
+
+ test("can load a module", () => {
+ let passed = false;
+ let error = null;
+
+ import("./empty.mjs")
+ .then(() => {
+ passed = true;
+ })
+ .catch(err => {
+ error = err;
+ });
+
+ runQueuedPromiseJobs();
+ if (error) throw error;
+
+ expect(passed).toBeTrue();
+ });
+
+ test("can load a module twice", () => {
+ let passed = false;
+ let error = null;
+
+ import("./empty.mjs")
+ .then(() => {
+ passed = true;
+ })
+ .catch(err => {
+ error = err;
+ });
+
+ runQueuedPromiseJobs();
+ if (error) throw error;
+
+ expect(passed).toBeTrue();
+ });
+
+ test("can retrieve exported value", () => {
+ async function getValue(filename) {
+ const imported = await import(filename);
+ expect(imported).toHaveProperty("passed", true);
+ }
+
+ let passed = false;
+ let error = null;
+
+ getValue("./single-const-export.mjs")
+ .then(obj => {
+ passed = true;
+ })
+ .catch(err => {
+ error = err;
+ });
+
+ runQueuedPromiseJobs();
+
+ if (error) throw error;
+
+ expect(passed).toBeTrue();
+ });
+
+ test("expectModulePassed works", () => {
+ expectModulePassed("./single-const-export.mjs");
+ });
+});
+
+describe("in- and exports", () => {
+ test("variable and lexical declarations", () => {
+ const result = expectModulePassed("./basic-export-types.mjs");
+ expect(result).not.toHaveProperty("default", null);
+ expect(result).toHaveProperty("constValue", 1);
+ expect(result).toHaveProperty("letValue", 2);
+ expect(result).toHaveProperty("varValue", 3);
+
+ expect(result).toHaveProperty("namedConstValue", 1 + 3);
+ expect(result).toHaveProperty("namedLetValue", 2 + 3);
+ expect(result).toHaveProperty("namedVarValue", 3 + 3);
+ });
+
+ test("default exports", () => {
+ const result = expectModulePassed("./module-with-default.mjs");
+ expect(result).toHaveProperty("defaultValue");
+ expect(result.default).toBe(result.defaultValue);
+ });
+
+ test("declaration exports which can be used in the module it self", () => {
+ expectModulePassed("./declarations-tests.mjs");
+ });
+});
+
+describe("loops", () => {
+ test("import and export from own file", () => {
+ expectModulePassed("./loop-self.mjs");
+ });
+
+ test("import something which imports a cycle", () => {
+ expectModulePassed("./loop-entry.mjs");
+ });
+});
diff --git a/Userland/Libraries/LibJS/Tests/modules/declarations-tests.mjs b/Userland/Libraries/LibJS/Tests/modules/declarations-tests.mjs
new file mode 100644
index 0000000000..1cccb86b8b
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/declarations-tests.mjs
@@ -0,0 +1,28 @@
+export function returnsOne() {
+ return 1;
+}
+
+export class hasStaticFieldTwo {
+ static two = 2;
+}
+
+const expectedValue = 10;
+const didNotHoistClass = (() => {
+ try {
+ new ShouldNotBeHoisted();
+ } catch (e) {
+ if (e instanceof ReferenceError) return 4;
+ }
+ return 0;
+})();
+
+export const passed =
+ returnsOne() + hasStaticFieldTwo.two + shouldBeHoisted() + didNotHoistClass === expectedValue;
+
+export function shouldBeHoisted() {
+ return 3;
+}
+
+export class ShouldNotBeHoisted {
+ static no = 5;
+}
diff --git a/Userland/Libraries/LibJS/Tests/modules/empty.mjs b/Userland/Libraries/LibJS/Tests/modules/empty.mjs
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/empty.mjs
diff --git a/Userland/Libraries/LibJS/Tests/modules/loop-a.mjs b/Userland/Libraries/LibJS/Tests/modules/loop-a.mjs
new file mode 100644
index 0000000000..fc994711f7
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/loop-a.mjs
@@ -0,0 +1,3 @@
+export { bValue } from "./loop-b.mjs";
+
+export const aValue = 1;
diff --git a/Userland/Libraries/LibJS/Tests/modules/loop-b.mjs b/Userland/Libraries/LibJS/Tests/modules/loop-b.mjs
new file mode 100644
index 0000000000..68151b4d85
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/loop-b.mjs
@@ -0,0 +1,3 @@
+import "./loop-a.mjs";
+
+export const bValue = 2;
diff --git a/Userland/Libraries/LibJS/Tests/modules/loop-entry.mjs b/Userland/Libraries/LibJS/Tests/modules/loop-entry.mjs
new file mode 100644
index 0000000000..c5e7f77ee7
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/loop-entry.mjs
@@ -0,0 +1,3 @@
+import { aValue, bValue } from "./loop-a.mjs";
+
+export const passed = aValue < bValue;
diff --git a/Userland/Libraries/LibJS/Tests/modules/loop-self.mjs b/Userland/Libraries/LibJS/Tests/modules/loop-self.mjs
new file mode 100644
index 0000000000..a37da73982
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/loop-self.mjs
@@ -0,0 +1,5 @@
+import { value as importValue } from "./loop-self.mjs";
+
+export const value = "loop de loop whooo";
+
+export const passed = value === importValue;
diff --git a/Userland/Libraries/LibJS/Tests/modules/module-with-default.mjs b/Userland/Libraries/LibJS/Tests/modules/module-with-default.mjs
new file mode 100644
index 0000000000..bcce016479
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/module-with-default.mjs
@@ -0,0 +1,7 @@
+const value = "Well hello importer :^)";
+
+export const defaultValue = value;
+
+export default value;
+
+export const passed = true;
diff --git a/Userland/Libraries/LibJS/Tests/modules/single-const-export.mjs b/Userland/Libraries/LibJS/Tests/modules/single-const-export.mjs
new file mode 100644
index 0000000000..58c9069908
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/modules/single-const-export.mjs
@@ -0,0 +1 @@
+export const passed = true;
diff --git a/Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp b/Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp
index 6ed697ea8e..a4aa7f7e44 100644
--- a/Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp
+++ b/Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp
@@ -182,8 +182,10 @@ int main(int argc, char** argv)
if (g_main_hook)
g_main_hook();
- if (!g_vm)
+ if (!g_vm) {
g_vm = JS::VM::create();
+ g_vm->enable_default_host_import_module_dynamically_hook();
+ }
Test::JS::TestRunner test_runner(test_root, common_path, print_times, print_progress, print_json);
test_runner.run(test_glob);
diff --git a/Userland/Utilities/js.cpp b/Userland/Utilities/js.cpp
index 40b960e1ab..ea486e09b9 100644
--- a/Userland/Utilities/js.cpp
+++ b/Userland/Utilities/js.cpp
@@ -1290,6 +1290,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
bool syntax_highlight = !disable_syntax_highlight;
vm = JS::VM::create();
+ vm->enable_default_host_import_module_dynamically_hook();
+
// NOTE: These will print out both warnings when using something like Promise.reject().catch(...) -
// which is, as far as I can tell, correct - a promise is created, rejected without handler, and a
// handler then attached to it. The Node.js REPL doesn't warn in this case, so it's something we