Skip to content

Commit 1d864b5

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 4b97e41 commit 1d864b5

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
@@ -1188,53 +1188,59 @@ const finalizeGraph = (
11881188
*
11891189
* @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
11901190
* @param {SomePolicy} policy Policy to validate
1191-
* @returns {Array<{canonicalName: CanonicalName, issue: string, path:
1192-
* string[]}>} Array of issue objects, or `undefined` if no issues were
1191+
* @returns {Array<{canonicalName: CanonicalName, message: string, path:
1192+
* string[], suggestion?: CanonicalName}>} Array of issue objects, or `undefined` if no issues were
11931193
* found
11941194
*/
11951195
const validatePolicyResources = (canonicalNames, policy) => {
11961196
/**
1197-
* Appends a suggestion to `issueMessage` if `badName` is a suffix of any
1197+
* Finds a suggestion for `badName` if it is a suffix of any
11981198
* canonical name in `canonicalNames`.
11991199
*
12001200
* @param {string} badName Unknown canonical name
1201-
* @param {string} issueMessage Existing issue message
1202-
* @returns {string}
1201+
* @returns {CanonicalName | undefined}
12031202
*/
1204-
const makeSuggestion = (badName, issueMessage) => {
1203+
const findSuggestion = badName => {
12051204
for (const canonicalName of canonicalNames) {
12061205
if (canonicalName.endsWith(`>${badName}`)) {
1207-
issueMessage += `; did you mean ${q(canonicalName)}?`;
1208-
break;
1206+
return canonicalName;
12091207
}
12101208
}
1211-
return issueMessage;
1209+
return undefined;
12121210
};
12131211

1214-
/** @type {Array<{canonicalName: CanonicalName, issue: string, path: string[]}>} */
1212+
/** @type {Array<{canonicalName: CanonicalName, message: string, path: string[], suggestion?: CanonicalName}>} */
12151213
const issues = [];
12161214
for (const [resourceName, resourcePolicy] of entries(
12171215
policy.resources ?? {},
12181216
)) {
12191217
if (!canonicalNames.has(resourceName)) {
1220-
let issueMessage = `Resource ${q(resourceName)} was not found`;
1221-
issueMessage = makeSuggestion(resourceName, issueMessage);
1222-
issues.push({
1218+
const issueMessage = `Resource ${q(resourceName)} was not found`;
1219+
const suggestion = findSuggestion(resourceName);
1220+
const issue = {
12231221
canonicalName: resourceName,
1224-
issue: issueMessage,
1222+
message: issueMessage,
12251223
path: ['resources', resourceName],
1226-
});
1224+
};
1225+
if (suggestion) {
1226+
issue.suggestion = suggestion;
1227+
}
1228+
issues.push(issue);
12271229
}
12281230
if (typeof resourcePolicy?.packages === 'object') {
12291231
for (const packageName of keys(resourcePolicy.packages)) {
12301232
if (!canonicalNames.has(packageName)) {
1231-
let issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1232-
issueMessage = makeSuggestion(packageName, issueMessage);
1233-
issues.push({
1233+
const issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
1234+
const suggestion = findSuggestion(packageName);
1235+
const issue = {
12341236
canonicalName: packageName,
1235-
issue: issueMessage,
1237+
message: issueMessage,
12361238
path: ['resources', resourceName, 'packages', packageName],
1237-
});
1239+
};
1240+
if (suggestion) {
1241+
issue.suggestion = suggestion;
1242+
}
1243+
issues.push(issue);
12381244
}
12391245
}
12401246
}
@@ -1322,16 +1328,27 @@ export const compartmentMapForNodeModules_ = async (
13221328
if (policy) {
13231329
const canonicalNames = new Set(canonicalNameMap.keys());
13241330
const issues = validatePolicyResources(canonicalNames, policy) ?? [];
1325-
for (const { issue, canonicalName, path } of issues) {
1326-
executeHook('unknownCanonicalName', {
1331+
for (const { message, canonicalName, path, suggestion } of issues) {
1332+
const hookInput = {
13271333
canonicalName,
1328-
issue,
1334+
message,
13291335
path,
13301336
log,
1331-
});
1337+
};
1338+
if (suggestion) {
1339+
hookInput.suggestion = suggestion;
1340+
}
1341+
executeHook('unknownCanonicalName', hookInput);
13321342
}
13331343
}
13341344

1345+
// Fire canonicalNames hook with all canonical names before translateGraph
1346+
const canonicalNames = new Set(canonicalNameMap.keys());
1347+
executeHook('canonicalNames', {
1348+
canonicalNames,
1349+
log,
1350+
});
1351+
13351352
const compartmentMap = translateGraph(
13361353
entryPackageLocation,
13371354
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)