diff --git a/packages/compartment-mapper/package.json b/packages/compartment-mapper/package.json index 65786bf292..be2df0b397 100644 --- a/packages/compartment-mapper/package.json +++ b/packages/compartment-mapper/package.json @@ -68,6 +68,7 @@ "ses": "workspace:^" }, "devDependencies": { + "@endo/env-options": "workspace:^", "ava": "^6.4.1", "babel-eslint": "^10.1.0", "c8": "^7.14.0", diff --git a/packages/compartment-mapper/src/archive-lite.js b/packages/compartment-mapper/src/archive-lite.js index 088c90ba99..31dd18c9fd 100644 --- a/packages/compartment-mapper/src/archive-lite.js +++ b/packages/compartment-mapper/src/archive-lite.js @@ -1,4 +1,5 @@ -/* Provides functions to create an archive (zip file with a +/** + * Provides functions to create an archive (zip file with a * compartment-map.json) from a partially completed compartment map (it must * mention all packages/compartments as well as inter-compartment references * but does not contain an entry for every module reachable from its entry @@ -26,6 +27,8 @@ * In fruition of https://github.com/endojs/endo/issues/400, we will be able to * use original source archives on XS and Node.js, but not on the web until the * web platform makes further progress on virtual module loaers. + * + * @module */ /* eslint no-shadow: 0 */ @@ -36,8 +39,8 @@ * ArchiveResult, * ArchiveWriter, * CaptureSourceLocationHook, - * CompartmentMapDescriptor, * HashPowers, + * PackageCompartmentMapDescriptor, * ReadFn, * ReadPowers, * Sources, @@ -58,16 +61,7 @@ import { digestCompartmentMap } from './digest.js'; const textEncoder = new TextEncoder(); -const { assign, create, freeze } = Object; - -/** - * @param {string} rel - a relative URL - * @param {string} abs - a fully qualified URL - * @returns {string} - */ -const resolveLocation = (rel, abs) => new URL(rel, abs).toString(); - -const { keys } = Object; +const { assign, create, freeze, keys } = Object; /** * @param {ArchiveWriter} archive @@ -77,12 +71,10 @@ const addSourcesToArchive = async (archive, sources) => { await null; for (const compartment of keys(sources).sort()) { const modules = sources[compartment]; - const compartmentLocation = resolveLocation(`${compartment}/`, 'file:///'); for (const specifier of keys(modules).sort()) { - const { bytes, location } = modules[specifier]; - if (location !== undefined) { - const moduleLocation = resolveLocation(location, compartmentLocation); - const path = new URL(moduleLocation).pathname.slice(1); // elide initial "/" + if ('location' in modules[specifier]) { + const { bytes, location } = modules[specifier]; + const path = `${compartment}/${location}`; if (bytes !== undefined) { // eslint-disable-next-line no-await-in-loop await archive.write(path, bytes); @@ -100,8 +92,8 @@ const captureSourceLocations = async (sources, captureSourceLocation) => { for (const compartmentName of keys(sources).sort()) { const modules = sources[compartmentName]; for (const moduleSpecifier of keys(modules).sort()) { - const { sourceLocation } = modules[moduleSpecifier]; - if (sourceLocation !== undefined) { + if ('sourceLocation' in modules[moduleSpecifier]) { + const { sourceLocation } = modules[moduleSpecifier]; captureSourceLocation(compartmentName, moduleSpecifier, sourceLocation); } } @@ -109,7 +101,7 @@ const captureSourceLocations = async (sources, captureSourceLocation) => { }; /** - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @returns {ArchiveResult} */ @@ -130,9 +122,11 @@ export const makeArchiveCompartmentMap = (compartmentMap, sources) => { }; }; +const noop = () => {}; + /** * @param {ReadFn | ReadPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] * @returns {Promise<{sources: Sources, compartmentMapBytes: Uint8Array, sha512?: string}>} */ @@ -146,6 +140,7 @@ const digestFromMap = async (powers, compartmentMap, options = {}) => { policy = undefined, sourceMapHook = undefined, parserForLanguage: parserForLanguageOption = {}, + log: _log = noop, } = options; const parserForLanguage = freeze( @@ -179,6 +174,7 @@ const digestFromMap = async (powers, compartmentMap, options = {}) => { importHook: consolidatedExitModuleImportHook, sourceMapHook, }); + // Induce importHook to record all the necessary modules to import the given module specifier. const { compartment, attenuatorsCompartment } = link(compartmentMap, { resolve, @@ -229,7 +225,7 @@ const digestFromMap = async (powers, compartmentMap, options = {}) => { /** * @param {ReadFn | ReadPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] * @returns {Promise<{bytes: Uint8Array, sha512?: string}>} */ @@ -254,7 +250,7 @@ export const makeAndHashArchiveFromMap = async ( /** * @param {ReadFn | ReadPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] * @returns {Promise} */ @@ -269,7 +265,7 @@ export const makeArchiveFromMap = async (powers, compartmentMap, options) => { /** * @param {ReadFn | ReadPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] * @returns {Promise} */ @@ -284,7 +280,7 @@ export const mapFromMap = async (powers, compartmentMap, options) => { /** * @param {HashPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] * @returns {Promise} */ @@ -302,7 +298,7 @@ export const hashFromMap = async (powers, compartmentMap, options) => { * @param {WriteFn} write * @param {ReadFn | ReadPowers} readPowers * @param {string} archiveLocation - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ArchiveLiteOptions} [options] */ export const writeArchiveFromMap = async ( diff --git a/packages/compartment-mapper/src/archive.js b/packages/compartment-mapper/src/archive.js index 9bb7708436..80ce846932 100644 --- a/packages/compartment-mapper/src/archive.js +++ b/packages/compartment-mapper/src/archive.js @@ -24,6 +24,7 @@ * ReadPowers, * HashPowers, * WriteFn, + * LogFn, * } from './types.js' */ @@ -169,6 +170,9 @@ export const mapLocation = async (powers, moduleLocation, options = {}) => { }); }; +/** @type {LogFn} */ +const noop = () => {}; + /** * @param {HashPowers} powers * @param {string} moduleLocation @@ -191,10 +195,12 @@ export const hashLocation = async (powers, moduleLocation, options = {}) => { workspaceLanguageForExtension, workspaceCommonjsLanguageForExtension, workspaceModuleLanguageForExtension, + log = noop, ...otherOptions } = assignParserForLanguage(options); const compartmentMap = await mapNodeModules(powers, moduleLocation, { + log, dev, strict, conditions, @@ -212,6 +218,7 @@ export const hashLocation = async (powers, moduleLocation, options = {}) => { return hashFromMap(powers, compartmentMap, { parserForLanguage, policy, + log, ...otherOptions, }); }; diff --git a/packages/compartment-mapper/src/bundle-lite.js b/packages/compartment-mapper/src/bundle-lite.js index 4ea4ff12a2..ed78021b0f 100644 --- a/packages/compartment-mapper/src/bundle-lite.js +++ b/packages/compartment-mapper/src/bundle-lite.js @@ -7,9 +7,9 @@ * } from 'ses' * @import { * BundleOptions, - * CompartmentDescriptor, - * CompartmentMapDescriptor, * CompartmentSources, + * PackageCompartmentDescriptors, + * PackageCompartmentMapDescriptor, * MaybeReadPowers, * ReadFn, * ReadPowers, @@ -110,6 +110,11 @@ import { defaultParserForLanguage } from './archive-parsers.js'; import mjsSupport from './bundle-mjs.js'; import cjsSupport from './bundle-cjs.js'; import jsonSupport from './bundle-json.js'; +import { + isErrorModuleSource, + isExitModuleSource, + isLocalModuleSource, +} from './guards.js'; const { quote: q } = assert; @@ -144,7 +149,7 @@ null, * The first modules are place-holders for the modules that exit * the compartment map to the host's module system. * - * @param {Record} compartmentDescriptors + * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Record} compartmentSources * @param {string} entryCompartmentName * @param {string} entryModuleSpecifier @@ -188,28 +193,18 @@ const sortedModules = ( const source = compartmentSources[compartmentName][moduleSpecifier]; if (source !== undefined) { - const { record, parser, deferredError, bytes, sourceDirname, exit } = - source; - if (exit !== undefined) { - return exit; - } - assert( - bytes !== undefined, - `No bytes for ${moduleSpecifier} in ${compartmentName}`, - ); - assert( - parser !== undefined, - `No parser for ${moduleSpecifier} in ${compartmentName}`, - ); - assert( - sourceDirname !== undefined, - `No sourceDirname for ${moduleSpecifier} in ${compartmentName}`, - ); - if (deferredError) { + if (isErrorModuleSource(source)) { throw Error( - `Cannot bundle: encountered deferredError ${deferredError}`, + `Cannot bundle: encountered deferredError ${source.deferredError}`, ); } + if (isExitModuleSource(source)) { + return source.exit; + } + if (!isLocalModuleSource(source)) { + throw new TypeError(`Unexpected source type ${JSON.stringify(source)}`); + } + const { record, parser, bytes, sourceDirname } = source; if (record) { const { imports = [], reexports = [] } = /** @type {PrecompiledStaticModuleInterface} */ (record); @@ -309,7 +304,7 @@ const getBundlerKitForModule = (module, params) => { /** * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {BundleOptions} [options] * @returns {Promise} */ @@ -651,7 +646,7 @@ ${m.bundlerKit.getFunctor()}`, /** * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {BundleOptions} [options] * @returns {Promise} */ diff --git a/packages/compartment-mapper/src/bundle.js b/packages/compartment-mapper/src/bundle.js index 4ccb7ae259..29c82e11e5 100644 --- a/packages/compartment-mapper/src/bundle.js +++ b/packages/compartment-mapper/src/bundle.js @@ -7,9 +7,9 @@ * } from 'ses' * @import { * BundleOptions, - * CompartmentDescriptor, - * CompartmentMapDescriptor, * CompartmentSources, + * PackageCompartmentDescriptors, + * PackageCompartmentMapDescriptor, * MaybeReadPowers, * ReadFn, * ReadPowers, @@ -112,6 +112,11 @@ import { defaultParserForLanguage } from './archive-parsers.js'; import mjsSupport from './bundle-mjs.js'; import cjsSupport from './bundle-cjs.js'; import jsonSupport from './bundle-json.js'; +import { + isErrorModuleSource, + isExitModuleSource, + isLocalModuleSource, +} from './guards.js'; const textEncoder = new TextEncoder(); @@ -148,7 +153,7 @@ null, * The first modules are place-holders for the modules that exit * the compartment map to the host's module system. * - * @param {Record} compartmentDescriptors + * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Record} compartmentSources * @param {string} entryCompartmentName * @param {string} entryModuleSpecifier @@ -192,28 +197,18 @@ const sortedModules = ( const source = compartmentSources[compartmentName][moduleSpecifier]; if (source !== undefined) { - const { record, parser, deferredError, bytes, sourceDirname, exit } = - source; - if (exit !== undefined) { - return exit; - } - assert( - bytes !== undefined, - `No bytes for ${moduleSpecifier} in ${compartmentName}`, - ); - assert( - parser !== undefined, - `No parser for ${moduleSpecifier} in ${compartmentName}`, - ); - assert( - sourceDirname !== undefined, - `No sourceDirname for ${moduleSpecifier} in ${compartmentName}`, - ); - if (deferredError) { + if (isErrorModuleSource(source)) { throw Error( - `Cannot bundle: encountered deferredError ${deferredError}`, + `Cannot bundle: encountered deferredError ${source.deferredError}`, ); } + if (isExitModuleSource(source)) { + return source.exit; + } + if (!isLocalModuleSource(source)) { + throw new TypeError(`Unexpected source type ${JSON.stringify(source)}`); + } + const { record, parser, bytes, sourceDirname } = source; if (record) { const { imports = [], reexports = [] } = /** @type {PrecompiledStaticModuleInterface} */ (record); @@ -314,7 +309,7 @@ const getBundlerKitForModule = (module, params) => { /** * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {BundleOptions} [options] * @returns {Promise} */ @@ -657,7 +652,7 @@ ${m.bundlerKit.getFunctor()}`, /** * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {BundleOptions} [options] * @returns {Promise} */ diff --git a/packages/compartment-mapper/src/capture-lite.js b/packages/compartment-mapper/src/capture-lite.js index cbdd94c338..c813053b90 100644 --- a/packages/compartment-mapper/src/capture-lite.js +++ b/packages/compartment-mapper/src/capture-lite.js @@ -35,29 +35,33 @@ * @import { * CaptureLiteOptions, * CaptureResult, - * CompartmentMapDescriptor, + * PackageCompartmentMapDescriptor, + * LogFn, + * MakeLoadCompartmentsOptions, * ReadFn, * ReadPowers, * Sources, * } from './types.js' */ +import { digestCompartmentMap } from './digest.js'; import { exitModuleImportHookMaker, makeImportHookMaker, } from './import-hook.js'; import { link } from './link.js'; import { resolve } from './node-module-specifier.js'; +import { ATTENUATORS_COMPARTMENT, ENTRY_COMPARTMENT } from './policy-format.js'; import { detectAttenuators } from './policy.js'; import { unpackReadPowers } from './powers.js'; -import { digestCompartmentMap } from './digest.js'; -const { freeze, assign, create } = Object; +const { freeze, assign, create, keys, entries } = Object; +const { quote: q } = assert; -const defaultCompartment = Compartment; +const DefaultCompartment = Compartment; /** - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @returns {CaptureResult} */ @@ -79,12 +83,169 @@ const captureCompartmentMap = (compartmentMap, sources) => { }; /** - * @param {ReadFn | ReadPowers} powers - * @param {CompartmentMapDescriptor} compartmentMap + * @type {LogFn} + */ +const noop = () => {}; + +/** + * Factory for a function that loads compartments. + * + * @param {PackageCompartmentMapDescriptor} compartmentMap Compartment map + * @param {Sources} sources Sources + * @param {MakeLoadCompartmentsOptions} [options] + * @returns {(linkedCompartments: Record, entryCompartment: Compartment, attenuatorsCompartment: Compartment) => Promise} + */ +const makeLoadCompartments = ( + compartmentMap, + sources, + { log = noop, policy, forceLoad = [] } = {}, +) => { + const { + entry: { module: entryModuleSpecifier }, + } = compartmentMap; + + /** + * Given {@link CompartmentDescriptor CompartmentDescriptors}, loads any which + * Iterates over compartment names in the {@link forceLoad forceLoad array} + * and loads those which have not yet been loaded. + * + * Will not load the "attenuators" `Compartment`, nor will it load any + * `Compartment` having a non-empty value in `sources` (since it is presumed + * it has already been loaded). + * + * @param {Record} compartments + * @returns {Promise} Resolves when all appropriate compartments are + * loaded. + */ + const forceLoadCompartments = async compartments => { + const compartmentsToLoad = forceLoad.reduce((acc, canonicalName) => { + // skip; should already be loaded + if ( + canonicalName === ATTENUATORS_COMPARTMENT || + canonicalName === ENTRY_COMPARTMENT + ) { + return acc; + } + + // TODO: A mapping of canonical name to compartment name is generated by + // mapNodeModules(), but it is not exposed. Expose it as an option on + // mapNodeModules() to be mutated and allow it as an option to + // captureFromMap() so we do not have to do this extra work. The data we + // need is _also_ generated by digestCompartmentMap() as the + // newToOldCompartmentNames property, but we cannot time-travel. + const [compartmentName, compartmentDescriptor] = entries( + compartmentMap.compartments, + ).find(([, compartment]) => compartment.label === canonicalName) ?? [ + canonicalName, + compartmentMap.compartments[canonicalName], + ]; + + if (!compartmentDescriptor) { + throw new ReferenceError( + `Failed attempting to force-load unknown compartment ${q(canonicalName)}`, + ); + } + + const compartmentSources = sources[compartmentName]; + + if (keys(compartmentSources).length) { + log( + `Refusing to force-load Compartment ${q(canonicalName)}; already loaded`, + ); + return acc; + } + + const compartment = compartments[compartmentName]; + if (!compartment) { + throw new ReferenceError( + `No compartment found for ${q(canonicalName)}`, + ); + } + const compartmentOwnModuleDescriptor = + compartmentDescriptor.modules[compartmentDescriptor.name]; + + if (!compartmentOwnModuleDescriptor?.module) { + throw new Error(`Cannot determine entry point of ${q(canonicalName)}`); + } + acc.push([ + canonicalName, + compartment, + compartmentOwnModuleDescriptor.module, + ]); + + return acc; + }, /** @type {[compartmentName: string, compartment: Compartment, moduleSpecifier: string][]} */ ([])); + + const { length: compartmentsToLoadCount } = compartmentsToLoad; + /** + * This index increments in the order in which compartments finish + * loading—_not_ the order in which they began loading. + */ + let loadedCompartmentIndex = 0; + await Promise.all( + compartmentsToLoad.map( + async ([compartmentName, compartment, moduleSpecifier]) => { + await compartment.load(moduleSpecifier); + log( + `Force-loaded Compartment: ${q(compartmentName)} (${(loadedCompartmentIndex += 1)}/${compartmentsToLoadCount})`, + ); + }, + ), + ); + }; + + /** + * Loads, in order: + * + * 1. The entry compartment + * 2. The attenuators compartment (_if and only if_ `policy` was provided) + * 3. All compartments in the `compartmentMap` that have the `load` bit set. + * + * @param {Record} linkedCompartments + * @param {Compartment} entryCompartment + * @param {Compartment} attenuatorsCompartment + * @returns {Promise} Resolves when all compartments are loaded. + */ + const loadCompartments = async ( + linkedCompartments, + entryCompartment, + attenuatorsCompartment, + ) => { + await entryCompartment.load(entryModuleSpecifier); + + if (policy) { + // retain all attenuators. + await Promise.all( + detectAttenuators(policy).map(attenuatorSpecifier => + attenuatorsCompartment.load(attenuatorSpecifier), + ), + ); + } + + await forceLoadCompartments(linkedCompartments); + }; + + return loadCompartments; +}; + +/** + * "Captures" the compartment map descriptors and sources from a partially + * completed compartment map—_without_ creating an archive. + * + * The resulting compartment map represents a well-formed dependency graph, + * laden with useful metadata. This, for example, could be used for automatic + * policy generation. + * + * @param {ReadFn | ReadPowers} readPowers Powers + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {CaptureLiteOptions} [options] * @returns {Promise} */ -export const captureFromMap = async (powers, compartmentMap, options = {}) => { +export const captureFromMap = async ( + readPowers, + compartmentMap, + options = {}, +) => { const { moduleTransforms, syncModuleTransforms, @@ -94,14 +255,15 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => { policy = undefined, sourceMapHook = undefined, parserForLanguage: parserForLanguageOption = {}, - Compartment = defaultCompartment, + Compartment: CompartmentOption = DefaultCompartment, + log = noop, + forceLoad = [], } = options; - const parserForLanguage = freeze( assign(create(null), parserForLanguageOption), ); - const { read, computeSha512 } = unpackReadPowers(powers); + const { read, computeSha512 } = unpackReadPowers(readPowers); const { compartments, @@ -111,6 +273,12 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => { /** @type {Sources} */ const sources = Object.create(null); + const loadCompartments = makeLoadCompartments(compartmentMap, sources, { + log, + policy, + forceLoad, + }); + const consolidatedExitModuleImportHook = exitModuleImportHookMaker({ modules: exitModules, exitModuleImportHook, @@ -128,25 +296,27 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => { importHook: consolidatedExitModuleImportHook, sourceMapHook, }); + // Induce importHook to record all the necessary modules to import the given module specifier. - const { compartment, attenuatorsCompartment } = link(compartmentMap, { + const { + compartment: entryCompartment, + compartments: linkedCompartments, + attenuatorsCompartment, + } = link(compartmentMap, { resolve, makeImportHook, moduleTransforms, syncModuleTransforms, parserForLanguage, archiveOnly: true, - Compartment, + Compartment: CompartmentOption, }); - await compartment.load(entryModuleSpecifier); - if (policy) { - // retain all attenuators. - await Promise.all( - detectAttenuators(policy).map(attenuatorSpecifier => - attenuatorsCompartment.load(attenuatorSpecifier), - ), - ); - } + + await loadCompartments( + linkedCompartments, + entryCompartment, + attenuatorsCompartment, + ); return captureCompartmentMap(compartmentMap, sources); }; diff --git a/packages/compartment-mapper/src/compartment-map-transforms/index.js b/packages/compartment-mapper/src/compartment-map-transforms/index.js new file mode 100644 index 0000000000..46c6443dbb --- /dev/null +++ b/packages/compartment-mapper/src/compartment-map-transforms/index.js @@ -0,0 +1,136 @@ +/** + * Functions for transforming {@link CompartmentMapDescriptor CompartmentMapDescriptors}. + * + * @module + */ + +import { makePackagePolicyForCompartment } from '../policy.js'; +import { defaultCompartmentMapTransforms } from './transforms.js'; + +export { defaultCompartmentMapTransforms }; + +/** + * @import {CompartmentMapDescriptor, + * CompartmentMapTransformContext, + * CompartmentMapTransformFn, + * CompartmentMapTransformOptions, + * LogFn, + * CompartmentDescriptorFromMap, + * PackageCompartmentMapDescriptor, + * } from '../types.js' + * @import {CanonicalNameMap} from '../types/node-modules.js' + */ + +const { quote: q } = assert; +const { freeze } = Object; + +/** + * Dummy logger + * @type {LogFn} + */ +const noop = () => {}; + +/** + * Creates a {@link CompartmentMapTransformContext} for use with + * {@link CompartmentMapTransformFn CompartmentMapTransformFns}. + * + * @template {CompartmentMapDescriptor} T + * @param {T} compartmentMap + * @param {CanonicalNameMap} canonicalNameToCompartmentNameMap + * @param {CompartmentMapTransformOptions} options + * + * @returns {Readonly>} + */ +const makeCompartmentMapTransformContext = ( + compartmentMap, + canonicalNameToCompartmentNameMap, + { policy, log: _log = noop }, +) => { + const { compartments } = compartmentMap; + + /** @type {CompartmentMapTransformContext} */ + const context = { + getCompartmentDescriptor: freeze(name => { + assert(name, 'name argument expected'); + return compartments[name]; + }), + getCanonicalName: freeze(compartmentDescriptorOrName => { + /** @type {CompartmentDescriptorFromMap|undefined} */ + let compartmentDescriptor; + if (typeof compartmentDescriptorOrName === 'string') { + compartmentDescriptor = context.getCompartmentDescriptor( + compartmentDescriptorOrName, + ); + } else { + compartmentDescriptor = compartmentDescriptorOrName; + } + return compartmentDescriptor?.label; + }), + getCompartmentName: freeze(canonicalName => { + assert(canonicalName, 'canonicalName argument expected'); + return canonicalNameToCompartmentNameMap.get(canonicalName); + }), + getPackagePolicy: freeze((compartmentDescriptor, somePolicy = policy) => { + assert(compartmentDescriptor, 'compartmentDescriptor argument expected'); + if (somePolicy === undefined) { + return undefined; + } + const { name, label } = compartmentDescriptor; + return makePackagePolicyForCompartment({ + name, + label, + policy: somePolicy, + }); + }), + }; + + return freeze(context); +}; + +/** + * Applies one or more {@link CompartmentMapTransformFn CompartmentMapTransformFns} to a + * {@link CompartmentMapDescriptor}. + * @template {ReadonlyArray>} Transforms + * @template {PackageCompartmentMapDescriptor} CompartmentMap + * @template {CompartmentMapTransformOptions} [Options=CompartmentMapTransformOptions] + * @param {CompartmentMap} compartmentMap + * @param {CanonicalNameMap} canonicalNameMap + * @param {Transforms} transforms + * @param {Options} optionsForTransforms + * @returns {Promise} Transformed compartment map + * @internal + */ +export const applyCompartmentMapTransforms = async ( + compartmentMap, + canonicalNameMap, + transforms, + optionsForTransforms, +) => { + await null; + assert(optionsForTransforms !== undefined, 'optionsForTransforms expected'); + + const context = makeCompartmentMapTransformContext( + compartmentMap, + canonicalNameMap, + optionsForTransforms, + ); + + for (const transform of transforms ?? defaultCompartmentMapTransforms) { + try { + // eslint-disable-next-line no-await-in-loop + compartmentMap = await transform( + freeze({ + compartmentMap, + options: optionsForTransforms, + context, + }), + ); + } catch (err) { + throw new Error( + `Compartment Map Transform ${q(transform.name)} errored during execution: ${err.message}`, + { cause: err }, + ); + } + } + return compartmentMap; +}; diff --git a/packages/compartment-mapper/src/compartment-map-transforms/transforms.js b/packages/compartment-mapper/src/compartment-map-transforms/transforms.js new file mode 100644 index 0000000000..6baa4e00fc --- /dev/null +++ b/packages/compartment-mapper/src/compartment-map-transforms/transforms.js @@ -0,0 +1,366 @@ +/* eslint-disable no-continue */ + +/** + * Compartment Map Transform implementations + * + * @module + */ + +import { + ATTENUATORS_COMPARTMENT, + ENTRY_COMPARTMENT, + WILDCARD_POLICY_VALUE, +} from '../policy-format.js'; + +/** + * @import {CompartmentDescriptor, + * CompartmentMapTransformFn, + * CompartmentModuleDescriptorConfiguration, + * FileUrlString, + * LogFn, + * ModuleDescriptorConfiguration, + * PackageCompartmentDescriptor, + * PackageCompartmentDescriptorName, + * PackageCompartmentMapDescriptor, + * PropertyPolicy, + * SomePackagePolicy, + * } from '../types.js' + */ + +/** + * Dummy logger + * @type {LogFn} + */ +const noop = () => {}; + +const { quote: q } = assert; +const { entries, values, freeze } = Object; +const { isArray } = Array; + +/** + * Type guard for a {@link PropertyPolicy} + * + * @param {unknown} value + * @returns {value is PropertyPolicy} + */ +const isPropertyPolicy = value => + !!value && + typeof value === 'object' && + !isArray(value) && + values(value).every(item => typeof item === 'boolean'); + +/** + * A transform which removes {@link ModuleDescriptorConfiguration ModuleDescriptors} from a + * {@link CompartmentDescriptor} based on package policy + * + * @type {CompartmentMapTransformFn} + */ +export const enforcePolicyTransform = ({ + compartmentMap, + context: { getPackagePolicy, getCompartmentDescriptor, getCanonicalName }, + options: { log = noop, policy }, +}) => { + if (!policy) { + log('No policy provided; skipping enforcePolicyTransform'); + return compartmentMap; + } + const { compartments } = compartmentMap; + + /** + * Returns `true` if package policy disallows the module + * + * @param {SomePackagePolicy|undefined} packagePolicy + * @param {CompartmentModuleDescriptorConfiguration} moduleDescriptor + */ + const shouldRemoveModule = (packagePolicy, moduleDescriptor) => { + // no package policy? delete it + if (!packagePolicy) { + return true; + } + assert( + moduleDescriptor.compartment, + `compartment expected in ModuleDescriptor: ${q(moduleDescriptor)}`, + ); + + const compartmentDescriptor = getCompartmentDescriptor( + moduleDescriptor.compartment, + ); + + // we cannot compute a canonical name, so we can't check policy + if (!compartmentDescriptor) { + throw new TypeError( + `CompartmentDescriptor for ModuleDescriptor ${q(moduleDescriptor.module)} cannot be found`, + ); + } + + const canonicalName = getCanonicalName(compartmentDescriptor); + + assert(canonicalName, 'canonicalName expected'); + + return packagePolicy.packages === WILDCARD_POLICY_VALUE + ? false + : packagePolicy.packages?.[canonicalName] !== true; + }; + + for (const [compartmentName, compartmentDescriptor] of entries( + compartments, + )) { + // we are not going to mess with the ATTENUATORS compartment + if (compartmentName === ATTENUATORS_COMPARTMENT) { + continue; + } + + /** + * Canonical name of {@link compartmentDescriptor} + * + * Only used for logging + * @type {string|undefined} + */ + let canonicalName; + + /** + * Prefer the package policy from `policy`; fall back to + * {@link CompartmentDescriptor.policy} otherwise + * @type {SomePackagePolicy|undefined} + */ + const packagePolicy = getPackagePolicy(compartmentDescriptor, policy); + + // bail if this compartment has no associated package policy + if (!packagePolicy) { + continue; + } + + for (const [moduleName, moduleDescriptor] of entries( + compartmentDescriptor.modules, + )) { + const { compartment: moduleDescriptorCompartmentName } = moduleDescriptor; + // ignore unknown ModuleDescriptors and self-referencing modules + if ( + !moduleDescriptorCompartmentName || + moduleDescriptorCompartmentName === compartmentName + ) { + continue; + } + + if (shouldRemoveModule(packagePolicy, moduleDescriptor)) { + delete compartmentDescriptor.modules[moduleName]; + + canonicalName ??= + getCanonicalName(compartmentDescriptor) ?? compartmentName; + + const compartmentDescriptorForModule = getCompartmentDescriptor( + moduleDescriptorCompartmentName, + ); + + /** + * Only used for logging + */ + const canonicalNameForModule = + getCanonicalName(compartmentDescriptorForModule) ?? moduleName; + + log( + `Policy: removed module descriptor ${q(moduleDescriptor.module)} of ${q(canonicalNameForModule)} from compartment ${q(canonicalName)}`, + ); + } + } + } + return compartmentMap; +}; + +/** + * A transform which adds references to compartments based on package policy. + * + * Values are added to {@link CompartmentDescriptor.modules}, + * {@link CompartmentDescriptor.scopes}, and + * {@link CompartmentDescriptor.compartments}. Each `ModuleDescriptor` in + * `modules` will have a `fromTransform` property set to `true`. + * + * @type {CompartmentMapTransformFn} + */ +export const createReferencesByPolicyTransform = ({ + compartmentMap, + context: { + getCompartmentName, + getCompartmentDescriptor, + getCanonicalName, + getPackagePolicy, + }, + options: { log = noop, policy, policyOverride }, +}) => { + if (!policy && !policyOverride) { + log('No policies provided; skipping createReferencesByPolicyTransform'); + return compartmentMap; + } + const { compartments } = compartmentMap; + const { compartment: entryCompartmentName } = compartmentMap.entry; + + /** + * Function which adds references to a {@link CompartmentDescriptor} based on + * package policy. + * @callback UpdateCompartmentDescriptorFn + * @param {FileUrlString|typeof ENTRY_COMPARTMENT} compartmentDescriptorNameFromPolicy The name of the policy + * compartment descriptor. + * @param {string} policyCanonicalName The canonical name of the policy + * compartment descriptor. + * @returns {void} + */ + + /** + * Factory for {@link UpdateCompartmentDescriptorFn} + * + * @param {PackageCompartmentDescriptor} compartmentDescriptor + * @returns {UpdateCompartmentDescriptorFn} + */ + const makeUpdateCompartmentDescriptor = compartmentDescriptor => { + /** + * Used for logging only + */ + const { label: canonicalName } = compartmentDescriptor; + + /** + * Updates the compartment descriptor by adding references to policy + * compartment descriptors and ensuring proper module and scope mappings + * between compartments. + * + * TODO: It is a bug if we ever add a `ModuleDescriptor` which was previously + * removed by {@link enforcePolicyTransform}. I don't have a solution for this yet. + * + * @type {UpdateCompartmentDescriptorFn} + * @throws {ReferenceError} If the policy compartment descriptor cannot be + * found. + */ + const updateCompartmentDescriptor = ( + compartmentDescriptorNameFromPolicy, + policyCanonicalName, + ) => { + const compartmentDescriptorFromPolicy = + compartmentDescriptorNameFromPolicy === ENTRY_COMPARTMENT + ? getCompartmentDescriptor(entryCompartmentName) + : getCompartmentDescriptor(compartmentDescriptorNameFromPolicy); + + assert( + compartmentDescriptorFromPolicy, + `No compartment descriptor found for ${q(compartmentDescriptorNameFromPolicy)}`, + ); + + const { name: moduleDescriptorName } = compartmentDescriptorFromPolicy; + + const moduleDescriptor = + compartmentDescriptor.modules[moduleDescriptorName]; + + let updated = false; + // NOTE: The keys of `modules` correspond to + // `CompartmentDescriptor.name`—not the keys of the + // `CompartmentMapDescriptor.compartments` object. + if (!moduleDescriptor) { + const moduleDescriptorFromPolicy = + compartmentDescriptorFromPolicy.modules[moduleDescriptorName]; + + assert( + moduleDescriptorFromPolicy, + `No module descriptor found for ${q(moduleDescriptorName)} in compartment ${q(compartmentDescriptorNameFromPolicy)}; policy may be malformed`, + ); + + compartmentDescriptor.modules[moduleDescriptorName] = { + module: + compartmentDescriptorFromPolicy.modules[moduleDescriptorName] + .module, + compartment: + compartmentDescriptorNameFromPolicy === ENTRY_COMPARTMENT + ? entryCompartmentName + : compartmentDescriptorNameFromPolicy, + __createdBy: 'transform', + }; + updated = true; + } + + // practically, this should be less common, since scopes are not removed by `enforcePolicyTransform` + if (!compartmentDescriptor.scopes[moduleDescriptorName]) { + compartmentDescriptor.scopes[moduleDescriptorName] = { + compartment: + compartmentDescriptorNameFromPolicy === ENTRY_COMPARTMENT + ? entryCompartmentName + : compartmentDescriptorNameFromPolicy, + }; + updated = true; + } + + if (updated) { + log( + `Policy: created reference from compartment ${q( + canonicalName, + )} to ${q(policyCanonicalName)}`, + ); + } + }; + return updateCompartmentDescriptor; + }; + + const compartmentEntries = + /** @type {[PackageCompartmentDescriptorName, PackageCompartmentDescriptor][]} */ ( + entries(compartments) + ); + for (const [ + compartmentDescriptorName, + compartmentDescriptor, + ] of compartmentEntries) { + // we are not going to mess with the ATTENUATORS compartment + if (compartmentDescriptorName === ATTENUATORS_COMPARTMENT) { + continue; + } + + assert( + compartmentDescriptor, + `No CompartmentDescriptor for name ${q(compartmentDescriptorName)}`, + ); + + const packagePolicy = getPackagePolicy( + compartmentDescriptor, + policy ?? policyOverride, + ); + + if (isPropertyPolicy(packagePolicy?.packages)) { + const { packages: packagePolicyPackages } = packagePolicy; + + /** + * Might be a {@link UpdateCompartmentDescriptorFn}; lazily created. + * @type {UpdateCompartmentDescriptorFn | undefined} + */ + let updateCompartmentDescriptor; + + for (const [policyCanonicalName, policyValue] of entries( + packagePolicyPackages, + )) { + // note that `any` is a valid policy value, but we cannot add references to every other CompartmentDescriptor! + if (policyValue === true) { + const compartmentDescriptorNameFromPolicy = + getCompartmentName(policyCanonicalName); + + if (!compartmentDescriptorNameFromPolicy) { + log( + `Warning: no compartment name found for ${q(policyCanonicalName)}; package policy may be malformed`, + ); + continue; + } + + updateCompartmentDescriptor ??= makeUpdateCompartmentDescriptor( + compartmentDescriptor, + ); + updateCompartmentDescriptor( + compartmentDescriptorNameFromPolicy, + policyCanonicalName, + ); + } + } + } + } + return compartmentMap; +}; + +/** + * @type {ReadonlyArray>} + */ +export const defaultCompartmentMapTransforms = freeze([ + enforcePolicyTransform, + createReferencesByPolicyTransform, +]); diff --git a/packages/compartment-mapper/src/compartment-map.js b/packages/compartment-mapper/src/compartment-map.js index 32b5910a75..3b657ac809 100644 --- a/packages/compartment-mapper/src/compartment-map.js +++ b/packages/compartment-mapper/src/compartment-map.js @@ -1,14 +1,44 @@ /* Validates a compartment map against its schema. */ -import { assertPackagePolicy } from './policy-format.js'; +import { + assertPackagePolicy, + ATTENUATORS_COMPARTMENT, + ENTRY_COMPARTMENT, +} from './policy-format.js'; -/** @import {CompartmentMapDescriptor} from './types.js' */ +/** + * @import { + * FileCompartmentDescriptor, + * FileCompartmentMapDescriptor, + * FileModuleDescriptorConfiguration, + * CompartmentMapDescriptor, + * EntryDescriptor, + * ModuleDescriptorConfiguration, + * ExitModuleDescriptorConfiguration, + * CompartmentModuleDescriptorConfiguration, + * CompartmentDescriptor, + * ScopeDescriptor, + * BaseModuleDescriptorConfiguration, + * DigestedCompartmentMapDescriptor, + * PackageCompartmentMapDescriptor, + * PackageCompartmentDescriptor, + * FileUrlString, + * LanguageForExtension, + * LanguageForModuleSpecifier, + * ModuleDescriptorConfigurationKind, + * ModuleDescriptorConfigurationKindToType, + * ErrorModuleDescriptorConfiguration, + * SourceModuleDescriptorConfiguration, + * DigestedCompartmentDescriptor} from './types.js' + */ // TODO convert to the new `||` assert style. // Deferred because this file pervasively uses simple template strings rather than // template strings tagged with `assert.details` (aka `X`), and uses // this definition of `q` rather than `assert.quote` const q = JSON.stringify; +const { keys, entries } = Object; +const { isArray } = Array; /** @type {(a: string, b: string) => number} */ // eslint-disable-next-line no-nested-ternary @@ -26,432 +56,903 @@ function* enumerate(iterable) { } } +/** + * Type guard for a string value. + * + * @overload + * @param {unknown} value + * @param {string} keypath + * @param {string} url + * @returns {asserts value is string} + */ + +/** + * Type guard for a string value with a custom assertion failure message. + * + * @overload + * @param {unknown} value + * @param {string} message + * @returns {asserts value is string} + */ + +/** + * Type guard for a string value. + * + * @param {unknown} value + * @param {string} pathOrMessage + * @param {string} url + * @returns {asserts value is string} + */ +const assertString = (value, pathOrMessage, url) => { + const keypath = pathOrMessage; + assert.typeof( + value, + 'string', + `${keypath} in ${q(url)} must be a string; got ${q(value)}`, + ); +}; + +/** + * Asserts the `label` field valid + * + * @param {unknown} allegedLabel + * @param {string} keypath + * @param {string} url + * @returns {asserts alleged is string} + */ +const assertLabel = (allegedLabel, keypath, url) => { + assertString(allegedLabel, keypath, url); + if (allegedLabel === ATTENUATORS_COMPARTMENT) { + return; + } + if (allegedLabel === ENTRY_COMPARTMENT) { + return; + } + assert( + /^(?:@[a-z][a-z0-9-.]*\/)?[a-z][a-z0-9-.]*(?:>(?:@[a-z][a-z0-9-.]*\/)?[a-z][a-z0-9-.]*)*$/.test( + allegedLabel, + ), + `${keypath} must be a canonical name in ${q(url)}; got ${q(allegedLabel)}`, + ); +}; + +/** + * @param {unknown} allegedObject + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedObject is Record} + */ +const assertPlainObject = (allegedObject, keypath, url) => { + const object = Object(allegedObject); + assert( + object === allegedObject && + !isArray(object) && + !(typeof object === 'function'), + `${keypath} must be an object; got ${q(allegedObject)} of type ${q(typeof allegedObject)} in ${q(url)}`, + ); +}; + +/** + * + * @param {unknown} value + * @param {string} keypath + * @param {string} url + * @returns {asserts value is boolean} + */ +const assertBoolean = (value, keypath, url) => { + assert.typeof( + value, + 'boolean', + `${keypath} in ${q(url)} must be a boolean; got ${q(value)}`, + ); +}; + /** * @param {Record} object * @param {string} message */ const assertEmptyObject = (object, message) => { - assert(Object.keys(object).length === 0, message); + assert(keys(object).length === 0, message); }; /** * @param {unknown} conditions * @param {string} url + * @returns {asserts conditions is CompartmentMapDescriptor['tags']} */ const assertConditions = (conditions, url) => { if (conditions === undefined) return; assert( - Array.isArray(conditions), - `conditions must be an array, got ${conditions} in ${q(url)}`, + isArray(conditions), + `conditions must be an array; got ${conditions} in ${q(url)}`, ); for (const [index, value] of enumerate(conditions)) { - assert.typeof( - value, - 'string', - `conditions[${index}] must be a string, got ${value} in ${q(url)}`, - ); + assertString(value, `conditions[${index}]`, url); } }; /** - * @param {Record} allegedModule - * @param {string} path + * @template {Partial} T + * @param {T} allegedModule + * @returns {Omit} + */ +const getModuleDescriptorSpecificProperties = allegedModule => { + const { + __createdBy: _createdBy, + retained: _retained, + deferredError: _deferredError, + ...other + } = allegedModule; + return other; +}; + +/** + * + * @param {Record} allegedModule + * @param {string} keypath * @param {string} url + * @returns {asserts allegedModule is ModuleDescriptorConfiguration} */ -const assertCompartmentModule = (allegedModule, path, url) => { - const { compartment, module, retained, ...extra } = allegedModule; +const assertBaseModuleDescriptorConfiguration = ( + allegedModule, + keypath, + url, +) => { + const { deferredError, retained, createdBy } = allegedModule; + if (deferredError !== undefined) { + assertString(deferredError, `${keypath}.deferredError`, url); + } + if (retained !== undefined) { + assertBoolean(retained, `${keypath}.retained`, url); + } + if (createdBy !== undefined) { + assertString(createdBy, `${keypath}.createdBy`, url); + } +}; + +/** + * @param {ModuleDescriptorConfiguration} moduleDescriptor + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedModule is CompartmentModuleDescriptorConfiguration} + */ +const assertCompartmentModuleDescriptor = (moduleDescriptor, keypath, url) => { + const { compartment, module, ...extra } = + getModuleDescriptorSpecificProperties( + /** @type {CompartmentModuleDescriptorConfiguration} */ ( + moduleDescriptor + ), + ); assertEmptyObject( extra, - `${path} must not have extra properties, got ${q({ - extra, - compartment, - })} in ${q(url)}`, - ); - assert.typeof( - compartment, - 'string', - `${path}.compartment must be a string, got ${q(compartment)} in ${q(url)}`, - ); - assert.typeof( - module, - 'string', - `${path}.module must be a string, got ${q(module)} in ${q(url)}`, + `${keypath} must not have extra properties; got ${q(extra)} in ${q(url)}`, ); - if (retained !== undefined) { - assert.typeof( - retained, - 'boolean', - `${path}.retained must be a boolean, got ${q(retained)} in ${q(url)}`, - ); - } + + assertString(compartment, `${keypath}.compartment`, url); + assertString(module, `${keypath}.module`, url); }; /** - * @param {Record} allegedModule - * @param {string} path + * @param {ModuleDescriptorConfiguration} moduleDescriptor + * @param {string} keypath * @param {string} url + * @returns {asserts allegedModule is FileModuleDescriptorConfiguration} */ -const assertFileModule = (allegedModule, path, url) => { - const { location, parser, sha512, ...extra } = allegedModule; +const assertFileModuleDescriptor = (moduleDescriptor, keypath, url) => { + const { location, parser, sha512, ...extra } = + getModuleDescriptorSpecificProperties( + /** @type {FileModuleDescriptorConfiguration} */ (moduleDescriptor), + ); assertEmptyObject( extra, - `${path} must not have extra properties, got ${q( - Object.keys(extra), + `${keypath} must not have extra properties; got ${q( + keys(extra), )} in ${q(url)}`, ); - assert.typeof( - location, - 'string', - `${path}.location must be a string, got ${q(location)} in ${q(url)}`, - ); - assert.typeof( - parser, - 'string', - `${path}.parser must be a string, got ${q(parser)} in ${q(url)}`, - ); + if (location !== undefined) { + assertString(location, `${keypath}.location`, url); + } + assertString(parser, `${keypath}.parser`, url); if (sha512 !== undefined) { - assert.typeof( - sha512, - 'string', - `${path}.sha512 must be a string, got ${q(sha512)} in ${q(url)}`, - ); + assertString(sha512, `${keypath}.sha512`, url); } }; /** - * @param {Record} allegedModule - * @param {string} path + * @param {ModuleDescriptorConfiguration} moduleDescriptor + * @param {string} keypath * @param {string} url + * @returns {asserts allegedModule is ExitModuleDescriptorConfiguration} */ -const assertExitModule = (allegedModule, path, url) => { - const { exit, ...extra } = allegedModule; +const assertExitModuleDescriptor = (moduleDescriptor, keypath, url) => { + const { exit, ...extra } = getModuleDescriptorSpecificProperties( + /** @type {ExitModuleDescriptorConfiguration} */ (moduleDescriptor), + ); assertEmptyObject( extra, - `${path} must not have extra properties, got ${q( - Object.keys(extra), + `${keypath} must not have extra properties; got ${q( + keys(extra), )} in ${q(url)}`, ); - assert.typeof( - exit, - 'string', - `${path}.exit must be a string, got ${q(exit)} in ${q(url)}`, - ); + assertString(exit, `${keypath}.exit`, url); }; /** + * + * @param {ModuleDescriptorConfiguration} moduleDescriptor + * @param {string} keypath + * @param {string} url + * @returns {asserts moduleDescriptor is ErrorModuleDescriptorConfiguration} + */ +const assertErrorModuleDescriptor = (moduleDescriptor, keypath, url) => { + const { deferredError } = moduleDescriptor; + if (deferredError) { + assertString(deferredError, `${keypath}.deferredError`, url); + } +}; + +/** + * @template {ModuleDescriptorConfigurationKind[]} Kinds + * @overload * @param {unknown} allegedModule - * @param {string} path + * @param {string} keypath * @param {string} url + * @param {Kinds} kinds + * @returns {asserts allegedModule is ModuleDescriptorConfigurationKindToType} */ -const assertModule = (allegedModule, path, url) => { - const moduleDescriptor = Object(allegedModule); + +/** + * @overload + * @param {unknown} allegedModule + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedModule is ModuleDescriptorConfiguration} + */ + +/** + * @param {unknown} allegedModule + * @param {string} keypath + * @param {string} url + * @param {ModuleDescriptorConfigurationKind[]} kinds + */ +function assertModuleConfiguration(allegedModule, keypath, url, kinds) { + assertPlainObject(allegedModule, keypath, url); + assertBaseModuleDescriptorConfiguration(allegedModule, keypath, url); + + const finalKinds = + kinds.length > 0 + ? kinds + : /** @type {ModuleDescriptorConfigurationKind[]} */ ([ + 'compartment', + 'file', + 'exit', + 'error', + ]); + /** @type {Error[]} */ + const errors = []; + for (const kind of finalKinds) { + switch (kind) { + case 'compartment': { + try { + assertCompartmentModuleDescriptor(allegedModule, keypath, url); + } catch (error) { + errors.push(error); + } + break; + } + case 'file': { + try { + assertFileModuleDescriptor(allegedModule, keypath, url); + } catch (error) { + errors.push(error); + } + break; + } + case 'exit': { + try { + assertExitModuleDescriptor(allegedModule, keypath, url); + } catch (error) { + errors.push(error); + } + break; + } + case 'error': { + try { + assertErrorModuleDescriptor(allegedModule, keypath, url); + } catch (error) { + errors.push(error); + } + break; + } + default: + throw new TypeError( + `Unknown module descriptor kind ${q(kind)} in ${q(url)}`, + ); + } + } + assert( - allegedModule === moduleDescriptor && !Array.isArray(moduleDescriptor), - `${path} must be an object, got ${allegedModule} in ${q(url)}`, + errors.length < finalKinds.length, + `invalid module descriptor in ${q(url)} at ${q(keypath)}; expected to match one of ${q(kinds)}: ${errors.map(err => err.message).join('; ')}`, ); +} - const { compartment, module, location, parser, exit, deferredError } = - moduleDescriptor; - if (compartment !== undefined || module !== undefined) { - assertCompartmentModule(moduleDescriptor, path, url); - } else if (location !== undefined || parser !== undefined) { - assertFileModule(moduleDescriptor, path, url); - } else if (exit !== undefined) { - assertExitModule(moduleDescriptor, path, url); - } else if (deferredError !== undefined) { - assert.typeof( - deferredError, - 'string', - `${path}.deferredError must be a string contaiing an error message`, +/** + * @param {unknown} allegedModules + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedModules is Record} + */ +const assertModuleDescriptorConfigurations = (allegedModules, keypath, url) => { + assertPlainObject(allegedModules, keypath, url); + for (const [key, value] of entries(allegedModules)) { + assertString( + key, + `all keys of ${keypath}.modules must be strings; got ${key} in ${q(url)}`, ); - } else { - assert.fail( - `${path} is not a valid module descriptor, got ${q(allegedModule)} in ${q( - url, - )}`, + assertModuleConfiguration(value, `${keypath}.modules[${q(key)}]`, url); + } +}; + +/** + * @param {unknown} allegedModules + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedModules is Record} + */ +const assertFileModuleDescriptorConfigurations = ( + allegedModules, + keypath, + url, +) => { + assertPlainObject(allegedModules, keypath, url); + for (const [key, value] of entries(allegedModules)) { + assertString( + key, + `all keys of ${keypath}.modules must be strings; got ${key} in ${q(url)}`, ); + assertModuleConfiguration(value, `${keypath}.modules[${q(key)}]`, url, [ + 'file', + 'compartment', + 'error', + ]); } }; /** * @param {unknown} allegedModules - * @param {string} path + * @param {string} keypath * @param {string} url + * @returns {asserts allegedModules is Record} */ -const assertModules = (allegedModules, path, url) => { - const modules = Object(allegedModules); - assert( - allegedModules === modules || !Array.isArray(modules), - `modules must be an object, got ${q(allegedModules)} in ${q(url)}`, - ); - for (const [key, value] of Object.entries(modules)) { - assertModule(value, `${path}.modules[${q(key)}]`, url); +const assertDigestedModuleDescriptorConfigurations = ( + allegedModules, + keypath, + url, +) => { + assertPlainObject(allegedModules, keypath, url); + for (const [key, value] of entries(allegedModules)) { + assertString( + key, + `all keys of ${keypath}.modules must be strings; got ${key} in ${q(url)}`, + ); + assertModuleConfiguration(value, `${keypath}.modules[${q(key)}]`, url, [ + 'file', + 'exit', + 'error', + ]); } }; /** * @param {unknown} allegedParsers - * @param {string} path + * @param {string} keypath * @param {string} url + * @returns {asserts allegedParsers is LanguageForExtension} */ -const assertParsers = (allegedParsers, path, url) => { - if (allegedParsers === undefined) { - return; - } - const parsers = Object(allegedParsers); - assert( - allegedParsers === parsers && !Array.isArray(parsers), - `${path}.parsers must be an object, got ${allegedParsers} in ${q(url)}`, - ); +const assertParsers = (allegedParsers, keypath, url) => { + assertPlainObject(allegedParsers, `${keypath}.parsers`, url); - for (const [key, value] of Object.entries(parsers)) { - assert.typeof( + for (const [key, value] of entries(allegedParsers)) { + assertString( key, - 'string', - `all keys of ${path}.parsers must be strings, got ${key} in ${q(url)}`, - ); - assert.typeof( - value, - 'string', - `${path}.parsers[${q(key)}] must be a string, got ${value} in ${q(url)}`, + `all keys of ${keypath}.parsers must be strings; got ${key} in ${q(url)}`, ); + assertString(value, `${keypath}.parsers[${q(key)}]`, url); } }; /** - * @param {unknown} allegedScope - * @param {string} path + * @overload + * @param {unknown} allegedTruthyValue + * @param {string} keypath * @param {string} url + * @returns {asserts allegedTruthyValue is NonNullable} */ -const assertScope = (allegedScope, path, url) => { - const scope = Object(allegedScope); + +/** + * + * @overload + * @param {unknown} allegedTruthyValue + * @param {string} message + * @returns {asserts allegedTruthyValue is NonNullable} + */ + +/** + * + * @param {unknown} allegedTruthyValue + * @param {string} keypath + * @param {string} [url] + * @returns {asserts allegedTruthyValue is NonNullable} + */ +const assertTruthy = (allegedTruthyValue, keypath, url) => { assert( - allegedScope === scope && !Array.isArray(scope), - `${path} must be an object, got ${allegedScope} in ${q(url)}`, + allegedTruthyValue, + url + ? `${keypath} in ${q(url)} must be truthy; got ${q(allegedTruthyValue)}` + : url, ); +}; + +/** + * @template [T=string] + * @typedef {(value: unknown, keypath: string, url: string) => void} AssertFn + */ + +/** + * @template [T=string] + * @param {unknown} allegedScope + * @param {string} keypath + * @param {string} url + * @param {AssertFn} [assertCompartmentValue] + * @returns {asserts allegedScope is ScopeDescriptor} + */ +const assertScope = (allegedScope, keypath, url, assertCompartmentValue) => { + assertPlainObject(allegedScope, keypath, url); - const { compartment, ...extra } = scope; + const { compartment, ...extra } = allegedScope; assertEmptyObject( extra, - `${path} must not have extra properties, got ${q( - Object.keys(extra), + `${keypath} must not have extra properties; got ${q( + keys(extra), )} in ${q(url)}`, ); - assert.typeof( - compartment, - 'string', - `${path}.compartment must be a string, got ${q(compartment)} in ${q(url)}`, - ); + if (assertCompartmentValue) { + assertCompartmentValue(compartment, `${keypath}.compartment`, url); + } else { + assertString(compartment, `${keypath}.compartment`, url); + } }; /** + * @template [T=string] * @param {unknown} allegedScopes - * @param {string} path + * @param {string} keypath * @param {string} url + * @param {AssertFn} [assertCompartmentValue] + * @returns {asserts allegedScopes is Record>} */ -const assertScopes = (allegedScopes, path, url) => { - if (allegedScopes === undefined) { - return; - } - const scopes = Object(allegedScopes); - assert( - allegedScopes === scopes && !Array.isArray(scopes), - `${path}.scopes must be an object, got ${q(allegedScopes)} in ${q(url)}`, - ); +const assertScopes = ( + allegedScopes, + keypath, + url, + assertCompartmentValue = assertString, +) => { + assertPlainObject(allegedScopes, keypath, url); - for (const [key, value] of Object.entries(scopes)) { - assert.typeof( + for (const [key, value] of entries(allegedScopes)) { + assertString( key, - 'string', - `all keys of ${path}.scopes must be strings, got ${key} in ${q(url)}`, + `all keys of ${keypath}.scopes must be strings; got ${key} in ${q(url)}`, + ); + assertScope( + value, + `${keypath}.scopes[${q(key)}]`, + url, + assertCompartmentValue, ); - assertScope(value, `${path}.scopes[${q(key)}]`, url); } }; /** * @param {unknown} allegedTypes - * @param {string} path + * @param {string} keypath * @param {string} url + * @returns {asserts allegedTypes is LanguageForModuleSpecifier} */ -const assertTypes = (allegedTypes, path, url) => { - if (allegedTypes === undefined) { - return; +const assertTypes = (allegedTypes, keypath, url) => { + assertPlainObject(allegedTypes, `${keypath}.types`, url); + + for (const [key, value] of entries(allegedTypes)) { + assertString( + key, + `all keys of ${keypath}.types must be strings; got ${key} in ${q(url)}`, + ); + assertString(value, `${keypath}.types[${q(key)}]`, url); + } +}; + +/** + * @template {Record} [M=Record] + * @param {unknown} allegedCompartment + * @param {string} keypath + * @param {string} url + * @param {AssertFn} [assertModuleConfigurations] + * @returns {asserts allegedCompartment is CompartmentDescriptor} + */ +const assertCompartmentDescriptor = ( + allegedCompartment, + keypath, + url, + assertModuleConfigurations, +) => { + assertPlainObject(allegedCompartment, keypath, url); + + const { + location, + name, + parsers, + types, + scopes, + modules, + policy, + sourceDirname, + retained, + } = allegedCompartment; + + assertString(location, `${keypath}.location`, url); + assertString(name, `${keypath}.name`, url); + + // TODO: It may be prudent to assert that there exists some module referring + // to its own compartment + + if (assertModuleConfigurations) { + assertModuleConfigurations(modules, keypath, url); + } else { + assertModuleDescriptorConfigurations(modules, keypath, url); + } + + if (parsers !== undefined) { + assertParsers(parsers, keypath, url); + } + if (scopes !== undefined) { + assertScopes(scopes, keypath, url); } - const types = Object(allegedTypes); + if (types !== undefined) { + assertTypes(types, keypath, url); + } + if (policy !== undefined) { + assertPackagePolicy(policy, keypath, url); + } + if (sourceDirname !== undefined) { + assertString(sourceDirname, `${keypath}.sourceDirname`, url); + } + if (retained !== undefined) { + assertBoolean(retained, `${keypath}.retained`, url); + } +}; + +/** + * Ensures a string is a file URL (a {@link FileUrlString}) + * + * @param {unknown} allegedFileUrlString - a package location to assert + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedFileUrlString is FileUrlString} + */ +const assertFileUrlString = (allegedFileUrlString, keypath, url) => { + assertString(allegedFileUrlString, keypath, url); assert( - allegedTypes === types && !Array.isArray(types), - `${path}.types must be an object, got ${allegedTypes} in ${q(url)}`, + allegedFileUrlString.startsWith('file://'), + `${keypath} must be a file URL in ${q(url)}; got ${q(allegedFileUrlString)}`, ); + assert( + allegedFileUrlString.length > 7, + `${keypath} must contain a non-empty path in ${q(url)}; got ${q(allegedFileUrlString)}`, + ); +}; - for (const [key, value] of Object.entries(types)) { - assert.typeof( +/** + * @param {unknown} allegedModules + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedModules is Record} + */ +const assertPackageModuleDescriptorConfigurations = ( + allegedModules, + keypath, + url, +) => { + assertPlainObject(allegedModules, keypath, url); + for (const [key, value] of entries(allegedModules)) { + assertString( key, - 'string', - `all keys of ${path}.types must be strings, got ${key} in ${q(url)}`, - ); - assert.typeof( - value, - 'string', - `${path}.types[${q(key)}] must be a string, got ${value} in ${q(url)}`, + `all keys of ${keypath}.modules must be strings; got ${key} in ${q(url)}`, ); + assertModuleConfiguration(value, `${keypath}.modules[${q(key)}]`, url, [ + 'compartment', + ]); } }; /** - * @param {unknown} allegedPolicy - * @param {string} path - * @param {string} [url] + * + * @param {unknown} allegedLocation + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedLocation is PackageCompartmentDescriptor['location']} */ - -const assertPolicy = ( - allegedPolicy, - path, - url = '', -) => { - assertPackagePolicy(allegedPolicy, `${path}.policy`, url); +const assertPackageLocation = (allegedLocation, keypath, url) => { + if (allegedLocation === ATTENUATORS_COMPARTMENT) { + return; + } + assertFileUrlString(allegedLocation, keypath, url); }; /** * @param {unknown} allegedCompartment - * @param {string} path + * @param {string} keypath * @param {string} url + * @returns {asserts allegedCompartment is PackageCompartmentDescriptor} */ -const assertCompartment = (allegedCompartment, path, url) => { - const compartment = Object(allegedCompartment); - assert( - allegedCompartment === compartment && !Array.isArray(compartment), - `${path} must be an object, got ${allegedCompartment} in ${q(url)}`, +const assertPackageCompartmentDescriptor = ( + allegedCompartment, + keypath, + url, +) => { + assertCompartmentDescriptor( + allegedCompartment, + keypath, + url, + assertPackageModuleDescriptorConfigurations, ); const { location, - name, - label, - parsers, - types, scopes, - modules, - policy, + label, + // these unused vars already validated by assertPackageModuleDescriptorConfigurations + name: _name, + sourceDirname: _sourceDirname, + modules: _modules, + parsers: _parsers, + types: _types, + policy: _policy, ...extra - } = compartment; + } = /** @type {PackageCompartmentDescriptor} */ (allegedCompartment); assertEmptyObject( extra, - `${path} must not have extra properties, got ${q( - Object.keys(extra), + `${keypath} must not have extra properties; got ${q( + keys(extra), )} in ${q(url)}`, ); - assert.typeof( - location, - 'string', - `${path}.location in ${q(url)} must be string, got ${q(location)}`, + assertPackageLocation(location, `${keypath}.location`, url); + assertLabel(label, `${keypath}.label`, url); + assertScopes(scopes, `${keypath}.scopes`, url, assertFileUrlString); +}; + +/** + * + * @param {unknown} allegedCompartment + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedCompartment is DigestedCompartmentDescriptor} + */ +const assertDigestedCompartmentDescriptor = ( + allegedCompartment, + keypath, + url, +) => { + assertCompartmentDescriptor( + allegedCompartment, + keypath, + url, + assertDigestedModuleDescriptorConfigurations, ); - assert.typeof( - name, - 'string', - `${path}.name in ${q(url)} must be string, got ${q(name)}`, + + const { + name: _name, + label: _label, + modules: _modules, + policy: _policy, + location: _location, + ...extra + } = allegedCompartment; + + assertEmptyObject( + extra, + `${keypath} must not have extra properties; got ${q( + keys(extra), + )} in ${q(url)}`, ); - assert.typeof( +}; + +/** + * @param {unknown} allegedCompartment + * @param {string} keypath + * @param {string} url + * @returns {asserts allegedCompartment is FileCompartmentDescriptor} + */ +const assertFileCompartmentDescriptor = (allegedCompartment, keypath, url) => { + assertCompartmentDescriptor( + allegedCompartment, + keypath, + url, + assertFileModuleDescriptorConfigurations, + ); + + const { + location: _location, + name: _name, label, - 'string', - `${path}.label in ${q(url)} must be string, got ${q(label)}`, + modules: _modules, + policy: _policy, + ...extra + } = /** @type {FileCompartmentDescriptor} */ (allegedCompartment); + + assertEmptyObject( + extra, + `${keypath} must not have extra properties; got ${q( + keys(extra), + )} in ${q(url)}`, ); - assertModules(modules, path, url); - assertParsers(parsers, path, url); - assertScopes(scopes, path, url); - assertTypes(types, path, url); - assertPolicy(policy, path, url); + assertString(label, `${keypath}.label`, url); }; /** * @param {unknown} allegedCompartments * @param {string} url + * @returns {asserts allegedCompartments is Record} */ -const assertCompartments = (allegedCompartments, url) => { - const compartments = Object(allegedCompartments); +const assertCompartmentDescriptors = (allegedCompartments, url) => { + assertPlainObject(allegedCompartments, 'compartments', url); + const compartmentNames = keys(allegedCompartments); assert( - allegedCompartments === compartments || !Array.isArray(compartments), - `compartments must be an object, got ${q(allegedCompartments)} in ${q( - url, - )}`, + compartmentNames.length > 0, + `compartments must not be empty in ${q(url)}`, ); - for (const [key, value] of Object.entries(compartments)) { - assertCompartment(value, `compartments[${q(key)}]`, url); + for (const key of keys(allegedCompartments)) { + assertString( + key, + `all keys of compartments must be strings; got ${key} in ${q(url)}`, + ); + } + assert( + compartmentNames.every(name => typeof name === 'string'), + `all keys of compartments must be strings; got ${q(compartmentNames)} in ${q(url)}`, + ); +}; + +/** + * @param {unknown} allegedCompartments + * @param {string} url + * @returns {asserts allegedCompartments is Record} + */ +const assertFileCompartmentDescriptors = (allegedCompartments, url) => { + assertCompartmentDescriptors(allegedCompartments, url); + for (const [key, value] of entries(allegedCompartments)) { + assertFileCompartmentDescriptor(value, `compartments[${q(key)}]`, url); } }; +/** + * @param {unknown} allegedCompartments + * @param {string} url + * @returns {asserts allegedCompartments is Record} + */ +const assertPackageCompartmentDescriptors = (allegedCompartments, url) => { + assertCompartmentDescriptors(allegedCompartments, url); + for (const [key, value] of entries(allegedCompartments)) { + assertPackageCompartmentDescriptor(value, `compartments[${q(key)}]`, url); + } +}; /** * @param {unknown} allegedEntry * @param {string} url + * @returns {asserts allegedEntry is EntryDescriptor} */ const assertEntry = (allegedEntry, url) => { - const entry = Object(allegedEntry); - assert( - allegedEntry === entry && !Array.isArray(entry), - `"entry" must be an object in compartment map, got ${allegedEntry} in ${q( - url, - )}`, - ); - const { compartment, module, ...extra } = entry; + assertPlainObject(allegedEntry, 'entry', url); + const { compartment, module, ...extra } = allegedEntry; assertEmptyObject( extra, - `"entry" must not have extra properties in compartment map, got ${q( - Object.keys(extra), + `"entry" must not have extra properties in compartment map; got ${q( + keys(extra), )} in ${q(url)}`, ); - assert.typeof( - compartment, - 'string', - `entry.compartment must be a string in compartment map, got ${compartment} in ${q( - url, - )}`, - ); - assert.typeof( - module, - 'string', - `entry.module must be a string in compartment map, got ${module} in ${q( - url, - )}`, - ); + assertString(compartment, 'entry.compartment', url); + assertString(module, 'entry.module', url); }; /** * @param {unknown} allegedCompartmentMap - * @param {string} [url] + * @param {string} url * @returns {asserts allegedCompartmentMap is CompartmentMapDescriptor} */ - -export const assertCompartmentMap = ( - allegedCompartmentMap, - url = '', -) => { - const compartmentMap = Object(allegedCompartmentMap); - assert( - allegedCompartmentMap === compartmentMap && !Array.isArray(compartmentMap), - `Compartment map must be an object, got ${allegedCompartmentMap} in ${q( - url, - )}`, - ); +const assertCompartmentMap = (allegedCompartmentMap, url) => { + assertPlainObject(allegedCompartmentMap, 'compartment map', url); const { // TODO migrate tags to conditions // https://github.com/endojs/endo/issues/2388 tags: conditions, entry, - compartments, + compartments: _compartments, ...extra - } = Object(compartmentMap); + } = allegedCompartmentMap; assertEmptyObject( extra, - `Compartment map must not have extra properties, got ${q( - Object.keys(extra), + `Compartment map must not have extra properties; got ${q( + keys(extra), )} in ${q(url)}`, ); assertConditions(conditions, url); assertEntry(entry, url); - assertCompartments(compartments, url); + assertTruthy( + allegedCompartmentMap.compartments?.[entry.compartment], + `compartments must contain entry compartment "${entry.compartment}" in ${q(url)}`, + ); +}; + +/** + * @param {unknown} allegedCompartmentMap + * @param {string} [url] + * @returns {asserts allegedCompartmentMap is FileCompartmentMapDescriptor} + */ +export const assertFileCompartmentMap = ( + allegedCompartmentMap, + url = '', +) => { + assertCompartmentMap(allegedCompartmentMap, url); + const { compartments } = allegedCompartmentMap; + assertFileCompartmentDescriptors(compartments, url); +}; + +/** + * + * @param {unknown} allegedCompartments + * @param {string} url + * @returns {asserts allegedCompartments is Record} + */ +export const assertDigestedCompartmentDescriptors = ( + allegedCompartments, + url = '', +) => { + assertCompartmentDescriptors(allegedCompartments, url); + for (const [key, value] of entries(allegedCompartments)) { + assertDigestedCompartmentDescriptor(value, `compartments[${q(key)}]`, url); + } +}; + +/** + * + * @param {unknown} allegedCompartmentMap + * @param {string} [url] + * @returns {asserts allegedCompartmentMap is DigestedCompartmentMapDescriptor} + */ +export const assertDigestedCompartmentMap = ( + allegedCompartmentMap, + url = '', +) => { + assertCompartmentMap(allegedCompartmentMap, url); + const { compartments } = allegedCompartmentMap; + assertDigestedCompartmentDescriptors(compartments, url); +}; + +/** + * @param {unknown} allegedCompartmentMap + * @param {string} [url] + * @returns {asserts allegedCompartmentMap is PackageCompartmentMapDescriptor} + */ +export const assertPackageCompartmentMap = ( + allegedCompartmentMap, + url = '', +) => { + assertCompartmentMap(allegedCompartmentMap, url); + const { compartments } = allegedCompartmentMap; + assertPackageCompartmentDescriptors(compartments, url); }; diff --git a/packages/compartment-mapper/src/digest.js b/packages/compartment-mapper/src/digest.js index 86b09f4230..6d466ac18c 100644 --- a/packages/compartment-mapper/src/digest.js +++ b/packages/compartment-mapper/src/digest.js @@ -8,19 +8,32 @@ /** * @import { - * CompartmentDescriptor, - * CompartmentMapDescriptor, + * DigestedCompartmentDescriptors, * DigestResult, - * ModuleDescriptor, + * PackageCompartmentDescriptors, + * PackageCompartmentMapDescriptor, + * SourceModuleDescriptorConfiguration, * Sources, + * ExitModuleDescriptorConfiguration, + * FileModuleDescriptorConfiguration, + * ErrorModuleDescriptorConfiguration, + * DigestedCompartmentMapDescriptor, + * DigestedCompartmentDescriptor, * } from './types.js' */ -import { pathCompare } from '@endo/path-compare'; -import { assertCompartmentMap, stringCompare } from './compartment-map.js'; +import { + assertDigestedCompartmentMap, + stringCompare, +} from './compartment-map.js'; +import { + isErrorModuleSource, + isExitModuleSource, + isLocalModuleSource, +} from './guards.js'; const { create, fromEntries, entries, keys } = Object; - +const { quote: q } = assert; /** * We attempt to produce compartment maps that are consistent regardless of * whether the packages were originally laid out on disk for development or @@ -49,14 +62,12 @@ const { create, fromEntries, entries, keys } = Object; * actual installation location, so should be orthogonal to the vagaries of the * package manager's deduplication algorithm. * - * @param {Record} compartments + * @param {PackageCompartmentDescriptors} compartments * @returns {Record} map from old to new compartment names. */ const renameCompartments = compartments => { /** @type {Record} */ const compartmentRenames = create(null); - let index = 0; - let prev = ''; // The sort below combines two comparators to avoid depending on sort // stability, which became standard as recently as 2019. @@ -65,46 +76,42 @@ const renameCompartments = compartments => { const compartmentsByPath = Object.entries(compartments) .map(([name, compartment]) => ({ name, - path: compartment.path, label: compartment.label, })) - .sort((a, b) => { - if (a.label === b.label) { - assert(a.path !== undefined && b.path !== undefined); - return pathCompare(a.path, b.path); - } - return stringCompare(a.label, b.label); - }); + .sort((a, b) => stringCompare(a.label, b.label)); for (const { name, label } of compartmentsByPath) { - if (label === prev) { - compartmentRenames[name] = `${label}-n${index}`; - index += 1; - } else { - compartmentRenames[name] = label; - prev = label; - index = 1; - } + compartmentRenames[name] = label; } return compartmentRenames; }; /** - * @param {Record} compartments + * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Sources} sources * @param {Record} compartmentRenames + * @returns {DigestedCompartmentDescriptors} */ -const translateCompartmentMap = (compartments, sources, compartmentRenames) => { +const translateCompartmentMap = ( + compartmentDescriptors, + sources, + compartmentRenames, +) => { const result = create(null); for (const compartmentName of keys(compartmentRenames)) { - const compartment = compartments[compartmentName]; - const { name, label, retained: compartmentRetained, policy } = compartment; + const compartmentDescriptor = compartmentDescriptors[compartmentName]; + const { + name, + label, + retained: compartmentRetained, + policy, + } = compartmentDescriptor; if (compartmentRetained) { // rename module compartments - /** @type {Record} */ + /** @type {Record} */ const modules = create(null); - const compartmentModules = compartment.modules; - if (compartment.modules) { + const compartmentModules = compartmentDescriptor.modules; + if (compartmentDescriptor.modules) { for (const name of keys(compartmentModules).sort()) { const { retained: moduleRetained, ...retainedModule } = compartmentModules[name]; @@ -126,25 +133,40 @@ const translateCompartmentMap = (compartments, sources, compartmentRenames) => { if (compartmentSources) { for (const name of keys(compartmentSources).sort()) { const source = compartmentSources[name]; - const { location, parser, exit, sha512, deferredError } = source; - if (location !== undefined) { + if (isLocalModuleSource(source)) { + const { location, parser, sha512 } = source; + /** @type {FileModuleDescriptorConfiguration} */ modules[name] = { location, parser, sha512, + __createdBy: 'digest', }; - } else if (exit !== undefined) { + } else if (isExitModuleSource(source)) { + const { exit } = source; + /** @type {ExitModuleDescriptorConfiguration} */ modules[name] = { exit, + __createdBy: 'digest', }; - } else if (deferredError !== undefined) { + } else if (isErrorModuleSource(source)) { + const { deferredError } = source; + /** @type {ErrorModuleDescriptorConfiguration} */ modules[name] = { deferredError, + __createdBy: 'digest', }; + } else { + throw new TypeError( + `Unexpected source type for compartment ${compartmentName} module ${name}: ${q( + source, + )}`, + ); } } } + /** @type {DigestedCompartmentDescriptor} */ result[compartmentRenames[compartmentName]] = { name, label, @@ -175,7 +197,7 @@ const renameSources = (sources, compartmentRenames) => { }; /** - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @returns {DigestResult} */ @@ -195,6 +217,7 @@ export const digestCompartmentMap = (compartmentMap, sources) => { oldToNewCompartmentNames[entryCompartmentName]; const digestSources = renameSources(sources, oldToNewCompartmentNames); + /** @type {DigestedCompartmentMapDescriptor} */ const digestCompartmentMap = { // TODO graceful migration from tags to conditions // https://github.com/endojs/endo/issues/2388 @@ -210,7 +233,16 @@ export const digestCompartmentMap = (compartmentMap, sources) => { // We assert that we have constructed a valid compartment map, not because it // might not be, but to ensure that the assertCompartmentMap function can // accept all valid compartment maps. - assertCompartmentMap(digestCompartmentMap); + try { + assertDigestedCompartmentMap(digestCompartmentMap); + } catch (err) { + throw new TypeError( + `Invalid compartment map; ${JSON.stringify( + digestCompartmentMap, + )}:\n${err.message}`, + { cause: err }, + ); + } const newToOldCompartmentNames = fromEntries( entries(oldToNewCompartmentNames).map(([oldName, newName]) => [ diff --git a/packages/compartment-mapper/src/generic-graph.js b/packages/compartment-mapper/src/generic-graph.js index b7aea0da2c..e942cfab5e 100644 --- a/packages/compartment-mapper/src/generic-graph.js +++ b/packages/compartment-mapper/src/generic-graph.js @@ -13,7 +13,7 @@ * @import {TraversalContext} from './types/generic-graph.js'; */ -const { stringify: q } = JSON; +const { quote: q } = assert; /** * Remove the node with the minimum weight from the priority queue. @@ -94,6 +94,11 @@ const getPath = ({ predecessors }, source, target) => { /** @type {T[]} */ const nodeList = []; + assert( + source !== target, + `Source ${q(source)} cannot be the same as target ${q(target)}`, + ); + let node = target; while (predecessors.has(node)) { @@ -108,7 +113,7 @@ const getPath = ({ predecessors }, source, target) => { assert( nodeList.length >= 2, - `The path from ${source} to ${target} should have a least two nodes`, + `The path from ${source} to ${target} should have at least two nodes`, ); return /** @type {[T, T, ...T[]]} */ (nodeList.reverse()); diff --git a/packages/compartment-mapper/src/guards.js b/packages/compartment-mapper/src/guards.js new file mode 100644 index 0000000000..6493e176dc --- /dev/null +++ b/packages/compartment-mapper/src/guards.js @@ -0,0 +1,100 @@ +/** + * Common type guards. + * + * @module + */ + +const { hasOwn } = Object; + +/** + * @import { + * ModuleDescriptorConfiguration, + * FileModuleDescriptorConfiguration, + * ErrorModuleDescriptorConfiguration, + * ModuleSource, + * ExitModuleSource, + * ErrorModuleSource, + * LocalModuleSource, + * ExitModuleDescriptorConfiguration, + * CompartmentModuleDescriptorConfiguration + * } from './types.js'; + */ + +/** + * Type guard for an {@link ErrorModuleDescriptorConfiguration}. + * @param {ModuleDescriptorConfiguration} value + * @returns {value is ErrorModuleDescriptorConfiguration} + */ +export const isErrorModuleDescriptorConfiguration = value => + hasOwn(value, 'deferredError') && + /** @type {any} */ (value).deferredError !== undefined; + +/** + * Type guard for a {@link FileModuleDescriptorConfiguration}. + * + * @param {ModuleDescriptorConfiguration} value + * @returns {value is FileModuleDescriptorConfiguration} + */ +export const isFileModuleDescriptorConfiguration = value => + hasOwn(value, 'parser') && + /** @type {any} */ (value).parser !== undefined && + !isErrorModuleDescriptorConfiguration(value); + +/** + * Type guard for an {@link ExitModuleDescriptorConfiguration}. + * @param {ModuleDescriptorConfiguration} value + * @returns {value is ExitModuleDescriptorConfiguration} + */ +export const isExitModuleDescriptorConfiguration = value => + hasOwn(value, 'exit') && + /** @type {any} */ (value).exit !== undefined && + !isErrorModuleDescriptorConfiguration(value); + +/** + * Type guard for an {@link CompartmentModuleDescriptorConfiguration}. + * @param {ModuleDescriptorConfiguration} value + * @returns {value is CompartmentModuleDescriptorConfiguration} + */ +export const isCompartmentModuleDescriptorConfiguration = value => + hasOwn(value, 'compartment') && + /** @type {any} */ (value).compartment !== undefined && + hasOwn(value, 'module') && + /** @type {any} */ (value).module !== undefined && + !isErrorModuleDescriptorConfiguration(value); +/** + * Type guard for an {@link ErrorModuleSource}. + * + * @param {ModuleSource} value + * @returns {value is ErrorModuleSource} + */ +export const isErrorModuleSource = value => + hasOwn(value, 'deferredError') && + /** @type {any} */ (value).deferredError !== undefined; + +/** + * Type guard for an {@link ExitModuleSource}. + * + * @param {ModuleSource} value + * @returns {value is ExitModuleSource} + */ +export const isExitModuleSource = value => + hasOwn(value, 'exit') && + /** @type {any} */ (value).exit !== undefined && + !isErrorModuleSource(value); + +/** + * Type guard for an {@link LocalModuleSource}. + * + * @param {ModuleSource} value + * @returns {value is LocalModuleSource} + */ +export const isLocalModuleSource = value => + hasOwn(value, 'bytes') && + /** @type {any} */ (value).bytes !== undefined && + hasOwn(value, 'parser') && + /** @type {any} */ (value).parser !== undefined && + hasOwn(value, 'sourceDirname') && + /** @type {any} */ (value).sourceDirname !== undefined && + hasOwn(value, 'location') && + /** @type {any} */ (value).location !== undefined && + !isErrorModuleSource(value); diff --git a/packages/compartment-mapper/src/import-archive-lite.js b/packages/compartment-mapper/src/import-archive-lite.js index 35c69f39f5..af2b24312d 100644 --- a/packages/compartment-mapper/src/import-archive-lite.js +++ b/packages/compartment-mapper/src/import-archive-lite.js @@ -23,7 +23,7 @@ * } from 'ses'; * @import { * Application, - * CompartmentDescriptor, + * FileCompartmentDescriptor, * ComputeSourceLocationHook, * ComputeSourceMapLocationHook, * ExecuteFn, @@ -44,9 +44,13 @@ import { link } from './link.js'; import { parseLocatedJson } from './json.js'; import { unpackReadPowers } from './powers.js'; import { join } from './node-module-specifier.js'; -import { assertCompartmentMap } from './compartment-map.js'; +import { assertFileCompartmentMap } from './compartment-map.js'; import { exitModuleImportHookMaker } from './import-hook.js'; import { attenuateModuleHook, enforceModulePolicy } from './policy.js'; +import { + isErrorModuleDescriptorConfiguration, + isFileModuleDescriptorConfiguration, +} from './guards.js'; const { Fail, quote: q } = assert; @@ -77,7 +81,7 @@ const postponeErrorToExecute = errorMessage => { /** * @param {(path: string) => Uint8Array} get - * @param {Record} compartments + * @param {Record} compartments * @param {string} archiveLocation * @param {ParserForLanguage} parserForLanguage * @param {HashFn} [computeSha512] @@ -161,22 +165,22 @@ const makeArchiveImportHookMaker = ( )} in archive ${q(archiveLocation)}`, ); } - if (module.deferredError !== undefined) { + if (isErrorModuleDescriptorConfiguration(module)) { return postponeErrorToExecute(module.deferredError); } - if (module.parser === undefined) { + if (!isFileModuleDescriptorConfiguration(module)) { throw Error( `Cannot parse module ${q(moduleSpecifier)} in package ${q( packageLocation, - )} in archive ${q(archiveLocation)}`, + )} in archive ${q(archiveLocation)}; missing parser`, ); } const parser = parserForLanguage[module.parser]; if (parser === undefined) { throw Error( - `Cannot parse ${q(module.parser)} module ${q( + `Cannot parse module ${q( moduleSpecifier, - )} in package ${q(packageLocation)} in archive ${q(archiveLocation)}`, + )} in package ${q(packageLocation)} in archive ${q(archiveLocation)}; unknown parser (${q(module.parser)})`, ); } const { parse } = parser; @@ -306,7 +310,7 @@ export const parseArchive = async ( compartmentMapText, 'compartment-map.json', ); - assertCompartmentMap(compartmentMap, archiveLocation); + assertFileCompartmentMap(compartmentMap, archiveLocation); const { compartments, diff --git a/packages/compartment-mapper/src/import-archive.js b/packages/compartment-mapper/src/import-archive.js index d2b8aed23c..8b1929a029 100644 --- a/packages/compartment-mapper/src/import-archive.js +++ b/packages/compartment-mapper/src/import-archive.js @@ -19,14 +19,12 @@ /** * @import { * Application, - * ComputeSourceLocationHook, - * ComputeSourceMapLocationHook, * ExecuteOptions, - * ExitModuleImportHook, - * HashFn, * LoadArchiveOptions, * ReadPowers, * ParserForLanguage, + * ParseArchiveOptions, + * ReadFn, * } from './types.js' */ @@ -39,28 +37,10 @@ import { const { assign, create, freeze } = Object; -// Must give the type of Compartment a name to capture the external meaning of -// Compartment Otherwise @param {typeof Compartment} takes the Compartment to -// mean the const variable defined within the function. -// -/** @typedef {typeof Compartment} CompartmentConstructor */ - -/** - * @typedef {object} Options - * @property {string} [expectedSha512] - * @property {HashFn} [computeSha512] - * @property {Record} [modules] - * @property {ExitModuleImportHook} [importHook] - * @property {CompartmentConstructor} [Compartment] - * @property {ComputeSourceLocationHook} [computeSourceLocation] - * @property {ComputeSourceMapLocationHook} [computeSourceMapLocation] - * @property {ParserForLanguage} [parserForLanguage] - */ - /** * Add the default parserForLanguage option. - * @param {Options} [options] - * @returns {Options} + * @param {ParseArchiveOptions} [options] + * @returns {ParseArchiveOptions} */ const assignParserForLanguage = (options = {}) => { const { parserForLanguage: parserForLanguageOption, ...rest } = options; @@ -74,7 +54,7 @@ const assignParserForLanguage = (options = {}) => { /** * @param {Uint8Array} archiveBytes * @param {string} [archiveLocation] - * @param {Options} [options] + * @param {ParseArchiveOptions} [options] * @returns {Promise} */ export const parseArchive = async ( @@ -89,7 +69,7 @@ export const parseArchive = async ( ); /** - * @param {import('@endo/zip').ReadFn | ReadPowers} readPowers + * @param {ReadFn | ReadPowers} readPowers * @param {string} archiveLocation * @param {LoadArchiveOptions} [options] * @returns {Promise} @@ -102,7 +82,7 @@ export const loadArchive = async (readPowers, archiveLocation, options) => ); /** - * @param {import('@endo/zip').ReadFn | ReadPowers} readPowers + * @param {ReadFn | ReadPowers} readPowers * @param {string} archiveLocation * @param {ExecuteOptions & LoadArchiveOptions} options * @returns {Promise} diff --git a/packages/compartment-mapper/src/import-hook.js b/packages/compartment-mapper/src/import-hook.js index bb13f0ffad..efbd40a2f6 100644 --- a/packages/compartment-mapper/src/import-hook.js +++ b/packages/compartment-mapper/src/import-hook.js @@ -29,14 +29,16 @@ * ImportNowHookMaker, * MakeImportHookMakerOptions, * MakeImportNowHookMakerOptions, - * ModuleDescriptor, * ParseResult, * ReadFn, * ReadPowers, * ReadNowPowers, * StrictlyRequiredFn, - * CompartmentSources, - * DeferErrorFn + * DeferErrorFn, + * ErrorModuleSource, + * FileUrlString, + * Sources, + * CompartmentSources * } from './types.js' */ @@ -45,14 +47,13 @@ import { resolve } from './node-module-specifier.js'; import { attenuateModuleHook, enforceModulePolicy, - enforcePackagePolicyByPath, + enforcePackagePolicyByCanonicalName, } from './policy.js'; import { ATTENUATORS_COMPARTMENT } from './policy-format.js'; import { unpackReadPowers } from './powers.js'; // q, as in quote, for quoting strings in error messages. -const q = JSON.stringify; - +const { quote: q } = assert; const { apply } = Reflect; /** @@ -63,7 +64,7 @@ const { apply } = Reflect; */ const freeze = Object.freeze; -const { entries, keys, assign, create } = Object; +const { hasOwn, keys, assign, create } = Object; const { hasOwnProperty } = Object.prototype; /** @@ -74,10 +75,11 @@ const has = (haystack, needle) => apply(hasOwnProperty, haystack, [needle]); /** * @param {string} rel - a relative URL - * @param {string} abs - a fully qualified URL - * @returns {string} + * @param {FileUrlString} abs - a fully qualified URL + * @returns {FileUrlString} */ -const resolveLocation = (rel, abs) => new URL(rel, abs).toString(); +const resolveLocation = (rel, abs) => + /** @type {FileUrlString} */ (new URL(rel, abs).toString()); // this is annoying function getImportsFromRecord(record) { @@ -150,7 +152,7 @@ const findRedirect = ({ for (;;) { if ( someLocation !== ATTENUATORS_COMPARTMENT && - someLocation in compartments + hasOwn(compartments, someLocation) ) { const location = someLocation; const someCompartmentDescriptor = compartmentDescriptors[location]; @@ -160,44 +162,8 @@ const findRedirect = ({ return undefined; } - // this tests the compartment referred to by the absolute path - // is a dependency of the compartment descriptor - if (compartmentDescriptor.compartments.has(location)) { - return { - specifier: relativeSpecifier(moduleSpecifierLocation, location), - compartment: compartments[location], - }; - } - - // this tests if the compartment descriptor is a dependency of the - // compartment referred to by the absolute path. - // it may be in scope, but disallowed by policy. - if ( - someCompartmentDescriptor.compartments.has( - compartmentDescriptor.location, - ) - ) { - enforceModulePolicy( - compartmentDescriptor.name, - someCompartmentDescriptor, - { - errorHint: `Blocked in importNow hook by relationship. ${q(absoluteModuleSpecifier)} is part of the compartment map and resolves to ${location}`, - }, - ); - return { - specifier: relativeSpecifier(moduleSpecifierLocation, location), - compartment: compartments[location], - }; - } - if (compartmentDescriptor.policy) { - /* c8 ignore next */ - if (!someCompartmentDescriptor.path) { - throw new Error( - `Cannot enforce package policy: compartment descriptor for ${location} unexpectedly missing a path; please report this issue`, - ); - } - enforcePackagePolicyByPath( + enforcePackagePolicyByCanonicalName( someCompartmentDescriptor, compartmentDescriptor, { @@ -416,6 +382,7 @@ function* chooseModuleDescriptor( retained: true, module: candidateSpecifier, compartment: packageLocation, + __createdBy: 'import-hook', }; } /** @type {StaticModuleType} */ @@ -480,9 +447,7 @@ const makeDeferError = ( packageSources, ) => { /** - * @param {string} specifier - * @param {Error} error - error to throw on execute - * @returns {StaticModuleType} + * @type {DeferErrorFn} */ const deferError = (specifier, error) => { // strictlyRequired is populated with imports declared by modules whose parser is not using heuristics to figure @@ -504,9 +469,11 @@ const makeDeferError = ( throw error; }, }); - packageSources[specifier] = { + /** @type {ErrorModuleSource} */ + const moduleSource = { deferredError: error.message, }; + packageSources[specifier] = moduleSource; return record; }; @@ -515,7 +482,7 @@ const makeDeferError = ( /** * @param {ReadFn|ReadPowers} readPowers - * @param {string} baseLocation + * @param {FileUrlString} baseLocation * @param {MakeImportHookMakerOptions} options * @returns {ImportHookMaker} */ @@ -681,7 +648,7 @@ export const makeImportHookMaker = ( * Synchronous import for dynamic requires. * * @param {ReadNowPowers} readPowers - * @param {string} baseLocation + * @param {FileUrlString} baseLocation * @param {MakeImportNowHookMakerOptions} options * @returns {ImportNowHookMaker} */ @@ -726,10 +693,13 @@ export function makeImportNowHookMaker( compartments, shouldDeferError, }) => { + packageLocation = resolveLocation(packageLocation, baseLocation); + const packageSources = sources[packageLocation] || create(null); + sources[packageLocation] = packageSources; const deferError = makeDeferError( strictlyRequiredForCompartment, packageLocation, - sources, + packageSources, ); /** @@ -753,7 +723,7 @@ export function makeImportNowHookMaker( // hook returns something. Otherwise, we need to fall back to the 'cannot find' error below. enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { exit: true, - errorHint: `Blocked in loading. ${q( + errorHint: `Blocked exit module. ${q( moduleSpecifier, )} was not in the compartment map and an attempt was made to load it as a builtin`, }); @@ -781,38 +751,11 @@ export function makeImportNowHookMaker( }; } - const compartmentDescriptor = compartmentDescriptors[packageLocation] || {}; - - packageLocation = resolveLocation(packageLocation, baseLocation); - const packageSources = sources[packageLocation] || create(null); - sources[packageLocation] = packageSources; - const { - modules: - moduleDescriptors = /** @type {Record} */ ( - create(null) - ), - } = compartmentDescriptor; + const compartmentDescriptor = + compartmentDescriptors[packageLocation] || create(null); + const { modules: moduleDescriptors = create(null) } = compartmentDescriptor; compartmentDescriptor.modules = moduleDescriptors; - let { policy } = compartmentDescriptor; - policy = policy || create(null); - - // Associates modules with compartment descriptors based on policy - // in cases where the association was not made when building the - // compartment map but is indicated by the policy. - if ('packages' in policy && typeof policy.packages === 'object') { - for (const [packageName, packagePolicyItem] of entries(policy.packages)) { - if ( - !(packageName in compartmentDescriptor.modules) && - packageName in compartmentDescriptor.scopes && - packagePolicyItem - ) { - compartmentDescriptor.modules[packageName] = - compartmentDescriptor.scopes[packageName]; - } - } - } - const { maybeReadNow, isAbsolute } = readPowers; /** @type {ImportNowHook} */ diff --git a/packages/compartment-mapper/src/import-lite.js b/packages/compartment-mapper/src/import-lite.js index 3a78b51d7d..84aa0495af 100644 --- a/packages/compartment-mapper/src/import-lite.js +++ b/packages/compartment-mapper/src/import-lite.js @@ -21,7 +21,6 @@ /** * @import { - * CompartmentMapDescriptor, * SyncImportLocationOptions, * ImportNowHookMaker, * ReadNowPowers, @@ -31,6 +30,7 @@ * ReadFn, * ReadPowers, * SomeObject, + * PackageCompartmentMapDescriptor, * } from './types.js' */ @@ -73,7 +73,7 @@ const isSyncOptions = value => { /** * @overload * @param {ReadNowPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {SyncImportLocationOptions} [opts] * @returns {Promise} */ @@ -81,14 +81,14 @@ const isSyncOptions = value => { /** * @overload * @param {ReadFn | ReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ImportLocationOptions} [opts] * @returns {Promise} */ /** * @param {ReadFn|ReadPowers|ReadNowPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ImportLocationOptions} [options] * @returns {Promise} */ @@ -235,7 +235,7 @@ export const loadFromMap = async (readPowers, compartmentMap, options = {}) => { /** * @param {ReadFn | ReadPowers} readPowers - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {ImportLocationOptions} [options] * @returns {Promise} the object of the imported modules exported * names. diff --git a/packages/compartment-mapper/src/import.js b/packages/compartment-mapper/src/import.js index 30530dbecd..145e7bfa10 100644 --- a/packages/compartment-mapper/src/import.js +++ b/packages/compartment-mapper/src/import.js @@ -82,6 +82,7 @@ export const loadLocation = async ( commonDependencies, policy, parserForLanguage, + log, languages, languageForExtension, commonjsLanguageForExtension, @@ -107,9 +108,11 @@ export const loadLocation = async ( workspaceCommonjsLanguageForExtension, workspaceModuleLanguageForExtension, languages, + log, }); return loadFromMap(readPowers, compartmentMap, { parserForLanguage, + log, ...otherOptions, }); }; diff --git a/packages/compartment-mapper/src/link.js b/packages/compartment-mapper/src/link.js index 5c24e6bba6..6e5c97cd96 100644 --- a/packages/compartment-mapper/src/link.js +++ b/packages/compartment-mapper/src/link.js @@ -13,17 +13,22 @@ /** * @import {ModuleMapHook} from 'ses' * @import { - * CompartmentDescriptor, - * CompartmentMapDescriptor, * ImportNowHookMaker, * LinkOptions, * LinkResult, - * ModuleDescriptor, * ParseFn, * AsyncParseFn, * ParserForLanguage, * ParserImplementation, * ShouldDeferError, + * ScopeDescriptor, + * CompartmentModuleDescriptorConfiguration, + * PackageCompartmentMapDescriptor, + * FileUrlString, + * PackageCompartmentDescriptor, + * FileCompartmentMapDescriptor, + * FileCompartmentDescriptor, + * FileModuleDescriptorConfiguration, * } from './types.js' */ @@ -35,10 +40,12 @@ import { makeDeferredAttenuatorsProvider, } from './policy.js'; import { ATTENUATORS_COMPARTMENT } from './policy-format.js'; +import { + isCompartmentModuleDescriptorConfiguration, + isExitModuleDescriptorConfiguration, +} from './guards.js'; -const { assign, create, entries, freeze } = Object; -const { hasOwnProperty } = Object.prototype; -const { apply } = Reflect; +const { assign, create, entries, freeze, hasOwn } = Object; const { allSettled } = Promise; /** @@ -47,17 +54,10 @@ const { allSettled } = Promise; */ const promiseAllSettled = allSettled.bind(Promise); -const defaultCompartment = Compartment; +const DefaultCompartment = Compartment; // q, as in quote, for strings in error messages. -const q = JSON.stringify; - -/** - * @param {Record} object - * @param {string} key - * @returns {boolean} - */ -const has = (object, key) => apply(hasOwnProperty, object, [key]); +const { quote: q } = assert; /** * For a full, absolute module specifier like "dependency", @@ -89,11 +89,11 @@ const trimModuleSpecifierPrefix = (moduleSpecifier, prefix) => { * Any module specifier with an absolute prefix should be captured by * the `moduleMap` or `moduleMapHook`. * - * @param {CompartmentDescriptor} compartmentDescriptor + * @param {FileCompartmentDescriptor|PackageCompartmentDescriptor} compartmentDescriptor * @param {Record} compartments * @param {string} compartmentName - * @param {Record} moduleDescriptors - * @param {Record} scopeDescriptors + * @param {Record} moduleDescriptors + * @param {Record>} scopeDescriptors * @returns {ModuleMapHook | undefined} */ const makeModuleMapHook = ( @@ -104,8 +104,7 @@ const makeModuleMapHook = ( scopeDescriptors, ) => { /** - * @param {string} moduleSpecifier - * @returns {string | object | undefined} + * @type {ModuleMapHook} */ const moduleMapHook = moduleSpecifier => { compartmentDescriptor.retained = true; @@ -114,40 +113,42 @@ const makeModuleMapHook = ( if (moduleDescriptor !== undefined) { moduleDescriptor.retained = true; + if (isExitModuleDescriptorConfiguration(moduleDescriptor)) { + return undefined; + } // "foreignCompartmentName" refers to the compartment which // may differ from the current compartment - const { - compartment: foreignCompartmentName = compartmentName, - module: foreignModuleSpecifier, - exit, - } = moduleDescriptor; - if (exit !== undefined) { - return undefined; // fall through to import hook - } - if (foreignModuleSpecifier !== undefined) { - // archive goes through foreignModuleSpecifier for local modules too - if (!moduleSpecifier.startsWith('./')) { - // This code path seems to only be reached on subsequent imports of the same specifier in the same compartment. - // The check should be redundant and is only left here out of abundance of caution. - enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { - exit: false, - errorHint: - 'This check should not be reachable. If you see this error, please file an issue.', - }); - } - - const foreignCompartment = compartments[foreignCompartmentName]; - if (foreignCompartment === undefined) { - throw Error( - `Cannot import from missing compartment ${q( - foreignCompartmentName, - )}}`, - ); + if (isCompartmentModuleDescriptorConfiguration(moduleDescriptor)) { + const { + compartment: foreignCompartmentName = compartmentName, + module: foreignModuleSpecifier, + } = moduleDescriptor; + if (foreignModuleSpecifier !== undefined) { + // archive goes through foreignModuleSpecifier for local modules too + if (!moduleSpecifier.startsWith('./')) { + // This code path seems to only be reached on subsequent imports of the same specifier in the same compartment. + // The check should be redundant and is only left here out of abundance of caution. + enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { + exit: false, + errorHint: + 'This check should not be reachable. If you see this error, please file an issue.', + }); + } + + const foreignCompartment = compartments[foreignCompartmentName]; + if (foreignCompartment === undefined) { + throw Error( + `Cannot import from missing compartment ${q( + foreignCompartmentName, + )}}`, + ); + } + // actual module descriptor + return { + compartment: foreignCompartment, + namespace: foreignModuleSpecifier, + }; } - return { - compartment: foreignCompartment, - namespace: foreignModuleSpecifier, - }; } } @@ -198,7 +199,9 @@ const makeModuleMapHook = ( retained: true, compartment: foreignCompartmentName, module: foreignModuleSpecifier, + __createdBy: 'link', }; + // actual module descriptor return { compartment: foreignCompartment, namespace: foreignModuleSpecifier, @@ -236,16 +239,10 @@ const impossibleImportNowHookMaker = () => { * - Passes the given globals and external modules into the root compartment * only. * - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor|FileCompartmentMapDescriptor} compartmentMap * @param {LinkOptions} options * @returns {LinkResult} the root compartment of the compartment DAG */ - -/** - * @param {CompartmentMapDescriptor} compartmentMap - * @param {LinkOptions} options - * @returns {LinkResult} - */ export const link = ( { entry, compartments: compartmentDescriptors }, options, @@ -262,7 +259,7 @@ export const link = ( __shimTransforms__ = [], __native__ = false, archiveOnly = false, - Compartment = defaultCompartment, + Compartment = DefaultCompartment, } = options; const { compartment: entryCompartmentName } = entry; @@ -291,9 +288,14 @@ export const link = ( syncModuleTransforms, }); - for (const [compartmentName, compartmentDescriptor] of entries( - compartmentDescriptors, - )) { + const compartmentDescriptorEntries = + /** @type {[string, PackageCompartmentDescriptor|FileCompartmentDescriptor][]} */ ( + entries(compartmentDescriptors) + ); + for (const [ + compartmentName, + compartmentDescriptor, + ] of compartmentDescriptorEntries) { const { location, name, @@ -319,7 +321,7 @@ export const link = ( /** @type {ShouldDeferError} */ const shouldDeferError = language => { - if (language && has(parserForLanguage, language)) { + if (language && hasOwn(parserForLanguage, language)) { return /** @type {ParserImplementation} */ (parserForLanguage[language]) .heuristicImports; } else { @@ -418,7 +420,7 @@ export const link = ( }; /** - * @param {CompartmentMapDescriptor} compartmentMap + * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {LinkOptions} options * @deprecated Use {@link link}. */ diff --git a/packages/compartment-mapper/src/node-modules.js b/packages/compartment-mapper/src/node-modules.js index 673e02970c..30d64ad94c 100644 --- a/packages/compartment-mapper/src/node-modules.js +++ b/packages/compartment-mapper/src/node-modules.js @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ /** * Provides functions for constructing a compartment map that has a * compartment descriptor corresponding to every reachable package from an @@ -13,14 +14,21 @@ /* eslint no-shadow: 0 */ +import { applyCompartmentMapTransforms } from './compartment-map-transforms/index.js'; +import { defaultCompartmentMapTransforms } from './compartment-map-transforms/transforms.js'; +import { assertPackageCompartmentMap } from './compartment-map.js'; +import { GenericGraph, makeShortestPath } from './generic-graph.js'; import { inferExportsAndAliases } from './infer-exports.js'; import { parseLocatedJson } from './json.js'; import { join } from './node-module-specifier.js'; -import { assertPolicy, ATTENUATORS_COMPARTMENT } from './policy-format.js'; -import { dependencyAllowedByPolicy, getPolicyForPackage } from './policy.js'; +import { + assertPolicy, + ATTENUATORS_COMPARTMENT, + generateCanonicalName, +} from './policy-format.js'; +import { makePackagePolicyForCompartment } from './policy.js'; import { unpackReadPowers } from './powers.js'; import { search, searchDescriptor } from './search.js'; -import { GenericGraph, makeShortestPath } from './generic-graph.js'; /** * @import { @@ -30,6 +38,8 @@ import { GenericGraph, makeShortestPath } from './generic-graph.js'; * CompartmentMapForNodeModulesOptions, * FileUrlString, * LanguageForExtension, + * PackageCompartmentDescriptor, + * PackageCompartmentMapDescriptor, * MapNodeModulesOptions, * MaybeReadDescriptorFn, * MaybeReadFn, @@ -37,8 +47,12 @@ import { GenericGraph, makeShortestPath } from './generic-graph.js'; * PackageDescriptor, * ReadFn, * ReadPowers, - * SomePackagePolicy, + * PackageCompartmentDescriptorName, * SomePolicy, + * PackageCompartmentDescriptors, + * CompartmentModuleDescriptorConfiguration, + * ScopeDescriptor, + * LogFn, * } from './types.js' * @import { * Graph, @@ -50,15 +64,19 @@ import { GenericGraph, makeShortestPath } from './generic-graph.js'; * GraphPackagesOptions, * LogicalPathGraph, * PackageDetails, + * FinalGraph, + * FinalNode, + * CanonicalNameMap * } from './types/node-modules.js' */ -const { assign, create, keys, values, entries } = Object; +const { assign, create, keys, values, entries, freeze } = Object; const decoder = new TextDecoder(); // q, as in quote, for enquoting strings in error messages. -const q = JSON.stringify; +const { quote: q } = assert; +const { stringify } = JSON; /** * Default logger that does nothing. @@ -102,11 +120,15 @@ const resolveLocation = (rel, abs) => { const assertFileUrlString = allegedPackageLocation => { assert( typeof allegedPackageLocation === 'string', - `Package location must be a string, got ${q(allegedPackageLocation)}`, + `Package location must be a string; got ${stringify(allegedPackageLocation)}`, ); assert( allegedPackageLocation.startsWith('file://'), - `Package location must be a file URL, got ${q(allegedPackageLocation)}`, + `Package location must be a file URL; got ${q(allegedPackageLocation)}`, + ); + assert( + allegedPackageLocation.length > 7, + `Path part of file URL string must not be empty in ${q(allegedPackageLocation)}`, ); }; @@ -378,7 +400,7 @@ const graphPackage = async ( languageOptions, strict, logicalPathGraph, - { commonDependencyDescriptors = {}, logicalPath = [], log = noop } = {}, + { commonDependencyDescriptors = {}, log = noop } = {}, ) => { if (graph[packageLocation] !== undefined) { // Returning the promise here would create a causal cycle and stall recursion. @@ -457,7 +479,6 @@ const graphPackage = async ( for (const dependencyName of [...allDependencies].sort()) { const optional = optionals.has(dependencyName); - const childLogicalPath = [...logicalPath, dependencyName]; children.push( // Mutual recursion ahead: // eslint-disable-next-line no-use-before-define @@ -473,7 +494,6 @@ const graphPackage = async ( strict, logicalPathGraph, { - childLogicalPath, optional, commonDependencyDescriptors, log, @@ -482,7 +502,7 @@ const graphPackage = async ( ); } - const { version = '', exports: exportsDescriptor } = packageDescriptor; + const { exports: exportsDescriptor } = packageDescriptor; /** @type {Node['types']} */ const types = {}; @@ -520,8 +540,6 @@ const graphPackage = async ( assign(result, { name, - path: logicalPath, - label: `${name}${version ? `-v${version}` : ''}`, sourceDirname, explicitExports: exportsDescriptor !== undefined, externalAliases, @@ -529,6 +547,7 @@ const graphPackage = async ( dependencyLocations, types, parsers, + packageDescriptor, }); await Promise.all( @@ -600,12 +619,7 @@ const gatherDependency = async ( languageOptions, strict, logicalPathGraph, - { - childLogicalPath = [], - optional = false, - commonDependencyDescriptors = {}, - log = noop, - } = {}, + { optional = false, commonDependencyDescriptors = {}, log = noop } = {}, ) => { const dependency = await findPackage( readDescriptor, @@ -641,7 +655,6 @@ const gatherDependency = async ( logicalPathGraph, { commonDependencyDescriptors, - logicalPath: childLogicalPath, log, }, ); @@ -752,24 +765,26 @@ const graphPackages = async ( /** * `translateGraph` converts the graph returned by graph packages (above) into a - * {@link CompartmentMapDescriptor compartment map}. + * {@link PackageCompartmentMapDescriptor compartment map}. * - * @param {string} entryPackageLocation + * @param {FileUrlString} entryPackageLocation * @param {string} entryModuleSpecifier - * @param {Graph} graph + * @param {FinalGraph} graph * @param {Set} conditions - build conditions about the target environment * for selecting relevant exports, e.g., "browser" or "node". - * @param {SomePolicy} [policy] - * @returns {CompartmentMapDescriptor} + * @param {object} [options] + * @param {SomePolicy} [options.policy] + * @param {LogFn} [options.log] - a function to log messages + * @returns {PackageCompartmentMapDescriptor} */ const translateGraph = ( entryPackageLocation, entryModuleSpecifier, graph, conditions, - policy, + { policy, log = noop } = {}, ) => { - /** @type {CompartmentMapDescriptor['compartments']} */ + /** @type {PackageCompartmentDescriptors} */ const compartments = create(null); // For each package, build a map of all the external modules the package can @@ -781,10 +796,11 @@ const translateGraph = ( // The full map includes every exported module from every dependencey // package and is a complete list of every external module that the // corresponding compartment can import. - for (const dependeeLocation of keys(graph).sort()) { + for (const dependeeLocation of /** @type {PackageCompartmentDescriptorName[]} */ ( + keys(graph).sort() + )) { const { name, - path, label, sourceDirname, dependencyLocations, @@ -792,60 +808,37 @@ const translateGraph = ( parsers, types, } = graph[dependeeLocation]; - /** @type {CompartmentDescriptor['modules']} */ + /** @type {Record} */ const moduleDescriptors = create(null); - /** @type {CompartmentDescriptor['scopes']} */ + /** @type {Record>} */ const scopes = create(null); - /** - * List of all the compartments (by name) that this compartment can import from. - * - * @type {Set} - */ - const compartmentNames = new Set(); - const packagePolicy = getPolicyForPackage( - { - isEntry: dependeeLocation === entryPackageLocation, - name, - path, - }, + const packagePolicy = makePackagePolicyForCompartment({ + label, + name, policy, - ); - - /* c8 ignore next */ - if (policy && !packagePolicy) { - // this should never happen - throw new TypeError('Unexpectedly falsy package policy'); - } + }); /** * @param {string} dependencyName - * @param {string} packageLocation + * @param {PackageCompartmentDescriptorName} packageLocation */ const digestExternalAliases = (dependencyName, packageLocation) => { - const { externalAliases, explicitExports, name, path } = - graph[packageLocation]; + if (packageLocation === ATTENUATORS_COMPARTMENT) { + return; + } + const node = graph[packageLocation]; + const { externalAliases, explicitExports } = node; for (const exportPath of keys(externalAliases).sort()) { const targetPath = externalAliases[exportPath]; // dependency name may be different from package's name, // as in the case of browser field dependency replacements const localPath = join(dependencyName, exportPath); - if ( - !policy || - (packagePolicy && - dependencyAllowedByPolicy( - { - name, - path, - }, - packagePolicy, - )) - ) { - moduleDescriptors[localPath] = { - compartment: packageLocation, - module: targetPath, - }; - } + moduleDescriptors[localPath] = { + compartment: packageLocation, + module: targetPath, + __createdBy: 'node-modules', + }; } // if the exports field is not present, then all modules must be accessible if (!explicitExports) { @@ -860,8 +853,8 @@ const translateGraph = ( for (const dependencyName of keys(dependencyLocations).sort()) { const dependencyLocation = dependencyLocations[dependencyName]; digestExternalAliases(dependencyName, dependencyLocation); - compartmentNames.add(dependencyLocation); } + // digest own internal aliases for (const modulePath of keys(internalAliases).sort()) { const facetTarget = internalAliases[modulePath]; @@ -870,25 +863,27 @@ const translateGraph = ( if (targetIsRelative) { // add target to moduleDescriptors moduleDescriptors[modulePath] = { - compartment: dependeeLocation, + compartment: /** @type {FileUrlString} */ (dependeeLocation), module: facetTarget, + __createdBy: 'node-modules', }; } } - compartments[dependeeLocation] = { + /** @type {PackageCompartmentDescriptor} */ + const compartmentDescriptor = { label, name, - path, location: dependeeLocation, sourceDirname, modules: moduleDescriptors, scopes, parsers, types, - policy: /** @type {SomePackagePolicy} */ (packagePolicy), - compartments: compartmentNames, + policy: packagePolicy, }; + + compartments[dependeeLocation] = compartmentDescriptor; } return { @@ -971,6 +966,111 @@ const makeLanguageOptions = ({ }; }; +/** + * Creates a `Node` in `graph` corresponding to the "attenuators" Compartment. + * + * Only does so if `policy` is provided. + * + * @param {Graph} graph Graph + * @param {Node} entryNode Entry node of the grpah + * @param {SomePolicy} [policy] + * @throws If there's already a `Node` in `graph` for the "attenuators" + * Compartment + * @returns {void} + */ +const makeAttenuatorsNode = (graph, entryNode, policy) => { + if (policy) { + assertPolicy(policy); + + assert( + graph[ATTENUATORS_COMPARTMENT] === undefined, + `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`, + ); + + graph[ATTENUATORS_COMPARTMENT] = { + ...entryNode, + internalAliases: {}, + externalAliases: {}, + packageDescriptor: { name: ATTENUATORS_COMPARTMENT }, + name: ATTENUATORS_COMPARTMENT, + }; + } +}; + +/** + * Transforms a `Graph` into a readonly `FinalGraph`, in preparation for + * conversion to a `CompartmentDescriptor`. + * + * @param {Graph} graph Graph + * @param {LogicalPathGraph} logicalPathGraph Logical path graph + * @param {FileUrlString} packageLocation Entry package location + * @param {CanonicalNameMap} canonicalNameMap Mapping of canonical names to `Node` names (keys in `graph`) + * @returns {Readonly} + */ +const finalizeGraph = ( + graph, + logicalPathGraph, + packageLocation, + canonicalNameMap, +) => { + const shortestPath = makeShortestPath(logicalPathGraph); + + // neither the entry package nor the attenuators compartment have a path; omit + const { + [ATTENUATORS_COMPARTMENT]: attenuatorsNode, + [packageLocation]: entryNode, + ...subgraph + } = graph; + + /** @type {FinalGraph} */ + const finalGraph = create(null); + + /** @type {Readonly} */ + finalGraph[packageLocation] = freeze({ + ...entryNode, + label: generateCanonicalName({ + isEntry: true, + path: [], + }), + }); + + if (attenuatorsNode) { + /** @type {Readonly} */ + finalGraph[ATTENUATORS_COMPARTMENT] = freeze({ + ...attenuatorsNode, + label: generateCanonicalName({ + name: ATTENUATORS_COMPARTMENT, + path: [], + }), + }); + } + + const subgraphEntries = /** @type {[FileUrlString, Node][]} */ ( + entries(subgraph) + ); + + for (const [location, node] of subgraphEntries) { + const shortestLogicalPath = shortestPath(packageLocation, location); + + // the first element will always be the root package location; this is omitted from the path. + shortestLogicalPath.shift(); + + const path = shortestLogicalPath.map(location => graph[location].name); + const canonicalName = generateCanonicalName({ path }); + + /** @type {Readonly} */ + const finalNode = freeze({ + ...node, + label: canonicalName, + }); + + canonicalNameMap.set(canonicalName, location); + + finalGraph[location] = finalNode; + } + return freeze(finalGraph); +}; + /** * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers * @param {FileUrlString} packageLocation @@ -978,10 +1078,9 @@ const makeLanguageOptions = ({ * @param {PackageDescriptor} packageDescriptor * @param {string} moduleSpecifier * @param {CompartmentMapForNodeModulesOptions} [options] - * @returns {Promise} - * @deprecated Use {@link mapNodeModules} instead. + * @returns {Promise} */ -export const compartmentMapForNodeModules = async ( +const compartmentMapForNodeModules_ = async ( readPowers, packageLocation, conditionsOption, @@ -993,8 +1092,10 @@ export const compartmentMapForNodeModules = async ( dev = false, commonDependencies = {}, policy, + policyOverride, strict = false, log = noop, + compartmentMapTransforms = [], } = options; const { maybeRead, canonical } = unpackReadPowers(readPowers); const languageOptions = makeLanguageOptions(options); @@ -1028,51 +1129,43 @@ export const compartmentMapForNodeModules = async ( { log }, ); - if (policy) { - assertPolicy(policy); - - assert( - graph[ATTENUATORS_COMPARTMENT] === undefined, - `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`, - ); - - graph[ATTENUATORS_COMPARTMENT] = { - ...graph[packageLocation], - externalAliases: {}, - label: ATTENUATORS_COMPARTMENT, - name: ATTENUATORS_COMPARTMENT, - }; - } - - const shortestPath = makeShortestPath(logicalPathGraph); - // neither the entry package nor the attenuators compartment have a path; omit - const { - [ATTENUATORS_COMPARTMENT]: _, - [packageLocation]: __, - ...subgraph - } = graph; + makeAttenuatorsNode(graph, graph[packageLocation], policy); - for (const [location, node] of entries(subgraph)) { - const shortestLogicalPath = shortestPath( - packageLocation, - // entries() loses some type information - /** @type {FileUrlString} */ (location), - ); + /** + * @type {CanonicalNameMap} + */ + const canonicalNameMap = new Map(); - // the first element will always be the root package location; this is omitted from the path. - shortestLogicalPath.shift(); - node.path = shortestLogicalPath.map(location => graph[location].name); - log(`Canonical name for package at ${location}: ${node.path.join('>')}`); - } + const finalGraph = finalizeGraph( + graph, + logicalPathGraph, + packageLocation, + canonicalNameMap, + ); const compartmentMap = translateGraph( packageLocation, moduleSpecifier, - graph, + finalGraph, conditions, - policy, + { policy, log }, + ); + + // always run our transforms first + const transforms = [ + ...defaultCompartmentMapTransforms, + ...compartmentMapTransforms, + ]; + + await applyCompartmentMapTransforms( + compartmentMap, + canonicalNameMap, + transforms, + { log, policy, policyOverride }, ); + assertPackageCompartmentMap(compartmentMap); + return compartmentMap; }; @@ -1085,13 +1178,14 @@ export const compartmentMapForNodeModules = async ( * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers * @param {string} moduleLocation * @param {MapNodeModulesOptions} [options] - * @returns {Promise} + * @returns {Promise} */ export const mapNodeModules = async ( readPowers, moduleLocation, { tags = new Set(), conditions = tags, log = noop, ...otherOptions } = {}, ) => { + log(`Mapping node_modules for ${moduleLocation}…`); const { packageLocation, packageDescriptorText, @@ -1106,7 +1200,7 @@ export const mapNodeModules = async ( assertPackageDescriptor(packageDescriptor); assertFileUrlString(packageLocation); - return compartmentMapForNodeModules( + return compartmentMapForNodeModules_( readPowers, packageLocation, conditions, @@ -1115,3 +1209,8 @@ export const mapNodeModules = async ( { log, ...otherOptions }, ); }; + +/** + * @deprecated Use `mapNodeModules()`. + */ +export const compartmentMapForNodeModules = compartmentMapForNodeModules_; diff --git a/packages/compartment-mapper/src/policy-format.js b/packages/compartment-mapper/src/policy-format.js index c524c2a956..e0a4ecd7b1 100644 --- a/packages/compartment-mapper/src/policy-format.js +++ b/packages/compartment-mapper/src/policy-format.js @@ -7,11 +7,9 @@ /** * @import {SomePackagePolicy, - * SomePolicy, * PackagePolicy, * AttenuationDefinition, * PolicyEnforcementField, - * WildcardPolicy, * UnifiedAttenuationDefinition, * PolicyItem, * TypeGuard, @@ -24,7 +22,7 @@ *} from './types.js' */ -const { entries, keys } = Object; +const { entries, keys, hasOwn } = Object; const { isArray } = Array; const q = JSON.stringify; @@ -33,6 +31,8 @@ const q = JSON.stringify; */ export const ATTENUATORS_COMPARTMENT = ''; +export const ENTRY_COMPARTMENT = '$root$'; + /** * @satisfies {keyof FullAttenuationDefinition} */ @@ -52,7 +52,7 @@ const ATTENUATOR_PARAMS_KEY = 'params'; export const generateCanonicalName = ({ isEntry = false, name, path }) => { if (isEntry) { - throw Error('Entry module cannot be identified with a canonicalName'); + return ENTRY_COMPARTMENT; } if (name === ATTENUATORS_COMPARTMENT) { return ATTENUATORS_COMPARTMENT; @@ -60,9 +60,6 @@ export const generateCanonicalName = ({ isEntry = false, name, path }) => { return path.join('>'); }; -/** - * @type {WildcardPolicy} - */ export const WILDCARD_POLICY_VALUE = 'any'; /** @@ -129,11 +126,10 @@ const isEmpty = item => keys(item).length === 0; * * @param {PackagePolicy} packagePolicy Package policy * @param {PolicyEnforcementField} field Package policy field to look up - * @param {string|string[]} nameOrPath A canonical name or a path which can - * be converted to a canonical name + * @param {string} canonicalName A canonical name * @returns {boolean | AttenuationDefinition} */ -export const policyLookupHelper = (packagePolicy, field, nameOrPath) => { +export const policyLookupHelper = (packagePolicy, field, canonicalName) => { assert( POLICY_ENFORCEMENT_FIELDS.includes(field), `Unknown or unsupported policy field ${q(field)}`, @@ -153,15 +149,8 @@ export const policyLookupHelper = (packagePolicy, field, nameOrPath) => { return true; } - if (isArray(nameOrPath)) { - nameOrPath = generateCanonicalName({ - path: nameOrPath, - isEntry: nameOrPath.length === 0, - }); - } - - if (nameOrPath in policyDefinition) { - return policyDefinition[nameOrPath]; + if (hasOwn(policyDefinition, canonicalName)) { + return policyDefinition[canonicalName]; } return false; @@ -405,9 +394,8 @@ export const assertPackagePolicy = (allegedPackagePolicy, keypath, url) => { builtins, globals, noGlobalFreeze, - defaultAttenuator: _ignore, // a carve out for the default attenuator in compartment map - // eslint-disable-next-line no-unused-vars - options, // any extra options + defaultAttenuator: _defaultAttenuator, // a carve out for the default attenuator in compartment map + options: _options, // any extra options ...extra } = allegedPackagePolicy; diff --git a/packages/compartment-mapper/src/policy.js b/packages/compartment-mapper/src/policy.js index 9d77a4cb53..a415fcfa7f 100644 --- a/packages/compartment-mapper/src/policy.js +++ b/packages/compartment-mapper/src/policy.js @@ -9,23 +9,26 @@ * Policy, * PackagePolicy, * AttenuationDefinition, - * PackageNamingKit, * DeferredAttenuatorsProvider, * CompartmentDescriptor, * Attenuator, * SomePolicy, * PolicyEnforcementField, + * SomePackagePolicy, + * CompartmentDescriptorWithPolicy, + * ModuleDescriptorConfiguration, * } from './types.js' * @import {ThirdPartyStaticModuleInterface} from 'ses' */ import { ATTENUATORS_COMPARTMENT, - generateCanonicalName, + ENTRY_COMPARTMENT, getAttenuatorFromDefinition, isAllowingEverything, isAttenuationDefinition, policyLookupHelper, + WILDCARD_POLICY_VALUE, } from './policy-format.js'; const { @@ -111,63 +114,38 @@ export const detectAttenuators = policy => { }; /** - * Verifies if a module identified by `namingKit` can be a dependency of a package per `packagePolicy`. - * `packagePolicy` is required, when policy is not set, skipping needs to be handled by the caller. + * Generates the {@link SomePackagePolicy} value to be used in {@link CompartmentDescriptor.policy} * - * @param {PackageNamingKit} namingKit - * @param {PackagePolicy} packagePolicy - * @returns {boolean} + * @param {object} [params] Options + * @param {SomePolicy} [params.policy] User-supplied policy + * @param {string} [params.label] Canonical name / label ({@link CompartmentDescriptor.label}) + * @param {string} [params.name] Compartment name field {@link CompartmentDescriptor.name} for identification of "attenuators" compartment + * @param {SomePackagePolicy} [params.packagePolicy] Package policy, if known + * @returns {SomePackagePolicy|undefined} Package policy from `policy` or empty object; returns `params.packagePolicy` if provided */ -export const dependencyAllowedByPolicy = (namingKit, packagePolicy) => { - if (namingKit.isEntry) { - // dependency on entry compartment should never be allowed - return false; - } - const canonicalName = generateCanonicalName(namingKit); - return !!policyLookupHelper(packagePolicy, 'packages', canonicalName); -}; - -/** - * Returns the policy applicable to the canonicalName of the package - * - * @overload - * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these - * @param {SomePolicy} policy - user supplied policy - * @returns {SomePackagePolicy} packagePolicy if policy was specified - */ - -/** - * Returns `undefined` - * - * @overload - * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these - * @param {SomePolicy} [policy] - user supplied policy - * @returns {SomePackagePolicy|undefined} packagePolicy if policy was specified - */ - -/** - * Returns the policy applicable to the canonicalName of the package - * - * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these - * @param {SomePolicy} [policy] - user supplied policy - */ -export const getPolicyForPackage = (namingKit, policy) => { - if (!policy) { - return undefined; - } - if (namingKit.isEntry) { - return policy.entry; - } - const canonicalName = generateCanonicalName(namingKit); - if (canonicalName === ATTENUATORS_COMPARTMENT) { - return { defaultAttenuator: policy.defaultAttenuator, packages: 'any' }; +export const makePackagePolicyForCompartment = ({ + label, + name, + policy, + packagePolicy = undefined, +} = {}) => { + if (packagePolicy) { + return packagePolicy; } - if (policy.resources && policy.resources[canonicalName] !== undefined) { - return policy.resources[canonicalName]; - } else { - // Allow skipping policy entries for packages with no powers. - return create(null); + if (policy) { + if (name === ATTENUATORS_COMPARTMENT) { + packagePolicy = { + defaultAttenuator: policy.defaultAttenuator, + packages: WILDCARD_POLICY_VALUE, + }; + } else if (label === ENTRY_COMPARTMENT) { + packagePolicy = policy.entry; + } else if (label) { + packagePolicy = policy.resources?.[label]; + } + return packagePolicy ?? create(null); } + return undefined; }; /** @@ -236,8 +214,10 @@ export const makeDeferredAttenuatorsProvider = ( throw Error(`No attenuators specified in policy`); }; } else { - defaultAttenuator = - compartmentDescriptors[ATTENUATORS_COMPARTMENT].policy.defaultAttenuator; + // TODO: the attenuators compartment must always have a non-empty policy; maybe throw if it doesn't + defaultAttenuator = /** @type {SomePackagePolicy} */ ( + compartmentDescriptors[ATTENUATORS_COMPARTMENT].policy + ).defaultAttenuator; // At the time of this function being called, attenuators compartment won't // exist yet, we need to defer looking them up in the compartment to the @@ -308,7 +288,7 @@ async function attenuateGlobalThis({ * * @param {object} globalThis * @param {object} globals - * @param {PackagePolicy} packagePolicy + * @param {PackagePolicy|undefined} packagePolicy * @param {DeferredAttenuatorsProvider} attenuators * @param {Array} pendingJobs * @param {string} name @@ -366,28 +346,21 @@ export const attenuateGlobals = ( * Generates a helpful error message for a policy enforcement failure * * @param {string} specifier - * @param {CompartmentDescriptor} referrerCompartmentDescriptor + * @param {CompartmentDescriptorWithPolicy} referrerCompartmentDescriptor * @param {object} options - * @param {string[]} [options.compartmentDescriptorPath] + * @param {string} [options.resourceCanonicalName] * @param {string} [options.errorHint] - * @param {string} [options.resourceNameFromPath] * @param {PolicyEnforcementField} [options.policyField] * @returns {string} */ const policyEnforcementFailureMessage = ( specifier, { label, policy }, - { - compartmentDescriptorPath, - resourceNameFromPath, - errorHint, - policyField = 'packages', - } = {}, + { resourceCanonicalName, errorHint, policyField = 'packages' } = {}, ) => { let message = `Importing ${q(specifier)}`; - if (compartmentDescriptorPath) { - resourceNameFromPath ??= compartmentDescriptorPath.join('>'); - message += ` in resource ${q(resourceNameFromPath)}`; + if (resourceCanonicalName) { + message += ` in resource ${q(resourceCanonicalName)}`; } message += ` in ${q(label)} was not allowed by`; if (keys(policy[policyField] ?? {}).length > 0) { @@ -401,6 +374,13 @@ const policyEnforcementFailureMessage = ( return message; }; +/** + * @template {ModuleDescriptorConfiguration} T + * @param {CompartmentDescriptor} compartmentDescriptor + * @returns {compartmentDescriptor is CompartmentDescriptorWithPolicy} + */ +const hasPolicy = compartmentDescriptor => !!compartmentDescriptor.policy; + /** * Options for {@link enforceModulePolicy} * @typedef EnforceModulePolicyOptions @@ -420,10 +400,11 @@ export const enforceModulePolicy = ( compartmentDescriptor, { exit, errorHint } = {}, ) => { - const { policy, modules } = compartmentDescriptor; - if (!policy) { + if (!hasPolicy(compartmentDescriptor)) { + // No policy, no enforcement return; } + const { policy, modules } = compartmentDescriptor; if (!exit) { if (!modules[specifier]) { @@ -454,39 +435,27 @@ export const enforceModulePolicy = ( * @param {CompartmentDescriptor} referrerCompartmentDescriptor * @param {EnforceModulePolicyOptions} [options] */ -export const enforcePackagePolicyByPath = ( +export const enforcePackagePolicyByCanonicalName = ( compartmentDescriptor, referrerCompartmentDescriptor, { errorHint } = {}, ) => { - const { policy: referrerPolicy } = referrerCompartmentDescriptor; - if (!referrerPolicy) { - throw Error( + if (!hasPolicy(referrerCompartmentDescriptor)) { + throw new Error( `Cannot enforce policy via ${q(referrerCompartmentDescriptor.label)}: no package policy defined`, ); } - const { path, name } = compartmentDescriptor; - - assert( - path, - `Compartment descriptor ${q(compartmentDescriptor.label)} does not have a path; cannot enforce policy via ${q(referrerCompartmentDescriptor.label)}`, - ); - - const resourceNameFromPath = generateCanonicalName({ - path, - name, - isEntry: path.length === 0, - }); + const { policy: referrerPolicy } = referrerCompartmentDescriptor; + const { label: resourceCanonicalName } = compartmentDescriptor; - if (!policyLookupHelper(referrerPolicy, 'packages', resourceNameFromPath)) { - throw Error( + if (!policyLookupHelper(referrerPolicy, 'packages', resourceCanonicalName)) { + throw new Error( policyEnforcementFailureMessage( - resourceNameFromPath, + resourceCanonicalName, referrerCompartmentDescriptor, { errorHint, - compartmentDescriptorPath: path, - resourceNameFromPath, + resourceCanonicalName, }, ), ); @@ -515,19 +484,21 @@ async function attenuateModule({ // An async attenuator maker could be introduced here to return a synchronous attenuator. // For async attenuators see PR https://github.com/endojs/endo/pull/1535 - return freeze({ - imports: originalModuleRecord.imports, - // It seems ok to declare the exports but then let the attenuator trim the values. - // Seems ok for attenuation to leave them undefined - accessing them is malicious behavior. - exports: originalModuleRecord.exports, - execute: (moduleExports, compartment, resolvedImports) => { - const ns = {}; - originalModuleRecord.execute(ns, compartment, resolvedImports); - const attenuated = attenuate(ns); - moduleExports.default = attenuated; - assign(moduleExports, attenuated); - }, - }); + return freeze( + /** @type {ThirdPartyStaticModuleInterface} */ ({ + imports: originalModuleRecord.imports, + // It seems ok to declare the exports but then let the attenuator trim the values. + // Seems ok for attenuation to leave them undefined - accessing them is malicious behavior. + exports: originalModuleRecord.exports, + execute: (moduleExports, compartment, resolvedImports) => { + const ns = {}; + originalModuleRecord.execute(ns, compartment, resolvedImports); + const attenuated = attenuate(ns); + moduleExports.default = attenuated; + assign(moduleExports, attenuated); + }, + }), + ); } /** @@ -535,7 +506,7 @@ async function attenuateModule({ * * @param {string} specifier - exit module name * @param {ThirdPartyStaticModuleInterface} originalModuleRecord - reference to the exit module - * @param {PackagePolicy} policy - local compartment policy + * @param {PackagePolicy|undefined} policy - local compartment policy * @param {DeferredAttenuatorsProvider} attenuators - a key-value where attenuations can be found * @returns {Promise} - the attenuated module */ @@ -545,8 +516,11 @@ export const attenuateModuleHook = async ( policy, attenuators, ) => { + if (!policy) { + return originalModuleRecord; + } const policyValue = policyLookupHelper(policy, 'builtins', specifier); - if (!policy || policyValue === true) { + if (policyValue === true) { return originalModuleRecord; } diff --git a/packages/compartment-mapper/src/types/canonical-name.ts b/packages/compartment-mapper/src/types/canonical-name.ts new file mode 100644 index 0000000000..9074015b18 --- /dev/null +++ b/packages/compartment-mapper/src/types/canonical-name.ts @@ -0,0 +1,59 @@ +/** + * Type-level helpers for representing and validating Canonical Names. + * + * A Canonical Name is a string containing one or more npm package names + * (scoped or unscoped) delimited by a '>' character. + * + * These utilities are purely type-level; at runtime they are just strings. + * When used with string literals, TypeScript can evaluate them and narrow or + * reject invalid shapes (yielding `never`). + * + * @module + */ + +/** A scoped npm package name, like "@scope/pkg" */ +export type ScopedPackageName = `@${string}/${string}`; + +/** + * An unscoped npm package name. We approximate this as any string that does + * not contain a '/'. + */ +export type UnscopedPackageName = + S extends `${string}/${string}` ? never : S; + +/** A scoped or unscoped npm package name. */ +export type NpmPackageName = + | ScopedPackageName + | UnscopedPackageName; + +/** Split a string on '>' into a tuple of segments. */ +export type SplitOnGt = + S extends `${infer Head}>${infer Tail}` ? [Head, ...SplitOnGt] : [S]; + +/** + * Validate that every element in a tuple of strings is a valid npm package name. + * Returns `never` if any element looks like a subpath (contains '/') but is not scoped. + */ +export type AllValidPackageNames = Parts extends [ + infer H extends string, + ...infer T extends string[], +] + ? H extends ScopedPackageName + ? AllValidPackageNames + : H extends `${string}/${string}` + ? never + : AllValidPackageNames + : Parts; + +/** + * A Canonical Name string comprised of one or more npm package names separated + * by '>' (e.g., "foo", "@scope/foo>bar", "foo>@scope/bar>baz"). + * + * When given a string literal type, invalid shapes narrow to `never`. + */ +export type CanonicalName = + AllValidPackageNames> extends never ? never : S; + +/** Convenience predicate-style helper: resolves to `true`/`false` for literals. */ +export type IsCanonicalName = + CanonicalName extends never ? false : true; diff --git a/packages/compartment-mapper/src/types/compartment-map-schema.ts b/packages/compartment-mapper/src/types/compartment-map-schema.ts index 0691058fba..754bcc2a07 100644 --- a/packages/compartment-mapper/src/types/compartment-map-schema.ts +++ b/packages/compartment-mapper/src/types/compartment-map-schema.ts @@ -8,80 +8,228 @@ /* eslint-disable no-use-before-define */ +import type { + ATTENUATORS_COMPARTMENT, + ENTRY_COMPARTMENT, +} from '../policy-format.js'; +import type { CanonicalName } from './canonical-name.js'; +import type { FileUrlString } from './external.js'; import type { SomePackagePolicy } from './policy-schema.js'; import type { LiteralUnion } from './typescript.js'; +/** + * The type of a {@link CompartmentMapDescriptor.compartments} property. + */ +export type CompartmentDescriptors< + T extends CompartmentDescriptor = CompartmentDescriptor, + K extends string = string, +> = Record; + /** * A compartment map describes how to construct an application as a graph of * Compartments, each corresponding to Node.js style packaged modules. */ -export type CompartmentMapDescriptor = { +export type CompartmentMapDescriptor< + T extends CompartmentDescriptor = CompartmentDescriptor, + Name extends string = string, + EntryName extends string = Name, +> = { tags: Array; - entry: EntryDescriptor; - compartments: Record; + entry: EntryDescriptor; + compartments: CompartmentDescriptors; }; +/** + * The type of a {@link PackageCompartmentMapDescriptor.compartments} property. + */ +export type PackageCompartmentDescriptors = CompartmentDescriptors< + PackageCompartmentDescriptor, + PackageCompartmentDescriptorName +>; + +/** + * The {@link CompartmentDescriptor} type in the + * {@link PackageCompartmentMapDescriptor} returned by `mapNodeModules()` + */ +export type PackageCompartmentMapDescriptor = CompartmentMapDescriptor< + PackageCompartmentDescriptor, + PackageCompartmentDescriptorName, + FileUrlString +>; + +export interface FileCompartmentDescriptor + extends CompartmentDescriptor< + FileModuleDescriptorConfiguration | CompartmentModuleDescriptorConfiguration + > { + location: FileUrlString; +} + +export type FileCompartmentDescriptors = + CompartmentDescriptors; + +export type FileCompartmentMapDescriptor = + CompartmentMapDescriptor; + /** * The entry descriptor of a compartment map denotes the root module of an * application and the compartment that contains it. */ -export type EntryDescriptor = { - compartment: string; +export type EntryDescriptor = { + compartment: K; module: string; }; +/** + * Keys of {@link PackageCompartmentMapDescriptor.compartments} + */ +export type PackageCompartmentDescriptorName = LiteralUnion< + typeof ATTENUATORS_COMPARTMENT, + FileUrlString +>; + +export interface PackageCompartmentDescriptor + extends CompartmentDescriptor { + label: LiteralUnion< + typeof ATTENUATORS_COMPARTMENT | typeof ENTRY_COMPARTMENT, + string + >; + + location: LiteralUnion; + + name: LiteralUnion< + typeof ATTENUATORS_COMPARTMENT | typeof ENTRY_COMPARTMENT, + string + >; + + scopes: Record>; + + sourceDirname: string; +} + /** * A compartment descriptor corresponds to a single Compartment * of an assembled Application and describes how to construct * one for a given library or application `package.json`. */ -export type CompartmentDescriptor = { - label: string; - /** - * name of the parent directory of the package from which the compartment is derived, - * for purposes of generating sourceURL comments that are most likely to unite with the original sources in an IDE workspace. - */ - sourceDirname?: string; - /** shortest path of dependency names to this compartment */ - path?: Array; +export interface CompartmentDescriptor< + T extends ModuleDescriptorConfiguration = ModuleDescriptorConfiguration, + U extends string = string, +> { + label: CanonicalName; /** * the name of the originating package suitable for constructing a sourceURL * prefix that will match it to files in a developer workspace. */ name: string; + modules: Record; + scopes?: Record; + /** language for extension */ + parsers?: LanguageForExtension; + /** language for module specifier */ + types?: LanguageForModuleSpecifier; + /** policy specific to compartment */ + policy?: SomePackagePolicy; + location: string; + /** + * name of the parent directory of the package from which the compartment is derived, + * for purposes of generating sourceURL comments that are most likely to unite with the original sources in an IDE workspace. + */ + sourceDirname?: string; + /** * whether this compartment was retained by any module in the solution. This * property should never appear in an archived compartment map. */ - retained?: boolean; - modules: Record; - scopes: Record; - /** language for extension */ - parsers: LanguageForExtension; - /** language for module specifier */ - types: LanguageForModuleSpecifier; - /** policy specific to compartment */ - policy: SomePackagePolicy; - /** List of compartment names this Compartment depends upon */ - compartments: Set; -}; + retained?: true; +} + +export type CompartmentDescriptorWithPolicy< + T extends ModuleDescriptorConfiguration = ModuleDescriptorConfiguration, +> = Omit, 'policy'> & { policy: SomePackagePolicy }; + +/** + * A compartment descriptor digested by `digestCompartmentMap()` + */ +export interface DigestedCompartmentDescriptor + extends CompartmentDescriptor { + path: never; + retained: never; + scopes: never; + parsers: never; + types: never; + __createdBy: never; + sourceDirname: never; +} + +export type DigestedCompartmentDescriptors = + CompartmentDescriptors; + +export type DigestedCompartmentMapDescriptor = + CompartmentMapDescriptor; /** * For every module explicitly mentioned in an `exports` field of a - * `package.json`, there is a corresponding module descriptor. + * `package.json`, there is a corresponding `ModuleDescriptorConfiguration`. */ -export type ModuleDescriptor = { - compartment?: string; - module?: string; +export type ModuleDescriptorConfiguration = + | SourceModuleDescriptorConfiguration + | CompartmentModuleDescriptorConfiguration; + +export type ModuleDescriptorConfigurationCreator = + | 'link' + | 'transform' + | 'import-hook' + | 'digest' + | 'node-modules'; + +export interface BaseModuleDescriptorConfiguration { + deferredError?: string; + + retained?: true; + + __createdBy?: ModuleDescriptorConfigurationCreator; +} + +export interface ErrorModuleDescriptorConfiguration + extends BaseModuleDescriptorConfiguration { + deferredError: string; +} + +/** + * For every module explicitly mentioned in an `exports` field of a + * `package.json`, there is a corresponding `CompartmentModuleDescriptorConfiguration`. + */ +export interface CompartmentModuleDescriptorConfiguration + extends BaseModuleDescriptorConfiguration { + /** + * The name of the compartment that contains this module. + */ + compartment: FileUrlString; + /** + * The module name within {@link CompartmentDescriptor.modules} of the + * `CompartmentDescriptor` referred to by {@link compartment} + */ + module: string; +} + +export interface ExitModuleDescriptorConfiguration + extends BaseModuleDescriptorConfiguration { + exit: string; +} + +export interface FileModuleDescriptorConfiguration + extends BaseModuleDescriptorConfiguration { location?: string; - parser?: Language; + parser: Language; /** in base 16, hex */ sha512?: string; - exit?: string; - deferredError?: string; - retained?: boolean; -}; +} + +export type SourceModuleDescriptorConfiguration = + | FileModuleDescriptorConfiguration + | ExitModuleDescriptorConfiguration + | ErrorModuleDescriptorConfiguration; /** * Scope descriptors link all names under a prefix to modules in another @@ -90,8 +238,11 @@ export type ModuleDescriptor = { * in a `package.json` file, when that `package.json` file does not have * an explicit `exports` map. */ -export type ScopeDescriptor = { - compartment: string; +export type ScopeDescriptor = { + /** + * A compartment name; not a `Compartment`. + */ + compartment: T; module?: string; }; @@ -121,3 +272,21 @@ export type LanguageForExtension = Record; * Mapping of module specifier to {@link Language Languages}. */ export type LanguageForModuleSpecifier = Record; + +export type ModuleDescriptorConfigurationKind = + | 'file' + | 'compartment' + | 'exit' + | 'error'; + +export type ModuleDescriptorConfigurationKindToType< + T extends ModuleDescriptorConfigurationKind, +> = T extends 'file' + ? FileModuleDescriptorConfiguration + : T extends 'compartment' + ? CompartmentModuleDescriptorConfiguration + : T extends 'exit' + ? ExitModuleDescriptorConfiguration + : T extends 'error' + ? ErrorModuleDescriptorConfiguration + : never; diff --git a/packages/compartment-mapper/src/types/external.ts b/packages/compartment-mapper/src/types/external.ts index 0405503990..ea9291f558 100644 --- a/packages/compartment-mapper/src/types/external.ts +++ b/packages/compartment-mapper/src/types/external.ts @@ -15,11 +15,15 @@ import type { import type { CompartmentDescriptor, CompartmentMapDescriptor, + DigestedCompartmentMapDescriptor, Language, LanguageForExtension, + PackageCompartmentMapDescriptor, } from './compartment-map-schema.js'; +import type { SomePackagePolicy, SomePolicy } from './policy-schema.js'; import type { HashFn, ReadFn, ReadPowers } from './powers.js'; -import type { SomePolicy } from './policy-schema.js'; + +export type { CanonicalName } from './canonical-name.js'; /** * Set of options available in the context of code execution. @@ -66,6 +70,7 @@ export interface LogOptions { */ export type MapNodeModulesOptions = MapNodeModulesOptionsOmitPolicy & PolicyOption & + PolicyOverrideOption & LogOptions; type MapNodeModulesOptionsOmitPolicy = Partial<{ @@ -125,26 +130,49 @@ type MapNodeModulesOptionsOmitPolicy = Partial<{ * from the `parserForLanguage` option. */ languages: Array; + + /** + * Non-empty list of {@link CompartmentMapTransformFn Compartment Map Transforms} to apply to the {@link CompartmentMapDescriptor} before returning it. + * + * If empty or not provided, the default transforms are applied. + */ + compartmentMapTransforms: ReadonlyArray< + CompartmentMapTransformFn + >; }>; -/** - * @deprecated Use `mapNodeModules()`. - */ export type CompartmentMapForNodeModulesOptions = Omit< MapNodeModulesOptions, 'conditions' | 'tags' >; +/** + * Options for `captureFromMap()` + */ export type CaptureLiteOptions = ImportingOptions & LinkingOptions & PolicyOption & - LogOptions; + LogOptions & + ForceLoadOption; + +/** + * Options bag containing a `forceLoad` array. + */ +export interface ForceLoadOption { + /** + * List of compartment names (the keys of + * {@link CompartmentMapDescriptor.compartments}) to force-load _after_ the + * entry compartment and any attenuators. + */ + forceLoad?: Array; +} export type ArchiveLiteOptions = SyncOrAsyncArchiveOptions & ModuleTransformsOption & ImportingOptions & ExitModuleImportHookOption & - LinkingOptions; + LinkingOptions & + LogOptions; export type SyncArchiveLiteOptions = SyncOrAsyncArchiveOptions & SyncModuleTransformsOption & @@ -299,6 +327,10 @@ export type PolicyOption = { policy?: SomePolicy; }; +export interface PolicyOverrideOption { + policyOverride?: SomePolicy; +} + export type LanguageForExtensionOption = { languageForExtension?: LanguageForExtension; }; @@ -337,7 +369,7 @@ export interface DigestResult { /** * Normalized `CompartmentMapDescriptor` */ - compartmentMap: CompartmentMapDescriptor; + compartmentMap: DigestedCompartmentMapDescriptor; /** * Sources found in the `CompartmentMapDescriptor` @@ -367,7 +399,7 @@ export interface DigestResult { * The result of `captureFromMap`. */ export type CaptureResult = Omit & { - captureCompartmentMap: DigestResult['compartmentMap']; + captureCompartmentMap: DigestedCompartmentMapDescriptor; captureSources: DigestResult['sources']; }; @@ -375,7 +407,7 @@ export type CaptureResult = Omit & { * The result of `makeArchiveCompartmentMap` */ export type ArchiveResult = Omit & { - archiveCompartmentMap: DigestResult['compartmentMap']; + archiveCompartmentMap: DigestedCompartmentMapDescriptor; archiveSources: DigestResult['sources']; }; @@ -386,18 +418,31 @@ export type ArchiveResult = Omit & { export type Sources = Record; export type CompartmentSources = Record; -// TODO unionize: -export type ModuleSource = Partial<{ +export type ModuleSource = + | LocalModuleSource + | ExitModuleSource + | ErrorModuleSource; + +export interface BaseModuleSource { /** module loading error deferred to later stage */ + + deferredError?: string; +} + +export interface ErrorModuleSource extends BaseModuleSource { deferredError: string; +} + +export interface LocalModuleSource extends BaseModuleSource { /** * package-relative location. * Not suitable for capture in an archive or bundle since it varies from host * to host and would frustrate integrity hash checks. */ location: string; - /** fully qualified location */ - sourceLocation: string; + parser: Language; + /** in lowercase base-16 (hexadecimal) */ + sha512?: string; /** * directory name of the original source. * This is safe to capture in a compartment map because it is _unlikely_ to @@ -413,15 +458,19 @@ export type ModuleSource = Partial<{ * https://github.com/endojs/endo/issues/2671 */ sourceDirname: string; + /** fully qualified location */ + + sourceLocation: string; bytes: Uint8Array; - /** in lowercase base-16 (hexadecimal) */ - sha512: string; - parser: Language; - /** indicates that this is a reference that exits the mapped compartments */ - exit: string; /** module for the module */ record: StaticModuleType; -}>; +} + +export interface ExitModuleSource extends BaseModuleSource { + /** indicates that this is a reference that exits the mapped compartments */ + + exit: string; +} export type SourceMapHook = ( sourceMap: string, @@ -557,3 +606,167 @@ export type LogFn = (...args: any[]) => void; * A string that represents a file URL. */ export type FileUrlString = `file://${string}`; + +// #region compartment map transforms + +/** + * Options provided to all Compartment Map Transforms. + */ +export type CompartmentMapTransformOptions = Required & + PolicyOption & + PolicyOverrideOption; + +/** + * Public API provided to a {@link CompartmentMapTransformFn} as the + * {@link CompartmentMapTransformFnArguments.context} object. + * + * @template CompartmentMap The specific type of `CompartmentMapDescriptor` + * being transformed; defaults to `PackageCompartmentMapDescriptor`. + */ +export interface CompartmentMapTransformContext< + CompartmentMap extends + CompartmentMapDescriptor = PackageCompartmentMapDescriptor, +> { + /** + * Returns the package policy from `policy` or + * {@link CompartmentMapTransformOptions.policy} if not provided. + * + * If you only need the `PackagePolicy` set in the `CompartmentDescriptor`, + * use {@link CompartmentDescriptor.policy} instead. + * + * @param compartmentDescriptor Compartment descriptor + * @param policy Optional policy + * + * @returns A package policy, if found + */ + readonly getPackagePolicy: ( + compartmentDescriptor: CompartmentDescriptorFromMap, + policy?: SomePolicy, + ) => SomePackagePolicy | undefined; + + /** + * Returns a compartment descriptor by name. + * + * @param name The name of the compartment descriptor; these are the _keys_ of {@link CompartmentDescriptor.compartments}. + * @returns A compartment descriptor, if found + */ + readonly getCompartmentDescriptor: ( + name: CompartmentNameFromMap, + ) => CompartmentDescriptorFromMap | undefined; + + /** + * Returns the canonical name for some {@link CompartmentDescriptor} (or its + * name, if a `string`). + * + * This function only resolves `CompartmentDescriptor` if given a name; it + * returns the value of {@link CompartmentDescriptor.label}. + * + * @param compartmentDescriptorOrName Compartment descriptor or compartment + * name + */ + readonly getCanonicalName: ( + compartmentDescriptorOrName: + | CompartmentNameFromMap + | CompartmentDescriptorFromMap + | undefined, + ) => string | undefined; + + /** + * Returns the compartment name for a given canonical name. + * + * @param canonicalName Canonical name of the compartment. + * @returns Compartment name or `undefined` if not found. + */ + readonly getCompartmentName: ( + canonicalName: string, + ) => CompartmentNameFromMap | undefined; +} + +/** + * Utility to infer the type of a `CompartmentDescriptor` from a `CompartmentMapDescriptor`. + */ +export type CompartmentDescriptorFromMap< + CompartmentMap extends CompartmentMapDescriptor< + any, + any + > = CompartmentMapDescriptor, +> = + CompartmentMap extends CompartmentMapDescriptor ? T : never; + +/** + * Utility to infer the type of the keys of + * {@link CompartmentDescriptor.compartments} from a `CompartmentMapDescriptor`. + */ +export type CompartmentNameFromMap< + CompartmentMap extends CompartmentMapDescriptor< + any, + any + > = CompartmentMapDescriptor, +> = CompartmentMap extends CompartmentMapDescriptor ? K : never; + +/** + * Arguments provided to a {@link CompartmentMapTransformFn}. + */ +export interface CompartmentMapTransformFnArguments< + CompartmentMap extends CompartmentMapDescriptor, +> { + /** + * The {@link CompartmentMapDescriptor} being transformed. May have already been transformed. + * + * This is considered mutable. + */ + readonly compartmentMap: CompartmentMap; + /** + * The {@link CompartmentMapTranfsormContext} API, for convenience. + */ + readonly context: Readonly>; + /** + * Common options for all transforms. + */ + readonly options: Readonly; +} + +/** + * Implementation of a Compartment Map Transform. + * + * It can be sync or async, but must return/fulfill a `CompartmentMapDescriptor`. + */ +export type CompartmentMapTransformFn< + CompartmentMap extends + CompartmentMapDescriptor = PackageCompartmentMapDescriptor, +> = ( + args: Readonly>, +) => Promise | CompartmentMap; +// #endregion + +/** + * The type of a `package.json` file containing relevant fields; used by `graphPackages` and its ilk + */ + +export interface PackageDescriptor { + /** + * TODO: In reality, this is optional, but `graphPackage` does not consider it to be. This will need to be fixed once support for "anonymous" packages lands; see https://github.com/endojs/endo/pull/2664 + */ + name: string; + version?: string; + /** + * TODO: Update with proper type when this field is handled. + */ + exports?: unknown; + type?: 'module' | 'commonjs'; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; + bundleDependencies?: string[]; + peerDependenciesMeta?: Record< + string, + { optional?: boolean; [k: string]: unknown } + >; + module?: string; + browser?: Record | string; + + main?: string; + + [k: string]: unknown; +} diff --git a/packages/compartment-mapper/src/types/internal.ts b/packages/compartment-mapper/src/types/internal.ts index 2accd51b12..406b9a2de0 100644 --- a/packages/compartment-mapper/src/types/internal.ts +++ b/packages/compartment-mapper/src/types/internal.ts @@ -13,15 +13,11 @@ import type { Language, LanguageForExtension, LanguageForModuleSpecifier, - ModuleDescriptor, + PackageCompartmentDescriptor, + PackageCompartmentDescriptors, + CompartmentModuleDescriptorConfiguration, + PackageCompartmentDescriptorName, } from './compartment-map-schema.js'; -import type { - MaybeReadFn, - MaybeReadNowFn, - ReadFn, - ReadPowers, -} from './powers.js'; -import type { DeferredAttenuatorsProvider } from './policy.js'; import type { ArchiveOnlyOption, AsyncParseFn, @@ -30,16 +26,27 @@ import type { ExecuteOptions, ExitModuleImportHookOption, ExitModuleImportNowHookOption, + FileUrlString, + ForceLoadOption, LogOptions, ModuleTransforms, + PackageDescriptor, ParseFn, ParserForLanguage, + PolicyOption, SearchSuffixesOption, SourceMapHook, SourceMapHookOption, Sources, SyncModuleTransforms, } from './external.js'; +import type { DeferredAttenuatorsProvider } from './policy.js'; +import type { + MaybeReadFn, + MaybeReadNowFn, + ReadFn, + ReadPowers, +} from './powers.js'; export type LinkOptions = { resolve?: ResolveHook; @@ -76,7 +83,7 @@ export type MakeImportHookMakersOptions = { /** * For depositing captured compartment descriptors. */ - compartmentDescriptors?: Record; + compartmentDescriptors?: PackageCompartmentDescriptors; } & ComputeSha512Option & SearchSuffixesOption & ArchiveOnlyOption & @@ -88,7 +95,7 @@ export type MakeImportNowHookMakerOptions = MakeImportHookMakersOptions & ExitModuleImportNowHookOption; export type ImportHookMaker = (params: { - packageLocation: string; + packageLocation: PackageCompartmentDescriptorName; packageName: string; attenuators: DeferredAttenuatorsProvider; parse: ParseFn | AsyncParseFn; @@ -97,7 +104,7 @@ export type ImportHookMaker = (params: { }) => ImportHook; export type ImportNowHookMaker = (params: { - packageLocation: string; + packageLocation: PackageCompartmentDescriptorName; packageName: string; parse: ParseFn | AsyncParseFn; compartments: Record; @@ -125,13 +132,13 @@ export type ChooseModuleDescriptorParams = { /** Module specifiers with each search suffix appended */ candidates: string[]; moduleSpecifier: string; - packageLocation: string; + packageLocation: FileUrlString; /** Compartment descriptor from the compartment map */ - compartmentDescriptor: CompartmentDescriptor; + compartmentDescriptor: PackageCompartmentDescriptor; /** All compartment descriptors from the compartment map */ - compartmentDescriptors: Record; + compartmentDescriptors: PackageCompartmentDescriptors; /** All module descriptors in same compartment */ - moduleDescriptors: Record; + moduleDescriptors: Record; /** All compartments */ compartments: Record; packageSources: CompartmentSources; @@ -276,37 +283,6 @@ export type MaybeReadDescriptorFn = ( location: string, ) => Promise; -/** - * The type of a `package.json` file containing relevant fields; used by `graphPackages` and its ilk - */ -export interface PackageDescriptor { - /** - * TODO: In reality, this is optional, but `graphPackage` does not consider it to be. This will need to be fixed once support for "anonymous" packages lands; see https://github.com/endojs/endo/pull/2664 - */ - name: string; - version?: string; - /** - * TODO: Update with proper type when this field is handled. - */ - exports?: unknown; - type?: 'module' | 'commonjs'; - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; - optionalDependencies?: Record; - bundleDependencies?: string[]; - peerDependenciesMeta?: Record< - string, - { optional?: boolean; [k: string]: unknown } - >; - module?: string; - browser?: Record | string; - - main?: string; - - [k: string]: unknown; -} - /** * Function returning a set of module names (scoped to the compartment) whose * parser is not using heuristics to determine imports. @@ -329,3 +305,7 @@ export type DeferErrorFn = ( */ error: Error, ) => StaticModuleType; + +export type MakeLoadCompartmentsOptions = LogOptions & + PolicyOption & + ForceLoadOption; diff --git a/packages/compartment-mapper/src/types/node-modules.ts b/packages/compartment-mapper/src/types/node-modules.ts index 96f8317bc8..59cc3fed9b 100644 --- a/packages/compartment-mapper/src/types/node-modules.ts +++ b/packages/compartment-mapper/src/types/node-modules.ts @@ -1,11 +1,19 @@ +import type { + PackageDescriptor, + FileUrlString, + LogOptions, +} from './external.js'; import type { GenericGraph } from '../generic-graph.js'; import type { + CompartmentMapDescriptor, Language, LanguageForExtension, + PackageCompartmentDescriptorName, + PackageCompartmentMapDescriptor, } from './compartment-map-schema.js'; -import type { LogOptions, FileUrlString } from './external.js'; -import type { PackageDescriptor } from './internal.js'; import type { LiteralUnion } from './typescript.js'; +import type { CanonicalName } from './canonical-name.js'; +import type { ATTENUATORS_COMPARTMENT } from '../policy-format.js'; export type CommonDependencyDescriptors = Record< string, @@ -26,9 +34,7 @@ export type CommonDependencyDescriptorsOptions = { /** * Options for `graphPackage()` */ -export type GraphPackageOptions = { - logicalPath?: string[]; -} & LogOptions & +export type GraphPackageOptions = LogOptions & CommonDependencyDescriptorsOptions; /** @@ -40,7 +46,6 @@ export type GraphPackagesOptions = LogOptions; * Options for `gatherDependency()` */ export type GatherDependencyOptions = { - childLogicalPath?: string[]; /** * If `true` the dependency is optional */ @@ -48,17 +53,14 @@ export type GatherDependencyOptions = { } & LogOptions & CommonDependencyDescriptorsOptions; +/** + * Value in {@link Graph} + */ export interface Node { - /** - * Informative compartment label based on the package name and version (if available) - */ - label: string; /** * Package name */ name: string; - path: Array; - logicalPath: Array; /** * `true` if the package's {@link PackageDescriptor} has an `exports` field */ @@ -77,9 +79,21 @@ export interface Node { * * The values are the keys of other {@link Node Nodes} in the {@link Graph}. */ - dependencyLocations: Record; + dependencyLocations: Record; parsers: LanguageForExtension; types: Record; + packageDescriptor: PackageDescriptor; +} + +/** + * A node in the graph that has been finalized, meaning it has a `label` and is + * ready for conversion into a `CompartmentDescriptor`. + */ +export interface FinalNode extends Node { + /** + * Canonical name of the package; used to identify it in policy + */ + label: string; } /** @@ -87,11 +101,13 @@ export interface Node { * build by exploring the `node_modules` tree dropped by tools like npm and * consumed by tools like Node.js. This gets translated finally into a * compartment map. - * - * Keys may either be a file URL string to a package or the special - * `` string. */ -export type Graph = Record', FileUrlString>, Node>; +export type Graph = Record; + +export type FinalGraph = Record< + PackageCompartmentDescriptorName, + Readonly +>; export interface LanguageOptions { commonjsLanguageForExtension: LanguageForExtension; @@ -114,3 +130,15 @@ export interface PackageDetails { * used by `mapNodeModules()` and its ilk. */ export type LogicalPathGraph = GenericGraph; + +/** + * This mapping is provided to `applyCompartmentMapTransforms()` to enable + * reverse-lookups of `CompartmentDescriptor`s from policy. + */ +export type CanonicalNameMap< + CompartmentMap extends + CompartmentMapDescriptor = PackageCompartmentMapDescriptor, +> = + CompartmentMap extends CompartmentMapDescriptor + ? Map + : never; diff --git a/packages/compartment-mapper/src/types/policy-schema.ts b/packages/compartment-mapper/src/types/policy-schema.ts index a39faa07bc..6189cfe467 100644 --- a/packages/compartment-mapper/src/types/policy-schema.ts +++ b/packages/compartment-mapper/src/types/policy-schema.ts @@ -5,6 +5,8 @@ * @module */ +import type { WILDCARD_POLICY_VALUE } from '../policy-format.js'; + /* eslint-disable no-use-before-define */ /** @@ -38,7 +40,7 @@ export type UnifiedAttenuationDefinition = { /** * A type representing a wildcard policy, which can be 'any'. */ -export type WildcardPolicy = 'any'; +export type WildcardPolicy = typeof WILDCARD_POLICY_VALUE; /** * A type representing a property policy, which is a record of string keys and @@ -51,7 +53,9 @@ export type PropertyPolicy = Record; * wildcard policy}, a property policy, `undefined`, or defined by an * attenuator */ -export type PolicyItem = WildcardPolicy | PropertyPolicy | T; +export type PolicyItem = [T] extends [void] + ? WildcardPolicy | PropertyPolicy + : WildcardPolicy | PropertyPolicy | T; /** * An object representing a nested attenuation definition. @@ -70,22 +74,36 @@ export type PackagePolicy< BuiltinsPolicyItem = void, ExtraOptions = unknown, > = { - /** The default attenuator. */ + /** + * The default attenuator, if any. + */ defaultAttenuator?: string | undefined; - /** The policy item for packages. */ + /** + * The policy item for packages. + */ packages?: PolicyItem | undefined; - /** The policy item or full attenuation definition for globals. */ + /** + * The policy item or full attenuation definition for globals. + */ globals?: AttenuationDefinition | PolicyItem | undefined; - /** The policy item or nested attenuation definition for builtins. */ + /** + * The policy item or nested attenuation definition for builtins. + */ builtins?: | NestedAttenuationDefinition | PolicyItem | undefined; - /** Whether to disable global freeze. */ + /** + * Whether to disable global freeze. + */ noGlobalFreeze?: boolean | undefined; - /** Whether to allow dynamic imports */ + /** + * Whether to allow dynamic imports + */ dynamic?: boolean | undefined; - /** Any additional user-defined options can be added to the policy here */ + /** + * Any additional user-defined options can be added to the policy here + */ options?: ExtraOptions | undefined; }; @@ -125,9 +143,4 @@ export type Policy< export type SomePolicy = Policy; /** Any {@link PackagePolicy} */ -export type SomePackagePolicy = PackagePolicy< - PolicyItem, - PolicyItem, - PolicyItem, - unknown ->; +export type SomePackagePolicy = PackagePolicy; diff --git a/packages/compartment-mapper/test/capture-lite.test.js b/packages/compartment-mapper/test/capture-lite.test.js index 90a8543c0f..e50582132e 100644 --- a/packages/compartment-mapper/test/capture-lite.test.js +++ b/packages/compartment-mapper/test/capture-lite.test.js @@ -1,4 +1,5 @@ import 'ses'; + import fs from 'node:fs'; import url from 'node:url'; import path from 'node:path'; @@ -7,10 +8,15 @@ import { captureFromMap } from '../capture-lite.js'; import { mapNodeModules } from '../src/node-modules.js'; import { makeReadPowers } from '../src/node-powers.js'; import { defaultParserForLanguage } from '../src/import-parsers.js'; +import { ENTRY_COMPARTMENT } from '../src/policy-format.js'; + +/** + * @import {LocalModuleSource} from '../src/types.js' + */ const { keys } = Object; -test('captureFromMap() should resolve with a CaptureResult', async t => { +test('captureFromMap() - should resolve with a CaptureResult', async t => { t.plan(5); const readPowers = makeReadPowers({ fs, url }); @@ -28,13 +34,13 @@ test('captureFromMap() should resolve with a CaptureResult', async t => { t.deepEqual( keys(captureSources).sort(), - ['bundle', 'bundle-dep-v0.0.0'], + [ENTRY_COMPARTMENT, 'bundle-dep'], 'captureSources should contain sources for each compartment map descriptor', ); t.deepEqual( keys(compartmentRenames).sort(), - ['bundle', 'bundle-dep-v0.0.0'], + [ENTRY_COMPARTMENT, 'bundle-dep'], 'compartmentRenames must contain same compartment names as in captureCompartmentMap', ); @@ -47,7 +53,7 @@ test('captureFromMap() should resolve with a CaptureResult', async t => { t.deepEqual( captureCompartmentMap.entry, { - compartment: 'bundle', + compartment: ENTRY_COMPARTMENT, module: './main.js', }, 'The entry compartment should point to the "bundle" compartment map', @@ -55,12 +61,78 @@ test('captureFromMap() should resolve with a CaptureResult', async t => { t.deepEqual( keys(captureCompartmentMap.compartments).sort(), - ['bundle', 'bundle-dep-v0.0.0'], - 'The "bundle" and "bundle-dep-v0.0.0" compartments should be present', + [ENTRY_COMPARTMENT, 'bundle-dep'], + 'The "bundle" and "bundle-dep" compartments should be present', + ); +}); + +test('captureFromMap() - should force-load', async t => { + const readPowers = makeReadPowers({ fs, url }); + const moduleLocation = `${new URL( + 'fixtures-digest/node_modules/app/index.js', + import.meta.url, + )}`; + + const nodeCompartmentMap = await mapNodeModules(readPowers, moduleLocation); + + const fjordCompartment = Object.values(nodeCompartmentMap.compartments).find( + c => c.name === 'fjord', + ); + if (!fjordCompartment) { + t.fail('Expected "fjord" compartment to be present in nodeCompartmentMap'); + return; + } + + const { captureCompartmentMap } = await captureFromMap( + readPowers, + nodeCompartmentMap, + { + forceLoad: [fjordCompartment.location], + parserForLanguage: defaultParserForLanguage, + }, + ); + + t.true( + 'fjord' in captureCompartmentMap.compartments, + '"fjord" should be retained in captureCompartmentMap', ); }); -test('captureFromMap() should round-trip sources based on parsers', async t => { +test('captureFromMap() - should discard unretained CompartmentDescriptors', async t => { + const readPowers = makeReadPowers({ fs, url }); + const moduleLocation = `${new URL( + 'fixtures-digest/node_modules/app/index.js', + import.meta.url, + )}`; + + const nodeCompartmentMap = await mapNodeModules(readPowers, moduleLocation); + + const nodeComartmentMapSize = keys(nodeCompartmentMap.compartments).length; + + const { captureCompartmentMap } = await captureFromMap( + readPowers, + nodeCompartmentMap, + { + parserForLanguage: defaultParserForLanguage, + }, + ); + + const captureCompartmentMapSize = keys( + captureCompartmentMap.compartments, + ).length; + + t.true( + captureCompartmentMapSize < nodeComartmentMapSize, + 'captureCompartmentMap should contain fewer CompartmentDescriptors than nodeCompartmentMap', + ); + + t.false( + 'fjord' in captureCompartmentMap.compartments, + '"fjord" should not be retained in captureCompartmentMap', + ); +}); + +test('captureFromMap() - should round-trip sources based on parsers', async t => { const readPowers = makeReadPowers({ fs, url }); const moduleLocation = `${new URL( 'fixtures-0/node_modules/bundle/main.js', @@ -79,10 +151,16 @@ test('captureFromMap() should round-trip sources based on parsers', async t => { ); const decoder = new TextDecoder(); + const bundleSource = /** @type {LocalModuleSource} */ ( + captureSources[ENTRY_COMPARTMENT]['./icando.cjs'] + ); // the actual source depends on the value of `parserForLanguage` above - const actual = decoder.decode(captureSources.bundle['./icando.cjs'].bytes); + const actual = decoder.decode(bundleSource.bytes); const expected = await fs.promises.readFile( - path.join(url.fileURLToPath(compartmentRenames.bundle), 'icando.cjs'), + path.join( + url.fileURLToPath(compartmentRenames[ENTRY_COMPARTMENT]), + 'icando.cjs', + ), 'utf-8', ); t.is(actual, expected, 'Source code should not be pre-compiled'); diff --git a/packages/compartment-mapper/test/compartment-map-transforms.test.js b/packages/compartment-mapper/test/compartment-map-transforms.test.js new file mode 100644 index 0000000000..63eab6aae0 --- /dev/null +++ b/packages/compartment-mapper/test/compartment-map-transforms.test.js @@ -0,0 +1,1023 @@ +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable no-shadow */ +import 'ses'; + +import fs from 'node:fs'; +import url from 'node:url'; +import test from 'ava'; +import Module from 'node:module'; +import { applyCompartmentMapTransforms } from '../src/compartment-map-transforms/index.js'; +import { + createReferencesByPolicyTransform, + enforcePolicyTransform, +} from '../src/compartment-map-transforms/transforms.js'; +import { mapNodeModules } from '../src/node-modules.js'; +import { + ATTENUATORS_COMPARTMENT, + ENTRY_COMPARTMENT, + WILDCARD_POLICY_VALUE, +} from '../src/policy-format.js'; +import { captureFromMap } from '../capture-lite.js'; +import { defaultParserForLanguage } from '../archive-parsers.js'; +import { makeReadPowers } from '../src/node-powers.js'; +import { + stripCaptureResult, + stripCompartmentMap, +} from './snapshot-utilities.js'; + +/** + * @import { + * CompartmentDescriptor, + * ModuleDescriptorConfiguration, + * CompartmentMapDescriptor, + * CompartmentMapTransformFn, + * SomePolicy, + * SomePackagePolicy, + * LogFn, + * ExitModuleImportHook, + * PackageCompartmentDescriptor, + * PackageCompartmentMapDescriptor, + * PackageCompartmentDescriptors, + * FileUrlString, + * } from '../src/types.js'; + * @import {CanonicalNameMap} from '../src/types/node-modules.js'; + * @import {ThirdPartyStaticModuleInterface} from 'ses'; + */ + +const { freeze, keys, assign, entries, fromEntries, isFrozen } = Object; + +/** + * Applies defaults to a dummy {@link CompartmentDescriptor} for testing. + * + * @param {Partial} [compartmentDescriptor] + * @returns {PackageCompartmentDescriptor} + */ +const makeCompartmentDescriptor = (compartmentDescriptor = {}) => { + const { + name = '', + label, + scopes = {}, + modules = {}, + policy, + types = {}, + parsers = {}, + ...rest + } = compartmentDescriptor; + // keep reference! + return assign(compartmentDescriptor, { + name, + label: label ?? name, + modules, + policy, + scopes, + location: `file://${name}`, + types, + parsers, + packageDescriptor: { name }, + sourceDirname: name, + ...rest, + }); +}; + +/** + * Creates a dummy {@link CompartmentMapDescriptor} for testing. + * + * @param {string} [entry] + * @param {object} [options] + * @param {Record} [options.compartments] + * @returns {PackageCompartmentMapDescriptor} + */ +const makeCompartmentMap = ( + entry = 'file://root', + { compartments = { 'file://root': makeCompartmentDescriptor() } } = {}, +) => { + entry = entry.startsWith('file://') ? entry : `file://${entry}`; + const map = { + entry: { + compartment: /** @type {FileUrlString} */ (entry), + module: 'main', + }, + compartments: /** @type {PackageCompartmentDescriptors} */ ( + fromEntries( + entries(compartments).map(([name, descriptor]) => { + if (name !== ATTENUATORS_COMPARTMENT && !name.startsWith('file://')) { + name = `file://${name}`; + } + return [name, makeCompartmentDescriptor(descriptor)]; + }), + ) + ), + tags: ['test'], + }; + assert(entry in map.compartments, `Entry compartment "${entry}" not found`); + return map; +}; + +/** + * No-op log function for compartment map transform tests + * @type {LogFn} + */ +const log = () => {}; + +test('enforcePolicyTransform - removes modules per policy', async t => { + t.plan(2); + + /** @type {SomePolicy} */ + const policy = { + resources: { + other: { packages: { some: false } }, + some: { packages: { other: true } }, + }, + }; + + /** @type {SomePolicy} */ + const policyOverride = { + resources: { + other: { packages: { some: true } }, + some: { packages: { other: true } }, + }, + }; + + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + other: { + module: 'other', + compartment: 'file://other', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { + other: { + module: './index.js', + compartment: 'file://other', + }, + some: { + module: 'some', + compartment: 'file://some', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [enforcePolicyTransform], + { log, policyOverride, policy }, + ); + t.false( + 'some' in otherDescriptor.modules, + 'ModuleDescriptor for "some" should have been removed', + ); + t.true( + 'other' in someDescriptor.modules, + 'ModuleDescriptor for "other" should have been retained', + ); +}); +test('enforcePolicyTransform - fallback to policy from CompartmentDescriptor', async t => { + t.plan(2); + + /** @type {SomePolicy} */ + const policy = { + resources: { + other: { packages: { some: false } }, + some: { packages: { other: true } }, + }, + }; + + /** @type {SomePolicy} */ + const policyOverride = { + resources: { + other: { packages: { some: true } }, + some: { packages: { other: true } }, + }, + }; + + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: { + packages: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, + globals: WILDCARD_POLICY_VALUE, + noGlobalFreeze: true, + }, + }); + + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + other: { + module: 'other', + compartment: 'file://other', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { + other: { + module: './index.js', + compartment: 'file://other', + }, + some: { + module: 'some', + compartment: 'file://some', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [enforcePolicyTransform], + { log, policyOverride, policy }, + ); + t.false( + 'some' in otherDescriptor.modules, + 'ModuleDescriptor for "some" should have been removed', + ); + t.true( + 'other' in someDescriptor.modules, + 'ModuleDescriptor for "other" should have been retained', + ); +}); + +test('enforcePolicyTransform - ignores policyOverride', async t => { + t.plan(2); + /** @type {SomePolicy} */ + const policy = { + resources: { + other: { packages: { some: false } }, + some: { packages: { other: true } }, + }, + }; + /** @type {SomePolicy} */ + const policyOverride = { + resources: { + other: { packages: { some: true } }, + some: { packages: { other: true } }, + }, + }; + + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + other: { + module: 'other', + compartment: 'file://other', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { + other: { + module: './index.js', + compartment: 'file://other', + }, + some: { + module: 'some', + compartment: 'file://some', + }, + }, + scopes: { + some: { compartment: 'file://some' }, + other: { compartment: 'file://other' }, + }, + policy: {}, + }); + + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [enforcePolicyTransform], + { log, policyOverride, policy }, + ); + t.truthy(otherDescriptor.scopes.some, 'scope should not have been removed'); + t.false( + 'file://some' in otherDescriptor.modules, + 'ModuleDescriptor for "some" should not have been added', + ); +}); + +test('createReferencesByPolicyTransform - adds references per policyOverride', async t => { + t.plan(2); + /** @type {SomePolicy} */ + const policy = { resources: {} }; + /** @type {SomePolicy} */ + const policyOverride = { resources: { some: { packages: { other: true } } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: {}, + policy: policy.resources.some, + }); + /** @type {ModuleDescriptorConfiguration} */ + const otherModule = { + module: 'other', + compartment: 'file://other', + }; + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { other: otherModule }, + policy: policy.resources.other, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [createReferencesByPolicyTransform], + { log, policyOverride }, + ); + t.truthy(someDescriptor.scopes.other, 'scope should have been added'); + t.truthy( + someDescriptor.modules.other, + 'ModuleDescriptor should have been added', + ); +}); + +test('createReferencesByPolicyTransform - adds references per policy', async t => { + t.plan(2); + /** @type {SomePolicy} */ + const policy = { resources: { some: { packages: { other: true } } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: {}, + policy: policy.resources.some, + }); + /** @type {ModuleDescriptorConfiguration} */ + const otherModule = { + module: 'other', + compartment: 'file://other', + }; + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { other: otherModule }, + policy: policy.resources.other, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [createReferencesByPolicyTransform], + { log, policy }, + ); + t.truthy(someDescriptor.scopes.other, 'scope should have been added'); + t.truthy( + someDescriptor.modules.other, + 'ModuleDescriptor should have been added', + ); +}); + +test('createReferencesByPolicyTransform - missing compartment descriptor for compartment name', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { other: { packages: { nuffin: true } } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + }, + policy: {}, + }); + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: {}, + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + // @ts-expect-error bad type + compartmentMap.compartments.nuffin = undefined; + + await t.throwsAsync( + applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [createReferencesByPolicyTransform], + { log, policy }, + ), + { + message: /No CompartmentDescriptor for name "nuffin"/, + }, + ); +}); + +test('createReferencesByPolicyTransform - warn on missing compartment descriptor for canonical name', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { other: { packages: { nuffin: true } } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + }, + policy: {}, + }); + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: {}, + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + /** @type {any[]} */ + const calls = []; + /** @type {LogFn} */ + const logger = (...args) => { + calls.push(args); + }; + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [createReferencesByPolicyTransform], + { log: logger, policy }, + ); + t.deepEqual(calls, [ + [ + `Warning: no compartment name found for "nuffin"; package policy may be malformed`, + ], + ]); +}); + +test('createReferencesByPolicyTransform - prefer policy over policyOverride', async t => { + t.plan(2); + /** @type {SomePolicy} */ + const policy = { resources: { other: { packages: { some: true } } } }; + /** @type {SomePolicy} */ + const policyOverride = { resources: { some: { packages: { other: true } } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + modules: { + some: { + module: './index.js', + compartment: 'file://some', + }, + }, + policy: {}, + }); + const otherDescriptor = makeCompartmentDescriptor({ + name: 'other', + label: 'other', + modules: { + other: { + module: './index.js', + compartment: 'file://other', + }, + }, + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { + some: someDescriptor, + other: otherDescriptor, + root: rootDescriptor, + }, + }); + await applyCompartmentMapTransforms( + compartmentMap, + new Map([ + ['some', 'file://some'], + ['other', 'file://other'], + ]), + [createReferencesByPolicyTransform], + { log, policyOverride, policy }, + ); + t.truthy(otherDescriptor.scopes.some, 'scope should have been added'); + t.like( + otherDescriptor.modules.some, + { + module: './index.js', + compartment: 'file://some', + createdBy: 'transform', + }, + 'ModuleDescriptor for "some" should have been added', + ); +}); + +test('CompartmentMapTransformContext - API is frozen', async t => { + t.plan(5); + /** @type {CompartmentMapTransformFn[]} */ + const transforms = [ + ({ compartmentMap, context }) => { + t.true(isFrozen(context), 'context should be frozen'); + for (const [propName, member] of entries(context)) { + t.true(isFrozen(member), `context.${propName} should be frozen`); + } + return compartmentMap; + }, + ]; + await applyCompartmentMapTransforms( + makeCompartmentMap(), + new Map(), + transforms, + { log }, + ); +}); + +test('CompartmentMapTransformContext - getCompartmentDescriptor', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { canonical: { packages: {} } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor }, + }); + /** + * @type {CompartmentDescriptor | undefined} + */ + let actual; + /** @type {CompartmentMapTransformFn} */ + const wrapperTransform = ({ context }) => { + actual = context.getCompartmentDescriptor(compartmentMap.entry.compartment); + return compartmentMap; + }; + await applyCompartmentMapTransforms( + compartmentMap, + new Map(), + [wrapperTransform], + { log, policy }, + ); + t.deepEqual(actual, rootDescriptor); +}); + +test('CompartmentMapTransformContext - getCanonicalName', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { canonical: { packages: {} } } }; + + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy, + }); + + const fooBarDescriptor = makeCompartmentDescriptor({ + name: 'fooBar', + label: 'foo>bar', + modules: {}, + policy, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor, fooBar: fooBarDescriptor }, + }); + /** + * @type {string | undefined} + */ + let actual; + /** @type {CompartmentMapTransformFn} */ + const wrapperTransform = ({ context }) => { + actual = context.getCanonicalName(fooBarDescriptor); + return compartmentMap; + }; + await applyCompartmentMapTransforms( + compartmentMap, + new Map(), + [wrapperTransform], + { log, policy }, + ); + t.is(actual, 'foo>bar'); +}); + +test('CompartmentMapTransformContext - getCompartmentName', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { canonical: { packages: {} } } }; + + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy, + }); + + const fooBarDescriptor = makeCompartmentDescriptor({ + name: 'fooBar', + label: 'foo>bar', + modules: {}, + policy, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor, 'file://foobar': fooBarDescriptor }, + }); + /** + * @type {string | undefined} + */ + let actual; + /** @type {CompartmentMapTransformFn} */ + const wrapperTransform = ({ context }) => { + const canonicalName = context.getCanonicalName(fooBarDescriptor); + actual = context.getCompartmentName(/** @type {string} */ (canonicalName)); + return compartmentMap; + }; + // Map canonical names to compartment keys + /** @type {CanonicalNameMap} */ + const canonicalNameToCompartmentNameMap = new Map([ + ['foo>bar', 'file://foobar'], + ]); + await applyCompartmentMapTransforms( + compartmentMap, + canonicalNameToCompartmentNameMap, + [wrapperTransform], + { log, policy }, + ); + t.is(actual, 'file://foobar'); +}); + +test('CompartmentMapTransformContext - getPackagePolicy', async t => { + /** @type {SomePolicy} */ + const policy = { + resources: { some: {} }, + entry: { packages: WILDCARD_POLICY_VALUE }, + }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + policy: {}, + }); + const someDescriptor = makeCompartmentDescriptor({ + name: 'some', + label: 'some', + policy: {}, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor, some: someDescriptor }, + }); + /** + * @type {SomePackagePolicy | undefined} + */ + let actual; + /** @type {CompartmentMapTransformFn} */ + const wrapperTransform = ({ context }) => { + actual = context.getPackagePolicy(someDescriptor, policy); + return compartmentMap; + }; + await applyCompartmentMapTransforms( + compartmentMap, + new Map(), + [wrapperTransform], + { log, policy }, + ); + t.is( + actual, + policy.resources.some, + 'getPackagePolicy should return object from policy instead of compartment descriptor', + ); +}); + +test('CompartmentMapTransformContext - transforms CompartmentMapDescriptor', async t => { + /** @type {SomePolicy} */ + const policy = { resources: {} }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy: policy.entry, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor }, + }); + /** @type {CompartmentMapTransformFn} */ + const addTagTransform = ({ compartmentMap: inputMap }) => { + // Clone and modify tags + return { + ...inputMap, + tags: [...(inputMap.tags || []), 'dummy'], + }; + }; + const result = await applyCompartmentMapTransforms( + compartmentMap, + new Map(), + [addTagTransform], + { log, policy }, + ); + t.like( + result, + { tags: ['test', 'dummy'] }, + 'result should include the new tag', + ); +}); + +test('applyCompartmentMapTransforms - throws if optionsForTransforms is undefined', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { canonical: { packages: {} } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor }, + }); + await t.throwsAsync( + applyCompartmentMapTransforms( + compartmentMap, + new Map(), + [], + /** @type {any} */ (undefined), + ), + { + message: /optionsForTransforms expected/, + }, + ); +}); + +test('applyCompartmentMapTransforms - wraps thrown error from transform', async t => { + /** @type {SomePolicy} */ + const policy = { resources: { canonical: { packages: {} } } }; + const rootDescriptor = makeCompartmentDescriptor({ + name: ENTRY_COMPARTMENT, + label: ENTRY_COMPARTMENT, + modules: {}, + policy, + }); + const compartmentMap = makeCompartmentMap('root', { + compartments: { root: rootDescriptor }, + }); + const badTransform = () => { + throw new Error('bad transform'); + }; + await t.throwsAsync( + applyCompartmentMapTransforms(compartmentMap, new Map(), [badTransform], { + log, + policy, + }), + { + message: + /Compartment Map Transform .+ errored during execution: bad transform/, + }, + ); +}); + +test('Compartment Map Transforms - integration with mapNodeModules()', async t => { + const readPowers = makeReadPowers({ fs, url }); + const entrypoint = new URL( + 'fixtures-dynamic-ancestor/node_modules/webpackish-app/build.js', + import.meta.url, + ).href; + + const compartmentMap = await mapNodeModules(readPowers, entrypoint, { + policy: { + entry: { + packages: WILDCARD_POLICY_VALUE, + globals: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, + }, + resources: { + pantspack: { + builtins: { + 'node:console': true, + 'node:path': true, + 'node:util': true, + }, + packages: { + 'pantspack>pantspack-folder-runner': true, + [ENTRY_COMPARTMENT]: true, + }, + }, + 'pantspack>pantspack-folder-runner': { + packages: { + 'jorts-folder': true, + }, + }, + }, + }, + }); + + t.snapshot(stripCompartmentMap(compartmentMap)); +}); + +test('Compartment Map Transforms - integration with captureFromMap()', async t => { + const readPowers = makeReadPowers({ fs, url }); + const entrypoint = new URL( + 'fixtures-dynamic-ancestor/node_modules/webpackish-app/build.js', + import.meta.url, + ).href; + /** + * @type {ExitModuleImportHook} + */ + const importHook = async (specifier, packageLocation) => { + const require = Module.createRequire(url.fileURLToPath(packageLocation)); + const ns = require(specifier); + return freeze( + /** @type {ThirdPartyStaticModuleInterface} */ ({ + imports: [], + exports: keys(ns), + execute: moduleExports => { + moduleExports.default = ns; + assign(moduleExports, ns); + }, + }), + ); + }; + + /** @type {SomePolicy} */ + const policy = { + entry: { + packages: WILDCARD_POLICY_VALUE, + globals: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, + }, + resources: { + pantspack: { + builtins: { + 'node:console': true, + 'node:path': true, + 'node:util': true, + }, + packages: { + 'pantspack>pantspack-folder-runner': true, + [ENTRY_COMPARTMENT]: true, + }, + }, + 'pantspack>pantspack-folder-runner': { + packages: { + 'jorts-folder': true, + }, + }, + }, + }; + const nodeCompartmentMap = await mapNodeModules(readPowers, entrypoint, { + dev: true, + policy, + }); + const result = await captureFromMap(readPowers, nodeCompartmentMap, { + policy, + parserForLanguage: defaultParserForLanguage, + importHook, + }); + t.snapshot(stripCaptureResult(result)); +}); diff --git a/packages/compartment-mapper/test/dynamic-require.test.js b/packages/compartment-mapper/test/dynamic-require.test.js index fa35df70c1..321acbfe9b 100644 --- a/packages/compartment-mapper/test/dynamic-require.test.js +++ b/packages/compartment-mapper/test/dynamic-require.test.js @@ -19,7 +19,10 @@ import path from 'node:path'; import url from 'node:url'; import { importLocation } from '../src/import.js'; import { makeReadNowPowers } from '../src/node-powers.js'; -import { WILDCARD_POLICY_VALUE } from '../src/policy-format.js'; +import { + ENTRY_COMPARTMENT, + WILDCARD_POLICY_VALUE, +} from '../src/policy-format.js'; const readPowers = makeReadNowPowers({ fs, url, path }); const { freeze, keys, assign } = Object; @@ -52,7 +55,7 @@ const defaultImportHook = async (specifier, packageLocation) => { return defaultImportNowHook(specifier, packageLocation); }; -test('intra-package dynamic require works without invoking the exitModuleImportNowHook', async t => { +test('dynamic require avoids exitModuleImportNowHook', async t => { t.plan(2); const fixture = new URL( 'fixtures-dynamic/node_modules/app/index.js', @@ -96,6 +99,11 @@ test('intra-package dynamic require works without invoking the exitModuleImportN 'dynamic>is-ok>is-not-ok': true, }, }, + 'sprunt>node-tammy-build': { + packages: { + sprunt: true, + }, + }, }, }; const { namespace } = await importLocation(readPowers, fixture, { @@ -119,7 +127,7 @@ test('intra-package dynamic require works without invoking the exitModuleImportN // figures out what file to require within that directory. there is no // reciprocal dependency on wherever that directory lives (usually it's // somewhere in the dependent package) -test('intra-package dynamic require with inter-package absolute path works without invoking the exitModuleImportNowHook', async t => { +test('dynamic require using absolute path avoids exitModuleImportNowHook', async t => { t.plan(2); const fixture = new URL( 'fixtures-dynamic/node_modules/absolute-app/index.js', @@ -144,7 +152,12 @@ test('intra-package dynamic require with inter-package absolute path works witho }), ); }; - /** @type {Policy} */ + + /** + * This policy allows bidirectional access between `sprunt` and + * `sprunt>node-tammy-build` + * @type {Policy} + */ const policy = { entry: { packages: WILDCARD_POLICY_VALUE, @@ -152,9 +165,14 @@ test('intra-package dynamic require with inter-package absolute path works witho builtins: WILDCARD_POLICY_VALUE, }, resources: { + 'sprunt>node-tammy-build': { + packages: { + sprunt: true, + }, + }, sprunt: { packages: { - 'node-tammy-build': true, + 'sprunt>node-tammy-build': true, }, }, }, @@ -177,7 +195,7 @@ test('intra-package dynamic require with inter-package absolute path works witho t.is(importNowHookCallCount, 0); }); -test('intra-package dynamic require using known-but-restricted absolute path fails', async t => { +test('dynamic require of disallowed package', async t => { const fixture = new URL( 'fixtures-dynamic/node_modules/broken-app/index.js', import.meta.url, @@ -209,7 +227,7 @@ test('intra-package dynamic require using known-but-restricted absolute path fai resources: { badsprunt: { packages: { - 'node-tammy-build': true, + 'badsprunt>node-tammy-build': true, }, }, 'badsprunt>node-tammy-build': { @@ -224,7 +242,7 @@ test('intra-package dynamic require using known-but-restricted absolute path fai importNowHook, }), { - message: /Blocked in importNow hook by relationship/, + message: /Blocked in importNow hook by package policy/, }, ); }); @@ -249,7 +267,7 @@ test('dynamic require fails without maybeReadNow in read powers', async t => { resources: { dynamic: { packages: { - 'is-ok': true, + 'dynamic>is-ok': true, }, }, }, @@ -281,7 +299,7 @@ test('dynamic require fails without isAbsolute & fileURLToPath in read powers', resources: { dynamic: { packages: { - 'is-ok': true, + 'dynamic>is-ok': true, }, }, }, @@ -338,7 +356,7 @@ test('inter-package and exit module dynamic require works', async t => { resources: { hooked: { packages: { - dynamic: true, + 'hooked>dynamic': true, }, }, 'hooked>dynamic': { @@ -412,7 +430,7 @@ test('inter-package and exit module dynamic require policy is enforced', async t resources: { hooked: { packages: { - dynamic: true, + 'hooked>dynamic': true, }, }, 'hooked>dynamic': { @@ -479,7 +497,7 @@ test('inter-package and exit module dynamic require works ("node:"-namespaced)', resources: { hooked: { packages: { - dynamic: true, + 'hooked>dynamic': true, }, }, 'hooked>dynamic': { @@ -612,7 +630,7 @@ test('dynamic require of missing module falls through to importNowHook', async t await t.throwsAsync( importLocation(readPowers, fixture, { - policy: { entry: { builtins: 'any' }, resources: {} }, + policy: { entry: { builtins: WILDCARD_POLICY_VALUE }, resources: {} }, importNowHook: (specifier, _packageLocation) => { throw new Error(`Blocked exit module: ${specifier}`); }, @@ -635,7 +653,7 @@ test('dynamic require of package missing an optional module', async t => { t.like(namespace, { isOk: true, default: { isOk: true } }); }); -test('dynamic require of ancestor relative path within known compartment should succeed', async t => { +test('dynamic require of ancestor relative path', async t => { const fixture = new URL( 'fixtures-dynamic/node_modules/grabby-app/index.js', import.meta.url, @@ -648,6 +666,20 @@ test('dynamic require of ancestor relative path within known compartment should const { namespace } = await importLocation(readPowers, fixture, { importNowHook, + policy: { + entry: { + packages: WILDCARD_POLICY_VALUE, + globals: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, + }, + resources: { + grabby: { + packages: { + [ENTRY_COMPARTMENT]: true, + }, + }, + }, + }, }); t.like(namespace, { value: 'buried treasure' }); }); @@ -689,7 +721,10 @@ test('dynamic require of ancestor', async t => { }, packages: { 'pantspack>pantspack-folder-runner': true, - 'webpackish-app': true, + [ENTRY_COMPARTMENT]: true, + }, + globals: { + 'process.exitCode': true, }, }, 'pantspack>pantspack-folder-runner': { @@ -697,6 +732,11 @@ test('dynamic require of ancestor', async t => { 'jorts-folder': true, }, }, + 'jorts-folder': { + packages: { + [ENTRY_COMPARTMENT]: true, + }, + }, }, }, }); @@ -716,6 +756,7 @@ test('dynamic require of ancestor disallowed by policy fails at require time', a import.meta.url, ).href; + /** @type {Policy} */ const policy = { entry: { packages: WILDCARD_POLICY_VALUE, @@ -731,7 +772,10 @@ test('dynamic require of ancestor disallowed by policy fails at require time', a }, packages: { 'pantspack>pantspack-folder-runner': true, - 'webpackish-app': true, + [ENTRY_COMPARTMENT]: true, + }, + globals: { + 'process.exitCode': true, }, }, 'pantspack>pantspack-folder-runner': { @@ -756,7 +800,7 @@ test('dynamic require of ancestor disallowed by policy fails at require time', a t.regex( err.cause.message, new RegExp( - `Importing "jorts-folder" in resource "jorts-folder" in "pantspack-folder-runner-v1\\.0\\.0" was not allowed by "packages" policy: ${JSON.stringify(policy.resources['pantspack>pantspack-folder-runner'].packages)}`, + `Importing "jorts-folder" in resource "jorts-folder" in "pantspack>pantspack-folder-runner" was not allowed by "packages" policy: ${JSON.stringify(policy.resources['pantspack>pantspack-folder-runner'].packages)}`, ), ); } @@ -769,16 +813,14 @@ test('dynamic require of ancestor disallowed if policy omitted', async t => { import.meta.url, ).href; - try { - // eslint-disable-next-line @jessie.js/safe-await-separator - await importLocation(readPowers, fixture, { + await t.throwsAsync( + importLocation(readPowers, fixture, { dev: true, importNowHook: defaultImportNowHook, importHook: defaultImportHook, - }); - t.fail('importLocation should have rejected'); - } catch (err) { - t.regex(err.message, /Could not require pantsFolder "jorts-folder"/); - t.regex(err.cause.message, /Could not import module/); - } + }), + { + message: /Could not import module.+webpackish-app\/pantspack\.config\.js/, + }, + ); }); diff --git a/packages/compartment-mapper/test/exit-hook.test.js b/packages/compartment-mapper/test/exit-hook.test.js index d8b26f7e4b..d3a505c1d9 100644 --- a/packages/compartment-mapper/test/exit-hook.test.js +++ b/packages/compartment-mapper/test/exit-hook.test.js @@ -3,6 +3,9 @@ import 'ses'; import test from 'ava'; import { scaffold } from './scaffold.js'; +/** + * @import {AssertionLocationFixtureNamespace} from './test.types.js' + */ const fixture = new URL( 'fixtures-policy/node_modules/app/importActualBuiltin.js', import.meta.url, diff --git a/packages/compartment-mapper/test/fixtures-digest/README.md b/packages/compartment-mapper/test/fixtures-digest/README.md new file mode 100644 index 0000000000..a135b3ae9e --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-digest/README.md @@ -0,0 +1 @@ +This fixture illustrates a case where `mapNodeModules()` should return a `CompartmentMapDescriptor` having more compartments than the result of passing it thru `captureFromMap()`. diff --git a/packages/compartment-mapper/test/fixtures-digest/node_modules/app/index.js b/packages/compartment-mapper/test/fixtures-digest/node_modules/app/index.js new file mode 100644 index 0000000000..44bdf8b9ec --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-digest/node_modules/app/index.js @@ -0,0 +1,3 @@ +module.exports = { + fjord: 'ria' +}; \ No newline at end of file diff --git a/packages/compartment-mapper/test/fixtures-digest/node_modules/app/package.json b/packages/compartment-mapper/test/fixtures-digest/node_modules/app/package.json new file mode 100644 index 0000000000..818d582ec7 --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-digest/node_modules/app/package.json @@ -0,0 +1,12 @@ +{ + "name": "app", + "version": "1.0.0", + "main": "./index.js", + "type": "commonjs", + "dependencies": { + "fjord": "^1.0.0" + }, + "scripts": { + "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1" + } +} diff --git a/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/index.js b/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/index.js new file mode 100644 index 0000000000..8e997a252b --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/index.js @@ -0,0 +1,3 @@ +module.exports = { + fjord: 'firth' +}; \ No newline at end of file diff --git a/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/package.json b/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/package.json new file mode 100644 index 0000000000..02757e9975 --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-digest/node_modules/fjord/package.json @@ -0,0 +1,9 @@ +{ + "name": "fjord", + "version": "1.0.0", + "main": "./index.js", + "type": "commonjs", + "scripts": { + "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1" + } +} diff --git a/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/index.js b/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/index.js index ef013d703c..1c64eba142 100644 --- a/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/index.js +++ b/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/index.js @@ -78,7 +78,7 @@ const foldPantsFolders = (pantsHeap, pantsFolders = []) => return fold(source, packageDescriptor); } catch (err) { throw Error( - `Error folding source "${source}" for package "${packageDescriptor.name}"`, + `Error folding source "${source}" for package "${packageDescriptor.name}": ${err.message}`, { cause: err }, ); } diff --git a/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack/pantspack.js b/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack/pantspack.js index 3d7c2a1cd5..751961d13f 100644 --- a/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack/pantspack.js +++ b/packages/compartment-mapper/test/fixtures-dynamic-ancestor/node_modules/pantspack/pantspack.js @@ -60,10 +60,8 @@ const main = () => { try { configResolved = require.resolve(configSpecifier); } catch (err) { - console.error(err); console.error(`Could not resolve config file: ${configSpecifier}`); - process.exitCode = 1; - return; + throw err; } /** @type {{entry: string, folders: string[]}} */ @@ -71,20 +69,14 @@ const main = () => { try { config = require(configResolved); } catch (err) { - console.error(err); console.error(`Error loading config file: ${configResolved}`); - process.exitCode = 1; - return; + throw err; } const { folders } = config; if (!folders.length) { - console.error( - `No folders specified in config file: ${configResolved}. Please specify at least one folder.`, - ); - process.exitCode = 1; - return; + throw new Error(`No folders specified in config file: ${configResolved}. Please specify at least one folder.`); } // find package descriptor for config diff --git a/packages/compartment-mapper/test/integrity.test.js b/packages/compartment-mapper/test/integrity.test.js index a0b6c69226..1e72a878fc 100644 --- a/packages/compartment-mapper/test/integrity.test.js +++ b/packages/compartment-mapper/test/integrity.test.js @@ -3,6 +3,7 @@ import test from 'ava'; import { ZipReader, ZipWriter } from '@endo/zip'; import { makeArchive, makeAndHashArchive, parseArchive } from '../index.js'; import { readPowers } from './scaffold.js'; +import { ENTRY_COMPARTMENT } from '../src/policy-format.js'; const fixture = new URL( 'fixtures-0/node_modules/app/main.js', @@ -51,7 +52,7 @@ test('extracting an archive with a missing file', async t => { const reader = new ZipReader(validBytes); const writer = new ZipWriter(); writer.files = reader.files; - writer.files.delete('app-v1.0.0/main.js'); + writer.files.delete(`${ENTRY_COMPARTMENT}/main.js`); const invalidBytes = writer.snapshot(); await t.throwsAsync( @@ -63,8 +64,7 @@ test('extracting an archive with a missing file', async t => { }, }), { - message: - 'Failed to load module "./main.js" in package "app-v1.0.0" (1 underlying failures: Cannot find file app-v1.0.0/main.js in Zip file missing.zip', + message: `Failed to load module "./main.js" in package "${ENTRY_COMPARTMENT}" (1 underlying failures: Cannot find file ${ENTRY_COMPARTMENT}/main.js in Zip file missing.zip`, }, ); @@ -84,7 +84,7 @@ test('extracting an archive with an inconsistent hash', async t => { writer.files = reader.files; // Add a null byte to one file. - const node = writer.files.get('app-v1.0.0/main.js'); + const node = writer.files.get(`${ENTRY_COMPARTMENT}/main.js`); const content = new Uint8Array(node.content.byteLength + 1); content.set(node.content, 0); node.content = content; @@ -100,7 +100,7 @@ test('extracting an archive with an inconsistent hash', async t => { }, }), { - message: `Failed to load module "./main.js" in package "app-v1.0.0" (1 underlying failures: Module "main.js" of package "app-v1.0.0" in archive "corrupt.zip" failed a SHA-512 integrity check`, + message: `Failed to load module "./main.js" in package "${ENTRY_COMPARTMENT}" (1 underlying failures: Module "main.js" of package "${ENTRY_COMPARTMENT}" in archive "corrupt.zip" failed a SHA-512 integrity check`, }, ); diff --git a/packages/compartment-mapper/test/map-node-modules.test.js b/packages/compartment-mapper/test/map-node-modules.test.js index 4266f0e76b..962debbeb5 100644 --- a/packages/compartment-mapper/test/map-node-modules.test.js +++ b/packages/compartment-mapper/test/map-node-modules.test.js @@ -12,12 +12,12 @@ import { } from './project-fixture.js'; /** - * @import {ProjectFixture, ProjectFixtureGraph} from './test.types.js' + * @import {ProjectFixture} from './test.types.js' */ const { keys, values } = Object; -const CORRECT_SHORTEST_PATH = ['paperino', 'topolino', 'goofy']; +const CORRECT_CANONICAL_NAME = 'paperino>topolino>goofy'; test(`mapNodeModules() should return compartment descriptors containing shortest path`, async t => { const readPowers = makeReadPowers({ fs, url }); @@ -26,25 +26,23 @@ test(`mapNodeModules() should return compartment descriptors containing shortest import.meta.url, ).href; - const targetLabel = 'goofy-v1.0.0'; - const compartmentMap = await mapNodeModules(readPowers, shortestPathFixture); const compartmentDescriptor = values(compartmentMap.compartments).find( - compartment => compartment.label === targetLabel, + compartment => compartment.label === CORRECT_CANONICAL_NAME, ); // not using AVA's assertions here because assertion types are not assert-style type guards (they just return `void`) which prevents the need for type assertions on `compartmentDescriptor` after if (!compartmentDescriptor) { t.fail( - `compartment descriptor for '${targetLabel}' should exist, but it does not`, + `compartment descriptor for '${CORRECT_CANONICAL_NAME}' should exist, but it does not`, ); return; } - t.deepEqual( - compartmentDescriptor.path, - CORRECT_SHORTEST_PATH, - `compartment descriptor should have shortest path: ${CORRECT_SHORTEST_PATH.join('>')}`, + t.is( + compartmentDescriptor.label, + CORRECT_CANONICAL_NAME, + `compartment descriptor should have canonical name: ${CORRECT_CANONICAL_NAME}`, ); }); @@ -107,7 +105,7 @@ test('mapNodeModules() should not consider peerDependenciesMeta without correspo /** @type {string|undefined} */ let expectedCanonicalName; - const targetLabel = 'goofy-v1.0.0'; + const targetLabel = 'paperino>topolino>goofy'; const readPowers = makeProjectFixtureReadPowers(fixture, { randomDelay: true, @@ -130,10 +128,10 @@ test('mapNodeModules() should not consider peerDependenciesMeta without correspo return; } - const { path } = compartmentDescriptor; - if (!path) { + const { label } = compartmentDescriptor; + if (!label) { t.fail( - `path for '${compartmentDescriptor.name}' should exist, but it does not`, + `label for '${compartmentDescriptor.name}' should exist, but it does not`, ); dumpProjectFixture(t, fixture); @@ -142,14 +140,14 @@ test('mapNodeModules() should not consider peerDependenciesMeta without correspo } if (i === 0) { - expectedCanonicalName = path.join('>'); + expectedCanonicalName = label; t.log( `Canonical name of compartment '${targetLabel}': ${expectedCanonicalName}`, ); } try { - t.deepEqual(path.join('>'), expectedCanonicalName); + t.is(label, /** @type {any} */ (expectedCanonicalName)); } catch (err) { dumpProjectFixture(t, fixture); dumpCompartmentMap(t, compartmentMap); diff --git a/packages/compartment-mapper/test/node-modules-basename.test.js b/packages/compartment-mapper/test/node-modules-basename.test.js index a5d19d5893..746da74dfa 100644 --- a/packages/compartment-mapper/test/node-modules-basename.test.js +++ b/packages/compartment-mapper/test/node-modules-basename.test.js @@ -1,3 +1,5 @@ +import 'ses'; + import test from 'ava'; import { basename } from '../src/node-modules.js'; diff --git a/packages/compartment-mapper/test/policy-format.test.js b/packages/compartment-mapper/test/policy-format.test.js index 0b419d17ab..4ea1252d79 100644 --- a/packages/compartment-mapper/test/policy-format.test.js +++ b/packages/compartment-mapper/test/policy-format.test.js @@ -1,7 +1,11 @@ import 'ses'; import test from 'ava'; -import { assertPackagePolicy, assertPolicy } from '../src/policy-format.js'; +import { + assertPackagePolicy, + assertPolicy, + WILDCARD_POLICY_VALUE, +} from '../src/policy-format.js'; const q = JSON.stringify; @@ -9,7 +13,11 @@ const q = JSON.stringify; {}, { packages: {}, globals: {}, builtins: {} }, { packages: {}, globals: {}, builtins: {}, noGlobalFreeze: true }, - { packages: 'any', globals: 'any', builtins: 'any' }, + { + packages: WILDCARD_POLICY_VALUE, + globals: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, + }, { packages: { foo: true, diff --git a/packages/compartment-mapper/test/policy.test.js b/packages/compartment-mapper/test/policy.test.js index 2dba185daf..e0752550f8 100644 --- a/packages/compartment-mapper/test/policy.test.js +++ b/packages/compartment-mapper/test/policy.test.js @@ -3,6 +3,7 @@ import 'ses'; import test from 'ava'; import { moduleify, scaffold, sanitizePaths } from './scaffold.js'; +import { WILDCARD_POLICY_VALUE } from '../src/policy-format.js'; function combineAssertions(...assertionFunctions) { return async (...args) => { @@ -75,9 +76,9 @@ const policy = { }, }; const ANY = { - globals: 'any', - packages: 'any', - builtins: 'any', + globals: WILDCARD_POLICY_VALUE, + packages: WILDCARD_POLICY_VALUE, + builtins: WILDCARD_POLICY_VALUE, }; const anyPolicy = { entry: policy.entry, diff --git a/packages/compartment-mapper/test/project-fixture.js b/packages/compartment-mapper/test/project-fixture.js index 22b37481a0..bb32f609b6 100644 --- a/packages/compartment-mapper/test/project-fixture.js +++ b/packages/compartment-mapper/test/project-fixture.js @@ -1,3 +1,4 @@ +/* eslint-disable no-shadow */ /** * Utilities for working with {@link ProjectFixture} objects * @@ -15,19 +16,29 @@ import nodeUrl from 'node:url'; import { inspect } from 'node:util'; import { makeReadPowers } from '../node-powers.js'; import { GenericGraph } from '../src/generic-graph.js'; +import { + ATTENUATORS_COMPARTMENT, + ENTRY_COMPARTMENT, + WILDCARD_POLICY_VALUE, +} from '../src/policy-format.js'; /** * @import {MakeMaybeReadProjectFixtureOptions, * MakeProjectFixtureReadPowersOptions, * MakeMaybeReadProjectFixtureOptionsWithRandomDelay, * ProjectFixture, - * RestParameters} from './test.types.js' - * @import {CompartmentMapDescriptor, LogFn, MaybeReadFn, PackageDescriptor} from '../src/types.js' + * FixedCustomInspectFunction, + * RestParameters, + * CustomInspectStyles} from './test.types.js' + * @import {CompartmentMapDescriptor, + * LogFn, + * MaybeReadFn, + * PackageDescriptor, + * PackagePolicy} from '../src/types.js' * @import {ExecutionContext} from 'ava' - * @import {CustomInspectFunction} from 'node:util' */ -const { entries, fromEntries, assign, getPrototypeOf } = Object; +const { entries, fromEntries, getPrototypeOf, freeze, keys } = Object; /** * Pretty-prints a {@link ProjectFixture} as an ASCII tree. @@ -88,6 +99,17 @@ const MIN_DELAY = 10; */ const MAX_DELAY = 100; +const customStyles = freeze( + /** @type {CustomInspectStyles} */ ({ + ...inspect.styles, + name: 'white', + undefined: 'dim', + endoKind: 'magenta', + endoCanonical: 'cyanBright', + endoConstant: 'magentaBright', + }), +); + /** * Creates a `maybeRead` function for use with a {@link ProjectFixture} having a * random delay. @@ -188,35 +210,104 @@ export const makeProjectFixtureReadPowers = ( * * Otherwise this function is just the identity * - * @param {unknown} value - * @returns {unknown} + * @template T + * @param {T} value + * @returns {T} */ -const styleObject = value => { - if (value && getPrototypeOf(value) === null) { - return fromEntries(entries(value).map(([k, v]) => [k, styleObject(v)])); +const unnullify = value => { + if (value === undefined) { + return value; + } + if (value !== null && getPrototypeOf(value) === null) { + return entries(value).reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: unnullify(v), + }), + /** @type {T} */ ({}), + ); } return value; }; /** - * Prepends a `CompartmentDescriptor.path` with the computed canonical name (or "[Root]" if entry compartment). + * Prepares a {@link PackagePolicy} for inspection by {@link dumpCompartmentMap}. * - * Returns a shallow copy of `path` with a {@link CustomInspectFunction}. - * - * @param {string[]} path + * @template {PackagePolicy|undefined} T + * @param {T} packagePolicy + * @returns {T} */ -const stylePath = path => { - const fancyPath = [...path]; - assign(fancyPath, { - /** @type {CustomInspectFunction} */ - [inspect.custom]: (_, { stylize }) => - path.length - ? `[${stylize('Canonical', 'date')}: ${stylize(path.join('>'), 'special')}] ${inspect(path)}` - : `[${stylize('Root', 'date')}]`, - }); - return fancyPath; +const stylePackagePolicy = packagePolicy => { + if (packagePolicy === undefined) { + return packagePolicy; + } + const policy = unnullify(packagePolicy); + return { + ...policy, + /** @type {FixedCustomInspectFunction} */ + [inspect.custom]: (_, options, inspect) => { + for (const [key, value] of entries(policy)) { + if (value === WILDCARD_POLICY_VALUE) { + // styles value as a constant. this could also be done by mutating styles, + // but since the value is just a string, we don't need to. we _do_ however + // need to turn it into an object so that it can have a custom inspect function. + policy[key] = { + /** @type {FixedCustomInspectFunction} */ + [inspect.custom]: (_, options) => + options.stylize(value, 'endoConstant'), + }; + } else if ( + key === 'packages' && + typeof value === 'object' && + keys(/** @type {any} */ (value)).length + ) { + // styles non-empty `packages` prop; all object keys are temporarily + // styled as canonical names. I was unable to find a nicer way to do + // this (e.g. with `stylize`), since the `inspect()` call will want to + // apply its own colors. + policy[key] = /** @type {any} */ ({ + ...value, + /** @type {FixedCustomInspectFunction} */ + [inspect.custom]: (_, options, inspect) => { + const { styles } = inspect; + try { + inspect.styles = /** @type {any} */ ({ + ...customStyles, + string: 'cyanBright', + name: 'cyanBright', + }); + return inspect(value, options); + } finally { + inspect.styles = styles; + } + }, + }); + } + } + return `${options.stylize('PackagePolicy', 'endoKind')}(${inspect(policy, { ...options })})`; + }, + }; }; +/** + * + * @param {string} label + */ +const styleLabel = label => ({ + /** @type {FixedCustomInspectFunction} */ + [inspect.custom]: (_, options) => { + const { stylize } = options; + const kind = stylize('Canonical', 'endoKind'); + if (label === ATTENUATORS_COMPARTMENT) { + return `${kind}(${stylize('Attenuators', 'endoConstant')})`; + } + if (label === ENTRY_COMPARTMENT) { + return `${kind}(${stylize('Entry', 'endoConstant')})`; + } + return `${kind}(${stylize(label, 'endoCanonical')})`; + }, +}); + /** * Dump a {@link CompartmentMapDescriptor}, omitting some fields. * @@ -233,43 +324,55 @@ const stylePath = path => { * @returns {void} */ export const dumpCompartmentMap = (logger, compartmentMap) => { - const compartmentMapForInspect = { - ...compartmentMap, - /** @type {CustomInspectFunction} */ - [inspect.custom]: () => ({ - compartments: fromEntries( - entries(compartmentMap.compartments).map( - ([ - compartmentName, - { - label, - name, - scopes, - location, - sourceDirname, - modules, - path = [], - }, - ]) => [ - compartmentName, - { - label, - name, - scopes: styleObject(scopes), - location: styleObject(location), - sourceDirname, - modules: styleObject(modules), - path: stylePath(path), - }, - ], - ), - ), + const originalStyles = inspect.styles; - entry: styleObject(compartmentMap.entry), - }), - }; + inspect.styles = customStyles; + + let inspected; + try { + const compartmentMapForInspect = { + ...compartmentMap, + /** @type {FixedCustomInspectFunction} */ + [inspect.custom]: () => { + return { + compartments: fromEntries( + entries(compartmentMap.compartments).map( + ([ + compartmentName, + { + label, + name, + scopes, + location, + sourceDirname, + modules, + policy, + ...rest + }, + ]) => [ + compartmentName, + { + label: styleLabel(label), + name, + location, // TODO: style if file URL + policy: stylePackagePolicy(policy), + sourceDirname, + ...unnullify(rest), + }, + ], + ), + ), + + entry: unnullify(compartmentMap.entry), + }; + }, + }; + inspected = inspect(compartmentMapForInspect, { depth: 4, colors: true }); + } finally { + inspect.styles = originalStyles; + } logger.log( - `Compartment map for entry compartment at "${compartmentMap.entry.compartment}":\n${inspect(compartmentMapForInspect, false, 4, true)}`, + `Compartment map for entry compartment at "${compartmentMap.entry.compartment}":\n${inspected}`, ); }; diff --git a/packages/compartment-mapper/test/retained.test.js b/packages/compartment-mapper/test/retained.test.js index 1c75e898e0..7a8f470a99 100644 --- a/packages/compartment-mapper/test/retained.test.js +++ b/packages/compartment-mapper/test/retained.test.js @@ -3,6 +3,7 @@ import test from 'ava'; import { ZipReader } from '@endo/zip'; import { makeArchive, parseArchive } from '../index.js'; import { readPowers } from './scaffold.js'; +import { ENTRY_COMPARTMENT } from '../src/policy-format.js'; const fixture = new URL( 'fixtures-retained/node_modules/app/app.js', @@ -25,14 +26,14 @@ test('archives only contain compartments retained by modules', async t => { const compartmentMapText = new TextDecoder().decode(compartmentMapBytes); const compartmentMap = JSON.parse(compartmentMapText); t.deepEqual(Object.keys(compartmentMap.compartments), [ - 'app-v1.0.0', - 'mk1-v1.0.0', - 'mk2-v1.0.0', + ENTRY_COMPARTMENT, + 'mk1', + 'mk1>mk2', // Notably absent: // 'sweep-v1.0.0', ]); t.deepEqual( - Object.keys(compartmentMap.compartments['app-v1.0.0'].modules).sort(), + Object.keys(compartmentMap.compartments[ENTRY_COMPARTMENT].modules).sort(), [ './app.js', // Notably absent: 'app', diff --git a/packages/compartment-mapper/test/scaffold.js b/packages/compartment-mapper/test/scaffold.js index 1532908197..0489578259 100644 --- a/packages/compartment-mapper/test/scaffold.js +++ b/packages/compartment-mapper/test/scaffold.js @@ -4,6 +4,7 @@ import fs from 'fs'; import crypto from 'crypto'; import url from 'url'; import { ZipReader, ZipWriter } from '@endo/zip'; +import { getEnvironmentOption } from '@endo/env-options'; import { loadLocation, importLocation, @@ -21,10 +22,13 @@ import { defaultParserForLanguage } from '../src/import-parsers.js'; import { defaultParserForLanguage as defaultArchiveParserForLanguage } from '../src/archive-parsers.js'; import { makeReadPowers } from '../src/node-powers.js'; +const shouldEnableLogging = + getEnvironmentOption('SCAFFOLD_LOGGING', '0') === '1'; + /** - * @import {TestFn, FailingFn} from 'ava'; + * @import {TestFn, FailingFn, ExecutionContext} from 'ava'; * @import {ScaffoldOptions, FixtureAssertionFn, TestCategoryHint, WrappedTestFn} from './test.types.js'; - * @import {HashPowers} from '../src/types.js'; + * @import {HashPowers, LogFn} from '../src/types.js'; */ export const readPowers = makeReadPowers({ fs, crypto, url }); @@ -36,6 +40,17 @@ export const sanitizePaths = (text = '', tolerateLineChange = false) => { return text.replace(/file:\/\/[^'"\n]+\/packages\//g, 'file://.../'); }; +/** + * @param {ExecutionContext} [t] + * @returns {LogFn} + */ +const getLogger = t => { + if (t && shouldEnableLogging) { + return t.log.bind(t); + } + return () => {}; +}; + /** * @returns {{getCompartments: () => Array, Compartment: typeof Compartment}} */ @@ -77,10 +92,13 @@ const builtinLocation = new URL( // all subsequent tests to satisfy the "builtin" module dependency of the // application package. -export async function setup() { +/** + * @param {LogFn} log + */ +export async function setup(log) { await null; if (modules === undefined) { - const utility = await loadLocation(readPowers, builtinLocation); + const utility = await loadLocation(readPowers, builtinLocation, { log }); const { namespace } = await utility.import({ globals }); // We pass the builtin module into the module map. modules = { builtin: namespace }; @@ -122,6 +140,7 @@ export function scaffold( workspaceLanguageForExtension = undefined, workspaceCommonjsLanguageForExtension = undefined, workspaceModuleLanguageForExtension = undefined, + log, additionalOptions = {}, } = {}, ) { @@ -170,7 +189,8 @@ export function scaffold( wrap(test, 'Location')(`${name} / loadLocation`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const application = await loadLocation(readPowers, fixture, { policy, @@ -184,6 +204,7 @@ export function scaffold( workspaceLanguageForExtension, workspaceCommonjsLanguageForExtension, workspaceModuleLanguageForExtension, + log, strict, ...additionalOptions, }); @@ -200,7 +221,8 @@ export function scaffold( `${name} / mapNodeModules / importFromMap`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const languages = Object.keys({ ...defaultParserForLanguage, @@ -219,6 +241,7 @@ export function scaffold( workspaceLanguageForExtension, workspaceCommonjsLanguageForExtension, workspaceModuleLanguageForExtension, + log, ...additionalOptions, }); @@ -243,7 +266,8 @@ export function scaffold( `${name} / mapNodeModules / loadFromMap / import`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const languages = Object.keys({ ...defaultParserForLanguage, @@ -289,7 +313,8 @@ export function scaffold( wrap(test, 'Location')(`${name} / importLocation`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const { namespace } = await importLocation(readPowers, fixture, { globals: { ...globals, ...addGlobals }, @@ -316,7 +341,8 @@ export function scaffold( `${name} / makeArchive / parseArchive`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const archive = await makeArchive(readPowers, fixture, { modules, @@ -361,7 +387,8 @@ export function scaffold( `${name} / makeArchive / parseArchive with a prefix`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); // Zip files support an arbitrary length prefix. const archive = await makeArchive(readPowers, fixture, { @@ -405,7 +432,8 @@ export function scaffold( t.plan( fixtureAssertionCount + (shouldFailBeforeArchiveOperations ? 0 : 2), ); - await setup(); + log = log ?? getLogger(t); + await setup(log); // Single file slot. let archive; @@ -456,7 +484,8 @@ export function scaffold( t.plan( fixtureAssertionCount + (shouldFailBeforeArchiveOperations ? 0 : 2), ); - await setup(); + log = log ?? getLogger(t); + await setup(log); // Single file slot. let archive; @@ -472,7 +501,7 @@ export function scaffold( const sourceMaps = new Set(); const sourceMapHook = (sourceMap, { sha512 }) => { sourceMaps.add(sha512); - t.log(sha512, sourceMap); + // t.log(sha512, sourceMap); }; const computeSourceMapLocation = ({ sha512 }) => { @@ -526,7 +555,8 @@ export function scaffold( `${name} / mapNodeModules / makeArchiveFromMap / importArchive`, async (t, Compartment) => { t.plan(fixtureAssertionCount); - await setup(); + log = log ?? getLogger(t); + await setup(log); const languages = Object.keys({ ...defaultArchiveParserForLanguage, @@ -585,13 +615,16 @@ export function scaffold( // @ts-expect-error XXX TS2345 test(`${name} / makeArchive / parseArchive / hashArchive consistency`, async (t, Compartment) => { t.plan(1); - await setup(); + log = log ?? getLogger(t); + await setup(log); const expectedSha512 = await hashLocation( /** @type {HashPowers} */ (readPowers), fixture, { + log, modules, + policy, conditions: new Set(['development', ...(conditions || [])]), strict, searchSuffixes, @@ -611,6 +644,7 @@ export function scaffold( modules, conditions: new Set(['development', ...(conditions || [])]), strict, + policy, searchSuffixes, commonDependencies, parserForLanguage, @@ -642,12 +676,14 @@ export function scaffold( // @ts-expect-error XXX TS2345 test(`${name} / makeArchive / parseArchive, but with sha512 corruption of a compartment map`, async (t, Compartment) => { t.plan(1); - await setup(); + log = log ?? getLogger(t); + await setup(log); const expectedSha512 = await hashLocation( /** @type {HashPowers} */ (readPowers), fixture, { + policy, modules, conditions: new Set(['development', ...(conditions || [])]), strict, @@ -660,11 +696,13 @@ export function scaffold( workspaceLanguageForExtension, workspaceCommonjsLanguageForExtension, workspaceModuleLanguageForExtension, + log, ...additionalOptions, }, ); const archive = await makeArchive(readPowers, fixture, { + policy, modules, conditions: new Set(['development', ...(conditions || [])]), strict, diff --git a/packages/compartment-mapper/test/snapshot-utilities.js b/packages/compartment-mapper/test/snapshot-utilities.js new file mode 100644 index 0000000000..024cc56c82 --- /dev/null +++ b/packages/compartment-mapper/test/snapshot-utilities.js @@ -0,0 +1,157 @@ +/** + * Test utilities for working with snapshots of `CompartmentMapDescriptor`s and other dat structures. + * + * @module + */ + +import { + isCompartmentModuleDescriptorConfiguration, + isLocalModuleSource, +} from '../src/guards.js'; + +/** + * @import { + * Sources, + * CompartmentSources, + * CaptureResult, + * PackageCompartmentMapDescriptor, + * } from '../src/types.js'; + */ + +const { entries, fromEntries, create } = Object; + +/** + * Strip absolute file URLs to relative test paths. The paths will be + * relative to the `compartment-mapper` workspace dir. + * @param {string} url + */ +export const stripPath = url => { + if (typeof url !== 'string' || !url.startsWith('file://')) { + return url; + } + const match = url.match(/file:\/\/.*?packages\/compartment-mapper\/(.*)/); + return match ? match[1] : url; +}; + +/** + * Strips absolute `file://` prefixes from locations in a + * {@link PackageCompartmentMapDescriptor} as produced by `mapNodeModules()`. + * + * @template {PackageCompartmentMapDescriptor} T + * @param {T} compartmentMap + * @returns {T} + */ +export const stripCompartmentMap = compartmentMap => { + // 1. entry.compartment + const strippedEntry = { + ...compartmentMap.entry, + compartment: stripPath(compartmentMap.entry.compartment), + }; + + // 2. keys of compartments + const stripedCompartments = {}; + for (const [key, value] of entries(compartmentMap.compartments)) { + const newKey = stripPath(key); + // 3. compartments[*].modules[*].compartment + const modules = value.modules + ? fromEntries( + entries(value.modules).map(([mKey, mValue]) => { + if (isCompartmentModuleDescriptorConfiguration(mValue)) { + return [ + mKey, + { + ...mValue, + compartment: stripPath(mValue.compartment), + }, + ]; + } + return [mKey, mValue]; + }), + ) + : value.modules; + // 4. compartments[*].scopes[*].compartment + const scopes = value.scopes + ? fromEntries( + entries(value.scopes).map(([sKey, sValue]) => [ + sKey, + { + ...sValue, + compartment: stripPath(sValue.compartment), + }, + ]), + ) + : value.scopes; + // 5. compartments[*].location + if (value.location) { + value.location = /** @type {any} */ (stripPath(value.location)); + } + stripedCompartments[newKey] = { + ...value, + modules, + scopes, + }; + } + + return { + ...compartmentMap, + entry: strippedEntry, + compartments: stripedCompartments, + }; +}; + +/** + * Returns a deep copy of {@link Sources} with absolute `file://` prefixes + * stripped from `sourceLocation` properties. + * + * @see {@link stripCaptureResult} + * @param {Sources} sources + * @returns {Sources} + */ +export const stripSources = sources => { + /** @type {Sources} */ + const result = create(null); + for (const [compartmentKey, compartmentSources] of entries(sources)) { + /** @type {CompartmentSources} */ + const compartmentCopy = create(null); + for (const [moduleKey, moduleSource] of entries(compartmentSources)) { + if (isLocalModuleSource(moduleSource)) { + compartmentCopy[moduleKey] = { + ...moduleSource, + sourceLocation: stripPath(moduleSource.sourceLocation), + }; + } else { + compartmentCopy[moduleKey] = moduleSource; + } + } + result[compartmentKey] = compartmentCopy; + } + return result; +}; + +/** + * Strips absolute `file://` prefixes from locations in "renames" Records of {@link CaptureResult}. + * + * @see {@link stripCaptureResult} + * @param {Record} renames + * @returns {Record} Stripped renames + */ +export const stripRenames = renames => { + /** @type {Record} */ + const result = create(null); + for (const [key, value] of entries(renames)) { + result[stripPath(key)] = stripPath(value); + } + return result; +}; + +/** + * Strips absolute `file://` prefixes from locations in a `CaptureResult`. + * @param {CaptureResult} result + */ +export const stripCaptureResult = result => ({ + ...result, + compartmentRenames: stripRenames(result.compartmentRenames), + oldToNewCompartmentNames: stripRenames(result.oldToNewCompartmentNames), + newToOldCompartmentNames: stripRenames(result.newToOldCompartmentNames), + captureSources: stripSources(result.captureSources), +}); diff --git a/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.md b/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.md new file mode 100644 index 0000000000..8c606aaf82 --- /dev/null +++ b/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.md @@ -0,0 +1,681 @@ +# Snapshot report for `test/compartment-map-transforms.test.js` + +The actual snapshot is saved in `compartment-map-transforms.test.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## Compartment Map Transforms - integration with mapNodeModules() + +> Snapshot 1 + + { + compartments: { + '': { + label: '', + location: '', + modules: {}, + name: '', + parsers: { + bytes: 'bytes', + cjs: 'cjs', + js: 'cjs', + json: 'json', + mjs: 'mjs', + text: 'text', + }, + policy: { + defaultAttenuator: undefined, + packages: 'any', + }, + scopes: {}, + sourceDirname: 'webpackish-app', + types: {}, + }, + 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/': { + label: '$root$', + location: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + modules: { + '.': { + compartment: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + createdBy: 'node-modules', + module: './index.js', + }, + 'webpackish-app': { + compartment: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + createdBy: 'node-modules', + module: './index.js', + }, + }, + name: 'webpackish-app', + parsers: { + bytes: 'bytes', + cjs: 'cjs', + js: 'cjs', + json: 'json', + mjs: 'mjs', + text: 'text', + }, + policy: { + builtins: 'any', + globals: 'any', + packages: 'any', + }, + scopes: { + 'webpackish-app': { + compartment: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + }, + }, + sourceDirname: 'webpackish-app', + types: {}, + }, + }, + entry: { + compartment: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + module: './build.js', + }, + tags: [], + } + +## Compartment Map Transforms - integration with captureFromMap() + +> Snapshot 1 + + { + captureCompartmentMap: { + compartments: { + $root$: { + label: '$root$', + location: '$root$', + modules: { + './build.js': { + createdBy: 'digest', + location: 'build.js', + parser: 'pre-cjs-json', + sha512: undefined, + }, + pantspack: { + compartment: 'pantspack', + createdBy: 'node-modules', + module: './pantspack.js', + }, + }, + name: 'webpackish-app', + policy: { + builtins: 'any', + globals: 'any', + packages: 'any', + }, + }, + pantspack: { + label: 'pantspack', + location: 'pantspack', + modules: { + './pantspack.js': { + createdBy: 'digest', + location: 'pantspack.js', + parser: 'pre-cjs-json', + sha512: undefined, + }, + 'pantspack-folder-runner': { + compartment: 'pantspack>pantspack-folder-runner', + createdBy: 'node-modules', + module: './index.js', + }, + }, + name: 'pantspack', + policy: { + builtins: { + 'node:console': true, + 'node:path': true, + 'node:util': true, + }, + packages: { + $root$: true, + 'pantspack>pantspack-folder-runner': true, + }, + }, + }, + 'pantspack>pantspack-folder-runner': { + label: 'pantspack>pantspack-folder-runner', + location: 'pantspack>pantspack-folder-runner', + modules: { + './index.js': { + createdBy: 'digest', + location: 'index.js', + parser: 'pre-cjs-json', + sha512: undefined, + }, + }, + name: 'pantspack-folder-runner', + policy: { + packages: { + 'jorts-folder': true, + }, + }, + }, + }, + entry: { + compartment: '$root$', + module: './build.js', + }, + tags: [], + }, + captureSources: { + $root$: { + './build.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 2270616e 74737061 636b225d 2c226578 706f7274 + 73223a5b 22646566 61756c74 225d2c22 72656578 706f7274 73223a5b 2270616e + 74737061 636b225d 2c22736f 75726365 223a2228 66756e63 74696f6e 20287265 + 71756972 652c2065 78706f72 74732c20 6d6f6475 6c652c20 5f5f6669 6c656e61 + 6d652c20 5f5f6469 726e616d 6529207b 20277573 65207374 72696374 273b206d + 6f64756c 652e6578 706f7274 73203d20 72657175 69726528 2770616e 74737061 + 636b2729 5c6e202f 2f2a2f5c 6e7d295c 6e227d + ], + location: 'build.js', + parser: 'pre-cjs-json', + record: { + cjsFunctor: `(function (require, exports, module, __filename, __dirname) { 'use strict'; module.exports = require('pantspack')␊ + //*/␊ + })␊ + `, + execute: Function noopExecute {}, + exports: [ + 'default', + ], + imports: [ + 'pantspack', + ], + reexports: [ + 'pantspack', + ], + }, + sha512: undefined, + sourceDirname: 'webpackish-app', + sourceLocation: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/build.js', + }, + }, + 'jorts-folder': {}, + pantspack: { + './pantspack.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 2270616e 74737061 636b2d66 6f6c6465 722d7275 + 6e6e6572 222c226e 6f64653a 636f6e73 6f6c6522 2c226e6f 64653a70 61746822 + 2c226e6f 64653a75 74696c22 5d2c2265 78706f72 7473223a 5b226465 6661756c + 74225d2c 22726565 78706f72 7473223a 5b5d2c22 736f7572 6365223a 22286675 + 6e637469 6f6e2028 72657175 6972652c 20657870 6f727473 2c206d6f 64756c65 + 2c205f5f 66696c65 6e616d65 2c205f5f 6469726e 616d6529 207b2027 75736520 + 73747269 6374273b 20277573 65207374 72696374 273b5c6e 5c6e636f 6e737420 + 7b20666f 6c645061 6e747346 6f6c6465 7273207d 203d2072 65717569 72652827 + 70616e74 73706163 6b2d666f 6c646572 2d72756e 6e657227 293b5c6e 636f6e73 + 7420636f 6e736f6c 65203d20 72657175 69726528 276e6f64 653a636f 6e736f6c + 6527293b 5c6e636f 6e737420 70617468 203d2072 65717569 72652827 6e6f6465 + 3a706174 6827293b 5c6e636f 6e737420 7574696c 203d2072 65717569 72652827 + 6e6f6465 3a757469 6c27293b 5c6e5c6e 2f2a2a5c 6e202a20 40696d70 6f727420 + 7b506163 6b616765 44657363 72697074 6f727d20 66726f6d 20272e2e 2f2e2e2f + 2e2e2f2e 2e2f7372 632f7479 7065732e 6a73275c 6e202a2f 5c6e5c6e 2f2a2a5c + 6e202a5c 6e202a20 40706172 616d207b 73747269 6e677d20 66726f6d 5c6e202a + 20407265 7475726e 73207b7b 7061636b 61676544 65736372 6970746f 723a2050 + 61636b61 67654465 73637269 70746f72 2c207061 636b6167 654c6f63 6174696f + 6e3a2073 7472696e 677d7d5c 6e202a2f 5c6e636f 6e737420 66696e64 5061636b + 61676544 65736372 6970746f 72203d20 66726f6d 203d3e20 7b5c6e20 20636f6e + 73742063 75727265 6e74203d 2066726f 6d3b5c6e 2020666f 7220283b 3b29207b + 5c6e2020 2020636f 6e737420 7061636b 6167654a 736f6e50 61746820 3d207061 + 74682e6a 6f696e28 63757272 656e742c 20277061 636b6167 652e6a73 6f6e2729 + 3b5c6e20 20202074 7279207b 5c6e2020 20202020 636f6e73 74207061 636b6167 + 65446573 63726970 746f7220 3d207265 71756972 65287061 636b6167 654a736f + 6e506174 68293b5c 6e202020 20202072 65747572 6e207b5c 6e202020 20202020 + 20706163 6b616765 44657363 72697074 6f722c5c 6e202020 20202020 20706163 + 6b616765 4c6f6361 74696f6e 3a207061 74682e64 69726e61 6d652870 61636b61 + 67654a73 6f6e5061 7468292c 5c6e2020 20202020 7d3b5c6e 20202020 7d206361 + 74636820 7b5c6e20 20202020 20636f6e 73742070 6172656e 74203d20 70617468 + 2e726573 6f6c7665 28637572 72656e74 2c20272e 2e27293b 5c6e2020 20202020 + 69662028 70617265 6e74203d 3d3d2063 75727265 6e742920 7b5c6e20 20202020 + 20202074 68726f77 20457272 6f722860 436f756c 64206e6f 74206669 6e642070 + 61636b61 67652e6a 736f6e20 66726f6d 20247b66 726f6d7d 60293b5c 6e202020 + 2020207d 5c6e2020 20207d5c 6e20207d 5c6e7d3b 5c6e5c6e 636f6e73 74206d61 + 696e203d 20282920 3d3e207b 5c6e2020 636f6e73 74207b5c 6e202020 2076616c + 7565733a 207b2063 6f6e6669 673a2063 6f6e6669 6746696c 65207d2c 5c6e2020 + 2020706f 73697469 6f6e616c 732c5c6e 20207d20 3d207574 696c2e70 61727365 + 41726773 287b5c6e 20202020 6f707469 6f6e733a 207b5c6e 20202020 2020636f + 6e666967 3a207b5c 6e202020 20202020 20747970 653a2027 73747269 6e67272c + 5c6e2020 20202020 20206465 6661756c 743a2070 6174682e 6a6f696e 285c6e20 + 20202020 20202020 205f5f64 69726e61 6d652c5c 6e202020 20202020 20202027 + 2e2e272c 5c6e2020 20202020 20202020 27776562 7061636b 6973682d 61707027 + 2c5c6e20 20202020 20202020 20277061 6e747370 61636b2e 636f6e66 69672e6a + 73272c5c 6e202020 20202020 20292c5c 6e202020 2020207d 2c5c6e20 2020207d + 2c5c6e20 20202061 6c6c6f77 506f7369 74696f6e 616c733a 20747275 652c5c6e + 20207d29 3b5c6e5c 6e20202f 2f207265 61642063 6f6e6669 67206669 6c655c6e + 5c6e2020 636f6e73 7420636f 6e666967 53706563 69666965 72203d20 70617468 + 2e726573 6f6c7665 28636f6e 66696746 696c6529 3b5c6e5c 6e20206c 65742063 + 6f6e6669 67526573 6f6c7665 643b5c6e 20207472 79207b5c 6e202020 20636f6e + 66696752 65736f6c 76656420 3d207265 71756972 652e7265 736f6c76 6528636f + 6e666967 53706563 69666965 72293b5c 6e20207d 20636174 63682028 65727229 + 207b5c6e 20202020 636f6e73 6f6c652e 6572726f 72286043 6f756c64 206e6f74 + 20726573 6f6c7665 20636f6e 66696720 66696c65 3a20247b 636f6e66 69675370 + 65636966 6965727d 60293b5c 6e202020 20746872 6f772065 72723b5c 6e20207d + 5c6e5c6e 20202f2a 2a204074 79706520 7b7b656e 7472793a 20737472 696e672c + 20666f6c 64657273 3a207374 72696e67 5b5d7d7d 202a2f5c 6e20206c 65742063 + 6f6e6669 673b5c6e 20207472 79207b5c 6e202020 20636f6e 66696720 3d207265 + 71756972 6528636f 6e666967 5265736f 6c766564 293b5c6e 20207d20 63617463 + 68202865 72722920 7b5c6e20 20202063 6f6e736f 6c652e65 72726f72 28604572 + 726f7220 6c6f6164 696e6720 636f6e66 69672066 696c653a 20247b63 6f6e6669 + 67526573 6f6c7665 647d6029 3b5c6e20 20202074 68726f77 20657272 3b5c6e20 + 207d5c6e 5c6e2020 636f6e73 74207b20 666f6c64 65727320 7d203d20 636f6e66 + 69673b5c 6e5c6e20 20696620 2821666f 6c646572 732e6c65 6e677468 29207b5c + 6e202020 20746872 6f77206e 65772045 72726f72 28604e6f 20666f6c 64657273 + 20737065 63696669 65642069 6e20636f 6e666967 2066696c 653a2024 7b636f6e + 66696752 65736f6c 7665647d 2e20506c 65617365 20737065 63696679 20617420 + 6c656173 74206f6e 6520666f 6c646572 2e60293b 5c6e2020 7d5c6e5c 6e20202f + 2f206669 6e642070 61636b61 67652064 65736372 6970746f 7220666f 7220636f + 6e666967 5c6e2020 636f6e73 74207b20 7061636b 61676544 65736372 6970746f + 72207d20 3d206669 6e645061 636b6167 65446573 63726970 746f7228 5c6e2020 + 20207061 74682e64 69726e61 6d652863 6f6e6669 67526573 6f6c7665 64292c5c + 6e202029 3b5c6e5c 6e202063 6f6e7374 20636f6e 66696744 6972203d 20706174 + 682e6469 726e616d 6528636f 6e666967 5265736f 6c766564 293b5c6e 2020636f + 6e737420 736f7572 63657320 3d205b5c 6e202020 20706174 682e7265 736f6c76 + 6528636f 6e666967 4469722c 20636f6e 6669672e 656e7472 79292c5c 6e202020 + 202e2e2e 706f7369 74696f6e 616c732e 6d617028 736f7572 6365203d 3e207061 + 74682e72 65736f6c 76652863 6f6e6669 67446972 2c20736f 75726365 29292c5c + 6e20205d 3b5c6e5c 6e202063 6f6e7374 2070616e 74734865 6170203d 205b5c6e + 20202020 7b5c6e20 20202020 20706163 6b616765 44657363 72697074 6f722c5c + 6e202020 20202073 6f757263 65732c5c 6e202020 207d2c5c 6e20205d 3b5c6e5c + 6e202063 6f6e7374 20666f6c 64656450 616e7473 48656170 203d2066 6f6c6450 + 616e7473 466f6c64 65727328 5c6e2020 20207061 6e747348 6561702c 5c6e2020 + 20202f2a 2a204074 79706520 7b5b7374 72696e67 2c202e2e 2e737472 696e675b + 5d5d7d20 2a2f2028 666f6c64 65727329 2c5c6e20 20293b5c 6e5c6e20 20666f72 + 2028636f 6e737420 7b20666f 6c646564 536f7572 63657320 7d206f66 20666f6c + 64656450 616e7473 48656170 29207b5c 6e202020 20666f72 2028636f 6e737420 + 666f6c64 6564536f 75726365 206f6620 666f6c64 6564536f 75726365 7329207b + 5c6e2020 20202020 636f6e73 6f6c652e 6c6f6728 666f6c64 6564536f 75726365 + 293b5c6e 20202020 7d5c6e20 207d5c6e 5c6e2020 64656275 67676572 5c6e2020 + 72657475 726e2066 6f6c6465 6450616e 74734865 61703b5c 6e7d3b5c 6e5c6e6d + 6f64756c 652e6578 706f7274 73203d20 6d61696e 28293b5c 6e202f2f 2a2f5c6e + 7d295c6e 227d + ], + location: 'pantspack.js', + parser: 'pre-cjs-json', + record: { + cjsFunctor: `(function (require, exports, module, __filename, __dirname) { 'use strict'; 'use strict';␊ + ␊ + const { foldPantsFolders } = require('pantspack-folder-runner');␊ + const console = require('node:console');␊ + const path = require('node:path');␊ + const util = require('node:util');␊ + ␊ + /**␊ + * @import {PackageDescriptor} from '../../../../src/types.js'␊ + */␊ + ␊ + /**␊ + *␊ + * @param {string} from␊ + * @returns {{packageDescriptor: PackageDescriptor, packageLocation: string}}␊ + */␊ + const findPackageDescriptor = from => {␊ + const current = from;␊ + for (;;) {␊ + const packageJsonPath = path.join(current, 'package.json');␊ + try {␊ + const packageDescriptor = require(packageJsonPath);␊ + return {␊ + packageDescriptor,␊ + packageLocation: path.dirname(packageJsonPath),␊ + };␊ + } catch {␊ + const parent = path.resolve(current, '..');␊ + if (parent === current) {␊ + throw Error(\`Could not find package.json from ${from}\`);␊ + }␊ + }␊ + }␊ + };␊ + ␊ + const main = () => {␊ + const {␊ + values: { config: configFile },␊ + positionals,␊ + } = util.parseArgs({␊ + options: {␊ + config: {␊ + type: 'string',␊ + default: path.join(␊ + __dirname,␊ + '..',␊ + 'webpackish-app',␊ + 'pantspack.config.js',␊ + ),␊ + },␊ + },␊ + allowPositionals: true,␊ + });␊ + ␊ + // read config file␊ + ␊ + const configSpecifier = path.resolve(configFile);␊ + ␊ + let configResolved;␊ + try {␊ + configResolved = require.resolve(configSpecifier);␊ + } catch (err) {␊ + console.error(\`Could not resolve config file: ${configSpecifier}\`);␊ + throw err;␊ + }␊ + ␊ + /** @type {{entry: string, folders: string[]}} */␊ + let config;␊ + try {␊ + config = require(configResolved);␊ + } catch (err) {␊ + console.error(\`Error loading config file: ${configResolved}\`);␊ + throw err;␊ + }␊ + ␊ + const { folders } = config;␊ + ␊ + if (!folders.length) {␊ + throw new Error(\`No folders specified in config file: ${configResolved}. Please specify at least one folder.\`);␊ + }␊ + ␊ + // find package descriptor for config␊ + const { packageDescriptor } = findPackageDescriptor(␊ + path.dirname(configResolved),␊ + );␊ + ␊ + const configDir = path.dirname(configResolved);␊ + const sources = [␊ + path.resolve(configDir, config.entry),␊ + ...positionals.map(source => path.resolve(configDir, source)),␊ + ];␊ + ␊ + const pantsHeap = [␊ + {␊ + packageDescriptor,␊ + sources,␊ + },␊ + ];␊ + ␊ + const foldedPantsHeap = foldPantsFolders(␊ + pantsHeap,␊ + /** @type {[string, ...string[]]} */ (folders),␊ + );␊ + ␊ + for (const { foldedSources } of foldedPantsHeap) {␊ + for (const foldedSource of foldedSources) {␊ + console.log(foldedSource);␊ + }␊ + }␊ + ␊ + debugger␊ + return foldedPantsHeap;␊ + };␊ + ␊ + module.exports = main();␊ + //*/␊ + })␊ + `, + execute: Function noopExecute {}, + exports: [ + 'default', + ], + imports: [ + 'pantspack-folder-runner', + 'node:console', + 'node:path', + 'node:util', + ], + reexports: [], + }, + sha512: undefined, + sourceDirname: 'pantspack', + sourceLocation: 'test/fixtures-dynamic-ancestor/node_modules/pantspack/pantspack.js', + }, + }, + 'pantspack>pantspack-folder-runner': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 5d2c2265 78706f72 7473223a 5b22666f 6c645061 + 6e747346 6f6c6465 7273222c 22646566 61756c74 225d2c22 72656578 706f7274 + 73223a5b 5d2c2273 6f757263 65223a22 2866756e 6374696f 6e202872 65717569 + 72652c20 6578706f 7274732c 206d6f64 756c652c 205f5f66 696c656e 616d652c + 205f5f64 69726e61 6d652920 7b202775 73652073 74726963 74273b20 2f2a2a5c + 6e202a20 40696d70 6f727420 7b506163 6b616765 44657363 72697074 6f727d20 + 66726f6d 20272e2e 2f2e2e2f 2e2e2f2e 2e2f7372 632f7479 7065732e 6a73275c + 6e202a2f 5c6e5c6e 2f2a2a5c 6e202a20 40747970 65646566 207b2866 696c6570 + 6174683a 20737472 696e672c 20706163 6b616765 44657363 72697074 6f723a20 + 5061636b 61676544 65736372 6970746f 7229203d 3e20756e 6b6e6f77 6e7d2046 + 6f6c6446 6e5c6e20 2a204074 79706564 6566207b 41727261 793c7b70 61636b61 + 67654465 73637269 70746f72 3a205061 636b6167 65446573 63726970 746f722c + 20736f75 72636573 3a207374 72696e67 5b5d7d3e 7d205061 6e747348 6561705c + 6e202a20 40747970 65646566 207b4172 7261793c 7b706163 6b616765 44657363 + 72697074 6f723a20 5061636b 61676544 65736372 6970746f 722c2066 6f6c6465 + 64536f75 72636573 3a20756e 6b6e6f77 6e5b5d7d 3e7d2046 6f6c6465 6450616e + 74734865 61705c6e 202a2f5c 6e5c6e2f 2a2a5c6e 202a2040 6f766572 6c6f6164 + 5c6e202a 20407061 72616d20 7b50616e 74734865 61707d20 70616e74 73486561 + 7020496e 666f726d 6174696f 6e206162 6f757420 736f7572 63657320 746f2070 + 726f7669 64652074 6f206c6f 61646572 735c6e20 2a204070 6172616d 207b5b73 + 7472696e 672c202e 2e2e7374 72696e67 5b5d5d7d 2070616e 7473466f 6c646572 + 73207061 6e747346 6f6c6465 72206e61 6d65735c 6e202a20 40726574 75726e73 + 207b466f 6c646564 50616e74 73486561 707d5c6e 202a2f5c 6e5c6e2f 2a2a5c6e + 202a2040 6f766572 6c6f6164 5c6e202a 20407061 72616d20 7b50616e 74734865 + 61707d20 70616e74 73486561 7020496e 666f726d 6174696f 6e206162 6f757420 + 736f7572 63657320 746f2070 726f7669 64652074 6f206c6f 61646572 735c6e20 + 2a204070 6172616d 207b7374 72696e67 5b5d7d20 5b70616e 7473466f 6c646572 + 735d2070 616e7473 466f6c64 6572206e 616d6573 5c6e202a 20407265 7475726e + 73207b50 616e7473 48656170 7c466f6c 64656450 616e7473 48656170 7d5c6e20 + 2a2f5c6e 5c6e2f2a 2a5c6e20 2a204070 6172616d 207b5061 6e747348 6561707d + 2070616e 74734865 61702049 6e666f72 6d617469 6f6e2061 626f7574 20736f75 + 72636573 20746f20 70726f76 69646520 746f206c 6f616465 72735c6e 202a2040 + 70617261 6d207b73 7472696e 675b5d7d 205b7061 6e747346 6f6c6465 72735d20 + 70616e74 73466f6c 64657220 6e616d65 735c6e20 2a204072 65747572 6e73207b + 50616e74 73486561 707c466f 6c646564 50616e74 73486561 707d5c6e 202a2f5c + 6e636f6e 73742066 6f6c6450 616e7473 466f6c64 65727320 3d202870 616e7473 + 48656170 2c207061 6e747346 6f6c6465 7273203d 205b5d29 203d3e5c 6e202070 + 616e7473 466f6c64 6572732e 6c656e67 74685c6e 20202020 3f207061 6e747346 + 6f6c6465 72732e66 6c61744d 61702870 616e7473 466f6c64 6572203d 3e207b5c + 6e202020 20202020 202f2a2a 20407479 7065207b 73747269 6e677d20 2a2f5c6e + 20202020 20202020 6c657420 70616e74 73466f6c 64657250 6174683b 5c6e2020 + 20202020 20207472 79207b5c 6e202020 20202020 20202070 616e7473 466f6c64 + 65725061 7468203d 20726571 75697265 2e726573 6f6c7665 2870616e 7473466f + 6c646572 293b5c6e 20202020 20202020 7d206361 74636820 28657272 29207b5c + 6e202020 20202020 20202074 68726f77 20457272 6f722860 436f756c 64206e6f + 74207265 736f6c76 65207061 6e747346 6f6c6465 72205c22 247b7061 6e747346 + 6f6c6465 727d5c22 602c207b 5c6e2020 20202020 20202020 20206361 7573653a + 20657272 2c5c6e20 20202020 20202020 207d293b 5c6e2020 20202020 20207d5c + 6e5c6e20 20202020 2020202f 2a2a2040 74797065 207b466f 6c64466e 7d202a2f + 5c6e2020 20202020 20206c65 7420666f 6c643b5c 6e202020 20202020 202f2a2a + 20407479 7065207b 73747269 6e675b5d 7d202a2f 5c6e2020 20202020 20206c65 + 74206578 74656e73 696f6e73 3b5c6e5c 6e202020 20202020 20747279 207b5c6e + 20202020 20202020 20202f2a 2a204074 79706520 7b7b666f 6c643a20 466f6c64 + 466e2c20 65787465 6e73696f 6e733a20 73747269 6e675b5d 7d7d202a 2f5c6e20 + 20202020 20202020 20287b20 666f6c64 2c206578 74656e73 696f6e73 207d203d + 20726571 75697265 2870616e 7473466f 6c646572 50617468 29293b5c 6e202020 + 20202020 207d2063 61746368 20286572 7229207b 5c6e2020 20202020 20202020 + 7468726f 77204572 726f7228 5c6e2020 20202020 20202020 20206043 6f756c64 + 206e6f74 20726571 75697265 2070616e 7473466f 6c646572 205c2224 7b70616e + 7473466f 6c646572 7d5c2220 76696120 247b7061 6e747346 6f6c6465 72506174 + 687d602c 5c6e2020 20202020 20202020 20207b20 63617573 653a2065 7272207d + 2c5c6e20 20202020 20202020 20293b5c 6e202020 20202020 207d5c6e 5c6e2020 + 20202020 20206966 20287479 70656f66 20666f6c 6420213d 3d202766 756e6374 + 696f6e27 29207b5c 6e202020 20202020 20202074 68726f77 20457272 6f72285c + 6e202020 20202020 20202020 20604578 70656374 65642070 616e7473 466f6c64 + 6572205c 22247b70 616e7473 466f6c64 65727d5c 22207669 6120247b 70616e74 + 73466f6c 64657250 6174687d 20746f20 6578706f 72742061 20666f6c 64282920 + 66756e63 74696f6e 3b20676f 7420247b 666f6c64 7d602c5c 6e202020 20202020 + 20202029 3b5c6e20 20202020 2020207d 5c6e5c6e 20202020 20202020 69662028 + 21417272 61792e69 73417272 61792865 7874656e 73696f6e 7329207c 7c206578 + 74656e73 696f6e73 2e6c656e 67746820 3d3d3d20 3029207b 5c6e2020 20202020 + 20202020 7468726f 77204572 726f7228 5c6e2020 20202020 20202020 20206045 + 78706563 74656420 70616e74 73466f6c 64657220 5c22247b 70616e74 73466f6c + 6465727d 5c222076 69612024 7b70616e 7473466f 6c646572 50617468 7d20746f + 20657870 6f727420 61206e6f 6e2d656d 70747920 61727261 79206f66 2066696c + 65206578 74656e73 696f6e73 3b20676f 7420247b 65787465 6e73696f 6e737d60 + 2c5c6e20 20202020 20202020 20293b5c 6e202020 20202020 207d5c6e 5c6e2020 + 20202020 20202f2f 2066696e 6420616c 6c20736f 75726365 73206d61 74636869 + 6e672074 68652065 7874656e 73696f6e 732c2074 68656e20 63616c6c 20666f6c + 64282920 6f6e2065 6163685c 6e202020 20202020 20636f6e 73742066 6f6c6465 + 6450616e 74734865 6170203d 2070616e 74734865 61702e6d 6170285c 6e202020 + 20202020 20202028 7b207061 636b6167 65446573 63726970 746f722c 20736f75 + 72636573 207d2920 3d3e207b 5c6e2020 20202020 20202020 2020636f 6e737420 + 736f7572 63657346 6f724c6f 61646572 203d2073 6f757263 65732e66 696c7465 + 7228736f 75726365 203d3e5c 6e202020 20202020 20202020 20202065 7874656e + 73696f6e 732e736f 6d652865 7874203d 3e20736f 75726365 2e656e64 73576974 + 68286578 7429292c 5c6e2020 20202020 20202020 2020293b 5c6e2020 20202020 + 20202020 2020636f 6e737420 666f6c64 6564536f 75726365 73203d20 736f7572 + 63657346 6f724c6f 61646572 2e6d6170 28736f75 72636520 3d3e207b 5c6e2020 + 20202020 20202020 20202020 74727920 7b5c6e20 20202020 20202020 20202020 + 20202072 65747572 6e20666f 6c642873 6f757263 652c2070 61636b61 67654465 + 73637269 70746f72 293b5c6e 20202020 20202020 20202020 20207d20 63617463 + 68202865 72722920 7b5c6e20 20202020 20202020 20202020 20202074 68726f77 + 20457272 6f72285c 6e202020 20202020 20202020 20202020 20202060 4572726f + 7220666f 6c64696e 6720736f 75726365 205c2224 7b736f75 7263657d 5c222066 + 6f722070 61636b61 6765205c 22247b70 61636b61 67654465 73637269 70746f72 + 2e6e616d 657d5c22 3a20247b 6572722e 6d657373 6167657d 602c5c6e 20202020 + 20202020 20202020 20202020 20207b20 63617573 653a2065 7272207d 2c5c6e20 + 20202020 20202020 20202020 20202029 3b5c6e20 20202020 20202020 20202020 + 207d5c6e 20202020 20202020 20202020 7d293b5c 6e202020 20202020 20202020 + 20726574 75726e20 7b207061 636b6167 65446573 63726970 746f722c 20666f6c + 64656453 6f757263 6573207d 3b5c6e20 20202020 20202020 207d2c5c 6e202020 + 20202020 20293b5c 6e5c6e20 20202020 20202072 65747572 6e20666f 6c646564 + 50616e74 73486561 703b5c6e 20202020 20207d29 5c6e2020 20203a20 70616e74 + 73486561 703b5c6e 5c6e6578 706f7274 732e666f 6c645061 6e747346 6f6c6465 + 7273203d 20666f6c 6450616e 7473466f 6c646572 733b5c6e 202f2f2a 2f5c6e7d + 295c6e22 7d + ], + location: 'index.js', + parser: 'pre-cjs-json', + record: { + cjsFunctor: `(function (require, exports, module, __filename, __dirname) { 'use strict'; /**␊ + * @import {PackageDescriptor} from '../../../../src/types.js'␊ + */␊ + ␊ + /**␊ + * @typedef {(filepath: string, packageDescriptor: PackageDescriptor) => unknown} FoldFn␊ + * @typedef {Array<{packageDescriptor: PackageDescriptor, sources: string[]}>} PantsHeap␊ + * @typedef {Array<{packageDescriptor: PackageDescriptor, foldedSources: unknown[]}>} FoldedPantsHeap␊ + */␊ + ␊ + /**␊ + * @overload␊ + * @param {PantsHeap} pantsHeap Information about sources to provide to loaders␊ + * @param {[string, ...string[]]} pantsFolders pantsFolder names␊ + * @returns {FoldedPantsHeap}␊ + */␊ + ␊ + /**␊ + * @overload␊ + * @param {PantsHeap} pantsHeap Information about sources to provide to loaders␊ + * @param {string[]} [pantsFolders] pantsFolder names␊ + * @returns {PantsHeap|FoldedPantsHeap}␊ + */␊ + ␊ + /**␊ + * @param {PantsHeap} pantsHeap Information about sources to provide to loaders␊ + * @param {string[]} [pantsFolders] pantsFolder names␊ + * @returns {PantsHeap|FoldedPantsHeap}␊ + */␊ + const foldPantsFolders = (pantsHeap, pantsFolders = []) =>␊ + pantsFolders.length␊ + ? pantsFolders.flatMap(pantsFolder => {␊ + /** @type {string} */␊ + let pantsFolderPath;␊ + try {␊ + pantsFolderPath = require.resolve(pantsFolder);␊ + } catch (err) {␊ + throw Error(\`Could not resolve pantsFolder "${pantsFolder}"\`, {␊ + cause: err,␊ + });␊ + }␊ + ␊ + /** @type {FoldFn} */␊ + let fold;␊ + /** @type {string[]} */␊ + let extensions;␊ + ␊ + try {␊ + /** @type {{fold: FoldFn, extensions: string[]}} */␊ + ({ fold, extensions } = require(pantsFolderPath));␊ + } catch (err) {␊ + throw Error(␊ + \`Could not require pantsFolder "${pantsFolder}" via ${pantsFolderPath}\`,␊ + { cause: err },␊ + );␊ + }␊ + ␊ + if (typeof fold !== 'function') {␊ + throw Error(␊ + \`Expected pantsFolder "${pantsFolder}" via ${pantsFolderPath} to export a fold() function; got ${fold}\`,␊ + );␊ + }␊ + ␊ + if (!Array.isArray(extensions) || extensions.length === 0) {␊ + throw Error(␊ + \`Expected pantsFolder "${pantsFolder}" via ${pantsFolderPath} to export a non-empty array of file extensions; got ${extensions}\`,␊ + );␊ + }␊ + ␊ + // find all sources matching the extensions, then call fold() on each␊ + const foldedPantsHeap = pantsHeap.map(␊ + ({ packageDescriptor, sources }) => {␊ + const sourcesForLoader = sources.filter(source =>␊ + extensions.some(ext => source.endsWith(ext)),␊ + );␊ + const foldedSources = sourcesForLoader.map(source => {␊ + try {␊ + return fold(source, packageDescriptor);␊ + } catch (err) {␊ + throw Error(␊ + \`Error folding source "${source}" for package "${packageDescriptor.name}": ${err.message}\`,␊ + { cause: err },␊ + );␊ + }␊ + });␊ + return { packageDescriptor, foldedSources };␊ + },␊ + );␊ + ␊ + return foldedPantsHeap;␊ + })␊ + : pantsHeap;␊ + ␊ + exports.foldPantsFolders = foldPantsFolders;␊ + //*/␊ + })␊ + `, + execute: Function noopExecute {}, + exports: [ + 'foldPantsFolders', + 'default', + ], + imports: [], + reexports: [], + }, + sha512: undefined, + sourceDirname: 'pantspack-folder-runner', + sourceLocation: 'test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/index.js', + }, + }, + undefined: {}, + }, + compartmentRenames: { + $root$: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + '': '', + 'jorts-folder': 'test/fixtures-dynamic-ancestor/node_modules/jorts-folder/', + pantspack: 'test/fixtures-dynamic-ancestor/node_modules/pantspack/', + 'pantspack>pantspack-folder-runner': 'test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/', + }, + newToOldCompartmentNames: { + $root$: 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/', + '': '', + 'jorts-folder': 'test/fixtures-dynamic-ancestor/node_modules/jorts-folder/', + pantspack: 'test/fixtures-dynamic-ancestor/node_modules/pantspack/', + 'pantspack>pantspack-folder-runner': 'test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/', + }, + oldToNewCompartmentNames: { + '': '', + 'test/fixtures-dynamic-ancestor/node_modules/jorts-folder/': 'jorts-folder', + 'test/fixtures-dynamic-ancestor/node_modules/pantspack-folder-runner/': 'pantspack>pantspack-folder-runner', + 'test/fixtures-dynamic-ancestor/node_modules/pantspack/': 'pantspack', + 'test/fixtures-dynamic-ancestor/node_modules/webpackish-app/': '$root$', + }, + } diff --git a/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.snap b/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.snap new file mode 100644 index 0000000000..f0c97c97e6 Binary files /dev/null and b/packages/compartment-mapper/test/snapshots/compartment-map-transforms.test.js.snap differ diff --git a/packages/compartment-mapper/test/snapshots/policy.test.js.md b/packages/compartment-mapper/test/snapshots/policy.test.js.md index 9dd489d6ea..41af97a244 100644 --- a/packages/compartment-mapper/test/snapshots/policy.test.js.md +++ b/packages/compartment-mapper/test/snapshots/policy.test.js.md @@ -8,109 +8,109 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / mapNodeModules / importFromMap > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / mapNodeModules / loadFromMap / import > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / importLocation > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / makeArchive / parseArchive > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / makeArchive / parseArchive with a prefix > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / writeArchive / loadArchive > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / writeArchive / importArchive > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - attack - browser alias - with alias hint / mapNodeModules / makeArchiveFromMap / importArchive > Snapshot 1 - 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve-v1.0.0" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' + 'Failed to load module "./attack.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (1 underlying failures: Importing "dan" in "eve" was not allowed by "packages" policy: {"dan":true} (info: Blocked in linking. "dan" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/hackity/".)' ## policy - disallowed package with error hint / loadLocation > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / mapNodeModules / importFromMap > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / mapNodeModules / loadFromMap / import > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / importLocation > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / makeArchive / parseArchive > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / makeArchive / parseArchive with a prefix > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / writeArchive / loadArchive > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / writeArchive / importArchive > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - disallowed package with error hint / mapNodeModules / makeArchiveFromMap / importArchive > Snapshot 1 - 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice-v1.0.0" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' + 'Failed to load module "./index.js" in package "file://.../compartment-mapper/test/fixtures-policy/node_modules/app/" (2 underlying failures: Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".), Importing "carol" in "alice" was not allowed by "packages" policy: {"alice>carol":false} (info: Blocked in linking. "carol" is part of the compartment map and resolves to "file://.../compartment-mapper/test/fixtures-policy/node_modules/alice/node_modules/carol/".)' ## policy - attenuator error aggregation / loadLocation @@ -140,28 +140,28 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - 'Globals attenuation errors: Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' + 'Globals attenuation errors: Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' ## policy - attenuator error aggregation / makeArchive / parseArchive with a prefix > Snapshot 1 - 'Globals attenuation errors: Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' + 'Globals attenuation errors: Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' ## policy - attenuator error aggregation / writeArchive / loadArchive > Snapshot 1 - 'Globals attenuation errors: Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' + 'Globals attenuation errors: Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' ## policy - attenuator error aggregation / writeArchive / importArchive > Snapshot 1 - 'Globals attenuation errors: Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' + 'Globals attenuation errors: Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' ## policy - attenuator error aggregation / mapNodeModules / makeArchiveFromMap / importArchive > Snapshot 1 - 'Globals attenuation errors: Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' + 'Globals attenuation errors: Error while attenuating globals for "app" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "alice" with "myattenuator": "I attenuate, I throw.", Error while attenuating globals for "carol" with "myattenuator": "I attenuate, I throw."' diff --git a/packages/compartment-mapper/test/snapshots/policy.test.js.snap b/packages/compartment-mapper/test/snapshots/policy.test.js.snap index 1b763c9016..a7fb9fd00a 100644 Binary files a/packages/compartment-mapper/test/snapshots/policy.test.js.snap and b/packages/compartment-mapper/test/snapshots/policy.test.js.snap differ diff --git a/packages/compartment-mapper/test/stability.test.js b/packages/compartment-mapper/test/stability.test.js index 293766cbf8..63dcd22860 100644 --- a/packages/compartment-mapper/test/stability.test.js +++ b/packages/compartment-mapper/test/stability.test.js @@ -3,6 +3,7 @@ import test from 'ava'; import { ZipReader } from '@endo/zip'; import { makeArchive, parseArchive } from '../index.js'; import { readPowers } from './scaffold.js'; +import { ENTRY_COMPARTMENT } from '../src/policy-format.js'; const fixture = new URL('fixtures-stability/a.js', import.meta.url).toString(); @@ -18,12 +19,12 @@ test('order of duplicate name/version packages', async t => { t.log(actualOrder); t.deepEqual(actualOrder, [ - 'a-v1.0.0', - 'a-v1.0.0-n1', - 'b-v1.0.0', - 'dep-v1.0.0', - 'dep-v1.0.0-n1', - 'dep-v1.0.0-n2', + ENTRY_COMPARTMENT, + 'a', + 'a>dep', + 'b', + 'b>dep', + 'dep', ]); await t.notThrowsAsync(async () => { diff --git a/packages/compartment-mapper/test/test.types.d.ts b/packages/compartment-mapper/test/test.types.d.ts index 808f02b334..34ba6a1d9c 100644 --- a/packages/compartment-mapper/test/test.types.d.ts +++ b/packages/compartment-mapper/test/test.types.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-shadow */ /** * Utility types for tests * @@ -5,6 +6,7 @@ */ import type { ExecutionContext } from 'ava'; +import type { inspect as nodeInspect, InspectOptionsStylized } from 'util'; import type { makeReadPowers } from '../src/node-powers.js'; import type { LoadLocationOptions, SomePolicy } from '../archive-lite.js'; @@ -81,6 +83,41 @@ export type MakeProjectFixtureReadPowersOptions = Simplify< MakeMaybeReadProjectFixtureOptions & SetOptional[0]> >; + +/** + * This is the same function as Node's `CustomInspectFunction`, but I added the + * third parameter which was missing from `@types/node`. + * TODO: Upstream & remove this. + */ +export type FixedCustomInspectFunction = ( + depth: number, + options: InspectOptionsStylized, + inspect: typeof nodeInspect, +) => any; + +/** + * Did you know you can define custom styles for `util.inspect`? It doesn't say + * anywhere that you can't!! + */ +export type CustomInspectStyles = Simplify< + (typeof nodeInspect)['styles'] & { + name: string; + endoKind: string; + endoCanonical: string; + endoConstant: string; + } +>; + +declare module 'node:util' { + /** + * Augments the `stylize` method of `InspectOptionsStylized` to allow + * {@link CustomInspectStyles} to be applied. + */ + interface InspectOptionsStylized { + stylize(text: string, styleType: keyof CustomInspectStyles): string; + } +} + // #endregion // #region scaffold.js diff --git a/packages/env-options/package.json b/packages/env-options/package.json index 15b408d222..e51ac6b654 100644 --- a/packages/env-options/package.json +++ b/packages/env-options/package.json @@ -18,7 +18,6 @@ "main": "./index.js", "module": "./index.js", "unpkg": null, - "types": null, "exports": { ".": "./index.js", "./package.json": "./package.json" diff --git a/yarn.lock b/yarn.lock index aacc9a2c97..fe3f5bbbc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -332,6 +332,7 @@ __metadata: resolution: "@endo/compartment-mapper@workspace:packages/compartment-mapper" dependencies: "@endo/cjs-module-analyzer": "workspace:^" + "@endo/env-options": "workspace:^" "@endo/module-source": "workspace:^" "@endo/path-compare": "workspace:^" "@endo/trampoline": "workspace:^"