diff --git a/packages/compartment-mapper/test/optional.test.js b/packages/compartment-mapper/test/optional.test.js index 8f5ea74779..61e9ab841d 100644 --- a/packages/compartment-mapper/test/optional.test.js +++ b/packages/compartment-mapper/test/optional.test.js @@ -15,7 +15,8 @@ const fixtureOptionalDepsCjs = new URL( scaffold( 'optionalDependencies/esm', - // this test fails because it relies on dynamic import + // fails for archives because dynamic import cannot reach modules not + // discovered during archival test, fixtureOptionalDepsEsm, async (t, { namespace }) => { @@ -41,7 +42,7 @@ scaffold( ); }, 4, - { knownFailure: true }, + { knownArchiveFailure: true }, ); scaffold( diff --git a/packages/compartment-mapper/test/scaffold.js b/packages/compartment-mapper/test/scaffold.js index aec454b739..6621648e36 100644 --- a/packages/compartment-mapper/test/scaffold.js +++ b/packages/compartment-mapper/test/scaffold.js @@ -93,6 +93,7 @@ export function scaffold( addGlobals = {}, policy, knownFailure = false, + knownArchiveFailure = false, tags = undefined, conditions = tags, strict = false, @@ -111,7 +112,10 @@ export function scaffold( // wrapping each time allows for convenient use of test.only const wrap = (testFunc, testCategoryHint) => (title, implementation) => { // mark as known failure if available (but fallback to support test.only) - if (knownFailure) { + if ( + knownFailure || + (knownArchiveFailure && testCategoryHint === 'Archive') + ) { testFunc = testFunc.failing || testFunc; } return testFunc(title, async t => { diff --git a/packages/module-source/NEWS.md b/packages/module-source/NEWS.md index 68e2ce077d..b5718cd69b 100644 --- a/packages/module-source/NEWS.md +++ b/packages/module-source/NEWS.md @@ -2,6 +2,10 @@ User-visible changes in `@endo/module-source`: # Next release +- Supports dynamic `import` within a `ModuleSource` in conjunction with + a related change in `ses`. + For example, `await import(specifier)` can now call through to the + surrounding compartment's `importHook` to load and evaluate further modules. - Provides an XS-specific variant of `@endo/module-source` that adapts the native `ModuleSource` instead of entraining Babel. diff --git a/packages/module-source/src/babelPlugin.js b/packages/module-source/src/babelPlugin.js index 198557a9ba..33be546883 100644 --- a/packages/module-source/src/babelPlugin.js +++ b/packages/module-source/src/babelPlugin.js @@ -49,6 +49,7 @@ function makeModulePlugins(options) { reexportMap, liveExportMap, importMeta, + dynamicImport, } = options; if (sourceType !== 'module') { @@ -286,6 +287,7 @@ function makeModulePlugins(options) { CallExpression(path) { // import(FOO) -> $h_import(FOO) if (path.node.callee.type === 'Import') { + dynamicImport.present = true; path.node.callee = hiddenIdentifier(h.HIDDEN_IMPORT); } }, diff --git a/packages/module-source/src/module-source.js b/packages/module-source/src/module-source.js index c5c253b590..78a046de2f 100644 --- a/packages/module-source/src/module-source.js +++ b/packages/module-source/src/module-source.js @@ -82,6 +82,7 @@ export function ModuleSource(source, opts = {}) { reexportMap, fixedExportMap, exportAlls, + needsImport, needsImportMeta, } = analyzeModule(source, opts); this.imports = freeze([...keys(imports)]); @@ -97,6 +98,7 @@ export function ModuleSource(source, opts = {}) { this.__liveExportMap__ = liveExportMap; this.__reexportMap__ = reexportMap; this.__fixedExportMap__ = fixedExportMap; + this.__needsImport__ = needsImport; this.__needsImportMeta__ = needsImportMeta; freeze(this); } diff --git a/packages/module-source/src/transform-analyze.js b/packages/module-source/src/transform-analyze.js index 588d918b13..7f7fb19787 100644 --- a/packages/module-source/src/transform-analyze.js +++ b/packages/module-source/src/transform-analyze.js @@ -41,6 +41,7 @@ const makeCreateStaticRecord = transformSource => hoistedDecls: [], importSources: Object.create(null), importDecls: [], + dynamicImport: { present: false }, // enables passing import.meta usage hints up. importMeta: { present: false }, }; @@ -103,6 +104,7 @@ const makeCreateStaticRecord = transformSource => imports: ${h.HIDDEN_IMPORTS}, \ liveVar: ${h.HIDDEN_LIVE}, \ onceVar: ${h.HIDDEN_ONCE}, \ + import: ${h.HIDDEN_IMPORT}, \ importMeta: ${h.HIDDEN_META}, \ }) => (function () { 'use strict'; \ ${preamble} \ @@ -119,6 +121,7 @@ const makeCreateStaticRecord = transformSource => liveExportMap: freeze(sourceOptions.liveExportMap), fixedExportMap: freeze(sourceOptions.fixedExportMap), reexportMap: freeze(sourceOptions.reexportMap), + needsImport: sourceOptions.dynamicImport.present, needsImportMeta: sourceOptions.importMeta.present, functorSource, }); diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index 09f7db561e..d1b8131e5a 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -2,7 +2,11 @@ User-visible changes in `ses`: # Next version -- Specifying the long discontinued `mathTaming` or `dateTaming` options logs a warning. +- Adds support for dynamic `import` in conjunction with an update to + `@endo/module-source`. + +- Specifying the long-discontinued `mathTaming` or `dateTaming` options logs a + warning. # v1.10.0 (2024-11-13) diff --git a/packages/ses/src/compartment.js b/packages/ses/src/compartment.js index c423a48f56..9860f935e0 100644 --- a/packages/ses/src/compartment.js +++ b/packages/ses/src/compartment.js @@ -19,7 +19,7 @@ import { setGlobalObjectMutableProperties, setGlobalObjectEvaluators, } from './global-object.js'; -import { assert, assertEqual } from './error/assert.js'; +import { assert, assertEqual, q } from './error/assert.js'; import { sharedGlobalPropertyNames } from './permits.js'; import { load, loadNow } from './module-load.js'; import { link } from './module-link.js'; @@ -27,7 +27,7 @@ import { getDeferredExports } from './module-proxy.js'; import { compartmentEvaluate } from './compartment-evaluate.js'; import { makeSafeEvaluator } from './make-safe-evaluator.js'; -/** @import {ModuleDescriptor} from '../types.js' */ +/** @import {ModuleDescriptor, ModuleExportsNamespace} from '../types.js' */ // moduleAliases associates every public module exports namespace with its // corresponding compartment and specifier so they can be used to link modules @@ -297,6 +297,43 @@ export const makeCompartmentConstructor = ( assign(globalObject, endowments); + /** + * In support dynamic import in a module source loaded by this compartment, + * like `await import(importSpecifier)`, induces this compartment to import + * a module, returning a promise for the resulting module exports + * namespace. + * Unlike `compartment.import`, never creates a box object for the + * namespace as that behavior is deprecated and inconsistent with the + * standard behavior of dynamic import. + * Obliges the caller to resolve import specifiers to their corresponding + * full specifier. + * That is, every module must have its own dynamic import function that + * closes over the surrounding module's full module specifier and calls + * through to this function. + * @param {string} fullSpecifier - A full specifier is a key in the + * compartment's module memo. + * The method `compartment.import` accepts a full specifier, but dynamic + * import accepts an import specifier and resolves it to a full specifier + * relative to the calling module's full specifier. + * @returns {Promise} + */ + const compartmentImport = async fullSpecifier => { + if (typeof resolveHook !== 'function') { + throw new TypeError( + `Compartment does not support dynamic import: no configured resolveHook for compartment ${q(name)}`, + ); + } + await load(privateFields, moduleAliases, this, fullSpecifier); + const { execute, exportsProxy } = link( + privateFields, + moduleAliases, + this, + fullSpecifier, + ); + execute(); + return exportsProxy; + }; + weakmapSet(privateFields, this, { name: `${name}`, globalTransforms, @@ -314,6 +351,7 @@ export const makeCompartmentConstructor = ( instances, parentCompartment, noNamespaceBox, + compartmentImport, }); } diff --git a/packages/ses/src/module-instance.js b/packages/ses/src/module-instance.js index 38a008b88f..ecd7101a7b 100644 --- a/packages/ses/src/module-instance.js +++ b/packages/ses/src/module-instance.js @@ -1,3 +1,5 @@ +/** @import {ModuleExportsNamespace} from '../types.js' */ + import { assert } from './error/assert.js'; import { getDeferredExports } from './module-proxy.js'; import { @@ -131,13 +133,15 @@ export const makeModuleInstance = ( __fixedExportMap__: fixedExportMap = {}, __liveExportMap__: liveExportMap = {}, __reexportMap__: reexportMap = {}, + __needsImport__: needsImport = false, __needsImportMeta__: needsImportMeta = false, __syncModuleFunctor__, } = moduleSource; const compartmentFields = weakmapGet(privateFields, compartment); - const { __shimTransforms__, importMetaHook } = compartmentFields; + const { __shimTransforms__, resolveHook, importMetaHook, compartmentImport } = + compartmentFields; const { exportsProxy, exportsTarget, activate } = getDeferredExports( compartment, @@ -171,6 +175,14 @@ export const makeModuleInstance = ( importMetaHook(moduleSpecifier, importMeta); } + /** @type {(fullSpecifier: string) => Promise} */ + let dynamicImport; + if (needsImport) { + /** @param {string} importSpecifier */ + dynamicImport = async importSpecifier => + compartmentImport(resolveHook(importSpecifier, moduleSpecifier)); + } + // {_localName_: [{get, set, notify}]} used to merge all the export updaters. const localGetNotify = create(null); @@ -462,6 +474,7 @@ export const makeModuleInstance = ( imports: freeze(imports), onceVar: freeze(onceVar), liveVar: freeze(liveVar), + import: dynamicImport, importMeta, }), ); diff --git a/packages/ses/test/import.test.js b/packages/ses/test/import.test.js index 86020cc796..5a71df2338 100644 --- a/packages/ses/test/import.test.js +++ b/packages/ses/test/import.test.js @@ -4,6 +4,7 @@ /* eslint max-lines: 0 */ import test from 'ava'; +import { ModuleSource } from '@endo/module-source'; import '../index.js'; import { resolveNode, makeNodeImporter } from './_node.js'; import { makeImporter, makeStaticRetriever } from './_import-commons.js'; @@ -600,3 +601,21 @@ test('importMetaHook and meta from record', async t => { const { default: metaurl } = await compartment.import('./index.js'); t.is(metaurl, 'https://example.com/index.js?foo'); }); + +test('dynamic import from source', async t => { + const c = new Compartment({ + __options__: true, + __noNamespaceBox__: true, + resolveHook: s => s, + modules: { + '-': { + source: new ModuleSource(` + export const dynamic = import('-'); + `), + }, + }, + }); + const namespace = await c.import('-'); + const namespace2 = await namespace.dynamic; + t.is(namespace, namespace2); +});