Skip to content

Commit

Permalink
feat(compartment-mapper): Relax package discovery (#2642)
Browse files Browse the repository at this point in the history
Closes: #2636 

## Description

The compartment mapper is currently strict about the interpretation of
`peerDependencies` and `peerDependenciesMeta.optional` as opposed to
`optionalDependencies` and is technically correct, but that correctness
and understanding is not evenly distributed among tools in the
ecosystem.
If the Compartment Mapper instead tolerates the physical absence of
expected packages, it defers an error that would be experienced under
`mapNodeModules` to `link/load` since that might attempt to load a
module from the missing package. This is not a particularly observable
difference in behavior, but the error message will be slightly less
informative.

In this change, we relax the behavior by default and provide a `strict`
option to provide the more informative error and tolerate fewer
ecosystem defficiencies.

### Security Considerations

None.

### Scaling Considerations

None.

### Documentation Considerations

News updated.

We do not yet publish API documentation for Compartment Mapper, but the
necessary annotations are present when these devices are more public.

### Testing Considerations

Includes positive and negative tests for the `strict` behavior.

### Compatibility Considerations

The nature of some errors during bundling may change. Bundling errors
are not observed on Agoric’s chain, so that sensitivity should not cause
problems.

### Upgrade Considerations

None.
  • Loading branch information
kriskowal authored Nov 26, 2024
2 parents 95df424 + ab885a2 commit 768c9cb
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 8 deletions.
15 changes: 15 additions & 0 deletions packages/compartment-mapper/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
User-visible changes to `@endo/compartment-mapper`:

# Next release

- `mapNodeModules` and all functions that use it now tolerate the absence of
expected packages.
These packages are now omitted from the generated package skeleton map.
So, loading a physically missing module now occurs during the load phase
instead of the mapping phase.
- Adds a `strict` option to all functions that `mapNodeModules` to restore old
behavior, which produces an error early if, for example, a non-optional
peer dependency is missing.
Peer dependencies are strictly required unless `peerDependenciesMeta` has an
object with a truthy `optional` entry.
Correct interpretation of `peerDependencies` is not distributed evenly, so
this behavior is no longer the default.

# v1.4.0 (2024-11-13)

- Adds options `languageForExtension`, `moduleLanguageForExtension`,
Expand Down
8 changes: 8 additions & 0 deletions packages/compartment-mapper/src/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const makeArchive = async (powers, moduleLocation, options = {}) => {
dev,
tags,
conditions = tags,
strict = false,
commonDependencies,
policy,
languageForExtension,
Expand All @@ -99,6 +100,7 @@ export const makeArchive = async (powers, moduleLocation, options = {}) => {
} = assignParserForLanguage(options);
const compartmentMap = await mapNodeModules(powers, moduleLocation, {
dev,
strict,
conditions,
commonDependencies,
policy,
Expand Down Expand Up @@ -129,6 +131,7 @@ export const mapLocation = async (powers, moduleLocation, options = {}) => {
dev,
tags,
conditions = tags,
strict = false,
commonDependencies,
policy,
parserForLanguage,
Expand All @@ -144,6 +147,7 @@ export const mapLocation = async (powers, moduleLocation, options = {}) => {

const compartmentMap = await mapNodeModules(powers, moduleLocation, {
dev,
strict,
conditions,
commonDependencies,
policy,
Expand Down Expand Up @@ -174,6 +178,7 @@ export const hashLocation = async (powers, moduleLocation, options = {}) => {
dev,
tags,
conditions = tags,
strict = false,
commonDependencies,
policy,
parserForLanguage,
Expand All @@ -189,6 +194,7 @@ export const hashLocation = async (powers, moduleLocation, options = {}) => {

const compartmentMap = await mapNodeModules(powers, moduleLocation, {
dev,
strict,
conditions,
commonDependencies,
policy,
Expand Down Expand Up @@ -226,6 +232,7 @@ export const writeArchive = async (
dev,
tags,
conditions = tags,
strict = false,
commonDependencies,
policy,
parserForLanguage,
Expand All @@ -240,6 +247,7 @@ export const writeArchive = async (
} = assignParserForLanguage(options);
const compartmentMap = await mapNodeModules(readPowers, moduleLocation, {
dev,
strict,
conditions,
commonDependencies,
policy,
Expand Down
2 changes: 2 additions & 0 deletions packages/compartment-mapper/src/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const loadLocation = async (
const {
dev,
tags,
strict,
commonDependencies,
policy,
parserForLanguage,
Expand All @@ -93,6 +94,7 @@ export const loadLocation = async (
'conditions' in options ? options.conditions || tags : tags;
const compartmentMap = await mapNodeModules(readPowers, moduleLocation, {
dev,
strict,
conditions,
commonDependencies,
policy,
Expand Down
33 changes: 26 additions & 7 deletions packages/compartment-mapper/src/node-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ const inferParsers = (descriptor, location, languageOptions) => {
* @param {boolean | undefined} dev
* @param {CommonDependencyDescriptors} commonDependencyDescriptors
* @param {LanguageOptions} languageOptions
* @param {boolean} strict
* @param {Map<string, Array<string>>} preferredPackageLogicalPathMap
* @param {Array<string>} logicalPath
* @returns {Promise<undefined>}
Expand All @@ -310,6 +311,7 @@ const graphPackage = async (
dev,
commonDependencyDescriptors,
languageOptions,
strict,
preferredPackageLogicalPathMap = new Map(),
logicalPath = [],
) => {
Expand Down Expand Up @@ -344,14 +346,18 @@ const graphPackage = async (
devDependencies = {},
} = packageDescriptor;
const allDependencies = {};
assign(allDependencies, commonDependencyDescriptors);
for (const [name, { spec }] of Object.entries(commonDependencyDescriptors)) {
allDependencies[name] = spec;
for (const [name, descriptor] of Object.entries(
commonDependencyDescriptors,
)) {
if (Object(descriptor) === descriptor) {
const { spec } = descriptor;
allDependencies[name] = spec;
}
}
assign(allDependencies, dependencies);
assign(allDependencies, peerDependencies);
for (const [name, { optional }] of Object.entries(peerDependenciesMeta)) {
if (optional) {
for (const [name, meta] of Object.entries(peerDependenciesMeta)) {
if (Object(meta) === meta && meta.optional) {
optionals.add(name);
}
}
Expand Down Expand Up @@ -380,6 +386,7 @@ const graphPackage = async (
conditions,
preferredPackageLogicalPathMap,
languageOptions,
strict,
childLogicalPath,
optional,
commonDependencyDescriptors,
Expand Down Expand Up @@ -481,6 +488,7 @@ const graphPackage = async (
* @param {Set<string>} conditions
* @param {Map<string, Array<string>>} preferredPackageLogicalPathMap
* @param {LanguageOptions} languageOptions
* @param {boolean} strict
* @param {Array<string>} [childLogicalPath]
* @param {boolean} [optional] - whether the dependency is optional
* @param {object} [commonDependencyDescriptors] - dependencies to be added to all packages
Expand All @@ -495,6 +503,7 @@ const gatherDependency = async (
conditions,
preferredPackageLogicalPathMap,
languageOptions,
strict,
childLogicalPath = [],
optional = false,
commonDependencyDescriptors = undefined,
Expand All @@ -507,7 +516,7 @@ const gatherDependency = async (
);
if (dependency === undefined) {
// allow the dependency to be missing if optional
if (optional) {
if (optional || !strict) {
return;
}
throw Error(`Cannot find dependency ${name} for ${packageLocation}`);
Expand All @@ -532,6 +541,7 @@ const gatherDependency = async (
false,
commonDependencyDescriptors,
languageOptions,
strict,
preferredPackageLogicalPathMap,
childLogicalPath,
);
Expand All @@ -556,6 +566,7 @@ const gatherDependency = async (
* only this package).
* @param {Record<string,string>} commonDependencies - dependencies to be added to all packages
* @param {LanguageOptions} languageOptions
* @param {boolean} strict
*/
const graphPackages = async (
maybeRead,
Expand All @@ -566,6 +577,7 @@ const graphPackages = async (
dev,
commonDependencies,
languageOptions,
strict,
) => {
const memo = create(null);
/**
Expand Down Expand Up @@ -623,6 +635,7 @@ const graphPackages = async (
dev,
commonDependencyDescriptors,
languageOptions,
strict,
);
return graph;
};
Expand Down Expand Up @@ -863,7 +876,12 @@ export const compartmentMapForNodeModules = async (
moduleSpecifier,
options = {},
) => {
const { dev = false, commonDependencies = {}, policy } = options;
const {
dev = false,
commonDependencies = {},
policy,
strict = false,
} = options;
const { maybeRead, canonical } = unpackReadPowers(readPowers);
const languageOptions = makeLanguageOptions(options);

Expand All @@ -883,6 +901,7 @@ export const compartmentMapForNodeModules = async (
dev || (conditions && conditions.has('development')),
commonDependencies,
languageOptions,
strict,
);

if (policy) {
Expand Down
7 changes: 7 additions & 0 deletions packages/compartment-mapper/src/types/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ type MapNodeModulesOptionsOmitPolicy = Partial<{
* of having the `"development"` condition.
*/
dev: boolean;
/**
* Indicates that the node_modules tree should fail to map if it does not
* strictly reach every expected package.
* By default, unreachable packages are simply omitted from the map,
* which defers some errors to when modules load.
*/
strict: boolean;
/** Dependencies to make reachable from any package */
commonDependencies: Record<string, string>;
/** Maps extensions to languages for all packages, like `txt` to `text` */
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/compartment-mapper/test/scaffold.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function scaffold(
knownFailure = false,
tags = undefined,
conditions = tags,
strict = false,
searchSuffixes = undefined,
commonDependencies = undefined,
parserForLanguage = undefined,
Expand Down Expand Up @@ -154,6 +155,7 @@ export function scaffold(
workspaceLanguageForExtension,
workspaceCommonjsLanguageForExtension,
workspaceModuleLanguageForExtension,
strict,
...additionalOptions,
});
const { namespace } = await application.import({
Expand Down Expand Up @@ -182,6 +184,7 @@ export function scaffold(
modules,
Compartment,
conditions: new Set(['development', ...(conditions || [])]),
strict,
commonDependencies,
languageForExtension,
commonjsLanguageForExtension,
Expand Down Expand Up @@ -226,6 +229,7 @@ export function scaffold(
modules,
Compartment,
conditions: new Set(['development', ...(conditions || [])]),
strict,
commonDependencies,
languageForExtension,
commonjsLanguageForExtension,
Expand Down Expand Up @@ -268,6 +272,7 @@ export function scaffold(
modules,
Compartment,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand All @@ -292,6 +297,7 @@ export function scaffold(
modules,
policy,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand Down Expand Up @@ -337,6 +343,7 @@ export function scaffold(
modules,
policy,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand Down Expand Up @@ -390,6 +397,7 @@ export function scaffold(
modules: { builtin: true },
policy,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand Down Expand Up @@ -451,6 +459,7 @@ export function scaffold(
policy,
modules,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
sourceMapHook,
Expand Down Expand Up @@ -500,6 +509,7 @@ export function scaffold(
const map = await mapNodeModules(readPowers, fixture, {
policy,
conditions: new Set(['development', ...(conditions || [])]),
strict,
commonDependencies,
languages,
languageForExtension,
Expand Down Expand Up @@ -553,6 +563,7 @@ export function scaffold(
modules,
Compartment,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand All @@ -568,6 +579,7 @@ export function scaffold(
const archiveBytes = await makeArchive(readPowers, fixture, {
modules,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand Down Expand Up @@ -605,6 +617,7 @@ export function scaffold(
modules,
Compartment,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand All @@ -620,6 +633,7 @@ export function scaffold(
const archive = await makeArchive(readPowers, fixture, {
modules,
conditions: new Set(['development', ...(conditions || [])]),
strict,
searchSuffixes,
commonDependencies,
parserForLanguage,
Expand Down
Loading

0 comments on commit 768c9cb

Please sign in to comment.