@endo/compartment-mapper 1.6.2 → 1.6.3

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.
@@ -13,18 +13,32 @@
13
13
 
14
14
  /* eslint no-shadow: 0 */
15
15
 
16
+ import { inferExportsAndAliases } from './infer-exports.js';
17
+ import { parseLocatedJson } from './json.js';
18
+ import { join } from './node-module-specifier.js';
19
+ import { assertPolicy } from './policy-format.js';
20
+ import {
21
+ ATTENUATORS_COMPARTMENT,
22
+ dependencyAllowedByPolicy,
23
+ getPolicyForPackage,
24
+ } from './policy.js';
25
+ import { unpackReadPowers } from './powers.js';
26
+ import { search, searchDescriptor } from './search.js';
27
+ import { GenericGraph, makeShortestPath } from './generic-graph.js';
28
+
16
29
  /**
17
30
  * @import {
18
31
  * CanonicalFn,
19
32
  * CompartmentDescriptor,
20
33
  * CompartmentMapDescriptor,
21
34
  * CompartmentMapForNodeModulesOptions,
35
+ * FileUrlString,
22
36
  * LanguageForExtension,
23
37
  * MapNodeModulesOptions,
38
+ * MaybeReadDescriptorFn,
24
39
  * MaybeReadFn,
25
40
  * MaybeReadPowers,
26
41
  * PackageDescriptor,
27
- * ReadDescriptorFn,
28
42
  * ReadFn,
29
43
  * ReadPowers,
30
44
  * SomePackagePolicy,
@@ -38,23 +52,11 @@
38
52
  * GatherDependencyOptions,
39
53
  * GraphPackageOptions,
40
54
  * GraphPackagesOptions,
55
+ * LogicalPathGraph,
41
56
  * PackageDetails,
42
57
  * } from './types/node-modules.js'
43
58
  */
44
59
 
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
60
  const { assign, create, keys, values, entries } = Object;
59
61
 
60
62
  const decoder = new TextDecoder();
@@ -68,14 +70,50 @@ const q = JSON.stringify;
68
70
  const noop = () => {};
69
71
 
70
72
  /**
73
+ * Given a relative path andd URL, return a fully qualified URL string.
74
+ *
75
+ * @overload
71
76
  * @param {string} rel - a relative URL
72
- * @param {string} abs - a fully qualified URL
73
- * @returns {string}
77
+ * @param {URL} abs - a fully qualified URL
78
+ * @returns {string} Fully qualified URL string
79
+ */
80
+
81
+ /**
82
+ * Given a relative path and fully qualified stringlike URL, return a fully
83
+ * qualified stringlike URL.
84
+ *
85
+ * @template {string} [T=string] Type of fully qualified URL string
86
+ * @overload
87
+ * @param {string} rel - a relative URL
88
+ * @param {T} abs - a fully qualified URL
89
+ * @returns {T} Fully qualified stringlike URL
90
+ */
91
+
92
+ /**
93
+ * @param {string} rel - a relative URL
94
+ * @param {string|URL} abs - a fully qualified URL
74
95
  */
75
96
  const resolveLocation = (rel, abs) => {
76
97
  return new URL(rel, abs).toString();
77
98
  };
78
99
 
100
+ /**
101
+ * Ensures a string is a file URL (a {@link FileUrlString})
102
+ *
103
+ * @param {unknown} allegedPackageLocation - a package location to assert
104
+ * @returns {asserts allegedPackageLocation is FileUrlString}
105
+ */
106
+ const assertFileUrlString = allegedPackageLocation => {
107
+ assert(
108
+ typeof allegedPackageLocation === 'string',
109
+ `Package location must be a string, got ${q(allegedPackageLocation)}`,
110
+ );
111
+ assert(
112
+ allegedPackageLocation.startsWith('file://'),
113
+ `Package location must be a file URL, got ${q(allegedPackageLocation)}`,
114
+ );
115
+ };
116
+
79
117
  // Exported for testing:
80
118
  /**
81
119
  * @param {string} location
@@ -93,10 +131,28 @@ export const basename = location => {
93
131
  return pathname.slice(index + 1);
94
132
  };
95
133
 
134
+ /**
135
+ * Asserts that the given value is a `PackageDescriptor`.
136
+ *
137
+ * TODO: This only validates that the value is a plain object. As mentioned in
138
+ * {@link PackageDescriptor}, `name` is currently a required field, but in the
139
+ * real world this is not so. We _do_ make assumptions about the shape of a
140
+ * `PackageDescriptor`, but it may not be worth eagerly validating further.
141
+ * @param {unknown} allegedPackageDescriptor
142
+ * @returns {asserts allegedPackageDescriptor is PackageDescriptor}
143
+ */
144
+ const assertPackageDescriptor = allegedPackageDescriptor => {
145
+ assert(
146
+ typeof allegedPackageDescriptor !== 'function' &&
147
+ Object(allegedPackageDescriptor) === allegedPackageDescriptor,
148
+ `Package descriptor must be a plain object, got ${q(allegedPackageDescriptor)}`,
149
+ );
150
+ };
151
+
96
152
  /**
97
153
  * @param {MaybeReadFn} maybeRead
98
154
  * @param {string} packageLocation
99
- * @returns {Promise<object>}
155
+ * @returns {Promise<PackageDescriptor|undefined>}
100
156
  */
101
157
  const readDescriptor = async (maybeRead, packageLocation) => {
102
158
  const descriptorLocation = resolveLocation('package.json', packageLocation);
@@ -106,16 +162,20 @@ const readDescriptor = async (maybeRead, packageLocation) => {
106
162
  }
107
163
  const descriptorText = decoder.decode(descriptorBytes);
108
164
  const descriptor = parseLocatedJson(descriptorText, descriptorLocation);
165
+ assertPackageDescriptor(descriptor);
109
166
  return descriptor;
110
167
  };
111
168
 
112
169
  /**
113
- * @param {Record<string, object>} memo
170
+ * Memoized {@link readDescriptor}
171
+ *
172
+ * @param {Record<string, Promise<PackageDescriptor|undefined>>} memo
114
173
  * @param {MaybeReadFn} maybeRead
115
174
  * @param {string} packageLocation
116
- * @returns {Promise<object>}
175
+ * @returns {Promise<PackageDescriptor|undefined>}
117
176
  */
118
177
  const readDescriptorWithMemo = async (memo, maybeRead, packageLocation) => {
178
+ /** @type {Promise<PackageDescriptor|undefined>} */
119
179
  let promise = memo[packageLocation];
120
180
  if (promise !== undefined) {
121
181
  return promise;
@@ -125,90 +185,6 @@ const readDescriptorWithMemo = async (memo, maybeRead, packageLocation) => {
125
185
  return promise;
126
186
  };
127
187
 
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
188
  /**
213
189
  * `findPackage` behaves as Node.js to find third-party modules by searching
214
190
  * parent to ancestor directories for a `node_modules` directory that contains
@@ -218,9 +194,9 @@ const updateShortestPaths = (
218
194
  * these are the locations that package managers drop a package so Node.js can
219
195
  * find it efficiently.
220
196
  *
221
- * @param {ReadDescriptorFn} readDescriptor
197
+ * @param {MaybeReadDescriptorFn} readDescriptor
222
198
  * @param {CanonicalFn} canonical
223
- * @param {string} directory
199
+ * @param {FileUrlString} directory
224
200
  * @param {string} name
225
201
  * @returns {Promise<PackageDetails|undefined>}
226
202
  */
@@ -232,6 +208,10 @@ const findPackage = async (readDescriptor, canonical, directory, name) => {
232
208
  resolveLocation(`node_modules/${name}/`, directory),
233
209
  );
234
210
 
211
+ // We have no guarantee that `canonical` will return a file URL; it spits
212
+ // back whatever we give it if `fs.promises.realpath()` rejects.
213
+ assertFileUrlString(packageLocation);
214
+
235
215
  // eslint-disable-next-line no-await-in-loop
236
216
  const packageDescriptor = await readDescriptor(packageLocation);
237
217
  if (packageDescriptor !== undefined) {
@@ -339,6 +319,37 @@ const inferParsers = (descriptor, location, languageOptions) => {
339
319
  return { ...commonjsLanguageForExtension, ...packageLanguageForExtension };
340
320
  };
341
321
 
322
+ /**
323
+ * This returns the "weight" of a package name, which is used when determining
324
+ * the shortest path.
325
+ *
326
+ * It is an analogue of the `pathCompare` function.
327
+ *
328
+ * The weight is calculated as follows:
329
+ *
330
+ * 1. The {@link String.length length} of the package name contributes a fixed
331
+ * value of `0x10000` per character. This is because the `pathCompare`
332
+ * algorithm first compares strings by length and only evaluates code unit
333
+ * values if the lengths of two strings are equal. `0x10000` is one (1)
334
+ * greater than the maximum value that {@link String.charCodeAt charCodeAt}
335
+ * can return (`0xFFFF`), which guarantees longer strings will have higher
336
+ * weights.
337
+ * 2. Each character in the package name contributes its UTF-16 code unit value
338
+ * (`0x0` thru `0xFFFF`) to the total. This is the same operation used when
339
+ * comparing two strings using comparison operators.
340
+ * 3. The total weight is the sum of 1. and 2.
341
+ *
342
+ * @param {string} packageName - Name of package to calculate weight for.
343
+ * @returns {number} Numeric weight
344
+ */
345
+ const calculatePackageWeight = packageName => {
346
+ let totalCodeValue = packageName.length * 65536; // each character contributes 65536
347
+ for (let i = 0; i < packageName.length; i += 1) {
348
+ totalCodeValue += packageName.charCodeAt(i);
349
+ }
350
+ return totalCodeValue;
351
+ };
352
+
342
353
  /**
343
354
  * `graphPackage` and {@link gatherDependency} are mutually recursive functions that
344
355
  * gather the metadata for a package and its transitive dependencies.
@@ -348,7 +359,7 @@ const inferParsers = (descriptor, location, languageOptions) => {
348
359
  * that the package exports.
349
360
  *
350
361
  * @param {string} name
351
- * @param {ReadDescriptorFn} readDescriptor
362
+ * @param {MaybeReadDescriptorFn} readDescriptor
352
363
  * @param {CanonicalFn} canonical
353
364
  * @param {Graph} graph
354
365
  * @param {PackageDetails} packageDetails
@@ -356,7 +367,8 @@ const inferParsers = (descriptor, location, languageOptions) => {
356
367
  * @param {boolean | undefined} dev
357
368
  * @param {LanguageOptions} languageOptions
358
369
  * @param {boolean} strict
359
- * @param {GraphPackageOptions} options
370
+ * @param {LogicalPathGraph} logicalPathGraph
371
+ * @param {GraphPackageOptions} [options]
360
372
  * @returns {Promise<undefined>}
361
373
  */
362
374
  const graphPackage = async (
@@ -369,21 +381,10 @@ const graphPackage = async (
369
381
  dev,
370
382
  languageOptions,
371
383
  strict,
372
- {
373
- commonDependencyDescriptors = {},
374
- preferredPackageLogicalPathMap = new Map(),
375
- logicalPath = [],
376
- log = noop,
377
- } = {},
384
+ logicalPathGraph,
385
+ { commonDependencyDescriptors = {}, logicalPath = [], log = noop } = {},
378
386
  ) => {
379
387
  if (graph[packageLocation] !== undefined) {
380
- updateShortestPaths(
381
- graph,
382
- packageLocation,
383
- logicalPath,
384
- preferredPackageLogicalPathMap,
385
- );
386
-
387
388
  // Returning the promise here would create a causal cycle and stall recursion.
388
389
  return undefined;
389
390
  }
@@ -447,20 +448,20 @@ const graphPackage = async (
447
448
  // use the peerDependenciesMeta field (because there was no way to define
448
449
  // an "optional" peerDependency prior to npm v7). this is plainly wrong,
449
450
  // but not exactly rare, either
450
- for (const [name, meta] of entries(peerDependenciesMeta)) {
451
+ for (const [dependencyName, meta] of entries(peerDependenciesMeta)) {
451
452
  if (Object(meta) === meta && meta.optional) {
452
- optionals.add(name);
453
- allDependencies.add(name);
453
+ optionals.add(dependencyName);
454
+ allDependencies.add(dependencyName);
454
455
  }
455
456
  }
456
457
 
457
- for (const name of keys(optionalDependencies)) {
458
- optionals.add(name);
458
+ for (const dependencyName of keys(optionalDependencies)) {
459
+ optionals.add(dependencyName);
459
460
  }
460
461
 
461
- for (const name of [...allDependencies].sort()) {
462
- const optional = optionals.has(name);
463
- const childLogicalPath = [...logicalPath, name];
462
+ for (const dependencyName of [...allDependencies].sort()) {
463
+ const optional = optionals.has(dependencyName);
464
+ const childLogicalPath = [...logicalPath, dependencyName];
464
465
  children.push(
465
466
  // Mutual recursion ahead:
466
467
  // eslint-disable-next-line no-use-before-define
@@ -470,11 +471,11 @@ const graphPackage = async (
470
471
  graph,
471
472
  dependencyLocations,
472
473
  packageLocation,
473
- name,
474
+ dependencyName,
474
475
  conditions,
475
- preferredPackageLogicalPathMap,
476
476
  languageOptions,
477
477
  strict,
478
+ logicalPathGraph,
478
479
  {
479
480
  childLogicalPath,
480
481
  optional,
@@ -579,17 +580,17 @@ const graphPackage = async (
579
580
  /**
580
581
  * Adds information for the dependency of the package at `packageLocation` to the `graph` object.
581
582
  *
582
- * @param {ReadDescriptorFn} readDescriptor
583
+ * @param {MaybeReadDescriptorFn} readDescriptor
583
584
  * @param {CanonicalFn} canonical
584
585
  * @param {Graph} graph - the partially build graph.
585
586
  * @param {Record<string, string>} dependencyLocations
586
- * @param {string} packageLocation - location of the package of interest.
587
+ * @param {FileUrlString} packageLocation - location of the package of interest.
587
588
  * @param {string} name - name of the package of interest.
588
589
  * @param {Set<string>} conditions
589
- * @param {Map<string, Array<string>>} preferredPackageLogicalPathMap
590
590
  * @param {LanguageOptions} languageOptions
591
591
  * @param {boolean} strict - If `true`, a missing dependency will throw an exception
592
- * @param {GatherDependencyOptions} options
592
+ * @param {LogicalPathGraph} logicalPathGraph
593
+ * @param {GatherDependencyOptions} [options]
593
594
  * @returns {Promise<void>}
594
595
  */
595
596
  const gatherDependency = async (
@@ -600,9 +601,9 @@ const gatherDependency = async (
600
601
  packageLocation,
601
602
  name,
602
603
  conditions,
603
- preferredPackageLogicalPathMap,
604
604
  languageOptions,
605
605
  strict,
606
+ logicalPathGraph,
606
607
  {
607
608
  childLogicalPath = [],
608
609
  optional = false,
@@ -625,19 +626,12 @@ const gatherDependency = async (
625
626
  }
626
627
  dependencyLocations[name] = dependency.packageLocation;
627
628
 
628
- const bestLogicalPath = currentBestLogicalPath(
629
- childLogicalPath,
629
+ logicalPathGraph.addEdge(
630
+ packageLocation,
630
631
  dependency.packageLocation,
631
- preferredPackageLogicalPathMap,
632
+ calculatePackageWeight(name),
632
633
  );
633
634
 
634
- if (bestLogicalPath === childLogicalPath) {
635
- preferredPackageLogicalPathMap.set(
636
- dependency.packageLocation,
637
- bestLogicalPath,
638
- );
639
- }
640
-
641
635
  await graphPackage(
642
636
  name,
643
637
  readDescriptor,
@@ -648,9 +642,9 @@ const gatherDependency = async (
648
642
  false,
649
643
  languageOptions,
650
644
  strict,
645
+ logicalPathGraph,
651
646
  {
652
647
  commonDependencyDescriptors,
653
- preferredPackageLogicalPathMap,
654
648
  logicalPath: childLogicalPath,
655
649
  log,
656
650
  },
@@ -663,7 +657,7 @@ const gatherDependency = async (
663
657
  *
664
658
  * @param {MaybeReadFn} maybeRead
665
659
  * @param {CanonicalFn} canonical
666
- * @param {string} packageLocation - location of the main package.
660
+ * @param {FileUrlString} packageLocation - location of the main package.
667
661
  * @param {Set<string>} conditions
668
662
  * @param {PackageDescriptor} mainPackageDescriptor - the parsed contents of the
669
663
  * main `package.json`, which was already read when searching for the
@@ -674,7 +668,9 @@ const gatherDependency = async (
674
668
  * to all packages
675
669
  * @param {LanguageOptions} languageOptions
676
670
  * @param {boolean} strict
677
- * @param {GraphPackagesOptions} options
671
+ * @param {LogicalPathGraph} logicalPathGraph
672
+ * @param {GraphPackagesOptions} [options]
673
+ * @returns {Promise<Graph>}
678
674
  */
679
675
  const graphPackages = async (
680
676
  maybeRead,
@@ -686,12 +682,12 @@ const graphPackages = async (
686
682
  commonDependencies,
687
683
  languageOptions,
688
684
  strict,
685
+ logicalPathGraph,
689
686
  { log = noop } = {},
690
687
  ) => {
691
688
  const memo = create(null);
692
689
  /**
693
- * @param {string} packageLocation
694
- * @returns {Promise<PackageDescriptor>}
690
+ * @type {MaybeReadDescriptorFn}
695
691
  */
696
692
  const readDescriptor = packageLocation =>
697
693
  readDescriptorWithMemo(memo, maybeRead, packageLocation);
@@ -700,19 +696,22 @@ const graphPackages = async (
700
696
  memo[packageLocation] = Promise.resolve(mainPackageDescriptor);
701
697
  }
702
698
 
703
- const packageDescriptor = await readDescriptor(packageLocation);
699
+ const allegedPackageDescriptor = await readDescriptor(packageLocation);
700
+
701
+ if (allegedPackageDescriptor === undefined) {
702
+ throw TypeError(
703
+ `Cannot find package.json for application at ${packageLocation}`,
704
+ );
705
+ }
706
+
707
+ assertPackageDescriptor(allegedPackageDescriptor);
708
+ const packageDescriptor = allegedPackageDescriptor;
704
709
 
705
710
  conditions = new Set(conditions || []);
706
711
  conditions.add('import');
707
712
  conditions.add('default');
708
713
  conditions.add('endo');
709
714
 
710
- if (packageDescriptor === undefined) {
711
- throw Error(
712
- `Cannot find package.json for application at ${packageLocation}`,
713
- );
714
- }
715
-
716
715
  // Resolve common dependencies.
717
716
  /** @type {CommonDependencyDescriptors} */
718
717
  const commonDependencyDescriptors = {};
@@ -730,6 +729,8 @@ const graphPackages = async (
730
729
  };
731
730
  }
732
731
 
732
+ logicalPathGraph.addNode(packageLocation);
733
+
733
734
  const graph = create(null);
734
735
  await graphPackage(
735
736
  packageDescriptor.name,
@@ -744,6 +745,7 @@ const graphPackages = async (
744
745
  dev,
745
746
  languageOptions,
746
747
  strict,
748
+ logicalPathGraph,
747
749
  {
748
750
  commonDependencyDescriptors,
749
751
  log,
@@ -974,8 +976,8 @@ const makeLanguageOptions = ({
974
976
  };
975
977
 
976
978
  /**
977
- * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers
978
- * @param {string} packageLocation
979
+ * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
980
+ * @param {FileUrlString} packageLocation
979
981
  * @param {Set<string>} conditionsOption
980
982
  * @param {PackageDescriptor} packageDescriptor
981
983
  * @param {string} moduleSpecifier
@@ -1003,10 +1005,18 @@ export const compartmentMapForNodeModules = async (
1003
1005
 
1004
1006
  const conditions = new Set(conditionsOption || []);
1005
1007
 
1008
+ /**
1009
+ * This graph will contain nodes for each package location (a
1010
+ * {@link FileUrlString}) and edges representing dependencies between packages.
1011
+ *
1012
+ * The edges are weighted by {@link calculatePackageWeight}.
1013
+ *
1014
+ * @type {LogicalPathGraph}
1015
+ */
1016
+ const logicalPathGraph = new GenericGraph();
1017
+
1006
1018
  // dev is only set for the entry package, and implied by the development
1007
1019
  // condition.
1008
- // The dev option is deprecated in favor of using conditions, since that
1009
- // covers more intentional behaviors of the development mode.
1010
1020
 
1011
1021
  const graph = await graphPackages(
1012
1022
  maybeRead,
@@ -1018,6 +1028,7 @@ export const compartmentMapForNodeModules = async (
1018
1028
  commonDependencies,
1019
1029
  languageOptions,
1020
1030
  strict,
1031
+ logicalPathGraph,
1021
1032
  { log },
1022
1033
  );
1023
1034
 
@@ -1037,6 +1048,27 @@ export const compartmentMapForNodeModules = async (
1037
1048
  };
1038
1049
  }
1039
1050
 
1051
+ const shortestPath = makeShortestPath(logicalPathGraph);
1052
+ // neither the entry package nor the attenuators compartment have a path; omit
1053
+ const {
1054
+ [ATTENUATORS_COMPARTMENT]: _,
1055
+ [packageLocation]: __,
1056
+ ...subgraph
1057
+ } = graph;
1058
+
1059
+ for (const [location, node] of entries(subgraph)) {
1060
+ const shortestLogicalPath = shortestPath(
1061
+ packageLocation,
1062
+ // entries() loses some type information
1063
+ /** @type {FileUrlString} */ (location),
1064
+ );
1065
+
1066
+ // the first element will always be the root package location; this is omitted from the path.
1067
+ shortestLogicalPath.shift();
1068
+ node.path = shortestLogicalPath.map(location => graph[location].name);
1069
+ log(`Canonical name for package at ${location}: ${node.path.join('>')}`);
1070
+ }
1071
+
1040
1072
  const compartmentMap = translateGraph(
1041
1073
  packageLocation,
1042
1074
  moduleSpecifier,
@@ -1054,7 +1086,7 @@ export const compartmentMapForNodeModules = async (
1054
1086
  *
1055
1087
  * Locates the {@link PackageDescriptor} for the module at `moduleLocation`
1056
1088
  *
1057
- * @param {ReadFn | ReadPowers | MaybeReadPowers} readPowers
1089
+ * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
1058
1090
  * @param {string} moduleLocation
1059
1091
  * @param {MapNodeModulesOptions} [options]
1060
1092
  * @returns {Promise<CompartmentMapDescriptor>}
@@ -1071,9 +1103,12 @@ export const mapNodeModules = async (
1071
1103
  moduleSpecifier,
1072
1104
  } = await search(readPowers, moduleLocation, { log });
1073
1105
 
1074
- const packageDescriptor = /** @type {PackageDescriptor} */ (
1075
- parseLocatedJson(packageDescriptorText, packageDescriptorLocation)
1076
- );
1106
+ const packageDescriptor = /** @type {typeof parseLocatedJson<unknown>} */ (
1107
+ parseLocatedJson
1108
+ )(packageDescriptorText, packageDescriptorLocation);
1109
+
1110
+ assertPackageDescriptor(packageDescriptor);
1111
+ assertFileUrlString(packageLocation);
1077
1112
 
1078
1113
  return compartmentMapForNodeModules(
1079
1114
  readPowers,
@@ -3,7 +3,7 @@ export function makeReadNowPowers({ fs, url, crypto, path, }: {
3
3
  url?: UrlInterface | undefined;
4
4
  crypto?: CryptoInterface | undefined;
5
5
  path?: PathInterface | undefined;
6
- }): MaybeReadPowers & ReadNowPowers;
6
+ }): ReadNowPowers<FileUrlString>;
7
7
  /**
8
8
  * The implementation of `makeReadPowers` and the deprecated
9
9
  * `makeNodeReadPowers` handles the case when the `url` power is not provided,
@@ -14,14 +14,14 @@ export function makeReadNowPowers({ fs, url, crypto, path, }: {
14
14
  * @param {UrlInterface} [args.url]
15
15
  * @param {CryptoInterface} [args.crypto]
16
16
  * @param {PathInterface} [args.path]
17
- * @returns {MaybeReadPowers}
17
+ * @returns {MaybeReadPowers<FileUrlString>}
18
18
  */
19
19
  export function makeReadPowers({ fs, url, crypto, path, }: {
20
20
  fs: FsInterface;
21
21
  url?: UrlInterface | undefined;
22
22
  crypto?: CryptoInterface | undefined;
23
23
  path?: PathInterface | undefined;
24
- }): MaybeReadPowers;
24
+ }): MaybeReadPowers<FileUrlString>;
25
25
  /**
26
26
  * The implementation of `makeWritePowers` and the deprecated
27
27
  * `makeNodeWritePowers` handles the case when the `url` power is not provided,
@@ -37,14 +37,15 @@ export function makeWritePowers({ fs, url }: {
37
37
  }): {
38
38
  write: (location: string, data: Uint8Array) => Promise<void>;
39
39
  };
40
- export function makeNodeReadPowers(fs: FsInterface, crypto?: CryptoInterface): ReadPowers;
40
+ export function makeNodeReadPowers(fs: FsInterface, crypto?: CryptoInterface): ReadPowers<FileUrlString>;
41
41
  export function makeNodeWritePowers(fs: FsInterface): WritePowers;
42
42
  import type { FsInterface } from './types/node-powers.js';
43
43
  import type { UrlInterface } from './types/node-powers.js';
44
44
  import type { CryptoInterface } from './types/node-powers.js';
45
45
  import type { PathInterface } from './types/node-powers.js';
46
- import type { MaybeReadPowers } from './types/powers.js';
46
+ import type { FileUrlString } from './types/external.js';
47
47
  import type { ReadNowPowers } from './types/powers.js';
48
+ import type { MaybeReadPowers } from './types/powers.js';
48
49
  import type { ReadPowers } from './types/powers.js';
49
50
  import type { WritePowers } from './types/powers.js';
50
51
  //# sourceMappingURL=node-powers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-powers.d.ts","sourceRoot":"","sources":["node-powers.js"],"names":[],"mappings":"AA4LO,8DANJ;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;IACA,MAAM;IACR,IAAI;CACjC,GAAU,eAAe,GAAG,aAAa,CAoC3C;AAlKD;;;;;;;;;;;GAWG;AACH,2DANG;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;IACA,MAAM;IACR,IAAI;CACjC,GAAU,eAAe,CA0G3B;AAgDD;;;;;;;;GAQG;AACH,6CAHG;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;CAAC;sBAOtB,MAAM,QACN,UAAU;EAYtB;AA2BM,uCALI,WAAW,WACX,eAAe,GACb,UAAU,CAKtB;AAWM,wCAJI,WAAW,GACT,WAAW,CAKvB;iCApRS,wBAAwB;kCAAxB,wBAAwB;qCAAxB,wBAAwB;mCAAxB,wBAAwB;qCAexB,mBAAmB;mCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB"}
1
+ {"version":3,"file":"node-powers.d.ts","sourceRoot":"","sources":["node-powers.js"],"names":[],"mappings":"AA+LO,8DANJ;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;IACA,MAAM;IACR,IAAI;CACjC,GAAU,cAAc,aAAa,CAAC,CAoCxC;AApKD;;;;;;;;;;;GAWG;AACH,2DANG;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;IACA,MAAM;IACR,IAAI;CACjC,GAAU,gBAAgB,aAAa,CAAC,CA4G1C;AAgDD;;;;;;;;GAQG;AACH,6CAHG;IAA0B,EAAE,EAApB,WAAW;IACS,GAAG;CAAC;sBAOtB,MAAM,QACN,UAAU;EAYtB;AA2BM,uCALI,WAAW,WACX,eAAe,GACb,WAAW,aAAa,CAAC,CAKrC;AAWM,wCAJI,WAAW,GACT,WAAW,CAKvB;iCAvRS,wBAAwB;kCAAxB,wBAAwB;qCAAxB,wBAAwB;mCAAxB,wBAAwB;mCACF,qBAAqB;mCAe3C,mBAAmB;qCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB"}