Skip to content

Commit c37edbe

Browse files
committed
feat(compartment-mapper): add MapNodeModulesHooks.canonicalNames
- This hook just dumps all known canonical names. - The `unknownCanonicalName` hook has changed its parameters.
1 parent 9dacaa3 commit c37edbe

File tree

10 files changed

+234
-171
lines changed

10 files changed

+234
-171
lines changed

packages/compartment-mapper/src/hooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,9 @@ export const makeDefaultHookConfiguration = (
337337
// these hooks for mapNodeModules are only applied if a policy was provided.
338338
mapNodeModules: policy
339339
? {
340-
unknownCanonicalName: ({ canonicalName, issue, log }) => {
340+
unknownCanonicalName: ({ canonicalName, message, log }) => {
341341
log(
342-
`WARN: Invalid resource ${q(canonicalName)} in policy: ${issue}`,
342+
`WARN: Invalid resource ${q(canonicalName)} in policy: ${message}`,
343343
);
344344
},
345345

packages/compartment-mapper/src/node-modules.js

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,53 +1189,59 @@ const finalizeGraph = (
11891189
*
11901190
* @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
11911191
* @param {SomePolicy} policy Policy to validate
1192-
* @returns {Array<{canonicalName: CanonicalName, issue: string, path:
1193-
* string[]}>} Array of issue objects, or `undefined` if no issues were
1192+
* @returns {Array<{canonicalName: CanonicalName, message: string, path:
1193+
* string[], suggestion?: CanonicalName}>} Array of issue objects, or `undefined` if no issues were
11941194
* found
11951195
*/
11961196
const validatePolicyResources = (canonicalNames, policy) => {
11971197
/**
1198-
* Appends a suggestion to `issueMessage` if `badName` is a suffix of any
1198+
* Finds a suggestion for `badName` if it is a suffix of any
11991199
* canonical name in `canonicalNames`.
12001200
*
12011201
* @param {string} badName Unknown canonical name
1202-
* @param {string} issueMessage Existing issue message
1203-
* @returns {string}
1202+
* @returns {CanonicalName | undefined}
12041203
*/
1205-
const makeSuggestion = (badName, issueMessage) => {
1204+
const findSuggestion = badName => {
12061205
for (const canonicalName of canonicalNames) {
12071206
if (canonicalName.endsWith(`>${badName}`)) {
1208-
issueMessage += `; did you mean ${q(canonicalName)}?`;
1209-
break;
1207+
return canonicalName;
12101208
}
12111209
}
1212-
return issueMessage;
1210+
return undefined;
12131211
};
12141212

1215-
/** @type {Array<{canonicalName: CanonicalName, issue: string, path: string[]}>} */
1213+
/** @type {Array<{canonicalName: CanonicalName, message: string, path: string[], suggestion?: CanonicalName}>} */
12161214
const issues = [];
12171215
for (const [resourceName, resourcePolicy] of entries(
12181216
policy.resources ?? {},
12191217
)) {
12201218
if (!canonicalNames.has(resourceName)) {
1221-
let issueMessage = `Resource ${q(resourceName)} was not found`;
1222-
issueMessage = makeSuggestion(resourceName, issueMessage);
1223-
issues.push({
1219+
const issueMessage = `Resource ${q(resourceName)} was not found`;
1220+
const suggestion = findSuggestion(resourceName);
1221+
const issue = {
12241222
canonicalName: resourceName,
1225-
issue: issueMessage,
1223+
message: issueMessage,
12261224
path: ['resources', resourceName],
1227-
});
1225+
};
1226+
if (suggestion) {
1227+
issue.suggestion = suggestion;
1228+
}
1229+
issues.push(issue);
12281230
}
12291231
if (typeof resourcePolicy?.packages === 'object') {
12301232
for (const packageName of keys(resourcePolicy.packages)) {
12311233
if (!canonicalNames.has(packageName)) {
1232-
let issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1233-
issueMessage = makeSuggestion(packageName, issueMessage);
1234-
issues.push({
1234+
const issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1235+
const suggestion = findSuggestion(packageName);
1236+
const issue = {
12351237
canonicalName: packageName,
1236-
issue: issueMessage,
1238+
message: issueMessage,
12371239
path: ['resources', resourceName, 'packages', packageName],
1238-
});
1240+
};
1241+
if (suggestion) {
1242+
issue.suggestion = suggestion;
1243+
}
1244+
issues.push(issue);
12391245
}
12401246
}
12411247
}
@@ -1323,16 +1329,27 @@ export const compartmentMapForNodeModules_ = async (
13231329
if (policy) {
13241330
const canonicalNames = new Set(canonicalNameMap.keys());
13251331
const issues = validatePolicyResources(canonicalNames, policy) ?? [];
1326-
for (const { issue, canonicalName, path } of issues) {
1327-
executeHook('unknownCanonicalName', {
1332+
for (const { message, canonicalName, path, suggestion } of issues) {
1333+
const hookInput = {
13281334
canonicalName,
1329-
issue,
1335+
message,
13301336
path,
13311337
log,
1332-
});
1338+
};
1339+
if (suggestion) {
1340+
hookInput.suggestion = suggestion;
1341+
}
1342+
executeHook('unknownCanonicalName', hookInput);
13331343
}
13341344
}
13351345

1346+
// Fire canonicalNames hook with all canonical names before translateGraph
1347+
const canonicalNames = new Set(canonicalNameMap.keys());
1348+
executeHook('canonicalNames', {
1349+
canonicalNames,
1350+
log,
1351+
});
1352+
13361353
const compartmentMap = translateGraph(
13371354
entryPackageLocation,
13381355
entryModuleSpecifier,

packages/compartment-mapper/src/policy-format.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
* ImplicitAttenuationDefinition,
1919
* FullAttenuationDefinition,
2020
* UnionToIntersection,
21-
* PackageNamingKit
21+
* PackageNamingKit,
22+
* WildcardPolicy,
23+
* SomePolicy
2224
*} from './types.js'
2325
*/
2426

packages/compartment-mapper/src/types/hooks.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,16 @@ export type MapNodeModulesHooks = {
190190
unknownCanonicalName: HookFn<{
191191
canonicalName: CanonicalName;
192192
path: string[];
193-
issue: string;
193+
message: string;
194+
suggestion?: CanonicalName;
195+
}>;
196+
197+
/**
198+
* Executed with all canonical names found in the compartment map.
199+
* Called once before translateGraph.
200+
*/
201+
canonicalNames: HookFn<{
202+
canonicalNames: Readonly<Set<CanonicalName>>;
194203
}>;
195204

196205
/**

packages/compartment-mapper/src/types/internal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import type {
3434
LinkHooks,
3535
LogOptions,
3636
MakeImportHookMakerHooks,
37-
MakeModuleMapHookHooks,
3837
ModuleTransforms,
3938
ParseFn,
4039
ParserForLanguage,

packages/compartment-mapper/test/map-node-modules.hooks.test.js

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,13 +537,13 @@ test('mapNodeModules - packageDependencies hook no modification (snapshot)', asy
537537
test('mapNodeModules - unknownCanonicalName hook called for missing policy resources', async t => {
538538
t.plan(6);
539539

540-
/** @type {Array<{canonicalName: CanonicalName, path: string[], issue: string}>} */
540+
/** @type {Array<{canonicalName: CanonicalName, path: string[], message: string}>} */
541541
const hookCalls = [];
542542

543543
/** @type {HookConfiguration<MapNodeModulesHooks>} */
544544
const hooks = {
545-
unknownCanonicalName: ({ canonicalName, path, issue }) => {
546-
hookCalls.push({ canonicalName, path, issue });
545+
unknownCanonicalName: ({ canonicalName, path, message }) => {
546+
hookCalls.push({ canonicalName, path, message });
547547
},
548548
};
549549

@@ -589,10 +589,10 @@ test('mapNodeModules - unknownCanonicalName hook called for missing policy resou
589589
'should provide correct path for unknown resource',
590590
);
591591
t.true(
592-
unknownResourceCall?.issue.includes(
592+
unknownResourceCall?.message.includes(
593593
'Resource "unknown-package" was not found',
594594
),
595-
'should provide descriptive issue message for unknown resource',
595+
'should provide descriptive message for unknown resource',
596596
);
597597

598598
// Check hook call for unknown nested package
@@ -625,3 +625,121 @@ test('mapNodeModules - unknownCanonicalName hook not called when all resources e
625625

626626
t.false(hookCalled, 'should not call hook when all policy resources exist');
627627
});
628+
629+
test('mapNodeModules - unknownCanonicalName hook includes suggestions when available', async t => {
630+
/** @type {Array<{canonicalName: CanonicalName, path: string[], message: string, suggestion?: CanonicalName}>} */
631+
const hookCalls = [];
632+
633+
/** @type {HookConfiguration<MapNodeModulesHooks>} */
634+
const hooks = {
635+
unknownCanonicalName: ({ canonicalName, path, message, suggestion }) => {
636+
hookCalls.push({ canonicalName, path, message, suggestion });
637+
},
638+
};
639+
640+
// Policy with typos that should trigger suggestions
641+
/** @type {SomePolicy} */
642+
const policyWithTypo = {
643+
entry: {
644+
packages: WILDCARD_POLICY_VALUE,
645+
},
646+
resources: {
647+
'dep-aa': {
648+
// Not close enough to 'dep-a' to suggest, but contains 'dep-c'
649+
// which should suggest 'dep-a>dep-c'
650+
packages: {
651+
'dep-c': true,
652+
},
653+
},
654+
},
655+
};
656+
657+
const readPowers = makeProjectFixtureReadPowers(testFixture);
658+
await mapNodeModules(readPowers, testFixture.entrypoint, {
659+
hooks,
660+
policy: policyWithTypo,
661+
});
662+
663+
t.is(
664+
hookCalls.length,
665+
2,
666+
'should call hook twice for both unknown resources',
667+
);
668+
669+
// Check the call for the unknown top-level resource (no close suggestion)
670+
const unknownResourceCall = hookCalls.find(
671+
call => call.canonicalName === 'dep-aa',
672+
);
673+
t.truthy(unknownResourceCall, 'should call hook for unknown resource');
674+
t.deepEqual(
675+
unknownResourceCall?.path,
676+
['resources', 'dep-aa'],
677+
'should provide correct path for unknown resource',
678+
);
679+
t.true(
680+
unknownResourceCall?.message.includes('Resource "dep-aa" was not found'),
681+
'should provide descriptive message for unknown resource',
682+
);
683+
t.is(
684+
unknownResourceCall?.suggestion,
685+
undefined,
686+
'should not suggest when no close match exists',
687+
);
688+
689+
// Check the call for the nested package (should have suggestion)
690+
const nestedPackageCall = hookCalls.find(
691+
call => call.canonicalName === 'dep-c',
692+
);
693+
t.truthy(nestedPackageCall, 'should call hook for nested unknown package');
694+
t.deepEqual(
695+
nestedPackageCall?.path,
696+
['resources', 'dep-aa', 'packages', 'dep-c'],
697+
'should provide correct path for nested unknown package',
698+
);
699+
t.true(
700+
nestedPackageCall?.message.includes(
701+
'Resource "dep-c" from resource "dep-aa" was not found',
702+
),
703+
'should provide descriptive message for nested unknown package',
704+
);
705+
t.is(
706+
nestedPackageCall?.suggestion,
707+
'dep-a>dep-c',
708+
'should suggest the closest matching canonical name',
709+
);
710+
});
711+
712+
test('mapNodeModules - canonicalNames hook called with all canonical names', async t => {
713+
t.plan(1);
714+
715+
/** @type {Set<CanonicalName>} */
716+
let receivedCanonicalNames = new Set();
717+
718+
/** @type {HookConfiguration<MapNodeModulesHooks>} */
719+
const hooks = {
720+
canonicalNames: ({ canonicalNames }) => {
721+
receivedCanonicalNames = new Set(canonicalNames);
722+
},
723+
};
724+
725+
const readPowers = makeProjectFixtureReadPowers(testFixture);
726+
await mapNodeModules(readPowers, testFixture.entrypoint, { hooks });
727+
728+
// Expected canonical names based on the test fixture:
729+
// - $root$ (the entry package 'app' becomes '$root$')
730+
// - dep-a (direct dependency)
731+
// - dep-b (direct dependency)
732+
// - dep-a>dep-c (transitive dependency through dep-a)
733+
const expectedCanonicalNames = new Set([
734+
'$root$',
735+
'dep-a',
736+
'dep-b',
737+
'dep-a>dep-c',
738+
]);
739+
740+
t.deepEqual(
741+
receivedCanonicalNames,
742+
expectedCanonicalNames,
743+
'should receive exactly the expected canonical names from the project fixture',
744+
);
745+
});

packages/compartment-mapper/test/policy.test.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ const defaultExpectations = {
107107
builtins2: '{"c":3,"default":{"c":3}}',
108108
}),
109109
};
110+
const anyExpectations = {
111+
namespace: moduleify({
112+
...defaultExpectations.namespace,
113+
carol: { bluePill: 'number', redPill: 'number', purplePill: 'number' },
114+
}),
115+
};
116+
110117

111118
const powerlessCarolExpectations = {
112119
namespace: moduleify({
@@ -173,15 +180,12 @@ scaffold(
173180
'policy - enforcement with "any" policy',
174181
test,
175182
fixture,
176-
assertTestAlwaysThrows,
183+
combineAssertions(
184+
makeResultAssertions(anyExpectations),
185+
assertExternalModuleNotFound,
186+
),
177187
2, // expected number of assertions
178188
{
179-
shouldFailBeforeArchiveOperations: true,
180-
onError: (t, { error }) => {
181-
t.regex(error.message, /unknown resources found in policy/i);
182-
// see the snapshot for the error hint in the message
183-
t.snapshot(sanitizePaths(error.message));
184-
},
185189
addGlobals: globals,
186190
policy: anyPolicy,
187191
},
@@ -195,10 +199,16 @@ scaffold(
195199
2, // expected number of assertions
196200
{
197201
shouldFailBeforeArchiveOperations: true,
198-
onError: (t, { error }) => {
199-
t.regex(error.message, /unknown resources found in policy/i);
202+
onError: (t, { error, testCategoryHint }) => {
203+
if (testCategoryHint === 'Archive') {
204+
t.regex(error.message, /unknown resources found in policy/i);
205+
t.snapshot(sanitizePaths(error.message), 'archive case error message');
206+
} else {
207+
t.regex(error.message, /cannot find external module/i);
208+
t.snapshot(sanitizePaths(error.message), 'location case error message');
209+
}
200210
// see the snapshot for the error hint in the message
201-
t.snapshot(sanitizePaths(error.message));
211+
202212
},
203213
addGlobals: globals,
204214
policy: {
@@ -437,16 +447,19 @@ scaffold(
437447
'policy - nested export in attenuator',
438448
test,
439449
fixture,
440-
assertTestAlwaysThrows,
441-
1, // expected number of assertions
450+
combineAssertions(
451+
makeResultAssertions(defaultExpectations),
452+
assertExternalModuleNotFound,
453+
),
454+
2, // expected number of assertions
442455
{
443-
shouldFailBeforeArchiveOperations: true,
444-
onError: (t, { error }) => {
445-
t.regex(
446-
error.message,
447-
/Resource "myattenuator\/attenuate" was not found/i,
448-
);
449-
},
456+
// shouldFailBeforeArchiveOperations: true,
457+
// onError: (t, { error }) => {
458+
// t.regex(
459+
// error.message,
460+
// /Resource "myattenuator\/attenuate" was not found/i,
461+
// );
462+
// },
450463
addGlobals: globals,
451464
policy: nestedAttenuator(policy),
452465
},

0 commit comments

Comments
 (0)