diff --git a/INTERPRETING.md b/INTERPRETING.md index b5cb6de4462..10ed16105d4 100644 --- a/INTERPRETING.md +++ b/INTERPRETING.md @@ -45,6 +45,27 @@ properties of the global scope prior to test execution. tests (via the `async` flag, described below). - **`$262`** An ordinary object with the following properties: - **`AbstractModuleSource`** - a reference to the `%AbstractModuleSource%` constructor which does not appear as a property of the global object. + - **`createModuleSource`** - a function which accepts two arguments and + returns a new + [ModuleSource](https://tc39.es/proposal-source-phase-imports/#sec-module-source-objects) + object with a valid `[[ModuleSourceRecord]]` internal slot: + + $262.createModuleSource(sourceText, id) + + - `sourceText` -- a string of ECMAScript module source text. + - `id` -- a string used as the module's registry key. Two calls with + the same `id` produce ModuleSource objects that share the same + underlying Module Record identity; two calls with different `id` + values produce distinct Module Records. The host must resolve any + module specifiers in `sourceText` against this `id`. The `id` + becomes the `import.meta.moduleId` for that module (see + `importMetaHook`). + + The returned object has `%ModuleSource.prototype%` as its prototype + (which inherits from `%AbstractModuleSource.prototype%`). + + **Tests using this function are guarded with the + `esm-phase-imports` feature flag.** - **`createRealm`** - a function which creates a new [ECMAScript Realm](https://tc39.github.io/ecma262/#sec-code-realms), defines this API on the new realm's global object, and returns the `$262` @@ -70,6 +91,23 @@ properties of the global scope prior to test execution. - **`gc`** - a function that wraps the host's garbage collection invocation mechanism, if such a capability exists. Must throw an exception if no capability exists. This is necessary for testing the semantics of any feature that relies on garbage collection, e.g. the `WeakRef` API. - **`global`** - a reference to the global object on which `$262` was initially defined + - **`importMetaHook`** - the host's implementation of + [HostGetImportMetaProperties](https://tc39.github.io/ecma262/#sec-hostgetimportmetaproperties) + must set the following properties on every `import.meta` object: + + - `moduleId` -- a string. The host's registry key for the module. + For modules created via `$262.createModuleSource(sourceText, id)`, + this must be the `id` that was passed. For file-based modules, the + value is implementation-defined. In all cases, two modules that + share the same Module Record must have the same `moduleId`, and + two modules backed by different Module Records must have different + `moduleId` values. + + This contract allows tests to verify module identity without depending + on host-specific URL schemes. + + **Tests relying on this hook are guarded with the + `esm-phase-imports` feature flag.** - **`IsHTMLDDA`** - (present only in implementations that can provide it) an object that: diff --git a/features.txt b/features.txt index 4d99a8878ca..8811aff6d76 100644 --- a/features.txt +++ b/features.txt @@ -60,6 +60,10 @@ source-phase-imports ## test262 special specifier source-phase-imports-module-source +# ESM Phase Imports +## https://github.com/tc39/proposal-esm-phase-imports +esm-phase-imports + # Atomics.pause # https://github.com/tc39/proposal-atomics-microwait Atomics.pause diff --git a/test/built-ins/ModuleSource/constructor-throws.js b/test/built-ins/ModuleSource/constructor-throws.js new file mode 100644 index 00000000000..727875edc0e --- /dev/null +++ b/test/built-ins/ModuleSource/constructor-throws.js @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-modulesource +description: > + ModuleSource() throws a TypeError. +info: | + ModuleSource ( ) + + 1. Throw a *TypeError* exception. + + The ModuleSource constructor is not intended to be called directly. + It always throws a TypeError. +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +// Get a reference to the ModuleSource constructor via the prototype chain. +var src = $262.createModuleSource('export var x = 1;', baseId + 'ctor-throws'); +var ModuleSource = Object.getPrototypeOf(src).constructor; + +assert.sameValue(typeof ModuleSource, 'function', + 'ModuleSource should be a function'); + +assert.throws(TypeError, function () { + ModuleSource(); +}, 'Calling ModuleSource() without new should throw TypeError'); + +assert.throws(TypeError, function () { + new ModuleSource(); +}, 'Calling new ModuleSource() should throw TypeError'); diff --git a/test/built-ins/ModuleSource/identity/distinct-sources.js b/test/built-ins/ModuleSource/identity/distinct-sources.js new file mode 100644 index 00000000000..881ddf8690c --- /dev/null +++ b/test/built-ins/ModuleSource/identity/distinct-sources.js @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-module-source-objects +description: > + Two ModuleSource objects created from the same source text are not identical. +info: | + Each call to $262.createModuleSource with a different id returns a new, + distinct ModuleSource object backed by a different Module Record. +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src1 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-a'); +var src2 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-b'); + +assert.notSameValue(src1, src2, + 'Two ModuleSource objects created with different ids must not be ==='); +assert.sameValue(typeof src1, 'object'); +assert.sameValue(typeof src2, 'object'); diff --git a/test/built-ins/ModuleSource/identity/prototype-chain-cross-realm.js b/test/built-ins/ModuleSource/identity/prototype-chain-cross-realm.js new file mode 100644 index 00000000000..221ee7758c9 --- /dev/null +++ b/test/built-ins/ModuleSource/identity/prototype-chain-cross-realm.js @@ -0,0 +1,68 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-module-source-objects +description: > + A ModuleSource created in another realm has that realm's prototype chain, + not the current realm's. instanceof checks reflect this difference. +info: | + Each realm has its own %ModuleSource.prototype% and + %AbstractModuleSource.prototype% intrinsics. A ModuleSource object created + in realm A has realm A's %ModuleSource.prototype% as its [[Prototype]]. + When accessed from realm B, the object's prototype chain still belongs to + realm A. + + This is the standard cross-realm prototype behavior for all built-in + objects in ECMAScript. The instanceof operator checks the prototype chain + against the constructor's .prototype from the realm where instanceof is + evaluated, so a cross-realm ModuleSource is not an instanceof the current + realm's ModuleSource or AbstractModuleSource constructors. +features: [source-phase-imports, esm-phase-imports, cross-realm] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var otherRealm = $262.createRealm(); +var other$262 = otherRealm.global.$262; + +// Create a ModuleSource in the other realm. +var crossRealmSrc = other$262.createModuleSource('export var x = 1;', baseId + 'cross-realm-proto'); + +// Create a ModuleSource in the current realm for comparison. +var localSrc = $262.createModuleSource('export var x = 2;', baseId + 'local-proto'); + +// The cross-realm source's prototype chain belongs to the other realm. +var crossProto = Object.getPrototypeOf(crossRealmSrc); +var localProto = Object.getPrototypeOf(localSrc); + +// Each realm has distinct %ModuleSource.prototype% objects. +assert.notSameValue(crossProto, localProto, + 'Cross-realm ModuleSource should have a different [[Prototype]] than local ModuleSource'); + +// The cross-realm source's grandparent is the other realm's %AbstractModuleSource.prototype%. +var crossGrandProto = Object.getPrototypeOf(crossProto); +var localGrandProto = Object.getPrototypeOf(localProto); + +assert.notSameValue(crossGrandProto, localGrandProto, + 'Cross-realm AbstractModuleSource.prototype should differ from local AbstractModuleSource.prototype'); + +// The cross-realm source is an instanceof the other realm's constructors. +assert(crossRealmSrc instanceof other$262.AbstractModuleSource, + 'Cross-realm source should be instanceof other realm AbstractModuleSource'); + +// The cross-realm source is NOT an instanceof the current realm's constructors +// (standard cross-realm prototype semantics). +assert(!(crossRealmSrc instanceof $262.AbstractModuleSource), + 'Cross-realm source should NOT be instanceof current realm AbstractModuleSource'); + +// The local source is an instanceof the current realm's constructors. +assert(localSrc instanceof $262.AbstractModuleSource, + 'Local source should be instanceof current realm AbstractModuleSource'); + +// The local source is NOT an instanceof the other realm's constructors. +assert(!(localSrc instanceof other$262.AbstractModuleSource), + 'Local source should NOT be instanceof other realm AbstractModuleSource'); diff --git a/test/built-ins/ModuleSource/identity/prototype-chain.js b/test/built-ins/ModuleSource/identity/prototype-chain.js new file mode 100644 index 00000000000..a423ec9b7ca --- /dev/null +++ b/test/built-ins/ModuleSource/identity/prototype-chain.js @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-module-source-objects +description: > + ModuleSource instances have the correct prototype chain. +info: | + A ModuleSource object has %ModuleSource.prototype% as its [[Prototype]], + which in turn has %AbstractModuleSource.prototype% as its [[Prototype]]. +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'proto-chain'); + +var srcProto = Object.getPrototypeOf(src); +var abstractProto = $262.AbstractModuleSource.prototype; + +// ModuleSource instance's [[Prototype]] is %ModuleSource.prototype% +assert.notSameValue(srcProto, abstractProto, + 'ModuleSource.prototype should be distinct from AbstractModuleSource.prototype'); + +// %ModuleSource.prototype%'s [[Prototype]] is %AbstractModuleSource.prototype% +assert.sameValue(Object.getPrototypeOf(srcProto), abstractProto, + 'ModuleSource.prototype.[[Prototype]] should be AbstractModuleSource.prototype'); + +// %AbstractModuleSource.prototype%'s [[Prototype]] is %Object.prototype% +assert.sameValue(Object.getPrototypeOf(abstractProto), Object.prototype, + 'AbstractModuleSource.prototype.[[Prototype]] should be Object.prototype'); + +// The instance is an instanceof AbstractModuleSource +assert(src instanceof $262.AbstractModuleSource, + 'ModuleSource instance should be instanceof AbstractModuleSource'); diff --git a/test/built-ins/ModuleSource/identity/tostringtag-cross-realm.js b/test/built-ins/ModuleSource/identity/tostringtag-cross-realm.js new file mode 100644 index 00000000000..7f533a984c7 --- /dev/null +++ b/test/built-ins/ModuleSource/identity/tostringtag-cross-realm.js @@ -0,0 +1,57 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag +description: > + The @@toStringTag getter works on cross-realm ModuleSource objects because + it uses a brand check ([[ModuleSourceRecord]] internal slot), not a + prototype check. +info: | + get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ] + + 1. Let O be the this value. + 2. If O is not an Object, return undefined. + 3. Let module be GetModuleSourceRecord(O). + 4. If module is ~not-a-source~, return undefined. + 5. Let name be module.[[ModuleSourceKind]]. + 6. Assert: name is a String. + 7. Return name. + + GetModuleSourceRecord checks for the [[ModuleSourceRecord]] internal slot, + which is an object brand — it works regardless of which realm created the + object. A ModuleSource from another realm still has the slot, so the getter + returns "ModuleSource". +features: [source-phase-imports, esm-phase-imports, cross-realm] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var otherRealm = $262.createRealm(); +var otherCreateModuleSource = otherRealm.global.$262.createModuleSource; + +// Create a ModuleSource in the other realm. +var crossRealmSrc = otherCreateModuleSource('export var x = 1;', baseId + 'cross-realm-tag'); + +// The @@toStringTag getter from the current realm should still work on +// a cross-realm ModuleSource, because it uses the [[ModuleSourceRecord]] +// internal slot (brand check), not a prototype chain check. +var getter = Object.getOwnPropertyDescriptor( + $262.AbstractModuleSource.prototype, Symbol.toStringTag +).get; + +assert.sameValue(getter.call(crossRealmSrc), 'ModuleSource', + 'Current realm @@toStringTag getter should return "ModuleSource" for a cross-realm ModuleSource'); + +// Direct property access uses the other realm's prototype chain, so +// crossRealmSrc[Symbol.toStringTag] resolves through the other realm's +// AbstractModuleSource.prototype getter — which also works. +assert.sameValue(crossRealmSrc[Symbol.toStringTag], 'ModuleSource', + 'Direct @@toStringTag access on a cross-realm ModuleSource should return "ModuleSource"'); + +// Object.prototype.toString also works cross-realm. +assert.sameValue(Object.prototype.toString.call(crossRealmSrc), '[object ModuleSource]', + 'Object.prototype.toString should return "[object ModuleSource]" for a cross-realm ModuleSource'); diff --git a/test/built-ins/ModuleSource/identity/tostringtag-descriptor.js b/test/built-ins/ModuleSource/identity/tostringtag-descriptor.js new file mode 100644 index 00000000000..16a35121fbc --- /dev/null +++ b/test/built-ins/ModuleSource/identity/tostringtag-descriptor.js @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag +description: > + The @@toStringTag property on %AbstractModuleSource%.prototype has the + correct property descriptor: accessor with set=undefined, + enumerable=false, configurable=true; the getter's name is + "get [Symbol.toStringTag]". +info: | + get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ] + + This property has the attributes { [[Enumerable]]: false, [[Configurable]]: true }. + The initial value of the "name" property of this function is "get [Symbol.toStringTag]". +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var desc = Object.getOwnPropertyDescriptor( + $262.AbstractModuleSource.prototype, Symbol.toStringTag +); + +assert.sameValue(typeof desc.get, 'function', + '@@toStringTag should be an accessor property with a get function'); +assert.sameValue(desc.set, undefined, + '@@toStringTag set accessor should be undefined'); +assert.sameValue(desc.enumerable, false, + '@@toStringTag should not be enumerable'); +assert.sameValue(desc.configurable, true, + '@@toStringTag should be configurable'); + +// The getter's name property. +assert.sameValue(desc.get.name, 'get [Symbol.toStringTag]', + 'The getter name should be "get [Symbol.toStringTag]"'); diff --git a/test/built-ins/ModuleSource/identity/tostringtag-non-source.js b/test/built-ins/ModuleSource/identity/tostringtag-non-source.js new file mode 100644 index 00000000000..c841cf36464 --- /dev/null +++ b/test/built-ins/ModuleSource/identity/tostringtag-non-source.js @@ -0,0 +1,46 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag +description: > + The @@toStringTag getter returns undefined for non-objects and objects + without a [[ModuleSourceRecord]] internal slot. +info: | + get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ] + + 1. Let O be the this value. + 2. If O is not an Object, return undefined. + 3. Let module be GetModuleSourceRecord(O). + 4. If module is ~not-a-source~, return undefined. + ... +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var getter = Object.getOwnPropertyDescriptor( + $262.AbstractModuleSource.prototype, Symbol.toStringTag +).get; + +// Non-object this values return undefined. +assert.sameValue(getter.call(undefined), undefined, + 'undefined this should return undefined'); +assert.sameValue(getter.call(null), undefined, + 'null this should return undefined'); +assert.sameValue(getter.call(42), undefined, + 'number this should return undefined'); +assert.sameValue(getter.call('string'), undefined, + 'string this should return undefined'); +assert.sameValue(getter.call(true), undefined, + 'boolean this should return undefined'); +assert.sameValue(getter.call(Symbol()), undefined, + 'symbol this should return undefined'); + +// Objects without [[ModuleSourceRecord]] return undefined. +assert.sameValue(getter.call({}), undefined, + 'plain object should return undefined'); +assert.sameValue(getter.call([]), undefined, + 'array should return undefined'); +assert.sameValue(getter.call(function () {}), undefined, + 'function should return undefined'); diff --git a/test/built-ins/ModuleSource/identity/tostringtag.js b/test/built-ins/ModuleSource/identity/tostringtag.js new file mode 100644 index 00000000000..277b1b6e91d --- /dev/null +++ b/test/built-ins/ModuleSource/identity/tostringtag.js @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag +description: > + Symbol.toStringTag getter returns "ModuleSource" for ModuleSource instances. +info: | + get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ] + + 1. Let O be the this value. + 2. If O is not an Object, return undefined. + 3. Let module be GetModuleSourceRecord(O). + 4. If module is ~not-a-source~, return undefined. + 5. Let name be module.[[ModuleSourceKind]]. + 6. Assert: name is a String. + 7. Return name. + + For Source Text Module Records, [[ModuleSourceKind]] is "ModuleSource". +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); +assert.sameValue(typeof $262.AbstractModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'tostringtag'); + +var toStringTag = src[Symbol.toStringTag]; +assert.sameValue(toStringTag, 'ModuleSource', + 'Symbol.toStringTag should return "ModuleSource" for a ModuleSource instance'); + +// Verify via Object.prototype.toString +assert.sameValue(Object.prototype.toString.call(src), '[object ModuleSource]', + 'Object.prototype.toString should return "[object ModuleSource]"'); + +// Verify the getter is inherited from AbstractModuleSource.prototype +var getter = Object.getOwnPropertyDescriptor( + $262.AbstractModuleSource.prototype, Symbol.toStringTag +).get; +assert.sameValue(typeof getter, 'function'); +assert.sameValue(getter.call(src), 'ModuleSource', + 'The inherited @@toStringTag getter should return "ModuleSource" when called on a ModuleSource instance'); diff --git a/test/built-ins/ModuleSource/prototype-constructor.js b/test/built-ins/ModuleSource/prototype-constructor.js new file mode 100644 index 00000000000..e918089db44 --- /dev/null +++ b/test/built-ins/ModuleSource/prototype-constructor.js @@ -0,0 +1,31 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-%modulesource%.prototype.constructor +description: > + %ModuleSource%.prototype.constructor is %ModuleSource%. +info: | + The initial value of %ModuleSource%.prototype.constructor is %ModuleSource%. +features: [source-phase-imports, esm-phase-imports] +flags: [module] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'proto-ctor'); +var ModuleSourcePrototype = Object.getPrototypeOf(src); +var ModuleSource = ModuleSourcePrototype.constructor; + +assert.sameValue(typeof ModuleSource, 'function', + '%ModuleSource%.prototype.constructor should be a function'); + +// The constructor property on the prototype should point back to %ModuleSource%. +assert.sameValue(ModuleSourcePrototype.constructor, ModuleSource, + '%ModuleSource%.prototype.constructor should be %ModuleSource%'); + +// Calling the constructor should throw TypeError (per sec-modulesource). +assert.throws(TypeError, function () { + ModuleSource(); +}, '%ModuleSource% should throw TypeError when called'); diff --git a/test/language/import/import-defer/deferred-namespace-object/import-defer-module-source.js b/test/language/import/import-defer/deferred-namespace-object/import-defer-module-source.js new file mode 100644 index 00000000000..0a068a9426d --- /dev/null +++ b/test/language/import/import-defer/deferred-namespace-object/import-defer-module-source.js @@ -0,0 +1,63 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-getmodulenamespace +description: > + import.defer(moduleSource) returns a deferred namespace object for the module. +info: | + GetModuleNamespace ( _module_, _phase_ ) + 1. ... + 1. If _phase_ is ~defer~, let _namespace_ be _module_.[[DeferredNamespace]], + otherwise let _namespace_ be _module_.[[Namespace]]. + 1. If _namespace_ is ~empty~, then + 1. ... + 1. Set _namespace_ to ModuleNamespaceCreate(_module_, _unambiguousNames_, _phase_). + 1. Return _namespace_. + + When import.defer() is called with a ModuleSource object, the host resolves + the module via the ModuleSource's id and returns a deferred namespace. + Accessing a property on the deferred namespace triggers evaluation. + The deferred namespace is distinct from the eager namespace. + Repeated import.defer() of the same ModuleSource returns the same object. +features: [import-defer, source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource( + 'export var foo = 1; export var bar = 2;', + baseId + 'defer-module-source' +); + +asyncTest(async function () { + // import.defer() of a ModuleSource returns a deferred namespace. + var nsDeferred1 = await import.defer(src); + assert.sameValue(typeof nsDeferred1, 'object', + 'import.defer(moduleSource) should return an object'); + + // Repeated import.defer() returns the same deferred namespace. + var nsDeferred2 = await import.defer(src); + assert.sameValue(nsDeferred1, nsDeferred2, + 'import.defer(moduleSource) twice should return the same deferred namespace'); + + // The deferred namespace has the correct @@toStringTag. + assert.sameValue(nsDeferred1[Symbol.toStringTag], 'Deferred Module', + 'Deferred namespace @@toStringTag should be "Deferred Module"'); + + // Accessing a property triggers evaluation and returns the exported value. + assert.sameValue(nsDeferred1.foo, 1, + 'Accessing a property on the deferred namespace should trigger evaluation'); + assert.sameValue(nsDeferred1.bar, 2, + 'All exports should be accessible on the deferred namespace'); + + // The eager namespace (via import()) is distinct from the deferred one. + var nsEager = await import(src); + assert.notSameValue(nsDeferred1, nsEager, + 'Deferred namespace should be distinct from eager namespace'); + assert.sameValue(nsEager.foo, 1, + 'Eager namespace should have the same exported values'); +}); diff --git a/test/language/module-code/source-phase-import/base-id_FIXTURE.js b/test/language/module-code/source-phase-import/base-id_FIXTURE.js new file mode 100644 index 00000000000..731b106640e --- /dev/null +++ b/test/language/module-code/source-phase-import/base-id_FIXTURE.js @@ -0,0 +1,7 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +// Fixture that exports its own moduleId so that script-context tests +// can discover the host's id scheme. + +export var moduleId = import.meta.moduleId; diff --git a/test/language/module-code/source-phase-import/dynamic-import-non-source-object.js b/test/language/module-code/source-phase-import/dynamic-import-non-source-object.js new file mode 100644 index 00000000000..93bb5daff01 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-non-source-object.js @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import({}) calls ToString on the object and follows the string specifier path. +info: | + An object without a [[ModuleSourceRecord]] internal slot is not treated as a + source import. Instead, import() calls ToString on the argument and attempts + to resolve it as a module specifier string. Since the ToString of a plain + object is "[object Object]", which is not a valid module specifier, the + import should reject. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +asyncTest(async function () { + // A plain object is not a ModuleSource -- ToString produces "[object Object]" + // which is not a resolvable specifier, so this should reject. + var plainObj = {}; + try { + await import(plainObj); + throw new Test262Error('import({}) should have rejected'); + } catch (e) { + if (e instanceof Test262Error) throw e; + // The error type is host-defined (likely TypeError or a resolution error), + // but it must not succeed silently. + } +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-cross-realm.js b/test/language/module-code/source-phase-import/dynamic-import-source-cross-realm.js new file mode 100644 index 00000000000..a15fee05c0c --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-cross-realm.js @@ -0,0 +1,110 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + A ModuleSource created in one realm can be dynamically imported in another + realm. The target realm gets a fresh module instance with the same moduleId. + Importing the same source twice in the target realm returns the same namespace. +info: | + PR #61 removes the prototype chain check from EvaluateImportCall that + previously rejected cross-realm ModuleSource objects. The removed code + verified the ModuleSource's grandparent prototype was the current realm's + %AbstractModuleSource.prototype%, effectively a same-realm check. + + With this change, a source import in one realm and imported in another realm + will have a unique instance and instance identity in that other realm. Its + ID string will define into the realm's module registry when its ID has not + previously been defined. + + The ModuleSource retains its Module Source Record identity across realms. + The HostLoadImportedModule invariant requires that if the [[ModuleSource]] + object is the same, the resolved Module Record must be the same within + that realm. + + Additionally, the source's ID string defines into the target realm's + module registry when that ID has not previously been defined. A subsequent + string-based import of the same ID in the target realm must resolve to the + same Module Record (HostLoadImportedModule condition 2: when one specifier + is a String and ModuleSourcesEqual matches the other's source record). +features: [source-phase-imports, esm-phase-imports, cross-realm] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var idA = baseId + 'cross-realm-a'; +var idB = baseId + 'cross-realm-b'; + +var srcA = $262.createModuleSource( + 'export var moduleId = import.meta.moduleId;', + idA +); + +var srcB = $262.createModuleSource( + 'export var moduleId = import.meta.moduleId;', + idB +); + +var otherRealm = $262.createRealm(); + +// Pass ModuleSource objects to the other realm via its global. +otherRealm.global.srcA = srcA; +otherRealm.global.srcB = srcB; + +// Pass the id string so the other realm can also import by string. +otherRealm.global.idA = idA; + +// Import both sources in the other realm, then re-import srcA by source +// and by string id. +otherRealm.evalScript( + 'globalThis.testPromise = (async () => {' + + ' globalThis.nsA = await import(srcA);' + + ' globalThis.nsB = await import(srcB);' + + ' globalThis.nsA2 = await import(srcA);' + + ' globalThis.nsAById = await import(idA);' + + '})();' +); + +asyncTest(async function () { + await otherRealm.global.testPromise; + + var nsA = otherRealm.global.nsA; + var nsB = otherRealm.global.nsB; + var nsA2 = otherRealm.global.nsA2; + var nsAById = otherRealm.global.nsAById; + + // 1. Verify import.meta.moduleId reflects the id from createModuleSource. + assert.sameValue(nsA.moduleId, idA, + 'import.meta.moduleId in other realm should match idA'); + assert.sameValue(nsB.moduleId, idB, + 'import.meta.moduleId in other realm should match idB'); + + // 2. Verify the two namespaces are distinct. + assert.notSameValue(nsA, nsB, + 'Namespaces from different sources in the other realm should be distinct'); + + // 3. Importing srcA a second time in the same realm should return the same + // namespace (same Module Record, idempotent within a realm). + assert.sameValue(nsA, nsA2, + 'Importing the same ModuleSource twice in the other realm should return the same namespace'); + + // 4. The source's ID string defines into the other realm's module registry. + // A subsequent string-based import of the same ID in the other realm + // should resolve to the same Module Record. + assert.sameValue(nsAById, nsA, + 'Importing by string ID in the other realm should return the same namespace as importing the source'); + assert.sameValue(nsAById.moduleId, idA, + 'String-based import in the other realm should have the same moduleId'); + + // 5. Import srcA in the current realm too — it should be a different + // instance (different realm = different module registry). + var nsAHere = await import(srcA); + assert.sameValue(nsAHere.moduleId, idA, + 'import.meta.moduleId in current realm should also match idA'); + assert.notSameValue(nsA, nsAHere, + 'Namespace from the other realm should be a different instance than the current realm namespace'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-distinct.js b/test/language/module-code/source-phase-import/dynamic-import-source-distinct.js new file mode 100644 index 00000000000..88f778b4698 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-distinct.js @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + Two distinct ModuleSource objects with the same source text produce different namespaces. +info: | + Two ModuleSource objects created with different ids have different + host-defined keys and therefore map to different Module Records. Different + Module Records produce different module namespace objects. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src1 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-a'); +var src2 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-b'); + +asyncTest(async function () { + var ns1 = await import(src1); + var ns2 = await import(src2); + assert.notSameValue(ns1, ns2, + 'Distinct ModuleSource objects should produce different namespace objects'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-evaluates.js b/test/language/module-code/source-phase-import/dynamic-import-source-evaluates.js new file mode 100644 index 00000000000..48631ef6de7 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-evaluates.js @@ -0,0 +1,32 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource) triggers evaluation from script context; side effects + are observable. +info: | + When a ModuleSource is dynamically imported from a script, the module is + evaluated and its side effects become observable. This test runs as a script + (not a module) to verify that import() of a ModuleSource works in script + context. It uses a fixture to discover the host's base id. +features: [source-phase-imports, esm-phase-imports] +flags: [async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +asyncTest(async function () { + var fixture = await import('./base-id_FIXTURE.js'); + var baseId = fixture.moduleId.slice(0, fixture.moduleId.lastIndexOf('/') + 1); + + var src = $262.createModuleSource( + 'export var counter = 0; counter++;', + baseId + 'evaluates' + ); + + var ns = await import(src); + assert.sameValue(ns.counter, 1, + 'Module should have been evaluated, incrementing counter from 0 to 1'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-idempotent.js b/test/language/module-code/source-phase-import/dynamic-import-source-idempotent.js new file mode 100644 index 00000000000..a3a6e21e0c3 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-idempotent.js @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource) twice with the same ModuleSource returns the same namespace. +info: | + When the same ModuleSource object (same identity) is imported multiple times, + the host returns the same Module Record. Because the same Module Record + produces the same module namespace object, both import() calls resolve to + the same namespace. + + Tested with both $262.createModuleSource and the import source declaration. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +import source srcDecl from ''; + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var srcCreated = $262.createModuleSource('export var x = 1;', baseId + 'idempotent'); + +asyncTest(async function () { + // Via $262.createModuleSource + var ns1 = await import(srcCreated); + var ns2 = await import(srcCreated); + assert.sameValue(ns1, ns2, + 'Importing the same created ModuleSource twice should return the same namespace'); + + // Via import source declaration + var ns3 = await import(srcDecl); + var ns4 = await import(srcDecl); + assert.sameValue(ns3, ns4, + 'Importing the same declared source twice should return the same namespace'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-import-meta-distinct.js b/test/language/module-code/source-phase-import/dynamic-import-source-import-meta-distinct.js new file mode 100644 index 00000000000..70ee773c3b2 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-import-meta-distinct.js @@ -0,0 +1,50 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + Different instances of the same ModuleSource (in different realms) each + receive their own distinct import.meta object, even though moduleId is + the same. +info: | + HostGetImportMetaProperties creates a new import.meta object for each + module instance. When the same ModuleSource is imported in two different + realms, each realm creates a separate Module Record and therefore a + separate import.meta object. The moduleId is the same (derived from the + source's id), but the import.meta objects are distinct. +features: [source-phase-imports, esm-phase-imports, cross-realm] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource( + 'export var meta = import.meta;', + baseId + 'meta-distinct' +); + +var otherRealm = $262.createRealm(); +otherRealm.global.src = src; + +otherRealm.evalScript( + 'globalThis.testPromise = (async () => {' + + ' globalThis.ns = await import(src);' + + '})();' +); + +asyncTest(async function () { + var nsHere = await import(src); + await otherRealm.global.testPromise; + var nsOther = otherRealm.global.ns; + + // Both instances export the same moduleId. + assert.sameValue(nsHere.meta.moduleId, nsOther.meta.moduleId, + 'Both instances should have the same import.meta.moduleId'); + + // But the import.meta objects themselves are distinct. + assert.notSameValue(nsHere.meta, nsOther.meta, + 'Different instances of the same source should have distinct import.meta objects'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-linking-error.js b/test/language/module-code/source-phase-import/dynamic-import-source-linking-error.js new file mode 100644 index 00000000000..baddf458954 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-linking-error.js @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + A ModuleSource whose source text imports a non-existent module: + import(moduleSource) rejects with an error during linking. +info: | + When a ModuleSource contains an import declaration that references a module + specifier that cannot be resolved, dynamically importing it must reject. + The error occurs during the linking phase when the host fails to resolve the + specifier. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource( + 'import { foo } from "nonexistent-module";', + baseId + 'linking-error' +); + +asyncTest(async function () { + try { + await import(src); + throw new Test262Error('import of a ModuleSource with an unresolvable dependency should have rejected'); + } catch (e) { + if (e instanceof Test262Error) throw e; + // The error type during linking is host-defined, but the import must reject. + // It should not be a SyntaxError (the source text itself is syntactically valid). + if (e instanceof SyntaxError) { + throw new Test262Error('Expected a linking error, not a SyntaxError'); + } + } +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-linking.js b/test/language/module-code/source-phase-import/dynamic-import-source-linking.js new file mode 100644 index 00000000000..f7a008416f2 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-linking.js @@ -0,0 +1,45 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + A ModuleSource whose source text has static imports correctly links and evaluates. +info: | + When a ModuleSource contains import declarations, the host must resolve those + specifiers against the module's id and link the dependency graph before + evaluation. This test creates a dependency module first (loading it into + the module map), then creates a module that imports from it by id. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var depId = baseId + 'linking-dep'; +var mainId = baseId + 'linking-main'; + +// Create the dependency module first. +var depSrc = $262.createModuleSource( + 'export var dep = "dependency";', + depId +); + +// Create the main module that imports from the dependency by its id. +var mainSrc = $262.createModuleSource( + 'import { dep } from "' + depId + '"; export { dep };', + mainId +); + +asyncTest(async function () { + // Ensure the dependency is loaded into the module map. + var nsDep = await import(depSrc); + assert.sameValue(nsDep.dep, 'dependency', 'dependency module should export dep'); + + // Import the main module which depends on the dependency. + var nsMain = await import(mainSrc); + assert.sameValue(nsMain.dep, 'dependency', + 'main module should re-export the dep binding from its dependency'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-module-id.js b/test/language/module-code/source-phase-import/dynamic-import-source-module-id.js new file mode 100644 index 00000000000..4c0fd6938ac --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-module-id.js @@ -0,0 +1,55 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import.meta.moduleId reflects the id passed to $262.createModuleSource. +info: | + Per the importMetaHook contract, the host sets import.meta.moduleId to the + id passed to $262.createModuleSource. Two modules with different ids have + different moduleId values. Two imports of the same ModuleSource share the + same moduleId and return the same namespace. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var idA = baseId + 'module-id-a'; +var idB = baseId + 'module-id-b'; + +var src1 = $262.createModuleSource( + 'export var moduleId = import.meta.moduleId;', + idA +); + +var src2 = $262.createModuleSource( + 'export var moduleId = import.meta.moduleId;', + idB +); + +asyncTest(async function () { + var ns1 = await import(src1); + assert.sameValue(ns1.moduleId, idA, + 'import.meta.moduleId should be the id passed to createModuleSource'); + + var ns2 = await import(src2); + assert.sameValue(ns2.moduleId, idB, + 'import.meta.moduleId should be the id passed to createModuleSource'); + + assert.notSameValue(ns1.moduleId, ns2.moduleId, + 'Two modules with different ids should have different moduleId values'); + assert.notSameValue(ns1, ns2, + 'Two modules with different ids should have different namespaces'); + + // Importing the same source again returns the same namespace -- + // and therefore the same moduleId. + var ns1Again = await import(src1); + assert.sameValue(ns1Again, ns1, + 'Re-importing the same ModuleSource should return the same namespace'); + assert.sameValue(ns1Again.moduleId, ns1.moduleId, + 'moduleId should be stable across re-imports'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-options-non-object.js b/test/language/module-code/source-phase-import/dynamic-import-source-options-non-object.js new file mode 100644 index 00000000000..9e56ba767f2 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-options-non-object.js @@ -0,0 +1,43 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource, "string") rejects with TypeError. +info: | + When import() is called with a second argument that is not an object + (and is not undefined), it must reject with a TypeError. + + 2.1.1.1 EvaluateImportCall ( specifierExpression [ , optionsExpression ] ) + + ... + 6. If optionsExpression is present, then + ... + e. If the Type of options is not Object, then + i. Perform ! Call(promiseCapability.[[Reject]], undefined, + a newly created TypeError object). + ii. Return promiseCapability.[[Promise]]. +features: [source-phase-imports, esm-phase-imports, import-attributes] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'options-non-obj'); + +asyncTest(async function () { + await assert.throwsAsync(TypeError, function () { + return import(src, 'string'); + }, 'import with a string options argument should reject with TypeError'); + + await assert.throwsAsync(TypeError, function () { + return import(src, 42); + }, 'import with a number options argument should reject with TypeError'); + + await assert.throwsAsync(TypeError, function () { + return import(src, true); + }, 'import with a boolean options argument should reject with TypeError'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-options-null.js b/test/language/module-code/source-phase-import/dynamic-import-source-options-null.js new file mode 100644 index 00000000000..35ca71e65c2 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-options-null.js @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-evaluate-import-call +description: > + import(moduleSource, null) rejects with TypeError because null is not an Object. +info: | + EvaluateImportCall ( specifierExpression, phase [, optionsExpression] ) + + ... + 11. If specifier is an Object, then + a. Let moduleSourceRecord be GetModuleSourceRecord(specifier). + b. If moduleSourceRecord is not ~not-a-source~, then + i. If options is not undefined, then + 1. If options is not an Object, then + a. Perform ! Call(promiseCapability.[[Reject]], undefined, + « a newly created TypeError object »). + b. Return promiseCapability.[[Promise]]. + + Since null is not undefined and not an Object, the import rejects with TypeError. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'options-null'); + +asyncTest(async function () { + await assert.throwsAsync(TypeError, function () { + return import(src, null); + }, 'import(moduleSource, null) should reject with TypeError'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-options-undefined-ok.js b/test/language/module-code/source-phase-import/dynamic-import-source-options-undefined-ok.js new file mode 100644 index 00000000000..22da0298069 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-options-undefined-ok.js @@ -0,0 +1,29 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource, {}) and import(moduleSource, { with: undefined }) succeed. +info: | + When the options argument is present but has no `with` property, or `with` is + undefined, the import should proceed normally without error. +features: [source-phase-imports, esm-phase-imports, import-attributes] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'options-undef'); + +asyncTest(async function () { + // Options present but no `with` property + var ns1 = await import(src, {}); + assert.sameValue(ns1.x, 1, 'import with empty options should succeed'); + + // `with` property is explicitly undefined + var ns2 = await import(src, { with: undefined }); + assert.sameValue(ns2.x, 1, 'import with { with: undefined } should succeed'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-order-source-first.js b/test/language/module-code/source-phase-import/dynamic-import-source-order-source-first.js new file mode 100644 index 00000000000..a505ba0cc8d --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-order-source-first.js @@ -0,0 +1,53 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + When a created ModuleSource is imported first, a later string import + via the same id unifies with it. +info: | + HostLoadImportedModule invariant (condition 3 of the equality rule): + + If moduleRequest2.[[Specifier]] is a String, and + ModuleSourcesEqual(moduleRequest1.[[Specifier]], module2.[[SourceRecord]]) + is true, then module1 must equal module2. + + Here a ModuleSource (exporting source = "first") is imported first via + source import (moduleRequest1). Then the same id is imported as a string + (moduleRequest2). Per the $262.createModuleSource contract, the id is the + module's registry key, so the string import resolves to the same Canonical + ModuleRequest Record. The source import's Module Source Record satisfies + ModuleSourcesEqual with the resolved module's [[SourceRecord]], so both + must be the same Module Record. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var inlineId = baseId + 'order-source-first'; + +var inlineSrc = $262.createModuleSource( + 'export var source = "first"; export var moduleId = import.meta.moduleId;', + inlineId +); + +asyncTest(async function () { + // Import the created ModuleSource first -- this populates the module map. + var nsFirst = await import(inlineSrc); + assert.sameValue(nsFirst.source, 'first', + 'Created ModuleSource should export source = "first"'); + assert.sameValue(nsFirst.moduleId, inlineId, + 'import.meta.moduleId should reflect the id passed to createModuleSource'); + + // Import via the id string second -- should unify with the first. + var nsSecond = await import(inlineId); + assert.sameValue(nsSecond.source, 'first', + 'String import should unify with the already-loaded created module'); + + assert.sameValue(nsFirst, nsSecond, + 'Both imports should return the same namespace object'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-order-url-first.js b/test/language/module-code/source-phase-import/dynamic-import-source-order-url-first.js new file mode 100644 index 00000000000..20269187811 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-order-url-first.js @@ -0,0 +1,58 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + When the file-system module is imported first by string specifier, a later + source import of a ModuleSource with the same id unifies with it. +info: | + HostLoadImportedModule invariant (condition 2 of the equality rule): + + If moduleRequest1.[[Specifier]] is a String, and + ModuleSourcesEqual(moduleRequest2.[[Specifier]], module1.[[SourceRecord]]) + is true, then module1 must equal module2. + + Additionally, the standalone constraint requires that when + moduleRequest.[[Specifier]] is a Module Source Record, + ModuleSourcesEqual(module.[[SourceRecord]], moduleRequest.[[Specifier]]) + must be true. + + Here the file-system module (exporting source = "file") is imported first + by string specifier (moduleRequest1). A ModuleSource is then created with + the same id (moduleRequest2). Per the $262.createModuleSource contract, + same id = same Module Record, so the created ModuleSource's + [[ModuleSourceRecord]] equals the file module's [[SourceRecord]]. The + source import must therefore unify with the already-loaded file module. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +asyncTest(async function () { + // Import the file-system module first -- this populates the module map. + var nsFile = await import('./import-source-order_FIXTURE.js'); + assert.sameValue(nsFile.source, 'file', + 'File-system module should export source = "file"'); + + // Discover the host-assigned id for this module via import.meta.moduleId. + var hostId = nsFile.moduleId; + assert.sameValue(typeof hostId, 'string', + 'The fixture should export its import.meta.moduleId as a string'); + + // Create a ModuleSource with that same id but different source text. + var inlineSrc = $262.createModuleSource( + 'export var source = "inline";', + hostId + ); + + // Import the created ModuleSource with the same id second. + // It should unify with the already-loaded file-system module. + var nsInline = await import(inlineSrc); + assert.sameValue(nsInline.source, 'file', + 'Created ModuleSource with same id should unify with the already-loaded file module'); + + assert.sameValue(nsFile, nsInline, + 'Both imports should return the same namespace object'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-rejects-attributes.js b/test/language/module-code/source-phase-import/dynamic-import-source-rejects-attributes.js new file mode 100644 index 00000000000..e59bc8db2c1 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-rejects-attributes.js @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource, { with: { type: "json" } }) rejects with TypeError. +info: | + Import attributes are not supported for source imports. When import() is + called with a ModuleSource and a non-empty `with` attribute, it must reject + with a TypeError. +features: [source-phase-imports, esm-phase-imports, import-attributes] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'rejects-attrs'); + +asyncTest(async function () { + await assert.throwsAsync(TypeError, function () { + return import(src, { with: { type: 'json' } }); + }, 'import with attributes on a ModuleSource should reject with TypeError'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-same-id-identity.js b/test/language/module-code/source-phase-import/dynamic-import-source-same-id-identity.js new file mode 100644 index 00000000000..883d71d892c --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-same-id-identity.js @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-HostLoadImportedModule +description: > + Two createModuleSource calls with the same id return ModuleSource objects + that share the same underlying Module Record, so importing either yields the + same namespace. +info: | + HostLoadImportedModule constraint: + + If moduleRequest.[[Specifier]] is a Module Source Record, then + ModuleSourcesEqual(module.[[SourceRecord]], moduleRequest.[[Specifier]]) + must be true. + + ModuleSourcesEqual checks [[ModuleSource]] object identity. Combined with + the host invariant (condition 4): when two source imports have + ModuleSourcesEqual specifiers, they must resolve to the same Module Record. + + Per the $262.createModuleSource contract, two calls with the same id + produce ModuleSource objects that share the same underlying Module Record + identity (and thus the same [[ModuleSource]] object). Importing either + must yield the same Module Record and therefore the same namespace. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); +var sharedId = baseId + 'same-id-identity'; + +var src1 = $262.createModuleSource('export var x = 1;', sharedId); +var src2 = $262.createModuleSource('export var x = 1;', sharedId); + +// The two ModuleSource objects share the same underlying Module Record, +// so they should be the same object (same [[ModuleSource]] on the shared +// Module Source Record). +assert.sameValue(src1, src2, + 'Two createModuleSource calls with the same id should return the same ModuleSource object'); + +asyncTest(async function () { + var ns1 = await import(src1); + var ns2 = await import(src2); + assert.sameValue(ns1, ns2, + 'Importing two ModuleSource objects with the same id should yield the same namespace'); + assert.sameValue(ns1.x, 1, 'The module should export x = 1'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty.js b/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty.js new file mode 100644 index 00000000000..5fa4cd92f15 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty.js @@ -0,0 +1,31 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-ContinueDynamicImport +description: > + import.source() of a module whose [[ModuleSource]] is ~empty~ rejects with SyntaxError. +info: | + ContinueDynamicImport ( promiseCapability, phase, moduleCompletion ) + + ... + 3. If phase is ~source~, then + a. Let moduleSource be module.[[SourceRecord]].[[ModuleSource]]. + b. If moduleSource is ~empty~, then + i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a new SyntaxError »). + c. Else, + i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « moduleSource »). + d. Return ~unused~. + + A JSON module (imported via a string specifier with the "json" import + attribute) is backed by a Synthetic Module Record whose [[ModuleSource]] is + ~empty~, so requesting its source phase must reject with SyntaxError. +features: [source-phase-imports, esm-phase-imports, import-attributes, json-modules] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +asyncTest(async function () { + await assert.throwsAsync(SyntaxError, function () { + return import.source('./dynamic-import-source-source-phase-empty_FIXTURE.json', { with: { type: 'json' } }); + }, 'import.source() of a JSON module should reject with SyntaxError because its [[ModuleSource]] is ~empty~'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty_FIXTURE.json b/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty_FIXTURE.json new file mode 100644 index 00000000000..7ba1b793e58 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-source-phase-empty_FIXTURE.json @@ -0,0 +1 @@ +{ "key": "value" } diff --git a/test/language/module-code/source-phase-import/dynamic-import-source-syntax-error.js b/test/language/module-code/source-phase-import/dynamic-import-source-syntax-error.js new file mode 100644 index 00000000000..1177fb6a49b --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source-syntax-error.js @@ -0,0 +1,27 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + A ModuleSource created from syntactically invalid source text: + import(moduleSource) rejects with SyntaxError. +info: | + When a ModuleSource is created from source text that contains a syntax error, + dynamically importing it must reject with a SyntaxError. The parse error is + surfaced at import time. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = ;', baseId + 'syntax-error'); + +asyncTest(async function () { + await assert.throwsAsync(SyntaxError, function () { + return import(src); + }, 'Importing a ModuleSource with a syntax error should reject with SyntaxError'); +}); diff --git a/test/language/module-code/source-phase-import/dynamic-import-source.js b/test/language/module-code/source-phase-import/dynamic-import-source.js new file mode 100644 index 00000000000..d397bf4e945 --- /dev/null +++ b/test/language/module-code/source-phase-import/dynamic-import-source.js @@ -0,0 +1,41 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-import-call-runtime-semantics-evaluation +description: > + import(moduleSource) resolves to a module namespace object with accessible bindings. +info: | + When import() is called with a ModuleSource object (an object with a + [[ModuleSourceRecord]] internal slot), the host uses that Module Source + Record to load and evaluate the module, returning the module namespace. + + Tested with both $262.createModuleSource and the import source declaration. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +import source srcDecl from ''; + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var srcCreated = $262.createModuleSource( + 'export var x = 42; export var y = "hello";', + baseId + 'dynamic-import' +); + +asyncTest(async function () { + // Via $262.createModuleSource + var ns = await import(srcCreated); + assert.sameValue(ns.x, 42, 'exported binding x should be 42'); + assert.sameValue(ns.y, 'hello', 'exported binding y should be "hello"'); + + // Via import source declaration -- the binding is a ModuleSource object + assert.sameValue(typeof srcDecl, 'object', 'import source binding should be an object'); + assert(srcDecl instanceof $262.AbstractModuleSource, + 'import source binding should be an instance of AbstractModuleSource'); + var nsDecl = await import(srcDecl); + assert.sameValue(typeof nsDecl, 'object', 'namespace from declared source should be an object'); +}); diff --git a/test/language/module-code/source-phase-import/import-source-cross-realm.js b/test/language/module-code/source-phase-import/import-source-cross-realm.js new file mode 100644 index 00000000000..da147203327 --- /dev/null +++ b/test/language/module-code/source-phase-import/import-source-cross-realm.js @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-ContinueDynamicImport +description: > + import.source() of a cross-realm ModuleSource returns the same ModuleSource + object. The source identity is preserved across realms. +info: | + ContinueDynamicImport ( promiseCapability, phase, moduleCompletion ) + + 3. If phase is ~source~, then + a. Let moduleSource be module.[[SourceRecord]].[[ModuleSource]]. + b. If moduleSource is ~empty~, then + i. Perform ! Call(promiseCapability.[[Reject]], ..., « a new SyntaxError »). + c. Else, + i. Perform ! Call(promiseCapability.[[Resolve]], ..., « moduleSource »). + + PR #61 removes the prototype chain check from EvaluateImportCall that + previously rejected cross-realm ModuleSource objects. With the check removed, + import.source(crossRealmSource) succeeds and resolves with the same + [[ModuleSource]] object from the Module Source Record, since + ModuleSourcesEqual checks [[ModuleSource]] object identity. +features: [source-phase-imports, esm-phase-imports, cross-realm] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'cross-realm-source'); + +var otherRealm = $262.createRealm(); +otherRealm.global.src = src; + +// Perform import.source() in the other realm. +otherRealm.evalScript( + 'globalThis.testPromise = (async () => {' + + ' globalThis.srcFromOther = await import.source(src);' + + '})();' +); + +asyncTest(async function () { + await otherRealm.global.testPromise; + + var srcFromOther = otherRealm.global.srcFromOther; + + // import.source() should return the same ModuleSource object, regardless + // of which realm performed the import.source() call. + assert.sameValue(srcFromOther, src, + 'import.source(crossRealmSource) should return the same ModuleSource object'); + + // Also verify import.source() in the current realm returns the same object. + var srcFromHere = await import.source(src); + assert.sameValue(srcFromHere, src, + 'import.source(src) in the current realm should also return the same object'); + + assert.sameValue(srcFromOther, srcFromHere, + 'Both realms should see the same ModuleSource object identity'); +}); diff --git a/test/language/module-code/source-phase-import/import-source-empty-source-error.js b/test/language/module-code/source-phase-import/import-source-empty-source-error.js new file mode 100644 index 00000000000..1f7a9fac34f --- /dev/null +++ b/test/language/module-code/source-phase-import/import-source-empty-source-error.js @@ -0,0 +1,27 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-source-text-module-record-initialize-environment +description: > + Static import source of a module whose [[ModuleSource]] is ~empty~ throws SyntaxError. +info: | + InitializeEnvironment ( ) + + ... + 7. For each ImportEntry Record in of module.[[ImportEntries]], do + ... + c. Else if in.[[ImportName]] is ~source~, then + i. Let moduleSourceObject be importedModule.[[SourceRecord]].[[ModuleSource]]. + ii. If moduleSourceObject is ~empty~, throw a *SyntaxError* exception. + + A JSON module has [[ModuleSource]] = ~empty~, so a static source import of it + must throw SyntaxError during linking. +negative: + phase: resolution + type: SyntaxError +features: [source-phase-imports, esm-phase-imports, import-attributes, json-modules] +flags: [module] +---*/ + +$DONOTEVALUATE(); +import source jsonSrc from './import-source-empty-source-error_FIXTURE.json' with { type: 'json' }; diff --git a/test/language/module-code/source-phase-import/import-source-empty-source-error_FIXTURE.json b/test/language/module-code/source-phase-import/import-source-empty-source-error_FIXTURE.json new file mode 100644 index 00000000000..7ba1b793e58 --- /dev/null +++ b/test/language/module-code/source-phase-import/import-source-empty-source-error_FIXTURE.json @@ -0,0 +1 @@ +{ "key": "value" } diff --git a/test/language/module-code/source-phase-import/import-source-of-module-source.js b/test/language/module-code/source-phase-import/import-source-of-module-source.js new file mode 100644 index 00000000000..f927ed03388 --- /dev/null +++ b/test/language/module-code/source-phase-import/import-source-of-module-source.js @@ -0,0 +1,51 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-ContinueDynamicImport +description: > + import.source(moduleSource) returns the same ModuleSource object. +info: | + ContinueDynamicImport ( promiseCapability, phase, moduleCompletion ) + + 3. If phase is ~source~, then + a. Let moduleSource be module.[[SourceRecord]].[[ModuleSource]]. + b. If moduleSource is ~empty~, then + i. Perform ! Call(promiseCapability.[[Reject]], ..., « a new SyntaxError »). + c. Else, + i. Perform ! Call(promiseCapability.[[Resolve]], ..., « moduleSource »). + + HostLoadImportedModule constraint: + + If moduleRequest.[[Specifier]] is a Module Source Record, then + ModuleSourcesEqual(module.[[SourceRecord]], moduleRequest.[[Specifier]]) + must be true. + + Since ModuleSourcesEqual checks [[ModuleSource]] object identity, and + ContinueDynamicImport resolves with module.[[SourceRecord]].[[ModuleSource]], + import.source(src) must return src itself. + + Tested with both $262.createModuleSource and the import source declaration. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +import source srcDecl from ''; + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var srcCreated = $262.createModuleSource('export var x = 1;', baseId + 'import-source-identity'); + +asyncTest(async function () { + // import.source() of a created ModuleSource returns the same object. + var srcFromCreated = await import.source(srcCreated); + assert.sameValue(srcFromCreated, srcCreated, + 'import.source(createdModuleSource) should return the same ModuleSource object'); + + // import.source() of a declared source binding returns the same object. + var srcFromDecl = await import.source(srcDecl); + assert.sameValue(srcFromDecl, srcDecl, + 'import.source(declaredModuleSource) should return the same ModuleSource object'); +}); diff --git a/test/language/module-code/source-phase-import/import-source-order_FIXTURE.js b/test/language/module-code/source-phase-import/import-source-order_FIXTURE.js new file mode 100644 index 00000000000..915e076da6c --- /dev/null +++ b/test/language/module-code/source-phase-import/import-source-order_FIXTURE.js @@ -0,0 +1,8 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +// Fixture module used by the import-source-order-* tests. +// The file-system version exports source = "file" and its moduleId. + +export var source = "file"; +export var moduleId = import.meta.moduleId; diff --git a/test/language/module-code/source-phase-import/module-sources-equal-different-source.js b/test/language/module-code/source-phase-import/module-sources-equal-different-source.js new file mode 100644 index 00000000000..6b651deb15c --- /dev/null +++ b/test/language/module-code/source-phase-import/module-sources-equal-different-source.js @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-modulesourcesequal +description: > + Two different ModuleSource objects (even with same text) produce different namespaces. +info: | + ModuleSourcesEqual ( left, right ) + + 1. Assert: left.[[ModuleSource]] is not ~empty~. + 2. Assert: right.[[ModuleSource]] is not ~empty~. + 3. If left.[[ModuleSource]] is not right.[[ModuleSource]], return *false*. + 4. Return *true*. + + Two ModuleSource objects created with different ids have different + [[ModuleSource]] objects, so ModuleSourcesEqual returns false. This means + they map to different Module Records and produce different namespaces. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src1 = $262.createModuleSource('export var x = 1;', baseId + 'mse-diff-a'); +var src2 = $262.createModuleSource('export var x = 1;', baseId + 'mse-diff-b'); + +asyncTest(async function () { + var ns1 = await import(src1); + var ns2 = await import(src2); + assert.notSameValue(ns1, ns2, + 'Different ModuleSource objects should produce different namespaces (ModuleRequestsEqual is false)'); +}); diff --git a/test/language/module-code/source-phase-import/module-sources-equal-same-source.js b/test/language/module-code/source-phase-import/module-sources-equal-same-source.js new file mode 100644 index 00000000000..16d14a9fb1f --- /dev/null +++ b/test/language/module-code/source-phase-import/module-sources-equal-same-source.js @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-modulesourcesequal +description: > + The same ModuleSource used in two import() calls produces the same namespace. +info: | + ModuleSourcesEqual ( left, right ) + + 1. Assert: left.[[ModuleSource]] is not ~empty~. + 2. Assert: right.[[ModuleSource]] is not ~empty~. + 3. If left.[[ModuleSource]] is not right.[[ModuleSource]], return *false*. + 4. Return *true*. + + When the same ModuleSource object is used in two import() calls, the + underlying Module Source Records share the same [[ModuleSource]] object, + so ModuleSourcesEqual returns true. The host invariant then requires + both calls to resolve to the same Module Record and thus the same namespace. +features: [source-phase-imports, esm-phase-imports] +flags: [module, async] +includes: [asyncHelpers.js] +---*/ + +assert.sameValue(typeof $262.createModuleSource, 'function'); + +var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1); + +var src = $262.createModuleSource('export var x = 1;', baseId + 'mse-same'); + +asyncTest(async function () { + var ns1 = await import(src); + var ns2 = await import(src); + assert.sameValue(ns1, ns2, + 'Same ModuleSource identity should produce the same namespace (ModuleRequestsEqual is true)'); +});