@endo/compartment-mapper 1.6.3 → 2.1.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 (95) hide show
  1. package/package.json +24 -14
  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 +81 -30
  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 +243 -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 +738 -254
  19. package/src/digest.d.ts +22 -2
  20. package/src/digest.d.ts.map +1 -1
  21. package/src/digest.js +180 -57
  22. package/src/generic-graph.d.ts +7 -25
  23. package/src/generic-graph.d.ts.map +1 -1
  24. package/src/generic-graph.js +83 -108
  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 +140 -70
  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 +4 -2
  44. package/src/infer-exports.d.ts.map +1 -1
  45. package/src/infer-exports.js +172 -23
  46. package/src/link.d.ts +4 -3
  47. package/src/link.d.ts.map +1 -1
  48. package/src/link.js +122 -52
  49. package/src/node-modules.d.ts +4 -3
  50. package/src/node-modules.d.ts.map +1 -1
  51. package/src/node-modules.js +513 -151
  52. package/src/parse-cjs-shared-export-wrapper.d.ts.map +1 -1
  53. package/src/parse-cjs-shared-export-wrapper.js +3 -1
  54. package/src/pattern-replacement.d.ts +6 -0
  55. package/src/pattern-replacement.d.ts.map +1 -0
  56. package/src/pattern-replacement.js +198 -0
  57. package/src/policy-format.d.ts +22 -5
  58. package/src/policy-format.d.ts.map +1 -1
  59. package/src/policy-format.js +342 -108
  60. package/src/policy.d.ts +13 -28
  61. package/src/policy.d.ts.map +1 -1
  62. package/src/policy.js +161 -106
  63. package/src/types/canonical-name.d.ts +97 -0
  64. package/src/types/canonical-name.d.ts.map +1 -0
  65. package/src/types/canonical-name.ts +151 -0
  66. package/src/types/compartment-map-schema.d.ts +121 -35
  67. package/src/types/compartment-map-schema.d.ts.map +1 -1
  68. package/src/types/compartment-map-schema.ts +211 -37
  69. package/src/types/external.d.ts +240 -76
  70. package/src/types/external.d.ts.map +1 -1
  71. package/src/types/external.ts +305 -74
  72. package/src/types/generic-graph.d.ts +8 -2
  73. package/src/types/generic-graph.d.ts.map +1 -1
  74. package/src/types/generic-graph.ts +7 -2
  75. package/src/types/internal.d.ts +31 -50
  76. package/src/types/internal.d.ts.map +1 -1
  77. package/src/types/internal.ts +60 -58
  78. package/src/types/node-modules.d.ts +112 -14
  79. package/src/types/node-modules.d.ts.map +1 -1
  80. package/src/types/node-modules.ts +152 -13
  81. package/src/types/pattern-replacement.d.ts +62 -0
  82. package/src/types/pattern-replacement.d.ts.map +1 -0
  83. package/src/types/pattern-replacement.ts +70 -0
  84. package/src/types/policy-schema.d.ts +26 -11
  85. package/src/types/policy-schema.d.ts.map +1 -1
  86. package/src/types/policy-schema.ts +29 -16
  87. package/src/types/policy.d.ts +6 -2
  88. package/src/types/policy.d.ts.map +1 -1
  89. package/src/types/policy.ts +7 -2
  90. package/src/types/powers.d.ts +11 -9
  91. package/src/types/powers.d.ts.map +1 -1
  92. package/src/types/powers.ts +11 -10
  93. package/src/types/typescript.d.ts +28 -0
  94. package/src/types/typescript.d.ts.map +1 -1
  95. 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,15 +14,16 @@
13
14
 
14
15
  /* eslint no-shadow: 0 */
15
16
 
16
- import { inferExportsAndAliases } from './infer-exports.js';
17
+ import { inferExportsAliasesAndPatterns } from './infer-exports.js';
17
18
  import { parseLocatedJson } from './json.js';
18
19
  import { join } from './node-module-specifier.js';
19
- import { assertPolicy } from './policy-format.js';
20
20
  import {
21
+ assertPolicy,
21
22
  ATTENUATORS_COMPARTMENT,
22
- dependencyAllowedByPolicy,
23
- getPolicyForPackage,
24
- } from './policy.js';
23
+ ENTRY_COMPARTMENT,
24
+ generateCanonicalName,
25
+ } from './policy-format.js';
26
+ import { dependencyAllowedByPolicy, makePackagePolicy } from './policy.js';
25
27
  import { unpackReadPowers } from './powers.js';
26
28
  import { search, searchDescriptor } from './search.js';
27
29
  import { GenericGraph, makeShortestPath } from './generic-graph.js';
@@ -41,8 +43,16 @@ import { GenericGraph, makeShortestPath } from './generic-graph.js';
41
43
  * PackageDescriptor,
42
44
  * ReadFn,
43
45
  * ReadPowers,
44
- * SomePackagePolicy,
45
46
  * SomePolicy,
47
+ * LogFn,
48
+ * CompartmentModuleConfiguration,
49
+ * PackageCompartmentDescriptor,
50
+ * PackageCompartmentMapDescriptor,
51
+ * ScopeDescriptor,
52
+ * CanonicalName,
53
+ * SomePackagePolicy,
54
+ * PackageCompartmentDescriptorName,
55
+ * PackageData,
46
56
  * } from './types.js'
47
57
  * @import {
48
58
  * Graph,
@@ -54,21 +64,78 @@ import { GenericGraph, makeShortestPath } from './generic-graph.js';
54
64
  * GraphPackagesOptions,
55
65
  * LogicalPathGraph,
56
66
  * PackageDetails,
67
+ * FinalGraph,
68
+ * CanonicalNameMap,
69
+ * FinalNode,
70
+ TranslateGraphOptions,
57
71
  * } from './types/node-modules.js'
58
72
  */
59
73
 
60
- const { assign, create, keys, values, entries } = Object;
74
+ const { assign, create, keys, values, entries, freeze } = Object;
61
75
 
62
76
  const decoder = new TextDecoder();
63
77
 
64
78
  // q, as in quote, for enquoting strings in error messages.
65
- const q = JSON.stringify;
79
+ const { quote: q } = assert;
66
80
 
67
81
  /**
68
82
  * Default logger that does nothing.
69
83
  */
70
84
  const noop = () => {};
71
85
 
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
+
72
139
  /**
73
140
  * Given a relative path andd URL, return a fully qualified URL string.
74
141
  *
@@ -319,37 +386,6 @@ const inferParsers = (descriptor, location, languageOptions) => {
319
386
  return { ...commonjsLanguageForExtension, ...packageLanguageForExtension };
320
387
  };
321
388
 
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
-
353
389
  /**
354
390
  * `graphPackage` and {@link gatherDependency} are mutually recursive functions that
355
391
  * gather the metadata for a package and its transitive dependencies.
@@ -368,7 +404,7 @@ const calculatePackageWeight = packageName => {
368
404
  * @param {LanguageOptions} languageOptions
369
405
  * @param {boolean} strict
370
406
  * @param {LogicalPathGraph} logicalPathGraph
371
- * @param {GraphPackageOptions} [options]
407
+ * @param {GraphPackageOptions} options
372
408
  * @returns {Promise<undefined>}
373
409
  */
374
410
  const graphPackage = async (
@@ -382,7 +418,12 @@ const graphPackage = async (
382
418
  languageOptions,
383
419
  strict,
384
420
  logicalPathGraph,
385
- { commonDependencyDescriptors = {}, logicalPath = [], log = noop } = {},
421
+ {
422
+ commonDependencyDescriptors = {},
423
+ log = noop,
424
+ packageDependenciesHook,
425
+ policy,
426
+ } = {},
386
427
  ) => {
387
428
  if (graph[packageLocation] !== undefined) {
388
429
  // Returning the promise here would create a causal cycle and stall recursion.
@@ -397,7 +438,7 @@ const graphPackage = async (
397
438
  });
398
439
  }
399
440
 
400
- const result = /** @type {Node} */ ({});
441
+ const result = /** @type {Node} */ ({ location: packageLocation });
401
442
  graph[packageLocation] = result;
402
443
 
403
444
  /** @type {Node['dependencyLocations']} */
@@ -461,7 +502,6 @@ const graphPackage = async (
461
502
 
462
503
  for (const dependencyName of [...allDependencies].sort()) {
463
504
  const optional = optionals.has(dependencyName);
464
- const childLogicalPath = [...logicalPath, dependencyName];
465
505
  children.push(
466
506
  // Mutual recursion ahead:
467
507
  // eslint-disable-next-line no-use-before-define
@@ -477,10 +517,11 @@ const graphPackage = async (
477
517
  strict,
478
518
  logicalPathGraph,
479
519
  {
480
- childLogicalPath,
481
520
  optional,
482
521
  commonDependencyDescriptors,
483
522
  log,
523
+ packageDependenciesHook,
524
+ policy,
484
525
  },
485
526
  ),
486
527
  );
@@ -505,13 +546,17 @@ const graphPackage = async (
505
546
  const externalAliases = {};
506
547
  /** @type {Node['internalAliases']} */
507
548
  const internalAliases = {};
549
+ /** @type {Node['patterns']} */
550
+ const patterns = [];
508
551
 
509
- inferExportsAndAliases(
552
+ inferExportsAliasesAndPatterns(
510
553
  packageDescriptor,
511
554
  externalAliases,
512
555
  internalAliases,
556
+ patterns,
513
557
  conditions,
514
558
  types,
559
+ log,
515
560
  );
516
561
 
517
562
  const parsers = inferParsers(
@@ -522,18 +567,21 @@ const graphPackage = async (
522
567
 
523
568
  const sourceDirname = basename(packageLocation);
524
569
 
525
- assign(result, {
570
+ /** @type {Partial<Node>} */
571
+ const partialNode = {
526
572
  name,
527
- path: logicalPath,
528
573
  label: `${name}${version ? `-v${version}` : ''}`,
529
574
  sourceDirname,
530
575
  explicitExports: exportsDescriptor !== undefined,
531
576
  externalAliases,
532
577
  internalAliases,
578
+ patterns,
533
579
  dependencyLocations,
534
580
  types,
535
581
  parsers,
536
- });
582
+ packageDescriptor,
583
+ };
584
+ assign(result, partialNode);
537
585
 
538
586
  await Promise.all(
539
587
  values(result.externalAliases).map(async item => {
@@ -605,10 +653,11 @@ const gatherDependency = async (
605
653
  strict,
606
654
  logicalPathGraph,
607
655
  {
608
- childLogicalPath = [],
609
656
  optional = false,
610
657
  commonDependencyDescriptors = {},
611
658
  log = noop,
659
+ packageDependenciesHook,
660
+ policy,
612
661
  } = {},
613
662
  ) => {
614
663
  const dependency = await findPackage(
@@ -617,6 +666,7 @@ const gatherDependency = async (
617
666
  packageLocation,
618
667
  name,
619
668
  );
669
+
620
670
  if (dependency === undefined) {
621
671
  // allow the dependency to be missing if optional
622
672
  if (optional || !strict) {
@@ -624,13 +674,10 @@ const gatherDependency = async (
624
674
  }
625
675
  throw Error(`Cannot find dependency ${name} for ${packageLocation}`);
626
676
  }
677
+
627
678
  dependencyLocations[name] = dependency.packageLocation;
628
679
 
629
- logicalPathGraph.addEdge(
630
- packageLocation,
631
- dependency.packageLocation,
632
- calculatePackageWeight(name),
633
- );
680
+ logicalPathGraph.addEdge(packageLocation, dependency.packageLocation);
634
681
 
635
682
  await graphPackage(
636
683
  name,
@@ -645,8 +692,9 @@ const gatherDependency = async (
645
692
  logicalPathGraph,
646
693
  {
647
694
  commonDependencyDescriptors,
648
- logicalPath: childLogicalPath,
649
695
  log,
696
+ packageDependenciesHook,
697
+ policy,
650
698
  },
651
699
  );
652
700
  };
@@ -669,7 +717,7 @@ const gatherDependency = async (
669
717
  * @param {LanguageOptions} languageOptions
670
718
  * @param {boolean} strict
671
719
  * @param {LogicalPathGraph} logicalPathGraph
672
- * @param {GraphPackagesOptions} [options]
720
+ * @param {GraphPackagesOptions} options
673
721
  * @returns {Promise<Graph>}
674
722
  */
675
723
  const graphPackages = async (
@@ -683,7 +731,7 @@ const graphPackages = async (
683
731
  languageOptions,
684
732
  strict,
685
733
  logicalPathGraph,
686
- { log = noop } = {},
734
+ { log = noop, packageDependenciesHook, policy } = {},
687
735
  ) => {
688
736
  const memo = create(null);
689
737
  /**
@@ -749,6 +797,8 @@ const graphPackages = async (
749
797
  {
750
798
  commonDependencyDescriptors,
751
799
  log,
800
+ packageDependenciesHook,
801
+ policy,
752
802
  },
753
803
  );
754
804
  return graph;
@@ -763,19 +813,101 @@ const graphPackages = async (
763
813
  * @param {Graph} graph
764
814
  * @param {Set<string>} conditions - build conditions about the target environment
765
815
  * for selecting relevant exports, e.g., "browser" or "node".
766
- * @param {SomePolicy} [policy]
767
- * @returns {CompartmentMapDescriptor}
816
+ * @param {TranslateGraphOptions} [options]
817
+ * @returns {PackageCompartmentMapDescriptor}
768
818
  */
769
819
  const translateGraph = (
770
820
  entryPackageLocation,
771
821
  entryModuleSpecifier,
772
822
  graph,
773
823
  conditions,
774
- policy,
824
+ { policy, log = noop, packageDependenciesHook } = {},
775
825
  ) => {
776
- /** @type {CompartmentMapDescriptor['compartments']} */
826
+ /** @type {Record<PackageCompartmentDescriptorName, PackageCompartmentDescriptor>} */
777
827
  const compartments = create(null);
778
828
 
829
+ /**
830
+ * Execute package dependencies hooks: default first (if policy exists), then user-provided.
831
+ *
832
+ * @param {CanonicalName} label
833
+ * @param {Record<string, FileUrlString>} dependencyLocations
834
+ * @returns {Record<string, FileUrlString>}
835
+ */
836
+ const executePackageDependenciesHook = (label, dependencyLocations) => {
837
+ const dependencies = new Set(
838
+ values(dependencyLocations).map(
839
+ dependencyLocation => graph[dependencyLocation].label,
840
+ ),
841
+ );
842
+
843
+ const packageDependenciesHookInput = {
844
+ canonicalName: label,
845
+ dependencies: new Set(dependencies),
846
+ log,
847
+ };
848
+
849
+ // Call default filter first if policy exists
850
+ let packageDependenciesHookResult;
851
+ if (policy) {
852
+ packageDependenciesHookResult = prePackageDependenciesFilter(
853
+ packageDependenciesHookInput,
854
+ policy,
855
+ );
856
+ }
857
+
858
+ // Then call user-provided hook if it exists
859
+ if (packageDependenciesHook) {
860
+ const userResult = packageDependenciesHook(packageDependenciesHookInput);
861
+ // If user hook also returned a result, use it (overrides default)
862
+ if (userResult?.dependencies) {
863
+ packageDependenciesHookResult = userResult;
864
+ }
865
+ }
866
+
867
+ // if "dependencies" are in here, then something changed the list.
868
+ if (packageDependenciesHookResult?.dependencies) {
869
+ const size = packageDependenciesHookResult.dependencies.size;
870
+ if (typeof size === 'number' && size > 0) {
871
+ // because the list of dependencies contains canonical names, we need to lookup any new ones.
872
+ const nodesByCanonicalName = new Map(
873
+ entries(graph).map(([location, node]) => [
874
+ node.label,
875
+ {
876
+ ...node,
877
+ packageLocation: /** @type {FileUrlString} */ (location),
878
+ },
879
+ ]),
880
+ );
881
+
882
+ /** @type {typeof dependencyLocations} */
883
+ const newDependencyLocations = {};
884
+ try {
885
+ for (const label of packageDependenciesHookResult.dependencies) {
886
+ const { name, packageLocation } =
887
+ nodesByCanonicalName.get(label) ?? create(null);
888
+ if (name && packageLocation) {
889
+ newDependencyLocations[name] = packageLocation;
890
+ } else {
891
+ log(
892
+ `WARNING: packageDependencies hook returned unknown package with label ${q(label)}`,
893
+ );
894
+ }
895
+ }
896
+ return newDependencyLocations;
897
+ } catch {
898
+ log(
899
+ `WARNING: packageDependencies hook returned invalid value ${q(
900
+ packageDependenciesHookResult,
901
+ )}; using original dependencies`,
902
+ );
903
+ }
904
+ } else {
905
+ dependencyLocations = create(null);
906
+ }
907
+ }
908
+ return dependencyLocations;
909
+ };
910
+
779
911
  // For each package, build a map of all the external modules the package can
780
912
  // import from other packages.
781
913
  // The keys of this map are the full specifiers of those modules from the
@@ -785,36 +917,25 @@ const translateGraph = (
785
917
  // The full map includes every exported module from every dependencey
786
918
  // package and is a complete list of every external module that the
787
919
  // corresponding compartment can import.
788
- for (const dependeeLocation of keys(graph).sort()) {
920
+ for (const dependeeLocation of /** @type {PackageCompartmentDescriptorName[]} */ (
921
+ keys(graph).sort()
922
+ )) {
789
923
  const {
790
924
  name,
791
- path,
792
925
  label,
793
926
  sourceDirname,
794
- dependencyLocations,
795
927
  internalAliases,
928
+ patterns,
796
929
  parsers,
797
930
  types,
931
+ packageDescriptor,
798
932
  } = graph[dependeeLocation];
799
- /** @type {CompartmentDescriptor['modules']} */
933
+ /** @type {Record<string, CompartmentModuleConfiguration>} */
800
934
  const moduleDescriptors = create(null);
801
- /** @type {CompartmentDescriptor['scopes']} */
935
+ /** @type {Record<string, ScopeDescriptor<PackageCompartmentDescriptorName>>} */
802
936
  const scopes = create(null);
803
937
 
804
- /**
805
- * List of all the compartments (by name) that this compartment can import from.
806
- *
807
- * @type {Set<string>}
808
- */
809
- const compartmentNames = new Set();
810
- const packagePolicy = getPolicyForPackage(
811
- {
812
- isEntry: dependeeLocation === entryPackageLocation,
813
- name,
814
- path,
815
- },
816
- policy,
817
- );
938
+ const packagePolicy = makePackagePolicy(label, { policy });
818
939
 
819
940
  /* c8 ignore next */
820
941
  if (policy && !packagePolicy) {
@@ -822,33 +943,50 @@ const translateGraph = (
822
943
  throw new TypeError('Unexpectedly falsy package policy');
823
944
  }
824
945
 
946
+ let dependencyLocations = graph[dependeeLocation].dependencyLocations;
947
+ dependencyLocations = executePackageDependenciesHook(
948
+ label,
949
+ dependencyLocations,
950
+ );
951
+
825
952
  /**
826
953
  * @param {string} dependencyName
827
- * @param {string} packageLocation
954
+ * @param {PackageCompartmentDescriptorName} packageLocation
828
955
  */
829
956
  const digestExternalAliases = (dependencyName, packageLocation) => {
830
- const { externalAliases, explicitExports, name, path } =
831
- graph[packageLocation];
957
+ const {
958
+ externalAliases,
959
+ explicitExports,
960
+ patterns: dependencyPatterns,
961
+ } = graph[packageLocation];
832
962
  for (const exportPath of keys(externalAliases).sort()) {
833
963
  const targetPath = externalAliases[exportPath];
834
964
  // dependency name may be different from package's name,
835
- // as in the case of browser field dependency replacements
965
+ // as in the case of browser field dependency replacements.
966
+ // note that policy still applies
836
967
  const localPath = join(dependencyName, exportPath);
837
- if (
838
- !policy ||
839
- (packagePolicy &&
840
- dependencyAllowedByPolicy(
841
- {
842
- name,
843
- path,
844
- },
845
- packagePolicy,
846
- ))
847
- ) {
848
- moduleDescriptors[localPath] = {
849
- compartment: packageLocation,
850
- module: targetPath,
851
- };
968
+ // if we have policy, this has already been vetted
969
+ moduleDescriptors[localPath] = {
970
+ compartment: packageLocation,
971
+ module: targetPath,
972
+ };
973
+ }
974
+ // Propagate export patterns from dependencies.
975
+ // Each dependency pattern like "./features/*.js" -> "./src/features/*.js"
976
+ // becomes "dep/features/*.js" -> "./src/features/*.js" on the dependee,
977
+ // resolving within the dependency's compartment.
978
+ if (dependencyPatterns) {
979
+ for (const { from, to } of dependencyPatterns) {
980
+ // Only propagate export patterns (starting with "./"), not
981
+ // import patterns (starting with "#") which are internal.
982
+ if (from.startsWith('./') || from === '.') {
983
+ const externalFrom = join(dependencyName, from);
984
+ patterns.push({
985
+ from: externalFrom,
986
+ to,
987
+ compartment: packageLocation,
988
+ });
989
+ }
852
990
  }
853
991
  }
854
992
  // if the exports field is not present, then all modules must be accessible
@@ -864,7 +1002,6 @@ const translateGraph = (
864
1002
  for (const dependencyName of keys(dependencyLocations).sort()) {
865
1003
  const dependencyLocation = dependencyLocations[dependencyName];
866
1004
  digestExternalAliases(dependencyName, dependencyLocation);
867
- compartmentNames.add(dependencyLocation);
868
1005
  }
869
1006
  // digest own internal aliases
870
1007
  for (const modulePath of keys(internalAliases).sort()) {
@@ -881,17 +1018,17 @@ const translateGraph = (
881
1018
  }
882
1019
 
883
1020
  compartments[dependeeLocation] = {
1021
+ version: packageDescriptor.version ? packageDescriptor.version : '',
884
1022
  label,
885
1023
  name,
886
- path,
887
1024
  location: dependeeLocation,
888
1025
  sourceDirname,
889
1026
  modules: moduleDescriptors,
890
1027
  scopes,
1028
+ ...(patterns.length > 0 ? { patterns } : {}),
891
1029
  parsers,
892
1030
  types,
893
1031
  policy: /** @type {SomePackagePolicy} */ (packagePolicy),
894
- compartments: compartmentNames,
895
1032
  };
896
1033
  }
897
1034
 
@@ -900,7 +1037,7 @@ const translateGraph = (
900
1037
  // https://github.com/endojs/endo/issues/2388
901
1038
  tags: [...conditions],
902
1039
  entry: {
903
- compartment: entryPackageLocation,
1040
+ compartment: /** @type {FileUrlString} */ (entryPackageLocation),
904
1041
  module: entryModuleSpecifier,
905
1042
  },
906
1043
  compartments,
@@ -974,23 +1111,200 @@ const makeLanguageOptions = ({
974
1111
  workspaceModuleLanguageForExtension,
975
1112
  };
976
1113
  };
1114
+ /**
1115
+ * Creates a `Node` in `graph` corresponding to the "attenuators" Compartment.
1116
+ *
1117
+ * Only does so if `policy` is provided.
1118
+ *
1119
+ * @param {Graph} graph Graph
1120
+ * @param {Node} entryNode Entry node of the grpah
1121
+ * @param {SomePolicy} [policy]
1122
+ * @throws If there's already a `Node` in `graph` for the "attenuators"
1123
+ * Compartment
1124
+ * @returns {void}
1125
+ */
1126
+ const makeAttenuatorsNode = (graph, entryNode, policy) => {
1127
+ if (policy) {
1128
+ assertPolicy(policy);
1129
+
1130
+ assert(
1131
+ graph[ATTENUATORS_COMPARTMENT] === undefined,
1132
+ `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`,
1133
+ );
1134
+
1135
+ graph[ATTENUATORS_COMPARTMENT] = {
1136
+ ...entryNode,
1137
+ internalAliases: {},
1138
+ externalAliases: {},
1139
+ packageDescriptor: { name: ATTENUATORS_COMPARTMENT },
1140
+ name: ATTENUATORS_COMPARTMENT,
1141
+ };
1142
+ }
1143
+ };
1144
+
1145
+ /**
1146
+ * Transforms a `Graph` into a readonly `FinalGraph`, in preparation for
1147
+ * conversion to a `CompartmentDescriptor`.
1148
+ *
1149
+ * @param {Graph} graph Graph
1150
+ * @param {LogicalPathGraph} logicalPathGraph Logical path graph
1151
+ * @param {FileUrlString} entryPackageLocation Entry package location
1152
+ * @param {CanonicalNameMap} canonicalNameMap Mapping of canonical names to `Node` names (keys in `graph`)
1153
+ * @returns {Readonly<FinalGraph>}
1154
+ */
1155
+ const finalizeGraph = (
1156
+ graph,
1157
+ logicalPathGraph,
1158
+ entryPackageLocation,
1159
+ canonicalNameMap,
1160
+ ) => {
1161
+ const shortestPath = makeShortestPath(logicalPathGraph);
1162
+
1163
+ // neither the entry package nor the attenuators compartment have a path; omit
1164
+ const {
1165
+ [ATTENUATORS_COMPARTMENT]: attenuatorsNode,
1166
+ [entryPackageLocation]: entryNode,
1167
+ ...subgraph
1168
+ } = graph;
1169
+
1170
+ /** @type {FinalGraph} */
1171
+ const finalGraph = create(null);
1172
+
1173
+ /** @type {Readonly<FinalNode>} */
1174
+ finalGraph[entryPackageLocation] = freeze({
1175
+ ...entryNode,
1176
+ label: generateCanonicalName({
1177
+ isEntry: true,
1178
+ path: [],
1179
+ }),
1180
+ });
1181
+
1182
+ canonicalNameMap.set(ENTRY_COMPARTMENT, entryPackageLocation);
1183
+
1184
+ if (attenuatorsNode) {
1185
+ /** @type {Readonly<FinalNode>} */
1186
+ finalGraph[ATTENUATORS_COMPARTMENT] = freeze({
1187
+ ...attenuatorsNode,
1188
+ label: generateCanonicalName({
1189
+ name: ATTENUATORS_COMPARTMENT,
1190
+ path: [],
1191
+ }),
1192
+ });
1193
+ }
1194
+
1195
+ const subgraphEntries = /** @type {[FileUrlString, Node][]} */ (
1196
+ entries(subgraph)
1197
+ );
1198
+
1199
+ for (const [location, node] of subgraphEntries) {
1200
+ const shortestLogicalPath = shortestPath(entryPackageLocation, location);
1201
+
1202
+ // the first element will always be the root package location; this is omitted from the path.
1203
+ shortestLogicalPath.shift();
1204
+
1205
+ const path = shortestLogicalPath.map(location => graph[location].name);
1206
+ const canonicalName = generateCanonicalName({ path });
1207
+
1208
+ /** @type {Readonly<FinalNode>} */
1209
+ const finalNode = freeze({
1210
+ ...node,
1211
+ label: canonicalName,
1212
+ });
1213
+
1214
+ canonicalNameMap.set(canonicalName, location);
1215
+
1216
+ finalGraph[location] = finalNode;
1217
+ }
1218
+
1219
+ for (const node of values(finalGraph)) {
1220
+ Object.freeze(node);
1221
+ }
1222
+
1223
+ return freeze(finalGraph);
1224
+ };
1225
+
1226
+ /**
1227
+ * Returns an array of "issue" objects if any resources referenced in `policy`
1228
+ * are unknown.
1229
+ *
1230
+ * @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
1231
+ * @param {SomePolicy} policy Policy to validate
1232
+ * @returns {Array<{canonicalName: CanonicalName, message: string, path:
1233
+ * string[], suggestion?: CanonicalName}>} Array of issue objects, or `undefined` if no issues were
1234
+ * found
1235
+ */
1236
+ const validatePolicyResources = (canonicalNames, policy) => {
1237
+ /**
1238
+ * Finds a suggestion for `badName` if it is a suffix of any
1239
+ * canonical name in `canonicalNames`.
1240
+ *
1241
+ * @param {string} badName Unknown canonical name
1242
+ * @returns {CanonicalName | undefined}
1243
+ */
1244
+ const findSuggestion = badName => {
1245
+ for (const canonicalName of canonicalNames) {
1246
+ if (canonicalName.endsWith(`>${badName}`)) {
1247
+ return canonicalName;
1248
+ }
1249
+ }
1250
+ return undefined;
1251
+ };
1252
+
1253
+ /** @type {Array<{canonicalName: CanonicalName, message: string, path: string[], suggestion?: CanonicalName}>} */
1254
+ const issues = [];
1255
+ for (const [resourceName, resourcePolicy] of entries(
1256
+ policy.resources ?? {},
1257
+ )) {
1258
+ if (!canonicalNames.has(resourceName)) {
1259
+ const issueMessage = `Resource ${q(resourceName)} was not found`;
1260
+ const suggestion = findSuggestion(resourceName);
1261
+ const issue = {
1262
+ canonicalName: resourceName,
1263
+ message: issueMessage,
1264
+ path: ['resources', resourceName],
1265
+ };
1266
+ if (suggestion) {
1267
+ issue.suggestion = suggestion;
1268
+ }
1269
+ issues.push(issue);
1270
+ }
1271
+ if (typeof resourcePolicy?.packages === 'object') {
1272
+ for (const packageName of keys(resourcePolicy.packages)) {
1273
+ if (!canonicalNames.has(packageName)) {
1274
+ const issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1275
+ const suggestion = findSuggestion(packageName);
1276
+ const issue = {
1277
+ canonicalName: packageName,
1278
+ message: issueMessage,
1279
+ path: ['resources', resourceName, 'packages', packageName],
1280
+ };
1281
+ if (suggestion) {
1282
+ issue.suggestion = suggestion;
1283
+ }
1284
+ issues.push(issue);
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ return issues;
1291
+ };
977
1292
 
978
1293
  /**
979
1294
  * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
980
- * @param {FileUrlString} packageLocation
1295
+ * @param {FileUrlString} entryPackageLocation
981
1296
  * @param {Set<string>} conditionsOption
982
1297
  * @param {PackageDescriptor} packageDescriptor
983
- * @param {string} moduleSpecifier
1298
+ * @param {string} entryModuleSpecifier
984
1299
  * @param {CompartmentMapForNodeModulesOptions} [options]
985
- * @returns {Promise<CompartmentMapDescriptor>}
986
- * @deprecated Use {@link mapNodeModules} instead.
1300
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
987
1301
  */
988
- export const compartmentMapForNodeModules = async (
1302
+ export const compartmentMapForNodeModules_ = async (
989
1303
  readPowers,
990
- packageLocation,
1304
+ entryPackageLocation,
991
1305
  conditionsOption,
992
1306
  packageDescriptor,
993
- moduleSpecifier,
1307
+ entryModuleSpecifier,
994
1308
  options = {},
995
1309
  ) => {
996
1310
  const {
@@ -999,6 +1313,9 @@ export const compartmentMapForNodeModules = async (
999
1313
  policy,
1000
1314
  strict = false,
1001
1315
  log = noop,
1316
+ unknownCanonicalNameHook,
1317
+ packageDataHook,
1318
+ packageDependenciesHook,
1002
1319
  } = options;
1003
1320
  const { maybeRead, canonical } = unpackReadPowers(readPowers);
1004
1321
  const languageOptions = makeLanguageOptions(options);
@@ -1021,7 +1338,7 @@ export const compartmentMapForNodeModules = async (
1021
1338
  const graph = await graphPackages(
1022
1339
  maybeRead,
1023
1340
  canonical,
1024
- packageLocation,
1341
+ entryPackageLocation,
1025
1342
  conditions,
1026
1343
  packageDescriptor,
1027
1344
  dev || (conditions && conditions.has('development')),
@@ -1029,52 +1346,76 @@ export const compartmentMapForNodeModules = async (
1029
1346
  languageOptions,
1030
1347
  strict,
1031
1348
  logicalPathGraph,
1032
- { log },
1349
+ { log, policy, packageDependenciesHook },
1033
1350
  );
1034
1351
 
1035
- if (policy) {
1036
- assertPolicy(policy);
1037
-
1038
- assert(
1039
- graph[ATTENUATORS_COMPARTMENT] === undefined,
1040
- `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`,
1041
- );
1352
+ makeAttenuatorsNode(graph, graph[entryPackageLocation], policy);
1042
1353
 
1043
- graph[ATTENUATORS_COMPARTMENT] = {
1044
- ...graph[packageLocation],
1045
- externalAliases: {},
1046
- label: ATTENUATORS_COMPARTMENT,
1047
- name: ATTENUATORS_COMPARTMENT,
1048
- };
1049
- }
1354
+ /**
1355
+ * @type {CanonicalNameMap}
1356
+ */
1357
+ const canonicalNameMap = new Map();
1050
1358
 
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;
1359
+ const finalGraph = finalizeGraph(
1360
+ graph,
1361
+ logicalPathGraph,
1362
+ entryPackageLocation,
1363
+ canonicalNameMap,
1364
+ );
1058
1365
 
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
- );
1366
+ // if policy exists, cross-reference the policy "resources" against the list
1367
+ // of known canonical names and fire the `unknownCanonicalName` hook for each
1368
+ // unknown resource, if found
1369
+ if (policy) {
1370
+ const canonicalNames = new Set(canonicalNameMap.keys());
1371
+ const issues = validatePolicyResources(canonicalNames, policy) ?? [];
1372
+ // Call default handler first if policy exists
1373
+ for (const { message, canonicalName, path, suggestion } of issues) {
1374
+ const hookInput = {
1375
+ canonicalName,
1376
+ message,
1377
+ path,
1378
+ log,
1379
+ };
1380
+ if (suggestion) {
1381
+ hookInput.suggestion = suggestion;
1382
+ }
1383
+ defaultUnknownCanonicalNameHandler(hookInput);
1384
+ // Then call user-provided hook if it exists
1385
+ if (unknownCanonicalNameHook) {
1386
+ unknownCanonicalNameHook(hookInput);
1387
+ }
1388
+ }
1389
+ }
1065
1390
 
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('>')}`);
1391
+ // Fire packageData hook with all package data before translateGraph
1392
+ if (packageDataHook) {
1393
+ const packageData =
1394
+ /** @type {Map<PackageCompartmentDescriptorName, PackageData>} */ (
1395
+ new Map(
1396
+ values(finalGraph).map(node => [
1397
+ node.label,
1398
+ {
1399
+ name: node.name,
1400
+ packageDescriptor: node.packageDescriptor,
1401
+ location: node.location,
1402
+ canonicalName: node.label,
1403
+ },
1404
+ ]),
1405
+ )
1406
+ );
1407
+ packageDataHook({
1408
+ packageData,
1409
+ log,
1410
+ });
1070
1411
  }
1071
1412
 
1072
1413
  const compartmentMap = translateGraph(
1073
- packageLocation,
1074
- moduleSpecifier,
1075
- graph,
1414
+ entryPackageLocation,
1415
+ entryModuleSpecifier,
1416
+ finalGraph,
1076
1417
  conditions,
1077
- policy,
1418
+ { policy, log, packageDependenciesHook },
1078
1419
  );
1079
1420
 
1080
1421
  return compartmentMap;
@@ -1089,12 +1430,21 @@ export const compartmentMapForNodeModules = async (
1089
1430
  * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
1090
1431
  * @param {string} moduleLocation
1091
1432
  * @param {MapNodeModulesOptions} [options]
1092
- * @returns {Promise<CompartmentMapDescriptor>}
1433
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
1093
1434
  */
1094
1435
  export const mapNodeModules = async (
1095
1436
  readPowers,
1096
1437
  moduleLocation,
1097
- { tags = new Set(), conditions = tags, log = noop, ...otherOptions } = {},
1438
+ {
1439
+ tags = new Set(),
1440
+ conditions = tags,
1441
+ log = noop,
1442
+ unknownCanonicalNameHook,
1443
+ packageDataHook,
1444
+ packageDependenciesHook,
1445
+ policy,
1446
+ ...otherOptions
1447
+ } = {},
1098
1448
  ) => {
1099
1449
  const {
1100
1450
  packageLocation,
@@ -1110,12 +1460,24 @@ export const mapNodeModules = async (
1110
1460
  assertPackageDescriptor(packageDescriptor);
1111
1461
  assertFileUrlString(packageLocation);
1112
1462
 
1113
- return compartmentMapForNodeModules(
1463
+ return compartmentMapForNodeModules_(
1114
1464
  readPowers,
1115
1465
  packageLocation,
1116
1466
  conditions,
1117
1467
  packageDescriptor,
1118
1468
  moduleSpecifier,
1119
- { log, ...otherOptions },
1469
+ {
1470
+ log,
1471
+ policy,
1472
+ unknownCanonicalNameHook,
1473
+ packageDependenciesHook,
1474
+ packageDataHook,
1475
+ ...otherOptions,
1476
+ },
1120
1477
  );
1121
1478
  };
1479
+
1480
+ /**
1481
+ * @deprecated Use {@link mapNodeModules} instead.
1482
+ */
1483
+ export const compartmentMapForNodeModules = compartmentMapForNodeModules_;