@usefragments/core 1.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.
- package/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseComponentContract, isContractFile } from '../contract-parser.js';
|
|
3
|
+
import { componentContractSchema } from '../component-contract.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function makeContract(overrides: Record<string, unknown> = {}): string {
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
12
|
+
name: 'Button',
|
|
13
|
+
description: 'A clickable button component',
|
|
14
|
+
category: 'actions',
|
|
15
|
+
sourcePath: 'src/components/Button/index.tsx',
|
|
16
|
+
exportName: 'Button',
|
|
17
|
+
propsSummary: [
|
|
18
|
+
'variant: primary|secondary|ghost (default: primary)',
|
|
19
|
+
'size: sm|md|lg (default: md)',
|
|
20
|
+
'disabled: boolean (default: false)',
|
|
21
|
+
],
|
|
22
|
+
props: {
|
|
23
|
+
variant: {
|
|
24
|
+
type: 'enum',
|
|
25
|
+
values: ['primary', 'secondary', 'ghost'],
|
|
26
|
+
default: 'primary',
|
|
27
|
+
description: 'Visual style variant',
|
|
28
|
+
},
|
|
29
|
+
size: {
|
|
30
|
+
type: 'enum',
|
|
31
|
+
values: ['sm', 'md', 'lg'],
|
|
32
|
+
default: 'md',
|
|
33
|
+
description: 'Button size',
|
|
34
|
+
},
|
|
35
|
+
disabled: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
default: false,
|
|
38
|
+
description: 'Whether the button is disabled',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
usage: {
|
|
42
|
+
when: ['User needs to trigger an action'],
|
|
43
|
+
whenNot: ['Navigation — use Link instead'],
|
|
44
|
+
},
|
|
45
|
+
provenance: {
|
|
46
|
+
source: 'manual',
|
|
47
|
+
verified: false,
|
|
48
|
+
},
|
|
49
|
+
...overrides,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// isContractFile
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
describe('isContractFile', () => {
|
|
58
|
+
it('detects .contract.json extension', () => {
|
|
59
|
+
expect(isContractFile('Button.contract.json')).toBe(true);
|
|
60
|
+
expect(isContractFile('src/components/Card.contract.json')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects non-contract files', () => {
|
|
64
|
+
expect(isContractFile('Button.fragment.tsx')).toBe(false);
|
|
65
|
+
expect(isContractFile('contract.json')).toBe(false);
|
|
66
|
+
expect(isContractFile('button.json')).toBe(false);
|
|
67
|
+
expect(isContractFile('button.contract.ts')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Schema validation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe('componentContractSchema', () => {
|
|
76
|
+
const minimalValid = {
|
|
77
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
78
|
+
name: 'Button',
|
|
79
|
+
description: 'A button',
|
|
80
|
+
category: 'actions',
|
|
81
|
+
sourcePath: 'src/Button.tsx',
|
|
82
|
+
exportName: 'Button',
|
|
83
|
+
propsSummary: [],
|
|
84
|
+
props: {},
|
|
85
|
+
usage: { when: ['click'], whenNot: ['nav'] },
|
|
86
|
+
provenance: { source: 'manual', verified: false },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
it('validates a minimal contract', () => {
|
|
90
|
+
const result = componentContractSchema.safeParse(minimalValid);
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects contract missing required fields', () => {
|
|
95
|
+
const data = { name: 'Button' };
|
|
96
|
+
const result = componentContractSchema.safeParse(data);
|
|
97
|
+
expect(result.success).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects contract missing $schema', () => {
|
|
101
|
+
const { $schema: _, ...noSchema } = minimalValid;
|
|
102
|
+
const result = componentContractSchema.safeParse(noSchema);
|
|
103
|
+
expect(result.success).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects contract missing sourcePath', () => {
|
|
107
|
+
const { sourcePath: _, ...noSource } = minimalValid;
|
|
108
|
+
const result = componentContractSchema.safeParse(noSource);
|
|
109
|
+
expect(result.success).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects contract missing exportName', () => {
|
|
113
|
+
const { exportName: _, ...noExport } = minimalValid;
|
|
114
|
+
const result = componentContractSchema.safeParse(noExport);
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('rejects contract missing provenance', () => {
|
|
119
|
+
const { provenance: _, ...noProv } = minimalValid;
|
|
120
|
+
const result = componentContractSchema.safeParse(noProv);
|
|
121
|
+
expect(result.success).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('rejects contract with invalid status', () => {
|
|
125
|
+
const result = componentContractSchema.safeParse({ ...minimalValid, status: 'invalid-status' });
|
|
126
|
+
expect(result.success).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('accepts all valid status values', () => {
|
|
130
|
+
for (const status of ['stable', 'beta', 'deprecated', 'experimental']) {
|
|
131
|
+
const result = componentContractSchema.safeParse({ ...minimalValid, status });
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('accepts all valid framework values', () => {
|
|
137
|
+
for (const framework of ['react', 'vue', 'svelte', 'web-components', 'angular']) {
|
|
138
|
+
const result = componentContractSchema.safeParse({ ...minimalValid, framework });
|
|
139
|
+
expect(result.success).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('rejects invalid relationship type', () => {
|
|
144
|
+
const result = componentContractSchema.safeParse({
|
|
145
|
+
...minimalValid,
|
|
146
|
+
relations: [{ component: 'Link', relationship: 'replaces', note: 'test' }],
|
|
147
|
+
});
|
|
148
|
+
expect(result.success).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('accepts all valid relationship types', () => {
|
|
152
|
+
for (const relationship of ['alternative', 'parent', 'child', 'sibling']) {
|
|
153
|
+
const result = componentContractSchema.safeParse({
|
|
154
|
+
...minimalValid,
|
|
155
|
+
relations: [{ component: 'Link', relationship, note: 'test' }],
|
|
156
|
+
});
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('validates ai metadata', () => {
|
|
162
|
+
const result = componentContractSchema.safeParse({
|
|
163
|
+
...minimalValid,
|
|
164
|
+
ai: {
|
|
165
|
+
compositionPattern: 'compound',
|
|
166
|
+
subComponents: ['Header', 'Body', 'Footer'],
|
|
167
|
+
requiredChildren: ['Body'],
|
|
168
|
+
commonPatterns: ['<Card><Card.Header /><Card.Body /></Card>'],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
expect(result.success).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('validates preview config', () => {
|
|
175
|
+
const result = componentContractSchema.safeParse({
|
|
176
|
+
...minimalValid,
|
|
177
|
+
preview: {
|
|
178
|
+
setupModule: './preview-setup.ts',
|
|
179
|
+
wrapperModule: './ThemeProvider.tsx',
|
|
180
|
+
wrapperExport: 'ThemeProvider',
|
|
181
|
+
css: ['./globals.css'],
|
|
182
|
+
theme: 'dark',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
expect(result.success).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('validates provenance with all fields', () => {
|
|
189
|
+
const result = componentContractSchema.safeParse({
|
|
190
|
+
...minimalValid,
|
|
191
|
+
provenance: {
|
|
192
|
+
source: 'extracted',
|
|
193
|
+
verified: true,
|
|
194
|
+
frameworkSupport: 'native',
|
|
195
|
+
sourceHash: 'abc123',
|
|
196
|
+
extractedAt: '2026-03-13T00:00:00Z',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
expect(result.success).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// parseComponentContract
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
describe('parseComponentContract', () => {
|
|
208
|
+
describe('basic parsing', () => {
|
|
209
|
+
it('parses a minimal contract', () => {
|
|
210
|
+
const content = makeContract();
|
|
211
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
212
|
+
|
|
213
|
+
expect(result.meta.name).toBe('Button');
|
|
214
|
+
expect(result.meta.description).toBe('A clickable button component');
|
|
215
|
+
expect(result.meta.category).toBe('actions');
|
|
216
|
+
expect(result.filePath).toBe('Button.contract.json');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('maps props correctly', () => {
|
|
220
|
+
const content = makeContract();
|
|
221
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
222
|
+
|
|
223
|
+
expect(Object.keys(result.props)).toHaveLength(3);
|
|
224
|
+
expect(result.props.variant.type).toBe('enum');
|
|
225
|
+
expect(result.props.variant.values).toEqual(['primary', 'secondary', 'ghost']);
|
|
226
|
+
expect(result.props.variant.default).toBe('primary');
|
|
227
|
+
expect(result.props.variant.description).toBe('Visual style variant');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('maps usage correctly', () => {
|
|
231
|
+
const content = makeContract();
|
|
232
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
233
|
+
|
|
234
|
+
expect(result.usage.when).toEqual(['User needs to trigger an action']);
|
|
235
|
+
expect(result.usage.whenNot).toEqual(['Navigation — use Link instead']);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('optional fields', () => {
|
|
240
|
+
it('handles tags', () => {
|
|
241
|
+
const content = makeContract({ tags: ['interactive', 'form'] });
|
|
242
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
243
|
+
|
|
244
|
+
expect(result.meta.tags).toEqual(['interactive', 'form']);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('handles status', () => {
|
|
248
|
+
const content = makeContract({ status: 'stable' });
|
|
249
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
250
|
+
|
|
251
|
+
expect(result.meta.status).toBe('stable');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('handles guidelines and accessibility in usage', () => {
|
|
255
|
+
const content = makeContract({
|
|
256
|
+
usage: {
|
|
257
|
+
when: ['Action needed'],
|
|
258
|
+
whenNot: ['No action'],
|
|
259
|
+
guidelines: ['Always provide a label'],
|
|
260
|
+
accessibility: ['Must have role="button"'],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
264
|
+
|
|
265
|
+
expect(result.usage.guidelines).toEqual(['Always provide a label']);
|
|
266
|
+
expect(result.usage.accessibility).toEqual(['Must have role="button"']);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('handles contract metadata', () => {
|
|
270
|
+
const content = makeContract({
|
|
271
|
+
contract: {
|
|
272
|
+
propsSummary: ['variant: primary|secondary|ghost (default: primary)'],
|
|
273
|
+
scenarioTags: ['form', 'action'],
|
|
274
|
+
a11yRules: ['no-empty-buttons'],
|
|
275
|
+
bans: [{ pattern: 'onClick without type', message: 'Always specify button type' }],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
279
|
+
|
|
280
|
+
expect(result.contract).toBeDefined();
|
|
281
|
+
expect(result.contract!.propsSummary).toHaveLength(1);
|
|
282
|
+
expect(result.contract!.scenarioTags).toEqual(['form', 'action']);
|
|
283
|
+
expect(result.contract!.bans).toHaveLength(1);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('handles tokens list', () => {
|
|
287
|
+
const content = makeContract({
|
|
288
|
+
tokens: ['--ds-color-primary', '--ds-space-4'],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// tokens is part of the contract but not mapped to CompiledFragment output
|
|
292
|
+
// Just verify it parses without error
|
|
293
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
294
|
+
expect(result.meta.name).toBe('Button');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('examples → variants mapping', () => {
|
|
299
|
+
it('maps examples to variants', () => {
|
|
300
|
+
const content = makeContract({
|
|
301
|
+
examples: [
|
|
302
|
+
{
|
|
303
|
+
name: 'Primary',
|
|
304
|
+
description: 'Primary button style',
|
|
305
|
+
code: '<Button variant="primary">Click me</Button>',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'With icon',
|
|
309
|
+
description: 'Button with leading icon',
|
|
310
|
+
code: '<Button><Icon name="plus" /> Add</Button>',
|
|
311
|
+
args: { variant: 'secondary' },
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
316
|
+
|
|
317
|
+
expect(result.variants).toHaveLength(2);
|
|
318
|
+
expect(result.variants[0].name).toBe('Primary');
|
|
319
|
+
expect(result.variants[0].code).toBe('<Button variant="primary">Click me</Button>');
|
|
320
|
+
expect(result.variants[1].args).toEqual({ variant: 'secondary' });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('returns empty variants when no examples', () => {
|
|
324
|
+
const content = makeContract();
|
|
325
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
326
|
+
|
|
327
|
+
expect(result.variants).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('relations mapping', () => {
|
|
332
|
+
it('maps all relationship types', () => {
|
|
333
|
+
const content = makeContract({
|
|
334
|
+
relations: [
|
|
335
|
+
{ component: 'Link', relationship: 'alternative', note: 'For navigation' },
|
|
336
|
+
{ component: 'Form', relationship: 'parent', note: 'Used inside forms' },
|
|
337
|
+
{ component: 'Icon', relationship: 'child', note: 'Can contain icons' },
|
|
338
|
+
{ component: 'Input', relationship: 'sibling', note: 'Often paired' },
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
342
|
+
|
|
343
|
+
expect(result.relations).toHaveLength(4);
|
|
344
|
+
expect(result.relations![0].relationship).toBe('alternative');
|
|
345
|
+
expect(result.relations![1].relationship).toBe('parent');
|
|
346
|
+
expect(result.relations![2].relationship).toBe('child');
|
|
347
|
+
expect(result.relations![3].relationship).toBe('sibling');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('figma mappings', () => {
|
|
352
|
+
it('maps figma nodeUrl to meta.figma', () => {
|
|
353
|
+
const content = makeContract({
|
|
354
|
+
figma: {
|
|
355
|
+
nodeUrl: 'https://www.figma.com/file/abc123/Design?node-id=1-2',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
359
|
+
|
|
360
|
+
expect(result.meta.figma).toBe('https://www.figma.com/file/abc123/Design?node-id=1-2');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('maps figma propMappings to meta.figmaProps', () => {
|
|
364
|
+
const content = makeContract({
|
|
365
|
+
figma: {
|
|
366
|
+
propMappings: {
|
|
367
|
+
variant: {
|
|
368
|
+
type: 'enum',
|
|
369
|
+
figmaProperty: 'Style',
|
|
370
|
+
values: { primary: 'Primary', secondary: 'Secondary' },
|
|
371
|
+
},
|
|
372
|
+
disabled: {
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
figmaProperty: 'Disabled',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
380
|
+
|
|
381
|
+
expect(result.meta.figmaProps).toBeDefined();
|
|
382
|
+
const mappings = result.meta.figmaProps as Record<string, unknown>;
|
|
383
|
+
expect(mappings.variant).toBeDefined();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('prop constraints', () => {
|
|
388
|
+
it('preserves prop constraints', () => {
|
|
389
|
+
const content = makeContract({
|
|
390
|
+
props: {
|
|
391
|
+
maxLength: {
|
|
392
|
+
type: 'number',
|
|
393
|
+
description: 'Maximum character length',
|
|
394
|
+
constraints: ['Must be positive', 'Max 1000'],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
399
|
+
|
|
400
|
+
expect(result.props.maxLength.constraints).toEqual(['Must be positive', 'Max 1000']);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('preserves required flag', () => {
|
|
404
|
+
const content = makeContract({
|
|
405
|
+
props: {
|
|
406
|
+
label: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'Button label',
|
|
409
|
+
required: true,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
414
|
+
|
|
415
|
+
expect(result.props.label.required).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('error handling', () => {
|
|
420
|
+
it('throws on invalid JSON', () => {
|
|
421
|
+
expect(() => parseComponentContract('not json', 'test.contract.json')).toThrow();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('throws on missing required fields', () => {
|
|
425
|
+
const content = JSON.stringify({ name: 'Button' });
|
|
426
|
+
expect(() => parseComponentContract(content, 'test.contract.json')).toThrow();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('throws on invalid prop type structure', () => {
|
|
430
|
+
const content = JSON.stringify({
|
|
431
|
+
name: 'Button',
|
|
432
|
+
description: 'A button',
|
|
433
|
+
category: 'actions',
|
|
434
|
+
props: {
|
|
435
|
+
variant: 'string', // Should be an object, not a string
|
|
436
|
+
},
|
|
437
|
+
usage: { when: [], whenNot: [] },
|
|
438
|
+
});
|
|
439
|
+
expect(() => parseComponentContract(content, 'test.contract.json')).toThrow();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('CompiledFragment compatibility', () => {
|
|
444
|
+
it('output has all required CompiledFragment fields', () => {
|
|
445
|
+
const content = makeContract();
|
|
446
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
447
|
+
|
|
448
|
+
// Required fields
|
|
449
|
+
expect(result).toHaveProperty('filePath');
|
|
450
|
+
expect(result).toHaveProperty('meta');
|
|
451
|
+
expect(result).toHaveProperty('usage');
|
|
452
|
+
expect(result).toHaveProperty('props');
|
|
453
|
+
expect(result).toHaveProperty('variants');
|
|
454
|
+
|
|
455
|
+
// Meta required fields
|
|
456
|
+
expect(result.meta).toHaveProperty('name');
|
|
457
|
+
expect(result.meta).toHaveProperty('description');
|
|
458
|
+
expect(result.meta).toHaveProperty('category');
|
|
459
|
+
|
|
460
|
+
// Usage required fields
|
|
461
|
+
expect(result.usage).toHaveProperty('when');
|
|
462
|
+
expect(result.usage).toHaveProperty('whenNot');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('real-world contracts', () => {
|
|
467
|
+
it('parses a Vue component contract', () => {
|
|
468
|
+
const content = JSON.stringify({
|
|
469
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
470
|
+
name: 'VButton',
|
|
471
|
+
description: 'Vuetify button component',
|
|
472
|
+
category: 'actions',
|
|
473
|
+
framework: 'vue',
|
|
474
|
+
status: 'stable',
|
|
475
|
+
tags: ['vuetify', 'material'],
|
|
476
|
+
sourcePath: 'src/VButton.vue',
|
|
477
|
+
exportName: 'VButton',
|
|
478
|
+
propsSummary: ['color: string', 'outlined: boolean', 'block: boolean'],
|
|
479
|
+
props: {
|
|
480
|
+
color: { type: 'string', description: 'Button color', default: 'primary' },
|
|
481
|
+
outlined: { type: 'boolean', description: 'Outlined style', default: false },
|
|
482
|
+
block: { type: 'boolean', description: 'Full width', default: false },
|
|
483
|
+
},
|
|
484
|
+
usage: {
|
|
485
|
+
when: ['User action needed'],
|
|
486
|
+
whenNot: ['Navigation — use v-link'],
|
|
487
|
+
},
|
|
488
|
+
examples: [
|
|
489
|
+
{
|
|
490
|
+
name: 'Default',
|
|
491
|
+
description: 'Default button',
|
|
492
|
+
code: '<v-btn color="primary">Click</v-btn>',
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
provenance: { source: 'manual', verified: false },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const result = parseComponentContract(content, 'VButton.contract.json');
|
|
499
|
+
|
|
500
|
+
expect(result.meta.name).toBe('VButton');
|
|
501
|
+
expect(result.meta.tags).toContain('vuetify');
|
|
502
|
+
expect(result.framework).toBe('vue');
|
|
503
|
+
expect(result.variants).toHaveLength(1);
|
|
504
|
+
expect(result.variants[0].code).toContain('<v-btn');
|
|
505
|
+
expect(result.provenance?.source).toBe('manual');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('parses a Web Components contract', () => {
|
|
509
|
+
const content = JSON.stringify({
|
|
510
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
511
|
+
name: 'MyButton',
|
|
512
|
+
description: 'Custom element button',
|
|
513
|
+
category: 'actions',
|
|
514
|
+
framework: 'web-components',
|
|
515
|
+
sourcePath: 'src/my-button.ts',
|
|
516
|
+
exportName: 'MyButton',
|
|
517
|
+
propsSummary: ['variant: primary|secondary'],
|
|
518
|
+
props: {
|
|
519
|
+
variant: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'Visual variant',
|
|
522
|
+
values: ['primary', 'secondary'],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
usage: {
|
|
526
|
+
when: ['Need a custom button'],
|
|
527
|
+
whenNot: ['Use native <button> when possible'],
|
|
528
|
+
},
|
|
529
|
+
provenance: { source: 'manual', verified: false },
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const result = parseComponentContract(content, 'MyButton.contract.json');
|
|
533
|
+
expect(result.meta.name).toBe('MyButton');
|
|
534
|
+
expect(result.framework).toBe('web-components');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('parses a contract with many props', () => {
|
|
538
|
+
const props: Record<string, unknown> = {};
|
|
539
|
+
const summaries: string[] = [];
|
|
540
|
+
for (let i = 0; i < 50; i++) {
|
|
541
|
+
props[`prop${i}`] = {
|
|
542
|
+
type: 'string',
|
|
543
|
+
description: `Property ${i}`,
|
|
544
|
+
};
|
|
545
|
+
summaries.push(`prop${i}: string`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const content = JSON.stringify({
|
|
549
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
550
|
+
name: 'ComplexComponent',
|
|
551
|
+
description: 'Component with many props',
|
|
552
|
+
category: 'complex',
|
|
553
|
+
sourcePath: 'src/Complex.tsx',
|
|
554
|
+
exportName: 'ComplexComponent',
|
|
555
|
+
propsSummary: summaries,
|
|
556
|
+
props,
|
|
557
|
+
usage: { when: ['Complex use case'], whenNot: [] },
|
|
558
|
+
provenance: { source: 'manual', verified: false },
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const result = parseComponentContract(content, 'Complex.contract.json');
|
|
562
|
+
expect(Object.keys(result.props)).toHaveLength(50);
|
|
563
|
+
expect(result.propsSummary).toHaveLength(50);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('new V2 fields', () => {
|
|
568
|
+
it('maps propsSummary to top-level and contract', () => {
|
|
569
|
+
const content = makeContract();
|
|
570
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
571
|
+
|
|
572
|
+
expect(result.propsSummary).toHaveLength(3);
|
|
573
|
+
expect(result.contract?.propsSummary).toHaveLength(3);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('maps provenance', () => {
|
|
577
|
+
const content = makeContract();
|
|
578
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
579
|
+
|
|
580
|
+
expect(result.provenance?.source).toBe('manual');
|
|
581
|
+
expect(result.provenance?.verified).toBe(false);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('maps sourcePath and exportName', () => {
|
|
585
|
+
const content = makeContract();
|
|
586
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
587
|
+
|
|
588
|
+
expect(result.sourcePath).toBe('src/components/Button/index.tsx');
|
|
589
|
+
expect(result.exportName).toBe('Button');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('maps ai metadata', () => {
|
|
593
|
+
const content = makeContract({
|
|
594
|
+
ai: {
|
|
595
|
+
compositionPattern: 'compound',
|
|
596
|
+
subComponents: ['Header', 'Body'],
|
|
597
|
+
requiredChildren: ['Body'],
|
|
598
|
+
commonPatterns: ['<Card><Card.Body>content</Card.Body></Card>'],
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
602
|
+
|
|
603
|
+
expect(result.ai?.compositionPattern).toBe('compound');
|
|
604
|
+
expect(result.ai?.subComponents).toEqual(['Header', 'Body']);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('maps framework', () => {
|
|
608
|
+
const content = makeContract({ framework: 'react' });
|
|
609
|
+
const result = parseComponentContract(content, 'Button.contract.json');
|
|
610
|
+
|
|
611
|
+
expect(result.framework).toBe('react');
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|