diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 02bcce5378..cd57be17e1 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -242,6 +242,7 @@ namespace JS { P(id) \ P(ignoreCase) \ P(imul) \ + P(importValue) \ P(includes) \ P(index) \ P(indexOf) \ diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp b/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp index 0d966394e8..c9369ae74f 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include #include @@ -146,6 +149,118 @@ ThrowCompletionOr perform_shadow_realm_eval(GlobalObject& global_object, // NOTE: Also see "Editor's Note" in the spec regarding the TypeError above. } +// 3.1.2 ShadowRealmImportValue ( specifierString, exportNameString, callerRealm, evalRealm, evalContext ), https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue +ThrowCompletionOr shadow_realm_import_value(GlobalObject& global_object, String specifier_string, String export_name_string, Realm& caller_realm, Realm& eval_realm, ExecutionContext& eval_context) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(specifierString) is String. + // 2. Assert: Type(exportNameString) is String. + // 3. Assert: callerRealm is a Realm Record. + // 4. Assert: evalRealm is a Realm Record. + // 5. Assert: evalContext is an execution context associated to a ShadowRealm instance's [[ExecutionContext]]. + + // 6. Let innerCapability be ! NewPromiseCapability(%Promise%). + auto inner_capability = new_promise_capability(global_object, global_object.promise_constructor()); + VERIFY(!vm.exception()); + + // 7. Let runningContext be the running execution context. + // 8. If runningContext is not already suspended, suspend runningContext. + // NOTE: We don't support this concept yet. + + // 9. Push evalContext onto the execution context stack; evalContext is now the running execution context. + vm.push_execution_context(eval_context, eval_realm.global_object()); + + // 10. Perform ! HostImportModuleDynamically(null, specifierString, innerCapability). + // FIXME: We don't have this yet. We generally have very little support for modules and imports. + // So, in the meantime we just do the "Failure path" step, and pretend to call FinishDynamicImport + // with the rejected promise. This should be easy to complete once those missing module AOs are added. + + // HostImportModuleDynamically: 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); + promise->reject(Error::create(global_object, String::formatted("Import of '{}' from '{}' failed", export_name_string, specifier_string))); + + // FinishDynamicImport, 5. Perform ! PerformPromiseThen(innerPromise, onFulfilled, onRejected). + promise->perform_then( + NativeFunction::create(global_object, "", [](auto&, auto&) -> Value { + // Not called because we hardcoded a rejection above. + TODO(); + }), + NativeFunction::create(global_object, "", [reject = make_handle(inner_capability.reject)](auto& vm, auto& global_object) -> Value { + auto error = vm.argument(0); + + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « error »). + MUST(call(global_object, reject.cell(), js_undefined(), error)); + + // b. Return undefined. + return js_undefined(); + }), + {}); + + // 11. Suspend evalContext and remove it from the execution context stack. + // NOTE: We don't support this concept yet. + vm.pop_execution_context(); + + // 12. Resume the context that is now on the top of the execution context stack as the running execution context. + // NOTE: We don't support this concept yet. + + // 13. Let steps be the steps of an ExportGetter function as described below. + // 14. Let onFulfilled be ! CreateBuiltinFunction(steps, 1, "", « [[ExportNameString]] », callerRealm). + // 15. Set onFulfilled.[[ExportNameString]] to exportNameString. + // FIXME: Support passing a realm to NativeFunction::create() + (void)caller_realm; + auto* on_fulfilled = NativeFunction::create( + global_object, + "", + [string = move(export_name_string)](auto& vm, auto& global_object) -> Value { + // 1. Assert: exports is a module namespace exotic object. + auto& exports = vm.argument(0).as_object(); + + // 2. Let f be the active function object. + auto* function = vm.running_execution_context().function; + + // 3. Let string be f.[[ExportNameString]]. + // 4. Assert: Type(string) is String. + + // 5. Let hasOwn be ? HasOwnProperty(exports, string). + auto has_own = TRY_OR_DISCARD(exports.has_own_property(string)); + + // 6. If hasOwn is false, throw a TypeError exception. + if (!has_own) { + vm.template throw_exception(global_object, ErrorType::MissingRequiredProperty, string); + return {}; + } + + // 7. Let value be ? Get(exports, string). + auto value = TRY_OR_DISCARD(exports.get(string)); + + // 8. Let realm be f.[[Realm]]. + auto* realm = function->realm(); + VERIFY(realm); + + // 9. Return ? GetWrappedValue(realm, value). + return TRY_OR_DISCARD(get_wrapped_value(global_object, *realm, value)); + }); + on_fulfilled->define_direct_property(vm.names.length, Value(1), Attribute::Configurable); + on_fulfilled->define_direct_property(vm.names.name, js_string(vm, String::empty()), Attribute::Configurable); + + // 16. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto promise_capability = new_promise_capability(global_object, global_object.promise_constructor()); + VERIFY(!vm.exception()); + + // NOTE: Even though the spec tells us to use %ThrowTypeError%, it's not observable if we actually do. + // Throw a nicer TypeError forwarding the import error message instead (we know the argument is an Error object). + auto* throw_type_error = NativeFunction::create(global_object, {}, [](auto& vm, auto& global_object) -> Value { + vm.template throw_exception(global_object, vm.argument(0).as_object().get_without_side_effects(vm.names.message).as_string().string()); + return {}; + }); + + // 17. Return ! PerformPromiseThen(innerCapability.[[Promise]], onFulfilled, callerRealm.[[Intrinsics]].[[%ThrowTypeError%]], promiseCapability). + return verify_cast(inner_capability.promise)->perform_then(on_fulfilled, throw_type_error, promise_capability); +} + // 3.1.3 GetWrappedValue ( callerRealm, value ), https://tc39.es/proposal-shadowrealm/#sec-getwrappedvalue ThrowCompletionOr get_wrapped_value(GlobalObject& global_object, Realm& caller_realm, Value value) { diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealm.h b/Userland/Libraries/LibJS/Runtime/ShadowRealm.h index 79b203a3ce..9e57ff86a8 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealm.h +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealm.h @@ -33,6 +33,7 @@ private: }; ThrowCompletionOr perform_shadow_realm_eval(GlobalObject&, StringView source_text, Realm& caller_realm, Realm& eval_realm); +ThrowCompletionOr shadow_realm_import_value(GlobalObject&, String specifier_string, String export_name_string, Realm& caller_realm, Realm& eval_realm, ExecutionContext& eval_context); ThrowCompletionOr get_wrapped_value(GlobalObject&, Realm& caller_realm, Value); } diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp index 7221118254..157abd8fe8 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp @@ -23,6 +23,7 @@ void ShadowRealmPrototype::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.evaluate, evaluate, 1, attr); + define_native_function(vm.names.importValue, import_value, 2, attr); // 3.4.3 ShadowRealm.prototype [ @@toStringTag ], https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype-@@tostringtag define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, vm.names.ShadowRealm.as_string()), Attribute::Configurable); @@ -55,4 +56,35 @@ JS_DEFINE_NATIVE_FUNCTION(ShadowRealmPrototype::evaluate) return TRY_OR_DISCARD(perform_shadow_realm_eval(global_object, source_text.as_string().string(), *caller_realm, eval_realm)); } +// 3.4.2 ShadowRealm.prototype.importValue ( specifier, exportName ), https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype.importvalue +JS_DEFINE_NATIVE_FUNCTION(ShadowRealmPrototype::import_value) +{ + auto specifier = vm.argument(0); + auto export_name = vm.argument(1); + + // 1. Let O be this value. + // 2. Perform ? ValidateShadowRealmObject(O). + auto* object = typed_this_object(global_object); + if (vm.exception()) + return {}; + + // 3. Let specifierString be ? ToString(specifier). + auto specifier_string = TRY_OR_DISCARD(specifier.to_string(global_object)); + + // 4. Let exportNameString be ? ToString(exportName). + auto export_name_string = TRY_OR_DISCARD(export_name.to_string(global_object)); + + // 5. Let callerRealm be the current Realm Record. + auto* caller_realm = vm.current_realm(); + + // 6. Let evalRealm be O.[[ShadowRealm]]. + auto& eval_realm = object->shadow_realm(); + + // 7. Let evalContext be O.[[ExecutionContext]]. + auto& eval_context = object->execution_context(); + + // 8. Return ? ShadowRealmImportValue(specifierString, exportNameString, callerRealm, evalRealm, evalContext). + return TRY_OR_DISCARD(shadow_realm_import_value(global_object, move(specifier_string), move(export_name_string), *caller_realm, eval_realm, eval_context)); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h index 8aa0e41b07..74786eb20b 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h @@ -21,6 +21,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(evaluate); + JS_DECLARE_NATIVE_FUNCTION(import_value); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.importValue.js b/Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.importValue.js new file mode 100644 index 0000000000..d79c9bc1b7 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.importValue.js @@ -0,0 +1,27 @@ +describe("normal behavior", () => { + test("length is 2", () => { + expect(ShadowRealm.prototype.importValue).toHaveLength(2); + }); + + test("basic functionality", () => { + // NOTE: The actual import is currently not implemented and always pretends to fail for now. + const shadowRealm = new ShadowRealm(); + const promise = shadowRealm.importValue("./myModule.js", "foo"); + let error; + promise.catch(value => { + error = value; + }); + expect(promise).toBeInstanceOf(Promise); + runQueuedPromiseJobs(); + expect(error).toBeInstanceOf(TypeError); + expect(error.message).toBe("Import of 'foo' from './myModule.js' failed"); + }); +}); + +describe("errors", () => { + test("this value must be a ShadowRealm object", () => { + expect(() => { + ShadowRealm.prototype.importValue.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type ShadowRealm"); + }); +});