@endo/compartment-mapper 1.6.3 → 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 (81) 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.map +1 -1
  23. package/src/generic-graph.js +8 -3
  24. package/src/guards.d.ts +18 -0
  25. package/src/guards.d.ts.map +1 -0
  26. package/src/guards.js +109 -0
  27. package/src/hooks.md +124 -0
  28. package/src/import-archive-lite.d.ts.map +1 -1
  29. package/src/import-archive-lite.js +15 -11
  30. package/src/import-archive.d.ts +5 -19
  31. package/src/import-archive.d.ts.map +1 -1
  32. package/src/import-archive.js +7 -27
  33. package/src/import-hook.d.ts +4 -3
  34. package/src/import-hook.d.ts.map +1 -1
  35. package/src/import-hook.js +138 -69
  36. package/src/import-lite.d.ts +6 -6
  37. package/src/import-lite.d.ts.map +1 -1
  38. package/src/import-lite.js +8 -5
  39. package/src/import.d.ts +3 -3
  40. package/src/import.d.ts.map +1 -1
  41. package/src/import.js +16 -6
  42. package/src/infer-exports.d.ts.map +1 -1
  43. package/src/infer-exports.js +16 -6
  44. package/src/link.d.ts +4 -3
  45. package/src/link.d.ts.map +1 -1
  46. package/src/link.js +70 -58
  47. package/src/node-modules.d.ts +4 -3
  48. package/src/node-modules.d.ts.map +1 -1
  49. package/src/node-modules.js +482 -114
  50. package/src/parse-cjs-shared-export-wrapper.d.ts.map +1 -1
  51. package/src/parse-cjs-shared-export-wrapper.js +3 -1
  52. package/src/policy-format.d.ts +22 -5
  53. package/src/policy-format.d.ts.map +1 -1
  54. package/src/policy-format.js +342 -108
  55. package/src/policy.d.ts +13 -28
  56. package/src/policy.d.ts.map +1 -1
  57. package/src/policy.js +161 -106
  58. package/src/types/canonical-name.d.ts +97 -0
  59. package/src/types/canonical-name.d.ts.map +1 -0
  60. package/src/types/canonical-name.ts +151 -0
  61. package/src/types/compartment-map-schema.d.ts +114 -35
  62. package/src/types/compartment-map-schema.d.ts.map +1 -1
  63. package/src/types/compartment-map-schema.ts +202 -37
  64. package/src/types/external.d.ts +168 -28
  65. package/src/types/external.d.ts.map +1 -1
  66. package/src/types/external.ts +215 -26
  67. package/src/types/internal.d.ts +23 -42
  68. package/src/types/internal.d.ts.map +1 -1
  69. package/src/types/internal.ts +51 -50
  70. package/src/types/node-modules.d.ts +71 -10
  71. package/src/types/node-modules.d.ts.map +1 -1
  72. package/src/types/node-modules.ts +107 -9
  73. package/src/types/policy-schema.d.ts +26 -11
  74. package/src/types/policy-schema.d.ts.map +1 -1
  75. package/src/types/policy-schema.ts +29 -16
  76. package/src/types/policy.d.ts +6 -2
  77. package/src/types/policy.d.ts.map +1 -1
  78. package/src/types/policy.ts +7 -2
  79. package/src/types/typescript.d.ts +28 -0
  80. package/src/types/typescript.d.ts.map +1 -1
  81. 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
@@ -16,12 +17,13 @@
16
17
  import { inferExportsAndAliases } 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
  *
@@ -368,7 +435,7 @@ const calculatePackageWeight = packageName => {
368
435
  * @param {LanguageOptions} languageOptions
369
436
  * @param {boolean} strict
370
437
  * @param {LogicalPathGraph} logicalPathGraph
371
- * @param {GraphPackageOptions} [options]
438
+ * @param {GraphPackageOptions} options
372
439
  * @returns {Promise<undefined>}
373
440
  */
374
441
  const graphPackage = async (
@@ -382,7 +449,12 @@ const graphPackage = async (
382
449
  languageOptions,
383
450
  strict,
384
451
  logicalPathGraph,
385
- { commonDependencyDescriptors = {}, logicalPath = [], log = noop } = {},
452
+ {
453
+ commonDependencyDescriptors = {},
454
+ log = noop,
455
+ packageDependenciesHook,
456
+ policy,
457
+ } = {},
386
458
  ) => {
387
459
  if (graph[packageLocation] !== undefined) {
388
460
  // Returning the promise here would create a causal cycle and stall recursion.
@@ -397,7 +469,7 @@ const graphPackage = async (
397
469
  });
398
470
  }
399
471
 
400
- const result = /** @type {Node} */ ({});
472
+ const result = /** @type {Node} */ ({ location: packageLocation });
401
473
  graph[packageLocation] = result;
402
474
 
403
475
  /** @type {Node['dependencyLocations']} */
@@ -461,7 +533,6 @@ const graphPackage = async (
461
533
 
462
534
  for (const dependencyName of [...allDependencies].sort()) {
463
535
  const optional = optionals.has(dependencyName);
464
- const childLogicalPath = [...logicalPath, dependencyName];
465
536
  children.push(
466
537
  // Mutual recursion ahead:
467
538
  // eslint-disable-next-line no-use-before-define
@@ -477,10 +548,11 @@ const graphPackage = async (
477
548
  strict,
478
549
  logicalPathGraph,
479
550
  {
480
- childLogicalPath,
481
551
  optional,
482
552
  commonDependencyDescriptors,
483
553
  log,
554
+ packageDependenciesHook,
555
+ policy,
484
556
  },
485
557
  ),
486
558
  );
@@ -522,9 +594,9 @@ const graphPackage = async (
522
594
 
523
595
  const sourceDirname = basename(packageLocation);
524
596
 
525
- assign(result, {
597
+ /** @type {Partial<Node>} */
598
+ const partialNode = {
526
599
  name,
527
- path: logicalPath,
528
600
  label: `${name}${version ? `-v${version}` : ''}`,
529
601
  sourceDirname,
530
602
  explicitExports: exportsDescriptor !== undefined,
@@ -533,7 +605,9 @@ const graphPackage = async (
533
605
  dependencyLocations,
534
606
  types,
535
607
  parsers,
536
- });
608
+ packageDescriptor,
609
+ };
610
+ assign(result, partialNode);
537
611
 
538
612
  await Promise.all(
539
613
  values(result.externalAliases).map(async item => {
@@ -605,10 +679,11 @@ const gatherDependency = async (
605
679
  strict,
606
680
  logicalPathGraph,
607
681
  {
608
- childLogicalPath = [],
609
682
  optional = false,
610
683
  commonDependencyDescriptors = {},
611
684
  log = noop,
685
+ packageDependenciesHook,
686
+ policy,
612
687
  } = {},
613
688
  ) => {
614
689
  const dependency = await findPackage(
@@ -617,6 +692,7 @@ const gatherDependency = async (
617
692
  packageLocation,
618
693
  name,
619
694
  );
695
+
620
696
  if (dependency === undefined) {
621
697
  // allow the dependency to be missing if optional
622
698
  if (optional || !strict) {
@@ -624,6 +700,7 @@ const gatherDependency = async (
624
700
  }
625
701
  throw Error(`Cannot find dependency ${name} for ${packageLocation}`);
626
702
  }
703
+
627
704
  dependencyLocations[name] = dependency.packageLocation;
628
705
 
629
706
  logicalPathGraph.addEdge(
@@ -645,8 +722,9 @@ const gatherDependency = async (
645
722
  logicalPathGraph,
646
723
  {
647
724
  commonDependencyDescriptors,
648
- logicalPath: childLogicalPath,
649
725
  log,
726
+ packageDependenciesHook,
727
+ policy,
650
728
  },
651
729
  );
652
730
  };
@@ -669,7 +747,7 @@ const gatherDependency = async (
669
747
  * @param {LanguageOptions} languageOptions
670
748
  * @param {boolean} strict
671
749
  * @param {LogicalPathGraph} logicalPathGraph
672
- * @param {GraphPackagesOptions} [options]
750
+ * @param {GraphPackagesOptions} options
673
751
  * @returns {Promise<Graph>}
674
752
  */
675
753
  const graphPackages = async (
@@ -683,7 +761,7 @@ const graphPackages = async (
683
761
  languageOptions,
684
762
  strict,
685
763
  logicalPathGraph,
686
- { log = noop } = {},
764
+ { log = noop, packageDependenciesHook, policy } = {},
687
765
  ) => {
688
766
  const memo = create(null);
689
767
  /**
@@ -749,6 +827,8 @@ const graphPackages = async (
749
827
  {
750
828
  commonDependencyDescriptors,
751
829
  log,
830
+ packageDependenciesHook,
831
+ policy,
752
832
  },
753
833
  );
754
834
  return graph;
@@ -763,19 +843,101 @@ const graphPackages = async (
763
843
  * @param {Graph} graph
764
844
  * @param {Set<string>} conditions - build conditions about the target environment
765
845
  * for selecting relevant exports, e.g., "browser" or "node".
766
- * @param {SomePolicy} [policy]
767
- * @returns {CompartmentMapDescriptor}
846
+ * @param {TranslateGraphOptions} [options]
847
+ * @returns {PackageCompartmentMapDescriptor}
768
848
  */
769
849
  const translateGraph = (
770
850
  entryPackageLocation,
771
851
  entryModuleSpecifier,
772
852
  graph,
773
853
  conditions,
774
- policy,
854
+ { policy, log = noop, packageDependenciesHook } = {},
775
855
  ) => {
776
- /** @type {CompartmentMapDescriptor['compartments']} */
856
+ /** @type {Record<PackageCompartmentDescriptorName, PackageCompartmentDescriptor>} */
777
857
  const compartments = create(null);
778
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
+
779
941
  // For each package, build a map of all the external modules the package can
780
942
  // import from other packages.
781
943
  // The keys of this map are the full specifiers of those modules from the
@@ -785,36 +947,24 @@ const translateGraph = (
785
947
  // The full map includes every exported module from every dependencey
786
948
  // package and is a complete list of every external module that the
787
949
  // corresponding compartment can import.
788
- for (const dependeeLocation of keys(graph).sort()) {
950
+ for (const dependeeLocation of /** @type {PackageCompartmentDescriptorName[]} */ (
951
+ keys(graph).sort()
952
+ )) {
789
953
  const {
790
954
  name,
791
- path,
792
955
  label,
793
956
  sourceDirname,
794
- dependencyLocations,
795
957
  internalAliases,
796
958
  parsers,
797
959
  types,
960
+ packageDescriptor,
798
961
  } = graph[dependeeLocation];
799
- /** @type {CompartmentDescriptor['modules']} */
962
+ /** @type {Record<string, CompartmentModuleConfiguration>} */
800
963
  const moduleDescriptors = create(null);
801
- /** @type {CompartmentDescriptor['scopes']} */
964
+ /** @type {Record<string, ScopeDescriptor<PackageCompartmentDescriptorName>>} */
802
965
  const scopes = create(null);
803
966
 
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
- );
967
+ const packagePolicy = makePackagePolicy(label, { policy });
818
968
 
819
969
  /* c8 ignore next */
820
970
  if (policy && !packagePolicy) {
@@ -822,34 +972,29 @@ const translateGraph = (
822
972
  throw new TypeError('Unexpectedly falsy package policy');
823
973
  }
824
974
 
975
+ let dependencyLocations = graph[dependeeLocation].dependencyLocations;
976
+ dependencyLocations = executePackageDependenciesHook(
977
+ label,
978
+ dependencyLocations,
979
+ );
980
+
825
981
  /**
826
982
  * @param {string} dependencyName
827
- * @param {string} packageLocation
983
+ * @param {PackageCompartmentDescriptorName} packageLocation
828
984
  */
829
985
  const digestExternalAliases = (dependencyName, packageLocation) => {
830
- const { externalAliases, explicitExports, name, path } =
831
- graph[packageLocation];
986
+ const { externalAliases, explicitExports } = graph[packageLocation];
832
987
  for (const exportPath of keys(externalAliases).sort()) {
833
988
  const targetPath = externalAliases[exportPath];
834
989
  // dependency name may be different from package's name,
835
- // 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
836
992
  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
- };
852
- }
993
+ // if we have policy, this has already been vetted
994
+ moduleDescriptors[localPath] = {
995
+ compartment: packageLocation,
996
+ module: targetPath,
997
+ };
853
998
  }
854
999
  // if the exports field is not present, then all modules must be accessible
855
1000
  if (!explicitExports) {
@@ -864,7 +1009,6 @@ const translateGraph = (
864
1009
  for (const dependencyName of keys(dependencyLocations).sort()) {
865
1010
  const dependencyLocation = dependencyLocations[dependencyName];
866
1011
  digestExternalAliases(dependencyName, dependencyLocation);
867
- compartmentNames.add(dependencyLocation);
868
1012
  }
869
1013
  // digest own internal aliases
870
1014
  for (const modulePath of keys(internalAliases).sort()) {
@@ -881,9 +1025,9 @@ const translateGraph = (
881
1025
  }
882
1026
 
883
1027
  compartments[dependeeLocation] = {
1028
+ version: packageDescriptor.version ? packageDescriptor.version : '',
884
1029
  label,
885
1030
  name,
886
- path,
887
1031
  location: dependeeLocation,
888
1032
  sourceDirname,
889
1033
  modules: moduleDescriptors,
@@ -891,7 +1035,6 @@ const translateGraph = (
891
1035
  parsers,
892
1036
  types,
893
1037
  policy: /** @type {SomePackagePolicy} */ (packagePolicy),
894
- compartments: compartmentNames,
895
1038
  };
896
1039
  }
897
1040
 
@@ -900,7 +1043,7 @@ const translateGraph = (
900
1043
  // https://github.com/endojs/endo/issues/2388
901
1044
  tags: [...conditions],
902
1045
  entry: {
903
- compartment: entryPackageLocation,
1046
+ compartment: /** @type {FileUrlString} */ (entryPackageLocation),
904
1047
  module: entryModuleSpecifier,
905
1048
  },
906
1049
  compartments,
@@ -974,23 +1117,200 @@ const makeLanguageOptions = ({
974
1117
  workspaceModuleLanguageForExtension,
975
1118
  };
976
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
+ };
1150
+
1151
+ /**
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
+ };
977
1298
 
978
1299
  /**
979
1300
  * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
980
- * @param {FileUrlString} packageLocation
1301
+ * @param {FileUrlString} entryPackageLocation
981
1302
  * @param {Set<string>} conditionsOption
982
1303
  * @param {PackageDescriptor} packageDescriptor
983
- * @param {string} moduleSpecifier
1304
+ * @param {string} entryModuleSpecifier
984
1305
  * @param {CompartmentMapForNodeModulesOptions} [options]
985
- * @returns {Promise<CompartmentMapDescriptor>}
986
- * @deprecated Use {@link mapNodeModules} instead.
1306
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
987
1307
  */
988
- export const compartmentMapForNodeModules = async (
1308
+ export const compartmentMapForNodeModules_ = async (
989
1309
  readPowers,
990
- packageLocation,
1310
+ entryPackageLocation,
991
1311
  conditionsOption,
992
1312
  packageDescriptor,
993
- moduleSpecifier,
1313
+ entryModuleSpecifier,
994
1314
  options = {},
995
1315
  ) => {
996
1316
  const {
@@ -999,6 +1319,9 @@ export const compartmentMapForNodeModules = async (
999
1319
  policy,
1000
1320
  strict = false,
1001
1321
  log = noop,
1322
+ unknownCanonicalNameHook,
1323
+ packageDataHook,
1324
+ packageDependenciesHook,
1002
1325
  } = options;
1003
1326
  const { maybeRead, canonical } = unpackReadPowers(readPowers);
1004
1327
  const languageOptions = makeLanguageOptions(options);
@@ -1021,7 +1344,7 @@ export const compartmentMapForNodeModules = async (
1021
1344
  const graph = await graphPackages(
1022
1345
  maybeRead,
1023
1346
  canonical,
1024
- packageLocation,
1347
+ entryPackageLocation,
1025
1348
  conditions,
1026
1349
  packageDescriptor,
1027
1350
  dev || (conditions && conditions.has('development')),
@@ -1029,52 +1352,76 @@ export const compartmentMapForNodeModules = async (
1029
1352
  languageOptions,
1030
1353
  strict,
1031
1354
  logicalPathGraph,
1032
- { log },
1355
+ { log, policy, packageDependenciesHook },
1033
1356
  );
1034
1357
 
1035
- if (policy) {
1036
- assertPolicy(policy);
1037
-
1038
- assert(
1039
- graph[ATTENUATORS_COMPARTMENT] === undefined,
1040
- `${q(ATTENUATORS_COMPARTMENT)} is a reserved compartment name`,
1041
- );
1358
+ makeAttenuatorsNode(graph, graph[entryPackageLocation], policy);
1042
1359
 
1043
- graph[ATTENUATORS_COMPARTMENT] = {
1044
- ...graph[packageLocation],
1045
- externalAliases: {},
1046
- label: ATTENUATORS_COMPARTMENT,
1047
- name: ATTENUATORS_COMPARTMENT,
1048
- };
1049
- }
1360
+ /**
1361
+ * @type {CanonicalNameMap}
1362
+ */
1363
+ const canonicalNameMap = new Map();
1050
1364
 
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;
1365
+ const finalGraph = finalizeGraph(
1366
+ graph,
1367
+ logicalPathGraph,
1368
+ entryPackageLocation,
1369
+ canonicalNameMap,
1370
+ );
1058
1371
 
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
- );
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
+ }
1065
1396
 
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('>')}`);
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
+ });
1070
1417
  }
1071
1418
 
1072
1419
  const compartmentMap = translateGraph(
1073
- packageLocation,
1074
- moduleSpecifier,
1075
- graph,
1420
+ entryPackageLocation,
1421
+ entryModuleSpecifier,
1422
+ finalGraph,
1076
1423
  conditions,
1077
- policy,
1424
+ { policy, log, packageDependenciesHook },
1078
1425
  );
1079
1426
 
1080
1427
  return compartmentMap;
@@ -1089,12 +1436,21 @@ export const compartmentMapForNodeModules = async (
1089
1436
  * @param {ReadFn | ReadPowers<FileUrlString> | MaybeReadPowers<FileUrlString>} readPowers
1090
1437
  * @param {string} moduleLocation
1091
1438
  * @param {MapNodeModulesOptions} [options]
1092
- * @returns {Promise<CompartmentMapDescriptor>}
1439
+ * @returns {Promise<PackageCompartmentMapDescriptor>}
1093
1440
  */
1094
1441
  export const mapNodeModules = async (
1095
1442
  readPowers,
1096
1443
  moduleLocation,
1097
- { 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
+ } = {},
1098
1454
  ) => {
1099
1455
  const {
1100
1456
  packageLocation,
@@ -1110,12 +1466,24 @@ export const mapNodeModules = async (
1110
1466
  assertPackageDescriptor(packageDescriptor);
1111
1467
  assertFileUrlString(packageLocation);
1112
1468
 
1113
- return compartmentMapForNodeModules(
1469
+ return compartmentMapForNodeModules_(
1114
1470
  readPowers,
1115
1471
  packageLocation,
1116
1472
  conditions,
1117
1473
  packageDescriptor,
1118
1474
  moduleSpecifier,
1119
- { log, ...otherOptions },
1475
+ {
1476
+ log,
1477
+ policy,
1478
+ unknownCanonicalNameHook,
1479
+ packageDependenciesHook,
1480
+ packageDataHook,
1481
+ ...otherOptions,
1482
+ },
1120
1483
  );
1121
1484
  };
1485
+
1486
+ /**
1487
+ * @deprecated Use {@link mapNodeModules} instead.
1488
+ */
1489
+ export const compartmentMapForNodeModules = compartmentMapForNodeModules_;