@usefragments/core 2.0.1 → 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.
- package/dist/codes/index.d.ts +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{index-C8bcXVav.d.ts → index-xn5CNdf4.d.ts} +451 -451
- package/dist/index.d.ts +1438 -1426
- package/dist/index.js +5074 -4583
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +24 -24
- package/dist/preview-runtime.d.ts +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +380 -380
- package/dist/schemas/index.d.ts +1 -1
- package/dist/{governance-hOPXGbbs.d.ts → source-identity-DvOBz2yJ.d.ts} +675 -610
- package/dist/storyAdapter.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/conform.ts +10 -0
- package/src/contract/index.ts +24 -0
- package/src/contract/preimage.ts +17 -3
- package/src/contract/source-identity.test.ts +167 -0
- package/src/contract/source-identity.ts +303 -0
- package/src/evaluation/canonical-facts-trust.test.ts +398 -0
- package/src/evaluation/evaluate.ts +84 -11
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +28 -2
- package/src/evaluation/index.ts +7 -1
- package/src/evaluation/receipt.ts +17 -2
- package/src/evaluation/types.ts +19 -4
- package/src/index.ts +29 -0
- package/src/rules/components-prefer-library-classnames.test.ts +354 -0
- package/src/rules/components-prefer-library.test.ts +246 -0
- package/src/rules/components-prefer-library.ts +340 -61
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
parseAnalysisPlanV1,
|
|
8
8
|
} from "../analysis-plan/index.js";
|
|
9
9
|
import { canonicalPreimage, contractHash, sha256Hex } from "../contract/hash.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
12
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
13
|
+
type ContractPreimage,
|
|
14
|
+
} from "../contract/preimage.js";
|
|
11
15
|
import {
|
|
12
16
|
digestHexStringSchema,
|
|
13
17
|
evaluationReceiptIdFromDigest,
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
repositoryBindingDigestV1,
|
|
22
26
|
repositoryBindingKeyV1Schema,
|
|
23
27
|
} from "../repository-binding.js";
|
|
28
|
+
import { SOURCE_IDENTITY_EVALUATOR_VERSION_V2 } from "./types.js";
|
|
24
29
|
import type {
|
|
25
30
|
EffectiveEvaluationWaiverV2,
|
|
26
31
|
EvaluationInputV2,
|
|
@@ -51,7 +56,7 @@ const unique = (values: readonly string[]): boolean => new Set(values).size ===
|
|
|
51
56
|
|
|
52
57
|
const contractPreimageV1Schema: z.ZodType<ContractPreimage> = z
|
|
53
58
|
.object({
|
|
54
|
-
schema: z.
|
|
59
|
+
schema: z.enum([CONTRACT_PREIMAGE_SCHEMA, CONTRACT_PREIMAGE_SCHEMA_V2]),
|
|
55
60
|
domains: z
|
|
56
61
|
.object({
|
|
57
62
|
components: digestHexStringSchema,
|
|
@@ -205,6 +210,16 @@ const evaluationInputV2RawSchema = z
|
|
|
205
210
|
})
|
|
206
211
|
.strict()
|
|
207
212
|
.superRefine((input, context) => {
|
|
213
|
+
if (
|
|
214
|
+
input.contract.preimage.schema === CONTRACT_PREIMAGE_SCHEMA_V2 &&
|
|
215
|
+
input.evaluatorVersion !== SOURCE_IDENTITY_EVALUATOR_VERSION_V2
|
|
216
|
+
) {
|
|
217
|
+
context.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ["evaluatorVersion"],
|
|
220
|
+
message: "Source-qualified contracts require evaluation-kernel:v2.2",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
208
223
|
let plan;
|
|
209
224
|
try {
|
|
210
225
|
plan = parseAnalysisPlanV1(input.analysisPlan);
|
package/src/evaluation/types.ts
CHANGED
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
* contract — every scenario's authored kernel verdict must land.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
19
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
20
|
+
type ContractPreimage,
|
|
21
|
+
} from "../contract/preimage.js";
|
|
18
22
|
import type {
|
|
19
23
|
AnalysisPlanV1,
|
|
20
24
|
CoverageRegionV1,
|
|
@@ -26,9 +30,20 @@ import type { FactIndex } from "../facts/fact-index.js";
|
|
|
26
30
|
import type { RepositoryBindingKeyV1 } from "../repository-binding.js";
|
|
27
31
|
|
|
28
32
|
/** Bumped when kernel semantics change in a way that can flip a verdict. */
|
|
29
|
-
export const EVALUATOR_VERSION = "evaluation-kernel:v1" as const;
|
|
30
|
-
/**
|
|
31
|
-
export const EVALUATOR_VERSION_V2 = "evaluation-kernel:v2" as const;
|
|
33
|
+
export const EVALUATOR_VERSION = "evaluation-kernel:v1.1" as const;
|
|
34
|
+
/** Current facts-only evaluator. The input schema remains V2. */
|
|
35
|
+
export const EVALUATOR_VERSION_V2 = "evaluation-kernel:v2.1" as const;
|
|
36
|
+
/** Source-qualified contracts require this generation; older receipts retain their semantics. */
|
|
37
|
+
export const SOURCE_IDENTITY_EVALUATOR_VERSION_V2 = "evaluation-kernel:v2.2" as const;
|
|
38
|
+
/** Replay-only generation: its captured mapping judgments remain authoritative. */
|
|
39
|
+
export const LEGACY_EVALUATOR_VERSION_V2 = "evaluation-kernel:v2" as const;
|
|
40
|
+
|
|
41
|
+
/** Missing schema is a retained V1 row; unknown future generations fail closed. */
|
|
42
|
+
export function evaluatorVersionForContractSchema(schema?: string): string | null {
|
|
43
|
+
if (schema === undefined || schema === CONTRACT_PREIMAGE_SCHEMA) return EVALUATOR_VERSION_V2;
|
|
44
|
+
if (schema === CONTRACT_PREIMAGE_SCHEMA_V2) return SOURCE_IDENTITY_EVALUATOR_VERSION_V2;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* Four-state per-finding model (spec §2). `candidate` is the derivation step —
|
package/src/index.ts
CHANGED
|
@@ -881,6 +881,10 @@ export {
|
|
|
881
881
|
CONTRACT_ENFORCEMENT_FIELDS,
|
|
882
882
|
CONTRACT_PREIMAGE_CAPABILITY_HEADER,
|
|
883
883
|
CONTRACT_PREIMAGE_SCHEMA,
|
|
884
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
885
|
+
CONTRACT_PREIMAGE_CAPABILITIES,
|
|
886
|
+
SUPPORTED_CONTRACT_PREIMAGE_SCHEMAS,
|
|
887
|
+
isSupportedContractPreimageSchema,
|
|
884
888
|
ContractCatalogValidationError,
|
|
885
889
|
contractComponentsFromFragments,
|
|
886
890
|
contractHash,
|
|
@@ -928,6 +932,7 @@ export type {
|
|
|
928
932
|
ContractIdentityPin,
|
|
929
933
|
ContractPolicyInput,
|
|
930
934
|
ContractPreimage,
|
|
935
|
+
ContractPreimageSchema,
|
|
931
936
|
ContractPropMappingInput,
|
|
932
937
|
ContractStampPin,
|
|
933
938
|
ContractStampRecord,
|
|
@@ -1129,6 +1134,9 @@ export {
|
|
|
1129
1134
|
EVALUATION_RECEIPT_STRING_MAX_BYTES_V1,
|
|
1130
1135
|
EVALUATOR_VERSION,
|
|
1131
1136
|
EVALUATOR_VERSION_V2,
|
|
1137
|
+
SOURCE_IDENTITY_EVALUATOR_VERSION_V2,
|
|
1138
|
+
evaluatorVersionForContractSchema,
|
|
1139
|
+
LEGACY_EVALUATOR_VERSION_V2,
|
|
1132
1140
|
effectiveEvaluationWaiverV2Schema,
|
|
1133
1141
|
evaluate,
|
|
1134
1142
|
evaluationInputDigestV2,
|
|
@@ -1181,3 +1189,24 @@ export {
|
|
|
1181
1189
|
APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH,
|
|
1182
1190
|
isApprovedContractTokenNames,
|
|
1183
1191
|
} from "./approved-contract-tokens.js";
|
|
1192
|
+
|
|
1193
|
+
export {
|
|
1194
|
+
contractComponentSourceKey,
|
|
1195
|
+
contractComponentExportAddresses,
|
|
1196
|
+
approvedComponentGovernanceByteLimit,
|
|
1197
|
+
APPROVED_COMPONENT_GOVERNANCE_LEGACY_MAX_BYTES,
|
|
1198
|
+
APPROVED_COMPONENT_GOVERNANCE_QUALIFIED_MAX_BYTES,
|
|
1199
|
+
contractComponentReplacementImport,
|
|
1200
|
+
contractSourceFromFragment,
|
|
1201
|
+
normalizeContractComponentSource,
|
|
1202
|
+
normalizeContractSourcePath,
|
|
1203
|
+
contractComponentsFromFragmentsV2,
|
|
1204
|
+
projectContractPreimageV2,
|
|
1205
|
+
} from "./contract/source-identity.js";
|
|
1206
|
+
export type {
|
|
1207
|
+
ContractComponentSource,
|
|
1208
|
+
ContractComponentExport,
|
|
1209
|
+
ContractComponentInputV2,
|
|
1210
|
+
ContractCanonicalMappingInputV2,
|
|
1211
|
+
ContractCatalogInputV2,
|
|
1212
|
+
} from "./contract/source-identity.js";
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
compileGlobalGovernanceFacts,
|
|
4
|
+
FactIndex,
|
|
5
|
+
makeClassNameDynamicFact,
|
|
6
|
+
makeUsageNodeFact,
|
|
7
|
+
makeUsageImportFact,
|
|
8
|
+
ruleComponentsPreferLibrary,
|
|
9
|
+
} from "../index.js";
|
|
10
|
+
|
|
11
|
+
function scan(options: Record<string, unknown>) {
|
|
12
|
+
const ix = new FactIndex();
|
|
13
|
+
ix.addMany(
|
|
14
|
+
compileGlobalGovernanceFacts({
|
|
15
|
+
rules: {
|
|
16
|
+
"components/prefer-library": { enabled: true, severity: "error", options },
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
const location = { file: "src/App.tsx", line: 2, column: 1 };
|
|
21
|
+
const node = makeUsageNodeFact({ file: location.file, nodePath: "0", element: "div", location });
|
|
22
|
+
ix.add(node);
|
|
23
|
+
ix.add(
|
|
24
|
+
makeClassNameDynamicFact({
|
|
25
|
+
file: location.file,
|
|
26
|
+
nodeId: node.id,
|
|
27
|
+
attr: "className",
|
|
28
|
+
reason: "identifier",
|
|
29
|
+
snippet: "styles.alert",
|
|
30
|
+
originPath: "0",
|
|
31
|
+
location,
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
return ruleComponentsPreferLibrary(ix);
|
|
35
|
+
}
|
|
36
|
+
const mappings = ["first", "second"].map((folder) => ({
|
|
37
|
+
name: "Alert",
|
|
38
|
+
canonical: "Alert",
|
|
39
|
+
importPath: `@acme/${folder}`,
|
|
40
|
+
source: { kind: "repository", path: `${folder}/Alert.tsx`, symbol: "Alert" },
|
|
41
|
+
exportAddresses: [{ kind: "package", importPath: `@acme/${folder}`, exportName: "Alert" }],
|
|
42
|
+
}));
|
|
43
|
+
const qualified = { sourceIdentitySchema: "component-source:v1" };
|
|
44
|
+
|
|
45
|
+
describe("source-qualified class-name advice", () => {
|
|
46
|
+
it("preserves legacy authored-order import advice", () => {
|
|
47
|
+
const forward = scan({ canonicalMappings: mappings });
|
|
48
|
+
const reverse = scan({ canonicalMappings: [...mappings].reverse() });
|
|
49
|
+
expect(forward[0]?.attributes?.suggestedImport).toBe("@acme/first");
|
|
50
|
+
expect(reverse[0]?.attributes?.suggestedImport).toBe("@acme/second");
|
|
51
|
+
});
|
|
52
|
+
it("retains all same-name alternatives without choosing an import or replacement", () => {
|
|
53
|
+
const forward = scan({ ...qualified, canonicalMappings: mappings });
|
|
54
|
+
const reverse = scan({ ...qualified, canonicalMappings: [...mappings].reverse() });
|
|
55
|
+
const withoutEvidence = (value: typeof forward) =>
|
|
56
|
+
value.map(({ evidence: _, ...rest }) => rest);
|
|
57
|
+
expect(withoutEvidence(forward)).toEqual(withoutEvidence(reverse));
|
|
58
|
+
expect(forward).toHaveLength(1);
|
|
59
|
+
expect(forward[0]?.level).toBe("warn");
|
|
60
|
+
expect(forward[0]?.attributes?.advisory).toBe(true);
|
|
61
|
+
expect(forward[0]?.attributes?.canonicalAlternatives).toHaveLength(2);
|
|
62
|
+
expect(forward[0]?.attributes?.ambiguousCanonicalTarget).toBe(true);
|
|
63
|
+
expect(forward[0]?.attributes).not.toHaveProperty("suggestedComponent");
|
|
64
|
+
expect(forward[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
65
|
+
expect(forward[0]?.fix).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
it("uses a proven barrel alias instead of the implementation name", () => {
|
|
68
|
+
const findings = scan({
|
|
69
|
+
...qualified,
|
|
70
|
+
canonicalMappings: [
|
|
71
|
+
{
|
|
72
|
+
...mappings[0],
|
|
73
|
+
exportAddresses: [
|
|
74
|
+
{ kind: "package", importPath: "@acme/public", exportName: "PublicAlert" },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
expect(findings[0]?.attributes).toMatchObject({
|
|
80
|
+
suggestedComponent: "PublicAlert",
|
|
81
|
+
suggestedImport: "@acme/public",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it("does not grant guessed package labels public export authority", () => {
|
|
85
|
+
const findings = scan({
|
|
86
|
+
...qualified,
|
|
87
|
+
canonicalMappings: [{ ...mappings[0], exportAddresses: [] }],
|
|
88
|
+
});
|
|
89
|
+
expect(findings[0]?.attributes?.unresolvedCanonicalExport).toBe(true);
|
|
90
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
91
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedComponent");
|
|
92
|
+
});
|
|
93
|
+
it("recognizes a package inventory entry already proven by a repository export", () => {
|
|
94
|
+
const findings = scan({
|
|
95
|
+
...qualified,
|
|
96
|
+
canonicalMappings: [mappings[0]],
|
|
97
|
+
canonicalSources: [{ kind: "npm", specifier: "@acme/first", include: ["Alert"] }],
|
|
98
|
+
});
|
|
99
|
+
expect(findings[0]?.attributes?.suggestedImport).toBe("@acme/first");
|
|
100
|
+
expect(findings[0]?.attributes).not.toHaveProperty("ambiguousCanonicalTarget");
|
|
101
|
+
});
|
|
102
|
+
it("does not select one of two adopted packages with the same public name", () => {
|
|
103
|
+
const findings = scan({
|
|
104
|
+
...qualified,
|
|
105
|
+
canonicalSources: ["first", "second"].map((folder) => ({
|
|
106
|
+
kind: "npm",
|
|
107
|
+
specifier: `@acme/${folder}`,
|
|
108
|
+
include: ["Alert"],
|
|
109
|
+
})),
|
|
110
|
+
});
|
|
111
|
+
expect(findings[0]?.attributes?.canonicalAlternatives).toHaveLength(2);
|
|
112
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function componentScan(
|
|
117
|
+
options: Record<string, unknown>,
|
|
118
|
+
imported?: { source: string; name: string }
|
|
119
|
+
) {
|
|
120
|
+
const ix = new FactIndex();
|
|
121
|
+
ix.addMany(
|
|
122
|
+
compileGlobalGovernanceFacts({
|
|
123
|
+
rules: {
|
|
124
|
+
"components/prefer-library": { enabled: true, severity: "error", options },
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
const location = { file: "src/App.tsx", line: 2, column: 1 };
|
|
129
|
+
ix.add(
|
|
130
|
+
makeUsageNodeFact({
|
|
131
|
+
file: location.file,
|
|
132
|
+
nodePath: "0",
|
|
133
|
+
element: imported ? "Button" : "button",
|
|
134
|
+
location,
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
if (imported)
|
|
138
|
+
ix.add(
|
|
139
|
+
makeUsageImportFact({
|
|
140
|
+
file: location.file,
|
|
141
|
+
local: "Button",
|
|
142
|
+
imported: imported.name,
|
|
143
|
+
source: imported.source,
|
|
144
|
+
location,
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
return ruleComponentsPreferLibrary(ix);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
describe("qualified suggestion path consistency", () => {
|
|
151
|
+
const sources = ["first", "second"].map((name) => ({
|
|
152
|
+
kind: "npm",
|
|
153
|
+
specifier: `@acme/${name}`,
|
|
154
|
+
include: ["Button"],
|
|
155
|
+
}));
|
|
156
|
+
it("keeps legacy built-in source order but leaves qualified choices advisory and unresolved", () => {
|
|
157
|
+
expect(componentScan({ canonicalSources: sources })[0]?.attributes?.suggestedImport).toBe(
|
|
158
|
+
"@acme/first"
|
|
159
|
+
);
|
|
160
|
+
const forward = componentScan({ ...qualified, canonicalSources: sources });
|
|
161
|
+
const reverse = componentScan({ ...qualified, canonicalSources: [...sources].reverse() });
|
|
162
|
+
expect(forward[0]?.attributes).toEqual(reverse[0]?.attributes);
|
|
163
|
+
expect(forward[0]?.level).toBe("warn");
|
|
164
|
+
expect(forward[0]?.attributes).toMatchObject({
|
|
165
|
+
advisory: true,
|
|
166
|
+
ambiguousCanonicalTarget: true,
|
|
167
|
+
});
|
|
168
|
+
expect(forward[0]?.attributes?.canonicalAlternatives).toHaveLength(2);
|
|
169
|
+
expect(forward[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
170
|
+
expect(forward[0]?.fix).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
it("does not select between public addresses of the same definition using inventory order or package metadata", () => {
|
|
173
|
+
const addresses = ["first", "second"].map((name) => ({
|
|
174
|
+
kind: "package",
|
|
175
|
+
importPath: `@acme/${name}`,
|
|
176
|
+
exportName: "Alert",
|
|
177
|
+
}));
|
|
178
|
+
const mapping = { ...mappings[0], exportAddresses: addresses };
|
|
179
|
+
const inventories = sources.map((source) => ({ ...source, include: ["Alert"] }));
|
|
180
|
+
const first = scan({
|
|
181
|
+
...qualified,
|
|
182
|
+
canonicalMappings: [mapping],
|
|
183
|
+
canonicalSources: inventories,
|
|
184
|
+
});
|
|
185
|
+
const second = scan({
|
|
186
|
+
...qualified,
|
|
187
|
+
canonicalMappings: [{ ...mapping, exportAddresses: [...addresses].reverse() }],
|
|
188
|
+
canonicalSources: [...inventories].reverse(),
|
|
189
|
+
});
|
|
190
|
+
expect(first[0]?.attributes).toEqual(second[0]?.attributes);
|
|
191
|
+
expect(first[0]?.attributes?.canonicalAlternatives).toHaveLength(2);
|
|
192
|
+
expect(first[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
193
|
+
expect(first[0]?.attributes).not.toHaveProperty("suggestedComponent");
|
|
194
|
+
expect(first[0]?.fix).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
it("deduplicates repeated evidence for the same public address", () => {
|
|
197
|
+
const address = mappings[0]!.exportAddresses[0]!;
|
|
198
|
+
const findings = scan({
|
|
199
|
+
...qualified,
|
|
200
|
+
canonicalMappings: [{ ...mappings[0], exportAddresses: [address, address] }],
|
|
201
|
+
});
|
|
202
|
+
expect(findings[0]?.attributes).toMatchObject({
|
|
203
|
+
suggestedImport: "@acme/first",
|
|
204
|
+
suggestedComponent: "Alert",
|
|
205
|
+
});
|
|
206
|
+
expect(findings[0]?.attributes).not.toHaveProperty("ambiguousCanonicalTarget");
|
|
207
|
+
});
|
|
208
|
+
it.each([
|
|
209
|
+
{ source: "@acme/first", name: "Wrong" },
|
|
210
|
+
{ source: "@acme/first/unproven-subpath", name: "Button" },
|
|
211
|
+
])("does not let legacy root/name matching hide ambiguity for $source#$name", (imported) => {
|
|
212
|
+
const canonicalMappings = sources.map((source) => ({
|
|
213
|
+
name: "Button",
|
|
214
|
+
canonical: "Button",
|
|
215
|
+
importPath: source.specifier,
|
|
216
|
+
source: { kind: "package", importPath: source.specifier, exportName: "Button" },
|
|
217
|
+
}));
|
|
218
|
+
expect(componentScan({ canonicalMappings, canonicalSources: sources }, imported)).toEqual([]);
|
|
219
|
+
const findings = componentScan(
|
|
220
|
+
{ ...qualified, canonicalMappings, canonicalSources: sources },
|
|
221
|
+
imported
|
|
222
|
+
);
|
|
223
|
+
expect(findings).toHaveLength(1);
|
|
224
|
+
expect(findings[0]?.attributes?.ambiguousCanonicalTarget).toBe(true);
|
|
225
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
226
|
+
expect(findings[0]?.fix).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
it("accepts both exact approved public exports", () => {
|
|
229
|
+
for (const source of sources)
|
|
230
|
+
expect(
|
|
231
|
+
componentScan(
|
|
232
|
+
{ ...qualified, canonicalSources: sources },
|
|
233
|
+
{ source: source.specifier, name: "Button" }
|
|
234
|
+
)
|
|
235
|
+
).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("qualified owned-package aliases", () => {
|
|
240
|
+
const spellings = ["@usefragments/ui", "@fragments-sdk/ui"];
|
|
241
|
+
const addresses = spellings.map((importPath) => ({
|
|
242
|
+
kind: "package",
|
|
243
|
+
importPath,
|
|
244
|
+
exportName: "Alert",
|
|
245
|
+
}));
|
|
246
|
+
it("joins an owned alias inventory to the same proven repository export", () => {
|
|
247
|
+
const findings = scan({
|
|
248
|
+
...qualified,
|
|
249
|
+
canonicalMappings: [{ ...mappings[0], exportAddresses: [addresses[0]] }],
|
|
250
|
+
canonicalSources: [{ kind: "npm", specifier: spellings[1], include: ["Alert"] }],
|
|
251
|
+
});
|
|
252
|
+
expect(findings[0]?.attributes?.suggestedImport).toBe("@usefragments/ui");
|
|
253
|
+
expect(findings[0]?.attributes).not.toHaveProperty("ambiguousCanonicalTarget");
|
|
254
|
+
});
|
|
255
|
+
it("deduplicates equivalent exports before selecting canonical spelling in either order", () => {
|
|
256
|
+
for (const exportAddresses of [addresses, [...addresses].reverse()]) {
|
|
257
|
+
const findings = scan({
|
|
258
|
+
...qualified,
|
|
259
|
+
canonicalMappings: [{ ...mappings[0], exportAddresses }],
|
|
260
|
+
});
|
|
261
|
+
expect(findings[0]?.attributes?.suggestedImport).toBe("@usefragments/ui");
|
|
262
|
+
expect(findings[0]?.attributes).not.toHaveProperty("ambiguousCanonicalTarget");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
it("normalizes built-in fallback guidance independently of inventory order", () => {
|
|
266
|
+
const inventories = spellings.map((specifier) => ({
|
|
267
|
+
kind: "npm",
|
|
268
|
+
specifier,
|
|
269
|
+
include: ["Button"],
|
|
270
|
+
}));
|
|
271
|
+
for (const canonicalSources of [inventories, [...inventories].reverse()]) {
|
|
272
|
+
const findings = componentScan({ ...qualified, canonicalSources });
|
|
273
|
+
expect(findings[0]?.attributes?.suggestedImport).toBe("@usefragments/ui");
|
|
274
|
+
expect(findings[0]?.attributes).not.toHaveProperty("ambiguousCanonicalTarget");
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("qualified JSX replacement names", () => {
|
|
280
|
+
it("recognizes an existing exact default import using its real local binding", () => {
|
|
281
|
+
expect(
|
|
282
|
+
componentScan(
|
|
283
|
+
{
|
|
284
|
+
...qualified,
|
|
285
|
+
canonicalMappings: [
|
|
286
|
+
{
|
|
287
|
+
name: "Button",
|
|
288
|
+
canonical: "Button",
|
|
289
|
+
htmlEquivalent: "button",
|
|
290
|
+
source: { kind: "repository", path: "src/Button.tsx", symbol: "Button" },
|
|
291
|
+
exportAddresses: [
|
|
292
|
+
{ kind: "package", importPath: "@acme/ui/button", exportName: "default" },
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{ source: "@acme/ui/button", name: "default" }
|
|
298
|
+
)
|
|
299
|
+
).toEqual([]);
|
|
300
|
+
});
|
|
301
|
+
it.each(["default", "lowercase", "not-an-identifier"])(
|
|
302
|
+
"keeps %s unresolved without inventing a local binding",
|
|
303
|
+
(exportName) => {
|
|
304
|
+
const canonicalMappings = [
|
|
305
|
+
{
|
|
306
|
+
name: "Button",
|
|
307
|
+
canonical: "Button",
|
|
308
|
+
htmlEquivalent: "button",
|
|
309
|
+
source: { kind: "repository", path: "src/Button.tsx", symbol: "Button" },
|
|
310
|
+
exportAddresses: [{ kind: "package", importPath: "@acme/ui", exportName }],
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
const findings = componentScan({ ...qualified, canonicalMappings });
|
|
314
|
+
expect(findings).toHaveLength(1);
|
|
315
|
+
expect(findings[0]?.level).toBe("error");
|
|
316
|
+
expect(findings[0]?.attributes?.unresolvedCanonicalExport).toBe(true);
|
|
317
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedComponent");
|
|
318
|
+
expect(findings[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
319
|
+
expect(findings[0]?.fix).toBeUndefined();
|
|
320
|
+
const classFindings = scan({
|
|
321
|
+
...qualified,
|
|
322
|
+
canonicalMappings: canonicalMappings.map((mapping) => ({
|
|
323
|
+
...mapping,
|
|
324
|
+
name: "Alert",
|
|
325
|
+
canonical: "Alert",
|
|
326
|
+
htmlEquivalent: undefined,
|
|
327
|
+
})),
|
|
328
|
+
});
|
|
329
|
+
expect(classFindings[0]?.attributes?.advisory).toBe(true);
|
|
330
|
+
expect(classFindings[0]?.attributes).not.toHaveProperty("suggestedComponent");
|
|
331
|
+
expect(classFindings[0]?.attributes).not.toHaveProperty("suggestedImport");
|
|
332
|
+
expect(classFindings[0]?.fix).toBeUndefined();
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
it.each(["Button", "PublicButton"])(
|
|
336
|
+
"keeps the proven %s named export actionable",
|
|
337
|
+
(exportName) => {
|
|
338
|
+
const findings = componentScan({
|
|
339
|
+
...qualified,
|
|
340
|
+
canonicalMappings: [
|
|
341
|
+
{
|
|
342
|
+
name: "Button",
|
|
343
|
+
canonical: "Button",
|
|
344
|
+
htmlEquivalent: "button",
|
|
345
|
+
source: { kind: "repository", path: "src/Button.tsx", symbol: "Button" },
|
|
346
|
+
exportAddresses: [{ kind: "package", importPath: "@acme/ui", exportName }],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
expect(findings[0]?.attributes?.suggestedComponent).toBe(exportName);
|
|
351
|
+
expect(findings[0]?.fix).toMatchObject({ kind: "replaceComponent", to: exportName });
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
});
|