@endo/compartment-mapper 1.6.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/package.json +12 -16
  2. package/src/archive-lite.d.ts +7 -7
  3. package/src/archive-lite.d.ts.map +1 -1
  4. package/src/archive-lite.js +78 -27
  5. package/src/archive.d.ts.map +1 -1
  6. package/src/archive.js +7 -0
  7. package/src/bundle-lite.d.ts +3 -3
  8. package/src/bundle-lite.d.ts.map +1 -1
  9. package/src/bundle-lite.js +19 -24
  10. package/src/bundle.d.ts +3 -3
  11. package/src/bundle.d.ts.map +1 -1
  12. package/src/bundle.js +19 -24
  13. package/src/capture-lite.d.ts +2 -2
  14. package/src/capture-lite.d.ts.map +1 -1
  15. package/src/capture-lite.js +217 -25
  16. package/src/compartment-map.d.ts +9 -2
  17. package/src/compartment-map.d.ts.map +1 -1
  18. package/src/compartment-map.js +737 -254
  19. package/src/digest.d.ts +22 -2
  20. package/src/digest.d.ts.map +1 -1
  21. package/src/digest.js +179 -56
  22. package/src/generic-graph.d.ts +84 -0
  23. package/src/generic-graph.d.ts.map +1 -0
  24. package/src/generic-graph.js +356 -0
  25. package/src/guards.d.ts +18 -0
  26. package/src/guards.d.ts.map +1 -0
  27. package/src/guards.js +109 -0
  28. package/src/hooks.md +124 -0
  29. package/src/import-archive-lite.d.ts.map +1 -1
  30. package/src/import-archive-lite.js +15 -11
  31. package/src/import-archive.d.ts +5 -19
  32. package/src/import-archive.d.ts.map +1 -1
  33. package/src/import-archive.js +7 -27
  34. package/src/import-hook.d.ts +4 -3
  35. package/src/import-hook.d.ts.map +1 -1
  36. package/src/import-hook.js +156 -71
  37. package/src/import-lite.d.ts +6 -6
  38. package/src/import-lite.d.ts.map +1 -1
  39. package/src/import-lite.js +8 -5
  40. package/src/import.d.ts +3 -3
  41. package/src/import.d.ts.map +1 -1
  42. package/src/import.js +16 -6
  43. package/src/infer-exports.d.ts.map +1 -1
  44. package/src/infer-exports.js +16 -6
  45. package/src/json.d.ts +1 -1
  46. package/src/json.d.ts.map +1 -1
  47. package/src/json.js +10 -3
  48. package/src/link.d.ts +4 -3
  49. package/src/link.d.ts.map +1 -1
  50. package/src/link.js +70 -58
  51. package/src/node-modules.d.ts +5 -3
  52. package/src/node-modules.d.ts.map +1 -1
  53. package/src/node-modules.js +648 -245
  54. package/src/node-powers.d.ts +6 -5
  55. package/src/node-powers.d.ts.map +1 -1
  56. package/src/node-powers.js +11 -8
  57. package/src/parse-cjs-shared-export-wrapper.d.ts.map +1 -1
  58. package/src/parse-cjs-shared-export-wrapper.js +3 -1
  59. package/src/policy-format.d.ts +22 -5
  60. package/src/policy-format.d.ts.map +1 -1
  61. package/src/policy-format.js +342 -108
  62. package/src/policy.d.ts +13 -28
  63. package/src/policy.d.ts.map +1 -1
  64. package/src/policy.js +161 -106
  65. package/src/types/canonical-name.d.ts +97 -0
  66. package/src/types/canonical-name.d.ts.map +1 -0
  67. package/src/types/canonical-name.ts +151 -0
  68. package/src/types/compartment-map-schema.d.ts +114 -35
  69. package/src/types/compartment-map-schema.d.ts.map +1 -1
  70. package/src/types/compartment-map-schema.ts +202 -37
  71. package/src/types/external.d.ts +173 -29
  72. package/src/types/external.d.ts.map +1 -1
  73. package/src/types/external.ts +221 -27
  74. package/src/types/generic-graph.d.ts +17 -0
  75. package/src/types/generic-graph.d.ts.map +1 -0
  76. package/src/types/generic-graph.ts +17 -0
  77. package/src/types/internal.d.ts +24 -42
  78. package/src/types/internal.d.ts.map +1 -1
  79. package/src/types/internal.ts +52 -50
  80. package/src/types/node-modules.d.ts +101 -17
  81. package/src/types/node-modules.d.ts.map +1 -1
  82. package/src/types/node-modules.ts +142 -17
  83. package/src/types/policy-schema.d.ts +26 -11
  84. package/src/types/policy-schema.d.ts.map +1 -1
  85. package/src/types/policy-schema.ts +29 -16
  86. package/src/types/policy.d.ts +6 -2
  87. package/src/types/policy.d.ts.map +1 -1
  88. package/src/types/policy.ts +7 -2
  89. package/src/types/powers.d.ts +38 -11
  90. package/src/types/powers.d.ts.map +1 -1
  91. package/src/types/powers.ts +50 -17
  92. package/src/types/typescript.d.ts +28 -0
  93. package/src/types/typescript.d.ts.map +1 -1
  94. package/src/types/typescript.ts +37 -1
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-underscore-dangle */
1
2
  /**
2
3
  * Provides functions for constructing a compartment map that has a
3
4
  * compartment descriptor corresponding to every reachable package from an
@@ -13,22 +14,45 @@
13
14
 
14
15
  /* eslint no-shadow: 0 */
15
16
 
17
+ import { inferExportsAndAliases } from './infer-exports.js';
18
+ import { parseLocatedJson } from './json.js';
19
+ import { join } from './node-module-specifier.js';
20
+ import {
21
+ assertPolicy,
22
+ ATTENUATORS_COMPARTMENT,
23
+ ENTRY_COMPARTMENT,
24
+ generateCanonicalName,
25
+ } from './policy-format.js';
26
+ import { dependencyAllowedByPolicy, makePackagePolicy } from './policy.js';
27
+ import { unpackReadPowers } from './powers.js';
28
+ import { search, searchDescriptor } from './search.js';
29
+ import { GenericGraph, makeShortestPath } from './generic-graph.js';
30
+
16
31
  /**
17
32
  * @import {
18
33
  * CanonicalFn,
19
34
  * CompartmentDescriptor,
20
35
  * CompartmentMapDescriptor,
21
36
  * CompartmentMapForNodeModulesOptions,
37
+ * FileUrlString,
22
38
  * LanguageForExtension,
23
39
  * MapNodeModulesOptions,
40
+ * MaybeReadDescriptorFn,
24
41
  * MaybeReadFn,
25
42
  * MaybeReadPowers,
26
43
  * PackageDescriptor,
27
- * ReadDescriptorFn,
28
44
  * ReadFn,
29
45
  * ReadPowers,
30
- * SomePackagePolicy,
31
46
  * SomePolicy,
47
+ * LogFn,
48
+ * CompartmentModuleConfiguration,
49
+ * PackageCompartmentDescriptor,
50
+ * PackageCompartmentMapDescriptor,
51
+ * ScopeDescriptor,
52
+ * CanonicalName,
53
+ * SomePackagePolicy,
54
+ * PackageCompartmentDescriptorName,
55
+ * PackageData,
32
56
  * } from './types.js'
33
57
  * @import {
34
58
  * Graph,
@@ -38,29 +62,21 @@
38
62
  * GatherDependencyOptions,
39
63
  * GraphPackageOptions,
40
64
  * GraphPackagesOptions,
65
+ * LogicalPathGraph,
41
66
  * PackageDetails,
67
+ * FinalGraph,
68
+ * CanonicalNameMap,
69
+ * FinalNode,
70
+ TranslateGraphOptions,
42
71
  * } from './types/node-modules.js'
43
72
  */
44
73
 
45
- import { pathCompare } from '@endo/path-compare';
46
- import { inferExportsAndAliases } from './infer-exports.js';
47
- import { parseLocatedJson } from './json.js';
48
- import { join } from './node-module-specifier.js';
49
- import { assertPolicy } from './policy-format.js';
50
- import {
51
- ATTENUATORS_COMPARTMENT,
52
- dependencyAllowedByPolicy,
53
- getPolicyForPackage,
54
- } from './policy.js';
55
- import { unpackReadPowers } from './powers.js';
56
- import { search, searchDescriptor } from './search.js';
57
-
58
- const { assign, create, keys, values, entries } = Object;
74
+ const { assign, create, keys, values, entries, freeze } = Object;
59
75
 
60
76
  const decoder = new TextDecoder();
61
77
 
62
78
  // q, as in quote, for enquoting strings in error messages.
63
- const q = JSON.stringify;
79
+ const { quote: q } = assert;
64
80
 
65
81
  /**
66
82
  * Default logger that does nothing.
@@ -68,14 +84,103 @@ const q = JSON.stringify;
68
84
  const noop = () => {};
69
85
 
70
86
  /**
87
+ * Default handler for unknown canonical names found in policy.
88
+ * Logs a warning when a canonical name from policy is not found in the compartment map.
89
+ *
90
+ * @param {object} params
91
+ * @param {CanonicalName} params.canonicalName
92
+ * @param {string} params.message
93
+ * @param {LogFn} params.log
94
+ */
95
+ const defaultUnknownCanonicalNameHandler = ({
96
+ canonicalName,
97
+ message,
98
+ log,
99
+ }) => {
100
+ log(`WARN: Invalid resource ${q(canonicalName)} in policy: ${message}`);
101
+ };
102
+
103
+ /**
104
+ * Default filter for package dependencies based on policy.
105
+ * Filters out dependencies not allowed by the package policy.
106
+ *
107
+ * **Note:** This filter is _only_ applied if a policy is provided.
108
+ *
109
+ * @param {object} params - The parameters object
110
+ * @param {CanonicalName} params.canonicalName - The canonical name of the package
111
+ * @param {Readonly<Set<CanonicalName>>} params.dependencies - The set of dependencies
112
+ * @param {LogFn} params.log - The logging function
113
+ * @param {SomePolicy} policy - The policy to check against
114
+ * @returns {Partial<{ dependencies: Set<CanonicalName> }> | void}
115
+ */
116
+ const prePackageDependenciesFilter = (
117
+ { canonicalName, dependencies, log },
118
+ policy,
119
+ ) => {
120
+ const packagePolicy = makePackagePolicy(canonicalName, { policy });
121
+ if (!packagePolicy) {
122
+ return { dependencies };
123
+ }
124
+ const filteredDependencies = new Set(
125
+ [...dependencies].filter(dependency => {
126
+ const allowed = dependencyAllowedByPolicy(dependency, packagePolicy);
127
+ if (!allowed) {
128
+ log(
129
+ `Excluding dependency ${q(dependency)} of package ${q(canonicalName)} per policy`,
130
+ );
131
+ }
132
+ return allowed;
133
+ }),
134
+ );
135
+
136
+ return { dependencies: filteredDependencies };
137
+ };
138
+
139
+ /**
140
+ * Given a relative path andd URL, return a fully qualified URL string.
141
+ *
142
+ * @overload
71
143
  * @param {string} rel - a relative URL
72
- * @param {string} abs - a fully qualified URL
73
- * @returns {string}
144
+ * @param {URL} abs - a fully qualified URL
145
+ * @returns {string} Fully qualified URL string
146
+ */
147
+
148
+ /**
149
+ * Given a relative path and fully qualified stringlike URL, return a fully
150
+ * qualified stringlike URL.
151
+ *
152
+ * @template {string} [T=string] Type of fully qualified URL string
153
+ * @overload
154
+ * @param {string} rel - a relative URL
155
+ * @param {T} abs - a fully qualified URL
156
+ * @returns {T} Fully qualified stringlike URL
157
+ */
158
+
159
+ /**
160
+ * @param {string} rel - a relative URL
161
+ * @param {string|URL} abs - a fully qualified URL
74
162
  */
75
163
  const resolveLocation = (rel, abs) => {
76
164
  return new URL(rel, abs).toString();
77
165
  };
78
166
 
167
+ /**
168
+ * Ensures a string is a file URL (a {@link FileUrlString})
169
+ *
170
+ * @param {unknown} allegedPackageLocation - a package location to assert
171
+ * @returns {asserts allegedPackageLocation is FileUrlString}
172
+ */
173
+ const assertFileUrlString = allegedPackageLocation => {
174
+ assert(
175
+ typeof allegedPackageLocation === 'string',
176
+ `Package location must be a string, got ${q(allegedPackageLocation)}`,
177
+ );
178
+ assert(
179
+ allegedPackageLocation.startsWith('file://'),
180
+ `Package location must be a file URL, got ${q(allegedPackageLocation)}`,
181
+ );
182
+ };
183
+
79
184
  // Exported for testing:
80
185
  /**
81
186
  * @param {string} location
@@ -93,10 +198,28 @@ export const basename = location => {
93
198
  return pathname.slice(index + 1);
94
199
  };
95
200
 
201
+ /**
202
+ * Asserts that the given value is a `PackageDescriptor`.
203
+ *
204
+ * TODO: This only validates that the value is a plain object. As mentioned in
205
+ * {@link PackageDescriptor}, `name` is currently a required field, but in the
206
+ * real world this is not so. We _do_ make assumptions about the shape of a
207
+ * `PackageDescriptor`, but it may not be worth eagerly validating further.
208
+ * @param {unknown} allegedPackageDescriptor
209
+ * @returns {asserts allegedPackageDescriptor is PackageDescriptor}
210
+ */
211
+ const assertPackageDescriptor = allegedPackageDescriptor => {
212
+ assert(
213
+ typeof allegedPackageDescriptor !== 'function' &&
214
+ Object(allegedPackageDescriptor) === allegedPackageDescriptor,
215
+ `Package descriptor must be a plain object, got ${q(allegedPackageDescriptor)}`,
216
+ );
217
+ };
218
+
96
219
  /**
97
220
  * @param {MaybeReadFn} maybeRead
98
221
  * @param {string} packageLocation
99
- * @returns {Promise<object>}
222
+ * @returns {Promise<PackageDescriptor|undefined>}
100
223
  */
101
224
  const readDescriptor = async (maybeRead, packageLocation) => {
102
225
  const descriptorLocation = resolveLocation('package.json', packageLocation);
@@ -106,16 +229,20 @@ const readDescriptor = async (maybeRead, packageLocation) => {
106
229
  }
107
230
  const descriptorText = decoder.decode(descriptorBytes);
108
231
  const descriptor = parseLocatedJson(descriptorText, descriptorLocation);
232
+ assertPackageDescriptor(descriptor);
109
233
  return descriptor;
110
234
  };
111
235
 
112
236
  /**
113
- * @param {Record<string, object>} memo
237
+ * Memoized {@link readDescriptor}
238
+ *
239
+ * @param {Record<string, Promise<PackageDescriptor|undefined>>} memo
114
240
  * @param {MaybeReadFn} maybeRead
115
241
  * @param {string} packageLocation
116
- * @returns {Promise<object>}
242
+ * @returns {Promise<PackageDescriptor|undefined>}
117
243
  */
118
244
  const readDescriptorWithMemo = async (memo, maybeRead, packageLocation) => {
245
+ /** @type {Promise<PackageDescriptor|undefined>} */
119
246
  let promise = memo[packageLocation];
120
247
  if (promise !== undefined) {
121
248
  return promise;
@@ -125,90 +252,6 @@ const readDescriptorWithMemo = async (memo, maybeRead, packageLocation) => {
125
252
  return promise;
126
253
  };
127
254
 
128
- /**
129
- * Compares `logicalPath` to the current best logical path in `preferredPackageLogicalPathMap` for `packageLocation`.
130
- *
131
- * If no current best path exists, it returns `logicalPath`.
132
- *
133
- * @template {string[]} T
134
- * @template {string[]} U
135
- * @param {T} logicalPath
136
- * @param {string} packageLocation
137
- * @param {Map<string, U>} preferredPackageLogicalPathMap
138
- * @returns {T|U}
139
- */
140
- const currentBestLogicalPath = (
141
- logicalPath,
142
- packageLocation,
143
- preferredPackageLogicalPathMap,
144
- ) => {
145
- const theCurrentBest = preferredPackageLogicalPathMap.get(packageLocation);
146
- if (theCurrentBest === undefined) {
147
- return logicalPath;
148
- }
149
- return pathCompare(logicalPath, theCurrentBest) < 0
150
- ? logicalPath
151
- : theCurrentBest;
152
- };
153
-
154
- /**
155
- * Updates the shortest paths in a subgraph of `graph` starting with `packageLocation`.
156
- *
157
- * This should be called upon the second (and each subsequent) visit to a graph node.
158
- *
159
- * @param {Graph} graph Graph
160
- * @param {string} packageLocation Location of the package to start with
161
- * @param {string[]} logicalPath Current path parts of the same package
162
- * @param {Map<string, string[]>} [preferredPackageLogicalPathMap] Mapping of shortest known paths for each package location
163
- * @returns {void}
164
- */
165
- const updateShortestPaths = (
166
- graph,
167
- packageLocation,
168
- logicalPath,
169
- preferredPackageLogicalPathMap = new Map(),
170
- ) => {
171
- const node = graph[packageLocation];
172
- if (!node) {
173
- throw new ReferenceError(
174
- `Cannot find package at ${packageLocation} in graph`,
175
- );
176
- }
177
-
178
- const bestLogicalPath = currentBestLogicalPath(
179
- logicalPath,
180
- packageLocation,
181
- preferredPackageLogicalPathMap,
182
- );
183
-
184
- if (bestLogicalPath === logicalPath) {
185
- preferredPackageLogicalPathMap.set(packageLocation, bestLogicalPath);
186
-
187
- for (const name of keys(node.dependencyLocations).sort()) {
188
- const packageLocation = node.dependencyLocations[name];
189
- if (!packageLocation) {
190
- // "should never happen"
191
- throw new ReferenceError(
192
- `Expected graph node ${q(node.name)} to contain a dependency location for ${q(name)}`,
193
- );
194
- }
195
- updateShortestPaths(
196
- graph,
197
- packageLocation,
198
- [...logicalPath, name],
199
- preferredPackageLogicalPathMap,
200
- );
201
- }
202
- }
203
-
204
- // a path length of 0 means the node represents the eventual entry compartment.
205
- // we do not want to mess with that path.
206
- if (node.path.length && node.path !== bestLogicalPath) {
207
- node.path = bestLogicalPath;
208
- }
209
-
210
- return undefined;
211
- };
212
255
  /**
213
256
  * `findPackage` behaves as Node.js to find third-party modules by searching
214
257
  * parent to ancestor directories for a `node_modules` directory that contains
@@ -218,9 +261,9 @@ const updateShortestPaths = (
218
261
  * these are the locations that package managers drop a package so Node.js can
219
262
  * find it efficiently.
220
263
  *
221
- * @param {ReadDescriptorFn} readDescriptor
264
+ * @param {MaybeReadDescriptorFn} readDescriptor
222
265
  * @param {CanonicalFn} canonical
223
- * @param {string} directory
266
+ * @param {FileUrlString} directory
224
267
  * @param {string} name
225
268
  * @returns {Promise<PackageDetails|undefined>}
226
269
  */
@@ -232,6 +275,10 @@ const findPackage = async (readDescriptor, canonical, directory, name) => {
232
275
  resolveLocation(`node_modules/${name}/`, directory),
233
276
  );
234
277
 
278
+ // We have no guarantee that `canonical` will return a file URL; it spits
279
+ // back whatever we give it if `fs.promises.realpath()` rejects.
280
+ assertFileUrlString(packageLocation);
281
+
235
282
  // eslint-disable-next-line no-await-in-loop
236
283
  const packageDescriptor = await readDescriptor(packageLocation);
237
284
  if (packageDescriptor !== undefined) {
@@ -339,6 +386,37 @@ const inferParsers = (descriptor, location, languageOptions) => {
339
386
  return { ...commonjsLanguageForExtension, ...packageLanguageForExtension };
340
387
  };
341
388
 
389
+ /**
390
+ * This returns the "weight" of a package name, which is used when determining
391
+ * the shortest path.
392
+ *
393
+ * It is an analogue of the `pathCompare` function.
394
+ *
395
+ * The weight is calculated as follows:
396
+ *
397
+ * 1. The {@link String.length length} of the package name contributes a fixed
398
+ * value of `0x10000` per character. This is because the `pathCompare`
399
+ * algorithm first compares strings by length and only evaluates code unit
400
+ * values if the lengths of two strings are equal. `0x10000` is one (1)
401
+ * greater than the maximum value that {@link String.charCodeAt charCodeAt}
402
+ * can return (`0xFFFF`), which guarantees longer strings will have higher
403
+ * weights.
404
+ * 2. Each character in the package name contributes its UTF-16 code unit value
405
+ * (`0x0` thru `0xFFFF`) to the total. This is the same operation used when
406
+ * comparing two strings using comparison operators.
407
+ * 3. The total weight is the sum of 1. and 2.
408
+ *
409
+ * @param {string} packageName - Name of package to calculate weight for.
410
+ * @returns {number} Numeric weight
411
+ */
412
+ const calculatePackageWeight = packageName => {
413
+ let totalCodeValue = packageName.length * 65536; // each character contributes 65536
414
+ for (let i = 0; i < packageName.length; i += 1) {
415
+ totalCodeValue += packageName.charCodeAt(i);
416
+ }
417
+ return totalCodeValue;
418
+ };
419
+
342
420
  /**
343
421
  * `graphPackage` and {@link gatherDependency} are mutually recursive functions that
344
422
  * gather the metadata for a package and its transitive dependencies.
@@ -348,7 +426,7 @@ const inferParsers = (descriptor, location, languageOptions) => {
348
426
  * that the package exports.
349
427
  *
350
428
  * @param {string} name
351
- * @param {ReadDescriptorFn} readDescriptor
429
+ * @param {MaybeReadDescriptorFn} readDescriptor
352
430
  * @param {CanonicalFn} canonical
353
431
  * @param {Graph} graph
354
432
  * @param {PackageDetails} packageDetails
@@ -356,6 +434,7 @@ const inferParsers = (descriptor, location, languageOptions) => {
356
434
  * @param {boolean | undefined} dev
357
435
  * @param {LanguageOptions} languageOptions
358
436
  * @param {boolean} strict
437
+ * @param {LogicalPathGraph} logicalPathGraph
359
438
  * @param {GraphPackageOptions} options
360
439
  * @returns {Promise<undefined>}
361
440
  */
@@ -369,21 +448,15 @@ const graphPackage = async (
369
448
  dev,
370
449
  languageOptions,
371
450
  strict,
451
+ logicalPathGraph,
372
452
  {
373
453
  commonDependencyDescriptors = {},
374
- preferredPackageLogicalPathMap = new Map(),
375
- logicalPath = [],
376
454
  log = noop,
455
+ packageDependenciesHook,
456
+ policy,
377
457
  } = {},
378
458
  ) => {
379
459
  if (graph[packageLocation] !== undefined) {
380
- updateShortestPaths(
381
- graph,
382
- packageLocation,
383
- logicalPath,
384
- preferredPackageLogicalPathMap,
385
- );
386
-
387
460
  // Returning the promise here would create a causal cycle and stall recursion.
388
461
  return undefined;
389
462
  }
@@ -396,7 +469,7 @@ const graphPackage = async (
396
469
  });
397
470
  }
398
471
 
399
- const result = /** @type {Node} */ ({});
472
+ const result = /** @type {Node} */ ({ location: packageLocation });
400
473
  graph[packageLocation] = result;
401
474
 
402
475
  /** @type {Node['dependencyLocations']} */
@@ -447,20 +520,19 @@ const graphPackage = async (
447
520
  // use the peerDependenciesMeta field (because there was no way to define
448
521
  // an "optional" peerDependency prior to npm v7). this is plainly wrong,
449
522
  // but not exactly rare, either
450
- for (const [name, meta] of entries(peerDependenciesMeta)) {
523
+ for (const [dependencyName, meta] of entries(peerDependenciesMeta)) {
451
524
  if (Object(meta) === meta && meta.optional) {
452
- optionals.add(name);
453
- allDependencies.add(name);
525
+ optionals.add(dependencyName);
526
+ allDependencies.add(dependencyName);
454
527
  }
455
528
  }
456
529
 
457
- for (const name of keys(optionalDependencies)) {
458
- optionals.add(name);
530
+ for (const dependencyName of keys(optionalDependencies)) {
531
+ optionals.add(dependencyName);
459
532
  }
460
533
 
461
- for (const name of [...allDependencies].sort()) {
462
- const optional = optionals.has(name);
463
- const childLogicalPath = [...logicalPath, name];
534
+ for (const dependencyName of [...allDependencies].sort()) {
535
+ const optional = optionals.has(dependencyName);
464
536
  children.push(
465
537
  // Mutual recursion ahead:
466
538
  // eslint-disable-next-line no-use-before-define
@@ -470,16 +542,17 @@ const graphPackage = async (
470
542
  graph,
471
543
  dependencyLocations,
472
544
  packageLocation,
473
- name,
545
+ dependencyName,
474
546
  conditions,
475
- preferredPackageLogicalPathMap,
476
547
  languageOptions,
477
548
  strict,
549
+ logicalPathGraph,
478
550
  {
479
- childLogicalPath,
480
551
  optional,
481
552
  commonDependencyDescriptors,
482
553
  log,
554
+ packageDependenciesHook,
555
+ policy,
483
556
  },
484
557
  ),
485
558
  );
@@ -521,9 +594,9 @@ const graphPackage = async (
521
594
 
522
595
  const sourceDirname = basename(packageLocation);
523
596
 
524
- assign(result, {
597
+ /** @type {Partial<Node>} */
598
+ const partialNode = {
525
599
  name,
526
- path: logicalPath,
527
600
  label: `${name}${version ? `-v${version}` : ''}`,
528
601
  sourceDirname,
529
602
  explicitExports: exportsDescriptor !== undefined,
@@ -532,7 +605,9 @@ const graphPackage = async (
532
605
  dependencyLocations,
533
606
  types,
534
607
  parsers,
535
- });
608
+ packageDescriptor,
609
+ };
610
+ assign(result, partialNode);
536
611
 
537
612
  await Promise.all(
538
613
  values(result.externalAliases).map(async item => {
@@ -579,17 +654,17 @@ const graphPackage = async (
579
654
  /**
580
655
  * Adds information for the dependency of the package at `packageLocation` to the `graph` object.
581
656
  *
582
- * @param {ReadDescriptorFn} readDescriptor
657
+ * @param {MaybeReadDescriptorFn} readDescriptor
583
658
  * @param {CanonicalFn} canonical
584
659
  * @param {Graph} graph - the partially build graph.
585
660
  * @param {Record<string, string>} dependencyLocations
586
- * @param {string} packageLocation - location of the package of interest.
661
+ * @param {FileUrlString} packageLocation - location of the package of interest.
587
662
  * @param {string} name - name of the package of interest.
588
663
  * @param {Set<string>} conditions
589
- * @param {Map<string, Array<string>>} preferredPackageLogicalPathMap
590
664
  * @param {LanguageOptions} languageOptions
591
665
  * @param {boolean} strict - If `true`, a missing dependency will throw an exception
592
- * @param {GatherDependencyOptions} options
666
+ * @param {LogicalPathGraph} logicalPathGraph
667
+ * @param {GatherDependencyOptions} [options]
593
668
  * @returns {Promise<void>}
594
669
  */
595
670
  const gatherDependency = async (
@@ -600,14 +675,15 @@ const gatherDependency = async (
600
675
  packageLocation,
601
676
  name,
602
677
  conditions,
603
- preferredPackageLogicalPathMap,
604
678
  languageOptions,
605
679
  strict,
680
+ logicalPathGraph,
606
681
  {
607
- childLogicalPath = [],
608
682
  optional = false,
609
683
  commonDependencyDescriptors = {},
610
684
  log = noop,
685
+ packageDependenciesHook,
686
+ policy,
611
687
  } = {},
612
688
  ) => {
613
689
  const dependency = await findPackage(
@@ -616,6 +692,7 @@ const gatherDependency = async (
616
692
  packageLocation,
617
693
  name,
618
694
  );
695
+
619
696
  if (dependency === undefined) {
620
697
  // allow the dependency to be missing if optional
621
698
  if (optional || !strict) {
@@ -623,21 +700,15 @@ const gatherDependency = async (
623
700
  }
624
701
  throw Error(`Cannot find dependency ${name} for ${packageLocation}`);
625
702
  }
703
+
626
704
  dependencyLocations[name] = dependency.packageLocation;
627
705
 
628
- const bestLogicalPath = currentBestLogicalPath(
629
- childLogicalPath,
706
+ logicalPathGraph.addEdge(
707
+ packageLocation,
630
708
  dependency.packageLocation,
631
- preferredPackageLogicalPathMap,
709
+ calculatePackageWeight(name),
632
710
  );
633
711
 
634
- if (bestLogicalPath === childLogicalPath) {
635
- preferredPackageLogicalPathMap.set(
636
- dependency.packageLocation,
637
- bestLogicalPath,
638
- );
639
- }
640
-
641
712
  await graphPackage(
642
713
  name,
643
714
  readDescriptor,
@@ -648,11 +719,12 @@ const gatherDependency = async (
648
719
  false,
649
720
  languageOptions,
650
721
  strict,
722
+ logicalPathGraph,
651
723
  {
652
724
  commonDependencyDescriptors,
653
- preferredPackageLogicalPathMap,
654
- logicalPath: childLogicalPath,
655
725
  log,
726
+ packageDependenciesHook,
727
+ policy,
656
728
  },
657
729
  );
658
730
  };
@@ -663,7 +735,7 @@ const gatherDependency = async (
663
735
  *
664
736
  * @param {MaybeReadFn} maybeRead
665
737
  * @param {CanonicalFn} canonical
666
- * @param {string} packageLocation - location of the main package.
738
+ * @param {FileUrlString} packageLocation - location of the main package.
667
739
  * @param {Set<string>} conditions
668
740
  * @param {PackageDescriptor} mainPackageDescriptor - the parsed contents of the
669
741
  * main `package.json`, which was already read when searching for the
@@ -674,7 +746,9 @@ const gatherDependency = async (
674
746
  * to all packages
675
747
  * @param {LanguageOptions} languageOptions
676
748
  * @param {boolean} strict
749
+ * @param {LogicalPathGraph} logicalPathGraph
677
750
  * @param {GraphPackagesOptions} options
751
+ * @returns {Promise<Graph>}
678
752
  */
679
753
  const graphPackages = async (
680
754
  maybeRead,
@@ -686,12 +760,12 @@ const graphPackages = async (
686
760
  commonDependencies,
687
761
  languageOptions,
688
762
  strict,
689
- { log = noop } = {},
763
+ logicalPathGraph,
764
+ { log = noop, packageDependenciesHook, policy } = {},
690
765
  ) => {
691
766
  const memo = create(null);
692
767
  /**
693
- * @param {string} packageLocation
694
- * @returns {Promise<PackageDescriptor>}
768
+ * @type {MaybeReadDescriptorFn}
695
769
  */
696
770
  const readDescriptor = packageLocation =>
697
771
  readDescriptorWithMemo(memo, maybeRead, packageLocation);
@@ -700,19 +774,22 @@ const graphPackages = async (
700
774
  memo[packageLocation] = Promise.resolve(mainPackageDescriptor);
701
775
  }
702
776
 
703
- const packageDescriptor = await readDescriptor(packageLocation);
777
+ const allegedPackageDescriptor = await readDescriptor(packageLocation);
778
+
779
+ if (allegedPackageDescriptor === undefined) {
780
+ throw TypeError(
781
+ `Cannot find package.json for application at ${packageLocation}`,
782
+ );
783
+ }
784
+
785
+ assertPackageDescriptor(allegedPackageDescriptor);
786
+ const packageDescriptor = allegedPackageDescriptor;
704
787
 
705
788
  conditions = new Set(conditions || []);
706
789
  conditions.add('import');
707
790
  conditions.add('default');
708
791
  conditions.add('endo');
709
792
 
710
- if (packageDescriptor === undefined) {
711
- throw Error(
712
- `Cannot find package.json for application at ${packageLocation}`,
713
- );
714
- }
715
-
716
793
  // Resolve common dependencies.
717
794
  /** @type {CommonDependencyDescriptors} */
718
795
  const commonDependencyDescriptors = {};
@@ -730,6 +807,8 @@ const graphPackages = async (
730
807
  };
731
808
  }
732
809
 
810
+ logicalPathGraph.addNode(packageLocation);
811
+
733
812
  const graph = create(null);
734
813
  await graphPackage(
735
814
  packageDescriptor.name,
@@ -744,9 +823,12 @@ const graphPackages = async (
744
823
  dev,
745
824
  languageOptions,
746
825
  strict,
826
+ logicalPathGraph,
747
827
  {
748
828
  commonDependencyDescriptors,
749
829
  log,
830
+ packageDependenciesHook,
831
+ policy,
750
832
  },
751
833
  );
752
834
  return graph;
@@ -761,19 +843,101 @@ const graphPackages = async (
761
843
  * @param {Graph} graph
762
844
  * @param {Set<string>} conditions - build conditions about the target environment
763
845
  * for selecting relevant exports, e.g., "browser" or "node".
764
- * @param {SomePolicy} [policy]
765
- * @returns {CompartmentMapDescriptor}
846
+ * @param {TranslateGraphOptions} [options]
847
+ * @returns {PackageCompartmentMapDescriptor}
766
848
  */
767
849
  const translateGraph = (
768
850
  entryPackageLocation,
769
851
  entryModuleSpecifier,
770
852
  graph,
771
853
  conditions,
772
- policy,
854
+ { policy, log = noop, packageDependenciesHook } = {},
773
855
  ) => {
774
- /** @type {CompartmentMapDescriptor['compartments']} */
856
+ /** @type {Record<PackageCompartmentDescriptorName, PackageCompartmentDescriptor>} */
775
857
  const compartments = create(null);
776
858
 
859
+ /**
860
+ * Execute package dependencies hooks: default first (if policy exists), then user-provided.
861
+ *
862
+ * @param {CanonicalName} label
863
+ * @param {Record<string, FileUrlString>} dependencyLocations
864
+ * @returns {Record<string, FileUrlString>}
865
+ */
866
+ const executePackageDependenciesHook = (label, dependencyLocations) => {
867
+ const dependencies = new Set(
868
+ values(dependencyLocations).map(
869
+ dependencyLocation => graph[dependencyLocation].label,
870
+ ),
871
+ );
872
+
873
+ const packageDependenciesHookInput = {
874
+ canonicalName: label,
875
+ dependencies: new Set(dependencies),
876
+ log,
877
+ };
878
+
879
+ // Call default filter first if policy exists
880
+ let packageDependenciesHookResult;
881
+ if (policy) {
882
+ packageDependenciesHookResult = prePackageDependenciesFilter(
883
+ packageDependenciesHookInput,
884
+ policy,
885
+ );
886
+ }
887
+
888
+ // Then call user-provided hook if it exists
889
+ if (packageDependenciesHook) {
890
+ const userResult = packageDependenciesHook(packageDependenciesHookInput);
891
+ // If user hook also returned a result, use it (overrides default)
892
+ if (userResult?.dependencies) {
893
+ packageDependenciesHookResult = userResult;
894
+ }
895
+ }
896
+
897
+ // if "dependencies" are in here, then something changed the list.
898
+ if (packageDependenciesHookResult?.dependencies) {
899
+ const size = packageDependenciesHookResult.dependencies.size;
900
+ if (typeof size === 'number' && size > 0) {
901
+ // because the list of dependencies contains canonical names, we need to lookup any new ones.
902
+ const nodesByCanonicalName = new Map(
903
+ entries(graph).map(([location, node]) => [
904
+ node.label,
905
+ {
906
+ ...node,
907
+ packageLocation: /** @type {FileUrlString} */ (location),
908
+ },
909
+ ]),
910
+ );
911
+
912
+ /** @type {typeof dependencyLocations} */
913
+ const newDependencyLocations = {};
914
+ try {
915
+ for (const label of packageDependenciesHookResult.dependencies) {
916
+ const { name, packageLocation } =
917
+ nodesByCanonicalName.get(label) ?? create(null);
918
+ if (name && packageLocation) {
919
+ newDependencyLocations[name] = packageLocation;
920
+ } else {
921
+ log(
922
+ `WARNING: packageDependencies hook returned unknown package with label ${q(label)}`,
923
+ );
924
+ }
925
+ }
926
+ return newDependencyLocations;
927
+ } catch {
928
+ log(
929
+ `WARNING: packageDependencies hook returned invalid value ${q(
930
+ packageDependenciesHookResult,
931
+ )}; using original dependencies`,
932
+ );
933
+ }
934
+ } else {
935
+ dependencyLocations = create(null);
936
+ }
937
+ }
938
+ return dependencyLocations;
939
+ };
940
+
777
941
  // For each package, build a map of all the external modules the package can
778
942
  // import from other packages.
779
943
  // The keys of this map are the full specifiers of those modules from the
@@ -783,36 +947,24 @@ const translateGraph = (
783
947
  // The full map includes every exported module from every dependencey
784
948
  // package and is a complete list of every external module that the
785
949
  // corresponding compartment can import.
786
- for (const dependeeLocation of keys(graph).sort()) {
950
+ for (const dependeeLocation of /** @type {PackageCompartmentDescriptorName[]} */ (
951
+ keys(graph).sort()
952
+ )) {
787
953
  const {
788
954
  name,
789
- path,
790
955
  label,
791
956
  sourceDirname,
792
- dependencyLocations,
793
957
  internalAliases,
794
958
  parsers,
795
959
  types,
960
+ packageDescriptor,
796
961
  } = graph[dependeeLocation];
797
- /** @type {CompartmentDescriptor['modules']} */
962
+ /** @type {Record<string, CompartmentModuleConfiguration>} */
798
963
  const moduleDescriptors = create(null);
799
- /** @type {CompartmentDescriptor['scopes']} */
964
+ /** @type {Record<string, ScopeDescriptor<PackageCompartmentDescriptorName>>} */
800
965
  const scopes = create(null);
801
966
 
802
- /**
803
- * List of all the compartments (by name) that this compartment can import from.
804
- *
805
- * @type {Set<string>}
806
- */
807
- const compartmentNames = new Set();
808
- const packagePolicy = getPolicyForPackage(
809
- {
810
- isEntry: dependeeLocation === entryPackageLocation,
811
- name,
812
- path,
813
- },
814
- policy,
815
- );
967
+ const packagePolicy = makePackagePolicy(label, { policy });
816
968
 
817
969
  /* c8 ignore next */
818
970
  if (policy && !packagePolicy) {
@@ -820,34 +972,29 @@ const translateGraph = (
820
972
  throw new TypeError('Unexpectedly falsy package policy');
821
973
  }
822
974
 
975
+ let dependencyLocations = graph[dependeeLocation].dependencyLocations;
976
+ dependencyLocations = executePackageDependenciesHook(
977
+ label,
978
+ dependencyLocations,
979
+ );
980
+
823
981
  /**
824
982
  * @param {string} dependencyName
825
- * @param {string} packageLocation
983
+ * @param {PackageCompartmentDescriptorName} packageLocation
826
984
  */
827
985
  const digestExternalAliases = (dependencyName, packageLocation) => {
828
- const { externalAliases, explicitExports, name, path } =
829
- graph[packageLocation];
986
+ const { externalAliases, explicitExports } = graph[packageLocation];
830
987
  for (const exportPath of keys(externalAliases).sort()) {
831
988
  const targetPath = externalAliases[exportPath];
832
989
  // dependency name may be different from package's name,
833
- // as in the case of browser field dependency replacements
990
+ // as in the case of browser field dependency replacements.
991
+ // note that policy still applies
834
992
  const localPath = join(dependencyName, exportPath);
835
- if (
836
- !policy ||
837
- (packagePolicy &&
838
- dependencyAllowedByPolicy(
839
- {
840
- name,
841
- path,
842
- },
843
- packagePolicy,
844
- ))
845
- ) {
846
- moduleDescriptors[localPath] = {
847
- compartment: packageLocation,
848
- module: targetPath,
849
- };
850
- }
993
+ // if we have policy, this has already been vetted
994
+ moduleDescriptors[localPath] = {
995
+ compartment: packageLocation,
996
+ module: targetPath,
997
+ };
851
998
  }
852
999
  // if the exports field is not present, then all modules must be accessible
853
1000
  if (!explicitExports) {
@@ -862,7 +1009,6 @@ const translateGraph = (
862
1009
  for (const dependencyName of keys(dependencyLocations).sort()) {
863
1010
  const dependencyLocation = dependencyLocations[dependencyName];
864
1011
  digestExternalAliases(dependencyName, dependencyLocation);
865
- compartmentNames.add(dependencyLocation);
866
1012
  }
867
1013
  // digest own internal aliases
868
1014
  for (const modulePath of keys(internalAliases).sort()) {
@@ -879,9 +1025,9 @@ const translateGraph = (
879
1025
  }
880
1026
 
881
1027
  compartments[dependeeLocation] = {
1028
+ version: packageDescriptor.version ? packageDescriptor.version : '',
882
1029
  label,
883
1030
  name,
884
- path,
885
1031
  location: dependeeLocation,
886
1032
  sourceDirname,
887
1033
  modules: moduleDescriptors,
@@ -889,7 +1035,6 @@ const translateGraph = (
889
1035
  parsers,
890
1036
  types,
891
1037
  policy: /** @type {SomePackagePolicy} */ (packagePolicy),
892
- compartments: compartmentNames,
893
1038
  };
894
1039
  }
895
1040
 
@@ -898,7 +1043,7 @@ const translateGraph = (
898
1043
  // https://github.com/endojs/endo/issues/2388
899
1044
  tags: [...conditions],
900
1045
  entry: {
901
- compartment: entryPackageLocation,
1046
+ compartment: /** @type {FileUrlString} */ (entryPackageLocation),
902
1047
  module: entryModuleSpecifier,
903
1048
  },
904
1049
  compartments,
@@ -972,23 +1117,200 @@ const makeLanguageOptions = ({
972
1117
  workspaceModuleLanguageForExtension,
973
1118
  };
974
1119
  };
1120
+ /**
1121
+ * Creates a `Node` in `graph` corresponding to the "attenuators" Compartment.
1122
+ *
1123
+ * Only does so if `policy` is provided.
1124
+ *
1125
+ * @param {Graph} graph Graph
1126
+ * @param {Node} entryNode Entry node of the grpah
1127
+ * @param {SomePolicy} [policy]
1128
+ * @throws If there's already a `Node` in `graph` for the "attenuators"
1129
+ * Compartment
1130
+ * @returns {void}
1131
+ */
1132
+ const makeAttenuatorsNode = (graph, entryNode, policy) => {
1133
+ if (policy) {
1134
+ assertPolicy(policy);
1135
+
1136
+ assert(
1137
+ graph[ATTENUATORS_COMPARTMENT] === undefined,
1138
+ `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`,
1139
+ );
1140
+
1141
+ graph[ATTENUATORS_COMPARTMENT] = {
1142
+ ...entryNode,
1143
+ internalAliases: {},
1144
+ externalAliases: {},
1145
+ packageDescriptor: { name: ATTENUATORS_COMPARTMENT },
1146
+ name: ATTENUATORS_COMPARTMENT,
1147
+ };
1148
+ }
1149
+ };
975
1150
 
976
1151
  /**
977
- * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers
978
- * @param {string} packageLocation
1152
+ * Transforms a `Graph` into a readonly `FinalGraph`, in preparation for
1153
+ * conversion to a `CompartmentDescriptor`.
1154
+ *
1155
+ * @param {Graph} graph Graph
1156
+ * @param {LogicalPathGraph} logicalPathGraph Logical path graph
1157
+ * @param {FileUrlString} entryPackageLocation Entry package location
1158
+ * @param {CanonicalNameMap} canonicalNameMap Mapping of canonical names to `Node` names (keys in `graph`)
1159
+ * @returns {Readonly<FinalGraph>}
1160
+ */
1161
+ const finalizeGraph = (
1162
+ graph,
1163
+ logicalPathGraph,
1164
+ entryPackageLocation,
1165
+ canonicalNameMap,
1166
+ ) => {
1167
+ const shortestPath = makeShortestPath(logicalPathGraph);
1168
+
1169
+ // neither the entry package nor the attenuators compartment have a path; omit
1170
+ const {
1171
+ [ATTENUATORS_COMPARTMENT]: attenuatorsNode,
1172
+ [entryPackageLocation]: entryNode,
1173
+ ...subgraph
1174
+ } = graph;
1175
+
1176
+ /** @type {FinalGraph} */
1177
+ const finalGraph = create(null);
1178
+
1179
+ /** @type {Readonly<FinalNode>} */
1180
+ finalGraph[entryPackageLocation] = freeze({
1181
+ ...entryNode,
1182
+ label: generateCanonicalName({
1183
+ isEntry: true,
1184
+ path: [],
1185
+ }),
1186
+ });
1187
+
1188
+ canonicalNameMap.set(ENTRY_COMPARTMENT, entryPackageLocation);
1189
+
1190
+ if (attenuatorsNode) {
1191
+ /** @type {Readonly<FinalNode>} */
1192
+ finalGraph[ATTENUATORS_COMPARTMENT] = freeze({
1193
+ ...attenuatorsNode,
1194
+ label: generateCanonicalName({
1195
+ name: ATTENUATORS_COMPARTMENT,
1196
+ path: [],
1197
+ }),
1198
+ });
1199
+ }
1200
+
1201
+ const subgraphEntries = /** @type {[FileUrlString, Node][]} */ (
1202
+ entries(subgraph)
1203
+ );
1204
+
1205
+ for (const [location, node] of subgraphEntries) {
1206
+ const shortestLogicalPath = shortestPath(entryPackageLocation, location);
1207
+
1208
+ // the first element will always be the root package location; this is omitted from the path.
1209
+ shortestLogicalPath.shift();
1210
+
1211
+ const path = shortestLogicalPath.map(location => graph[location].name);
1212
+ const canonicalName = generateCanonicalName({ path });
1213
+
1214
+ /** @type {Readonly<FinalNode>} */
1215
+ const finalNode = freeze({
1216
+ ...node,
1217
+ label: canonicalName,
1218
+ });
1219
+
1220
+ canonicalNameMap.set(canonicalName, location);
1221
+
1222
+ finalGraph[location] = finalNode;
1223
+ }
1224
+
1225
+ for (const node of values(finalGraph)) {
1226
+ Object.freeze(node);
1227
+ }
1228
+
1229
+ return freeze(finalGraph);
1230
+ };
1231
+
1232
+ /**
1233
+ * Returns an array of "issue" objects if any resources referenced in `policy`
1234
+ * are unknown.
1235
+ *
1236
+ * @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
1237
+ * @param {SomePolicy} policy Policy to validate
1238
+ * @returns {Array<{canonicalName: CanonicalName, message: string, path:
1239
+ * string[], suggestion?: CanonicalName}>} Array of issue objects, or `undefined` if no issues were
1240
+ * found
1241
+ */
1242
+ const validatePolicyResources = (canonicalNames, policy) => {
1243
+ /**
1244
+ * Finds a suggestion for `badName` if it is a suffix of any
1245
+ * canonical name in `canonicalNames`.
1246
+ *
1247
+ * @param {string} badName Unknown canonical name
1248
+ * @returns {CanonicalName | undefined}
1249
+ */
1250
+ const findSuggestion = badName => {
1251
+ for (const canonicalName of canonicalNames) {
1252
+ if (canonicalName.endsWith(`>${badName}`)) {
1253
+ return canonicalName;
1254
+ }
1255
+ }
1256
+ return undefined;
1257
+ };
1258
+
1259
+ /** @type {Array<{canonicalName: CanonicalName, message: string, path: string[], suggestion?: CanonicalName}>} */
1260
+ const issues = [];
1261
+ for (const [resourceName, resourcePolicy] of entries(
1262
+ policy.resources ?? {},
1263
+ )) {
1264
+ if (!canonicalNames.has(resourceName)) {
1265
+ const issueMessage = `Resource ${q(resourceName)} was not found`;
1266
+ const suggestion = findSuggestion(resourceName);
1267
+ const issue = {
1268
+ canonicalName: resourceName,
1269
+ message: issueMessage,
1270
+ path: ['resources', resourceName],
1271
+ };
1272
+ if (suggestion) {
1273
+ issue.suggestion = suggestion;
1274
+ }
1275
+ issues.push(issue);
1276
+ }
1277
+ if (typeof resourcePolicy?.packages === 'object') {
1278
+ for (const packageName of keys(resourcePolicy.packages)) {
1279
+ if (!canonicalNames.has(packageName)) {
1280
+ const issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1281
+ const suggestion = findSuggestion(packageName);
1282
+ const issue = {
1283
+ canonicalName: packageName,
1284
+ message: issueMessage,
1285
+ path: ['resources', resourceName, 'packages', packageName],
1286
+ };
1287
+ if (suggestion) {
1288
+ issue.suggestion = suggestion;
1289
+ }
1290
+ issues.push(issue);
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+
1296
+ return issues;
1297
+ };
1298
+
1299
+ /**
1300
+ * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
1301
+ * @param {FileUrlString} entryPackageLocation
979
1302
  * @param {Set<string>} conditionsOption
980
1303
  * @param {PackageDescriptor} packageDescriptor
981
- * @param {string} moduleSpecifier
1304
+ * @param {string} entryModuleSpecifier
982
1305
  * @param {CompartmentMapForNodeModulesOptions} [options]
983
- * @returns {Promise<CompartmentMapDescriptor>}
984
- * @deprecated Use {@link mapNodeModules} instead.
1306
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
985
1307
  */
986
- export const compartmentMapForNodeModules = async (
1308
+ export const compartmentMapForNodeModules_ = async (
987
1309
  readPowers,
988
- packageLocation,
1310
+ entryPackageLocation,
989
1311
  conditionsOption,
990
1312
  packageDescriptor,
991
- moduleSpecifier,
1313
+ entryModuleSpecifier,
992
1314
  options = {},
993
1315
  ) => {
994
1316
  const {
@@ -997,52 +1319,109 @@ export const compartmentMapForNodeModules = async (
997
1319
  policy,
998
1320
  strict = false,
999
1321
  log = noop,
1322
+ unknownCanonicalNameHook,
1323
+ packageDataHook,
1324
+ packageDependenciesHook,
1000
1325
  } = options;
1001
1326
  const { maybeRead, canonical } = unpackReadPowers(readPowers);
1002
1327
  const languageOptions = makeLanguageOptions(options);
1003
1328
 
1004
1329
  const conditions = new Set(conditionsOption || []);
1005
1330
 
1331
+ /**
1332
+ * This graph will contain nodes for each package location (a
1333
+ * {@link FileUrlString}) and edges representing dependencies between packages.
1334
+ *
1335
+ * The edges are weighted by {@link calculatePackageWeight}.
1336
+ *
1337
+ * @type {LogicalPathGraph}
1338
+ */
1339
+ const logicalPathGraph = new GenericGraph();
1340
+
1006
1341
  // dev is only set for the entry package, and implied by the development
1007
1342
  // condition.
1008
- // The dev option is deprecated in favor of using conditions, since that
1009
- // covers more intentional behaviors of the development mode.
1010
1343
 
1011
1344
  const graph = await graphPackages(
1012
1345
  maybeRead,
1013
1346
  canonical,
1014
- packageLocation,
1347
+ entryPackageLocation,
1015
1348
  conditions,
1016
1349
  packageDescriptor,
1017
1350
  dev || (conditions && conditions.has('development')),
1018
1351
  commonDependencies,
1019
1352
  languageOptions,
1020
1353
  strict,
1021
- { log },
1354
+ logicalPathGraph,
1355
+ { log, policy, packageDependenciesHook },
1022
1356
  );
1023
1357
 
1024
- if (policy) {
1025
- assertPolicy(policy);
1358
+ makeAttenuatorsNode(graph, graph[entryPackageLocation], policy);
1026
1359
 
1027
- assert(
1028
- graph[ATTENUATORS_COMPARTMENT] === undefined,
1029
- `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`,
1030
- );
1360
+ /**
1361
+ * @type {CanonicalNameMap}
1362
+ */
1363
+ const canonicalNameMap = new Map();
1031
1364
 
1032
- graph[ATTENUATORS_COMPARTMENT] = {
1033
- ...graph[packageLocation],
1034
- externalAliases: {},
1035
- label: ATTENUATORS_COMPARTMENT,
1036
- name: ATTENUATORS_COMPARTMENT,
1037
- };
1365
+ const finalGraph = finalizeGraph(
1366
+ graph,
1367
+ logicalPathGraph,
1368
+ entryPackageLocation,
1369
+ canonicalNameMap,
1370
+ );
1371
+
1372
+ // if policy exists, cross-reference the policy "resources" against the list
1373
+ // of known canonical names and fire the `unknownCanonicalName` hook for each
1374
+ // unknown resource, if found
1375
+ if (policy) {
1376
+ const canonicalNames = new Set(canonicalNameMap.keys());
1377
+ const issues = validatePolicyResources(canonicalNames, policy) ?? [];
1378
+ // Call default handler first if policy exists
1379
+ for (const { message, canonicalName, path, suggestion } of issues) {
1380
+ const hookInput = {
1381
+ canonicalName,
1382
+ message,
1383
+ path,
1384
+ log,
1385
+ };
1386
+ if (suggestion) {
1387
+ hookInput.suggestion = suggestion;
1388
+ }
1389
+ defaultUnknownCanonicalNameHandler(hookInput);
1390
+ // Then call user-provided hook if it exists
1391
+ if (unknownCanonicalNameHook) {
1392
+ unknownCanonicalNameHook(hookInput);
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ // Fire packageData hook with all package data before translateGraph
1398
+ if (packageDataHook) {
1399
+ const packageData =
1400
+ /** @type {Map<PackageCompartmentDescriptorName, PackageData>} */ (
1401
+ new Map(
1402
+ values(finalGraph).map(node => [
1403
+ node.label,
1404
+ {
1405
+ name: node.name,
1406
+ packageDescriptor: node.packageDescriptor,
1407
+ location: node.location,
1408
+ canonicalName: node.label,
1409
+ },
1410
+ ]),
1411
+ )
1412
+ );
1413
+ packageDataHook({
1414
+ packageData,
1415
+ log,
1416
+ });
1038
1417
  }
1039
1418
 
1040
1419
  const compartmentMap = translateGraph(
1041
- packageLocation,
1042
- moduleSpecifier,
1043
- graph,
1420
+ entryPackageLocation,
1421
+ entryModuleSpecifier,
1422
+ finalGraph,
1044
1423
  conditions,
1045
- policy,
1424
+ { policy, log, packageDependenciesHook },
1046
1425
  );
1047
1426
 
1048
1427
  return compartmentMap;
@@ -1054,15 +1433,24 @@ export const compartmentMapForNodeModules = async (
1054
1433
  *
1055
1434
  * Locates the {@link PackageDescriptor} for the module at `moduleLocation`
1056
1435
  *
1057
- * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers
1436
+ * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
1058
1437
  * @param {string} moduleLocation
1059
1438
  * @param {MapNodeModulesOptions} [options]
1060
- * @returns {Promise<CompartmentMapDescriptor>}
1439
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
1061
1440
  */
1062
1441
  export const mapNodeModules = async (
1063
1442
  readPowers,
1064
1443
  moduleLocation,
1065
- { tags = new Set(), conditions = tags, log = noop, ...otherOptions } = {},
1444
+ {
1445
+ tags = new Set(),
1446
+ conditions = tags,
1447
+ log = noop,
1448
+ unknownCanonicalNameHook,
1449
+ packageDataHook,
1450
+ packageDependenciesHook,
1451
+ policy,
1452
+ ...otherOptions
1453
+ } = {},
1066
1454
  ) => {
1067
1455
  const {
1068
1456
  packageLocation,
@@ -1071,16 +1459,31 @@ export const mapNodeModules = async (
1071
1459
  moduleSpecifier,
1072
1460
  } = await search(readPowers, moduleLocation, { log });
1073
1461
 
1074
- const packageDescriptor = /** @type {PackageDescriptor} */ (
1075
- parseLocatedJson(packageDescriptorText, packageDescriptorLocation)
1076
- );
1462
+ const packageDescriptor = /** @type {typeof parseLocatedJson<unknown>} */ (
1463
+ parseLocatedJson
1464
+ )(packageDescriptorText, packageDescriptorLocation);
1077
1465
 
1078
- return compartmentMapForNodeModules(
1466
+ assertPackageDescriptor(packageDescriptor);
1467
+ assertFileUrlString(packageLocation);
1468
+
1469
+ return compartmentMapForNodeModules_(
1079
1470
  readPowers,
1080
1471
  packageLocation,
1081
1472
  conditions,
1082
1473
  packageDescriptor,
1083
1474
  moduleSpecifier,
1084
- { log, ...otherOptions },
1475
+ {
1476
+ log,
1477
+ policy,
1478
+ unknownCanonicalNameHook,
1479
+ packageDependenciesHook,
1480
+ packageDataHook,
1481
+ ...otherOptions,
1482
+ },
1085
1483
  );
1086
1484
  };
1485
+
1486
+ /**
1487
+ * @deprecated Use {@link mapNodeModules} instead.
1488
+ */
1489
+ export const compartmentMapForNodeModules = compartmentMapForNodeModules_;