@fragments-sdk/cli 0.15.0 → 0.15.1
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/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +463 -71
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
- package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
- package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
- package/dist/core/index.js +0 -1
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
- package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +2 -22
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
- package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
- package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
- package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +1 -2
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
- package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
- package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
- package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
- package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
- package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
- package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
- package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
- package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +5 -3
- package/src/bin.ts +90 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +1 -1
- package/src/commands/__tests__/scan-generate.test.ts +1 -1
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/govern-scan.ts +187 -8
- package/src/commands/govern.ts +65 -2
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +1 -1
- package/src/commands/scan.ts +13 -0
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-push.ts +199 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +1 -1
- package/src/core/drift-verifier.ts +1 -1
- package/src/core/extractor-adapter.ts +1 -1
- package/src/index.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +2 -2
- package/src/service/index.ts +8 -0
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/dist/chunk-65WSVDV5.js.map +0 -1
- package/dist/chunk-7WHVW72L.js +0 -2664
- package/dist/chunk-7WHVW72L.js.map +0 -1
- package/dist/chunk-CZD3AD4Q.js.map +0 -1
- package/dist/chunk-MN3TJ3D5.js +0 -695
- package/dist/chunk-MN3TJ3D5.js.map +0 -1
- package/dist/chunk-XJQ5BIWI.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/govern-scan-UCBZR6D6.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-generate-TWRHNU5M.js.map +0 -1
- package/src/build.ts +0 -736
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1121
- package/src/core/token-resolver.ts +0 -155
- package/src/viewer/preview-adapter.ts +0 -116
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
- /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
- /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
package/src/build.ts
DELETED
|
@@ -1,736 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import type {
|
|
5
|
-
FragmentsConfig,
|
|
6
|
-
CompiledFragmentsFile,
|
|
7
|
-
CompiledFragment,
|
|
8
|
-
CompiledBlock,
|
|
9
|
-
CompiledTokenData,
|
|
10
|
-
} from "./core/index.js";
|
|
11
|
-
import { BRAND, compileBlock, parseTokenFile, parseDTCGFile, isDTCGFile, isContractFile, parseComponentContract } from "./core/index.js";
|
|
12
|
-
import { resolveTokensWithSass } from "./core/token-resolver.js";
|
|
13
|
-
import type { BlockDefinition } from "./core/index.js";
|
|
14
|
-
import {
|
|
15
|
-
discoverFragmentFiles,
|
|
16
|
-
discoverBlockFiles,
|
|
17
|
-
discoverTokenFiles,
|
|
18
|
-
parseFragmentFile,
|
|
19
|
-
loadFragmentFile,
|
|
20
|
-
generateRegistry,
|
|
21
|
-
generateContextMd,
|
|
22
|
-
} from "./core/node.js";
|
|
23
|
-
import {
|
|
24
|
-
resolveComponentSourcePath,
|
|
25
|
-
} from "./core/auto-props.js";
|
|
26
|
-
import {
|
|
27
|
-
createComponentExtractor,
|
|
28
|
-
type PropMeta,
|
|
29
|
-
type ComponentMeta,
|
|
30
|
-
} from "./core/component-extractor.js";
|
|
31
|
-
import { createExtractorAdapter } from "./core/extractor-adapter.js";
|
|
32
|
-
import { verifyContractDrift, formatDriftReport } from "./core/drift-verifier.js";
|
|
33
|
-
import { buildComponentGraph } from "./core/graph-extractor.js";
|
|
34
|
-
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
35
|
-
import { resolvePerformanceConfig } from "./core/index.js";
|
|
36
|
-
import { measureBundleSizes, toPerformanceData } from "./core/bundle-measurer.js";
|
|
37
|
-
import type { PerformanceSummary } from "@fragments-sdk/context/types";
|
|
38
|
-
|
|
39
|
-
type CompiledProp = CompiledFragment["props"][string];
|
|
40
|
-
|
|
41
|
-
function normalizeParsedProps(
|
|
42
|
-
parsedProps: Record<string, Partial<CompiledProp>>
|
|
43
|
-
): Record<string, CompiledProp> {
|
|
44
|
-
return Object.fromEntries(
|
|
45
|
-
Object.entries(parsedProps).map(([name, prop]) => [
|
|
46
|
-
name,
|
|
47
|
-
{
|
|
48
|
-
type: prop.type ?? "custom",
|
|
49
|
-
description: prop.description ?? "",
|
|
50
|
-
default: prop.default,
|
|
51
|
-
required: prop.required,
|
|
52
|
-
values: prop.values,
|
|
53
|
-
constraints: prop.constraints,
|
|
54
|
-
},
|
|
55
|
-
])
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function mergeDocumentedAndAutoProps(
|
|
60
|
-
documentedProps: Record<string, CompiledProp>,
|
|
61
|
-
autoProps: Record<string, PropMeta>
|
|
62
|
-
): Record<string, CompiledProp> {
|
|
63
|
-
return Object.fromEntries(
|
|
64
|
-
Object.keys(autoProps)
|
|
65
|
-
// Strip inherited HTML/React props — they're identical across all components
|
|
66
|
-
// and bloat fragments.json. MCP consumers know these exist implicitly.
|
|
67
|
-
.filter((name) => autoProps[name].source === 'local' || name in documentedProps)
|
|
68
|
-
.map((name) => {
|
|
69
|
-
const documented = documentedProps[name];
|
|
70
|
-
const auto = autoProps[name];
|
|
71
|
-
|
|
72
|
-
return [
|
|
73
|
-
name,
|
|
74
|
-
{
|
|
75
|
-
type: auto.typeKind,
|
|
76
|
-
description: documented?.description ?? auto.description ?? "",
|
|
77
|
-
default: auto.default !== undefined ? auto.default : documented?.default,
|
|
78
|
-
required: auto.required,
|
|
79
|
-
values: auto.values ?? documented?.values,
|
|
80
|
-
constraints: documented?.constraints,
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Auto-compile a propsSummary for the contract from extracted props.
|
|
89
|
-
* Format: "variant: primary|secondary|ghost (required)"
|
|
90
|
-
*/
|
|
91
|
-
function compilePropsSummary(props: Record<string, PropMeta>): string[] {
|
|
92
|
-
return Object.entries(props)
|
|
93
|
-
.filter(([_, p]) => p.source === 'local')
|
|
94
|
-
.map(([name, prop]) => {
|
|
95
|
-
let summary = name + ': ';
|
|
96
|
-
if (prop.values && prop.values.length > 0) {
|
|
97
|
-
summary += prop.values.join('|');
|
|
98
|
-
} else {
|
|
99
|
-
summary += prop.typeKind;
|
|
100
|
-
}
|
|
101
|
-
if (prop.default !== undefined) {
|
|
102
|
-
summary += ` (default: ${prop.default})`;
|
|
103
|
-
}
|
|
104
|
-
if (prop.required) {
|
|
105
|
-
summary += ' (required)';
|
|
106
|
-
}
|
|
107
|
-
return summary;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface BuildResult {
|
|
112
|
-
success: boolean;
|
|
113
|
-
outputPath: string;
|
|
114
|
-
fragmentCount: number;
|
|
115
|
-
errors: Array<{ file: string; error: string }>;
|
|
116
|
-
warnings: Array<{ file: string; warning: string }>;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Build compiled fragments.json file for AI consumption.
|
|
121
|
-
*
|
|
122
|
-
* Uses AST parsing to extract metadata WITHOUT executing fragment files.
|
|
123
|
-
* This means the build works without any project dependencies installed.
|
|
124
|
-
*/
|
|
125
|
-
export async function buildFragments(
|
|
126
|
-
config: FragmentsConfig,
|
|
127
|
-
configDir: string
|
|
128
|
-
): Promise<BuildResult> {
|
|
129
|
-
const files = await discoverFragmentFiles(config, configDir);
|
|
130
|
-
const errors: Array<{ file: string; error: string }> = [];
|
|
131
|
-
const warnings: Array<{ file: string; warning: string }> = [];
|
|
132
|
-
const fragments: CompiledFragmentsFile["fragments"] = {};
|
|
133
|
-
const contractSourcedNames = new Set<string>();
|
|
134
|
-
|
|
135
|
-
// Create a persistent extractor — shared LanguageService across all fragments
|
|
136
|
-
// Try to find a tsconfig.json in the config directory
|
|
137
|
-
const tsconfigCandidates = [
|
|
138
|
-
resolve(configDir, 'tsconfig.json'),
|
|
139
|
-
resolve(configDir, '..', 'tsconfig.json'),
|
|
140
|
-
];
|
|
141
|
-
const tsconfigPath = tsconfigCandidates.find((p) => existsSync(p));
|
|
142
|
-
const extractor = createComponentExtractor(tsconfigPath);
|
|
143
|
-
|
|
144
|
-
for (const file of files) {
|
|
145
|
-
try {
|
|
146
|
-
// Read file content as text
|
|
147
|
-
const content = await readFile(file.absolutePath, "utf-8");
|
|
148
|
-
|
|
149
|
-
// Handle .contract.json files (framework-agnostic component contracts)
|
|
150
|
-
if (isContractFile(file.absolutePath)) {
|
|
151
|
-
try {
|
|
152
|
-
const parsed = parseComponentContract(content, file.relativePath);
|
|
153
|
-
const framework = parsed.framework ?? config.framework ?? 'react';
|
|
154
|
-
const contractAdapter = createExtractorAdapter(framework, tsconfigPath);
|
|
155
|
-
|
|
156
|
-
// Extract props from source (if adapter supports it)
|
|
157
|
-
let extractedMeta: ComponentMeta | null = null;
|
|
158
|
-
if (parsed.sourcePath && parsed.exportName && contractAdapter.canHandle(framework)) {
|
|
159
|
-
try {
|
|
160
|
-
// sourcePath is relative to fragments.config.ts root
|
|
161
|
-
const absSourcePath = resolve(configDir, parsed.sourcePath);
|
|
162
|
-
extractedMeta = contractAdapter.extract(absSourcePath, parsed.exportName);
|
|
163
|
-
} catch {
|
|
164
|
-
// Extraction failure is non-fatal for contracts
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Merge authored + extracted props
|
|
169
|
-
let mergedProps = parsed.props as Record<string, CompiledProp>;
|
|
170
|
-
if (extractedMeta && Object.keys(extractedMeta.props).length > 0) {
|
|
171
|
-
mergedProps = mergeDocumentedAndAutoProps(mergedProps, extractedMeta.props);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Auto-compile propsSummary if not manually authored
|
|
175
|
-
let contractPropsSummary = parsed.propsSummary;
|
|
176
|
-
if ((!contractPropsSummary || contractPropsSummary.length === 0) && extractedMeta) {
|
|
177
|
-
contractPropsSummary = compilePropsSummary(extractedMeta.props);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Auto-enrich AI metadata from composition detection
|
|
181
|
-
let ai = parsed.ai;
|
|
182
|
-
if (extractedMeta?.composition) {
|
|
183
|
-
const comp = extractedMeta.composition;
|
|
184
|
-
ai = {
|
|
185
|
-
compositionPattern: comp.pattern,
|
|
186
|
-
subComponents: comp.parts.map((p) => p.name),
|
|
187
|
-
...ai,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Drift verification for verified contracts
|
|
192
|
-
if (parsed.provenance?.verified && extractedMeta) {
|
|
193
|
-
const drift = verifyContractDrift(parsed, extractedMeta);
|
|
194
|
-
if (!drift.isClean) {
|
|
195
|
-
errors.push({
|
|
196
|
-
file: file.relativePath,
|
|
197
|
-
error: formatDriftReport(drift),
|
|
198
|
-
});
|
|
199
|
-
contractAdapter.dispose();
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Build compiled fragment with all enrichments
|
|
205
|
-
const compiled: CompiledFragment = {
|
|
206
|
-
filePath: file.relativePath,
|
|
207
|
-
meta: {
|
|
208
|
-
name: parsed.meta.name,
|
|
209
|
-
description: parsed.meta.description,
|
|
210
|
-
category: parsed.meta.category,
|
|
211
|
-
tags: parsed.meta.tags,
|
|
212
|
-
status: parsed.meta.status,
|
|
213
|
-
figma: parsed.meta.figma,
|
|
214
|
-
},
|
|
215
|
-
usage: parsed.usage,
|
|
216
|
-
props: mergedProps,
|
|
217
|
-
relations: parsed.relations,
|
|
218
|
-
variants: parsed.variants,
|
|
219
|
-
...(parsed.contract && { contract: parsed.contract }),
|
|
220
|
-
...(ai && { ai }),
|
|
221
|
-
framework: parsed.framework,
|
|
222
|
-
propsSummary: contractPropsSummary,
|
|
223
|
-
sourcePath: parsed.sourcePath,
|
|
224
|
-
exportName: parsed.exportName,
|
|
225
|
-
provenance: parsed.provenance,
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
fragments[compiled.meta.name] = compiled;
|
|
229
|
-
contractSourcedNames.add(compiled.meta.name);
|
|
230
|
-
contractAdapter.dispose();
|
|
231
|
-
} catch (error) {
|
|
232
|
-
errors.push({
|
|
233
|
-
file: file.relativePath,
|
|
234
|
-
error: `Contract parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Deprecation notice for .fragment.tsx files
|
|
241
|
-
if (file.absolutePath.endsWith('.fragment.tsx') || file.absolutePath.endsWith('.fragment.ts')) {
|
|
242
|
-
warnings.push({
|
|
243
|
-
file: file.relativePath,
|
|
244
|
-
warning: 'Deprecated format: .fragment.tsx — run `fragments migrate-contract` to generate .contract.json',
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Skip files that don't contain defineFragment() — e.g., story files
|
|
249
|
-
// that were accidentally included in the config
|
|
250
|
-
if (!content.includes("defineFragment")) {
|
|
251
|
-
warnings.push({
|
|
252
|
-
file: file.relativePath,
|
|
253
|
-
warning: "No defineFragment() call found",
|
|
254
|
-
});
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Parse using AST (no execution)
|
|
259
|
-
const parsed = parseFragmentFile(content, file.relativePath);
|
|
260
|
-
|
|
261
|
-
// Collect warnings
|
|
262
|
-
for (const warning of parsed.warnings) {
|
|
263
|
-
warnings.push({ file: file.relativePath, warning });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Check for required fields
|
|
267
|
-
if (!parsed.meta.name) {
|
|
268
|
-
warnings.push({
|
|
269
|
-
file: file.relativePath,
|
|
270
|
-
warning: "Missing meta.name in fragment definition — skipped",
|
|
271
|
-
});
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const documentedProps = normalizeParsedProps(parsed.props);
|
|
276
|
-
let mergedProps = documentedProps;
|
|
277
|
-
|
|
278
|
-
const componentExportName = parsed.componentName ?? parsed.meta.name;
|
|
279
|
-
const componentSourcePath = resolveComponentSourcePath(
|
|
280
|
-
file.absolutePath,
|
|
281
|
-
parsed.componentImport
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
// Extract full component metadata using persistent LanguageService
|
|
285
|
-
let extractedMeta: ComponentMeta | null = null;
|
|
286
|
-
if (componentExportName && componentSourcePath) {
|
|
287
|
-
try {
|
|
288
|
-
extractedMeta = extractor.extract(componentSourcePath, componentExportName);
|
|
289
|
-
} catch {
|
|
290
|
-
// Extraction failure is non-fatal — fall back to documented props
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (extractedMeta) {
|
|
294
|
-
const autoProps = extractedMeta.props;
|
|
295
|
-
const hasAutoProps = Object.keys(autoProps).length > 0;
|
|
296
|
-
|
|
297
|
-
if (hasAutoProps) {
|
|
298
|
-
const removedDocumentedProps = Object.keys(documentedProps).filter(
|
|
299
|
-
(propName) => !(propName in autoProps)
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
if (removedDocumentedProps.length > 0) {
|
|
303
|
-
warnings.push({
|
|
304
|
-
file: file.relativePath,
|
|
305
|
-
warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
mergedProps = mergeDocumentedAndAutoProps(documentedProps, autoProps);
|
|
310
|
-
} else if (Object.keys(documentedProps).length > 0) {
|
|
311
|
-
warnings.push({
|
|
312
|
-
file: file.relativePath,
|
|
313
|
-
warning: "Auto-props extraction returned no props; falling back to documented props",
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} else if (!componentExportName) {
|
|
318
|
-
warnings.push({
|
|
319
|
-
file: file.relativePath,
|
|
320
|
-
warning: "Unable to resolve component export name for auto-props extraction",
|
|
321
|
-
});
|
|
322
|
-
} else if (!componentSourcePath) {
|
|
323
|
-
warnings.push({
|
|
324
|
-
file: file.relativePath,
|
|
325
|
-
warning: `Unable to resolve component source path from import: ${parsed.componentImport ?? "unknown"}`,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Auto-compile contract if not manually authored
|
|
330
|
-
let contract = parsed.contract;
|
|
331
|
-
if (!contract?.propsSummary && extractedMeta) {
|
|
332
|
-
const summary = compilePropsSummary(extractedMeta.props);
|
|
333
|
-
if (summary.length > 0) {
|
|
334
|
-
contract = { ...contract, propsSummary: summary };
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Auto-enrich AI metadata from extractor's composition data
|
|
339
|
-
let ai = parsed.ai;
|
|
340
|
-
if (extractedMeta?.composition) {
|
|
341
|
-
const comp = extractedMeta.composition;
|
|
342
|
-
ai = {
|
|
343
|
-
compositionPattern: comp.pattern,
|
|
344
|
-
subComponents: comp.parts.map((p) => p.name),
|
|
345
|
-
...ai, // Manually authored ai fields take precedence
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Build compiled fragment from parsed metadata
|
|
350
|
-
const compiled: CompiledFragment = {
|
|
351
|
-
filePath: file.relativePath,
|
|
352
|
-
meta: {
|
|
353
|
-
name: parsed.meta.name,
|
|
354
|
-
description: parsed.meta.description ?? "",
|
|
355
|
-
category: parsed.meta.category ?? "Uncategorized",
|
|
356
|
-
status: parsed.meta.status,
|
|
357
|
-
tags: parsed.meta.tags,
|
|
358
|
-
since: parsed.meta.since,
|
|
359
|
-
...(parsed.meta.dependencies && { dependencies: parsed.meta.dependencies }),
|
|
360
|
-
figma: parsed.meta.figma,
|
|
361
|
-
},
|
|
362
|
-
usage: {
|
|
363
|
-
when: parsed.usage.when ?? [],
|
|
364
|
-
whenNot: parsed.usage.whenNot ?? [],
|
|
365
|
-
guidelines: parsed.usage.guidelines,
|
|
366
|
-
accessibility: parsed.usage.accessibility,
|
|
367
|
-
},
|
|
368
|
-
props: mergedProps,
|
|
369
|
-
relations: parsed.relations.map((rel) => ({
|
|
370
|
-
component: rel.component,
|
|
371
|
-
relationship: rel.relationship as
|
|
372
|
-
| "alternative"
|
|
373
|
-
| "sibling"
|
|
374
|
-
| "parent"
|
|
375
|
-
| "child"
|
|
376
|
-
| "composition",
|
|
377
|
-
note: rel.note,
|
|
378
|
-
})),
|
|
379
|
-
variants: parsed.variants.map((v) => ({
|
|
380
|
-
name: v.name,
|
|
381
|
-
description: v.description,
|
|
382
|
-
...(v.code && { code: v.code }),
|
|
383
|
-
...(v.figma && { figma: v.figma }),
|
|
384
|
-
...(v.args && { args: v.args }),
|
|
385
|
-
})),
|
|
386
|
-
// Include AI metadata (auto-enriched or manual)
|
|
387
|
-
...(ai && { ai }),
|
|
388
|
-
// Include contract metadata (auto-compiled or manual)
|
|
389
|
-
...(contract && { contract }),
|
|
390
|
-
// Provenance from TSX path
|
|
391
|
-
provenance: {
|
|
392
|
-
source: extractedMeta ? 'extracted' : 'manual',
|
|
393
|
-
verified: !!extractedMeta,
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// When both .contract.json and .fragment.tsx exist, prefer contract
|
|
398
|
-
if (contractSourcedNames.has(parsed.meta.name)) {
|
|
399
|
-
warnings.push({
|
|
400
|
-
file: file.relativePath,
|
|
401
|
-
warning: `Duplicate: "${parsed.meta.name}" already loaded from .contract.json — skipping .fragment.tsx`,
|
|
402
|
-
});
|
|
403
|
-
} else {
|
|
404
|
-
fragments[parsed.meta.name] = compiled;
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
errors.push({
|
|
408
|
-
file: file.relativePath,
|
|
409
|
-
error: error instanceof Error ? error.message : String(error),
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
extractor.dispose();
|
|
415
|
-
|
|
416
|
-
// Discover and compile block files
|
|
417
|
-
const blocks: Record<string, CompiledBlock> = {};
|
|
418
|
-
try {
|
|
419
|
-
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
420
|
-
for (const file of blockFiles) {
|
|
421
|
-
try {
|
|
422
|
-
// loadFragmentFile uses esbuild to bundle+evaluate, returns default export
|
|
423
|
-
// CJS/ESM interop may double-wrap the default export
|
|
424
|
-
let raw = await loadFragmentFile(file.absolutePath) as unknown as Record<string, unknown> | null;
|
|
425
|
-
// Unwrap double-default from CJS interop
|
|
426
|
-
if (raw && 'default' in raw && typeof raw.default === 'object') {
|
|
427
|
-
raw = raw.default as Record<string, unknown>;
|
|
428
|
-
}
|
|
429
|
-
const def = raw;
|
|
430
|
-
if (def && typeof def === 'object' && 'name' in def && 'code' in def && 'components' in def) {
|
|
431
|
-
const compiled = compileBlock(def as unknown as BlockDefinition, file.relativePath);
|
|
432
|
-
blocks[compiled.name] = compiled;
|
|
433
|
-
}
|
|
434
|
-
} catch (error) {
|
|
435
|
-
warnings.push({
|
|
436
|
-
file: file.relativePath,
|
|
437
|
-
warning: `Failed to load block: ${error instanceof Error ? error.message : String(error)}`,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
} catch {
|
|
442
|
-
// Block discovery failure is non-fatal
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Discover and extract design tokens from SCSS/CSS files
|
|
446
|
-
let tokens: CompiledTokenData | undefined;
|
|
447
|
-
try {
|
|
448
|
-
const tokenPatterns = config.tokens?.include;
|
|
449
|
-
const tokenFiles = await discoverTokenFiles(configDir, tokenPatterns, config.exclude);
|
|
450
|
-
if (tokenFiles.length > 0) {
|
|
451
|
-
// Merge tokens from all discovered files
|
|
452
|
-
const mergedCategories: Record<string, Array<{ name: string; value?: string; description?: string }>> = {};
|
|
453
|
-
let prefix = '--';
|
|
454
|
-
let total = 0;
|
|
455
|
-
|
|
456
|
-
// Read all file contents first for cross-file SCSS variable resolution
|
|
457
|
-
const fileContents: Array<{ content: string; path: string }> = [];
|
|
458
|
-
for (const file of tokenFiles) {
|
|
459
|
-
const content = await readFile(file.absolutePath, 'utf-8');
|
|
460
|
-
fileContents.push({ content, path: file.relativePath });
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Concatenate all contents so parseTokenFile can resolve SCSS vars across files
|
|
464
|
-
const allContent = fileContents.map((f) => f.content).join('\n');
|
|
465
|
-
|
|
466
|
-
for (const { content, path } of fileContents) {
|
|
467
|
-
// Route to the correct parser based on file extension
|
|
468
|
-
let fileParsed;
|
|
469
|
-
let parsed;
|
|
470
|
-
if (isDTCGFile(path)) {
|
|
471
|
-
// DTCG files are self-contained — no cross-file resolution needed
|
|
472
|
-
fileParsed = parseDTCGFile(content, path);
|
|
473
|
-
parsed = fileParsed;
|
|
474
|
-
} else {
|
|
475
|
-
// Parse SCSS/CSS with the combined content to enable cross-file var resolution
|
|
476
|
-
parsed = parseTokenFile(allContent, path);
|
|
477
|
-
// But only use tokens from THIS file's content to avoid duplicates
|
|
478
|
-
fileParsed = parseTokenFile(content, path);
|
|
479
|
-
}
|
|
480
|
-
prefix = fileParsed.prefix;
|
|
481
|
-
total += fileParsed.total;
|
|
482
|
-
for (const [cat, catTokens] of Object.entries(fileParsed.categories)) {
|
|
483
|
-
if (!mergedCategories[cat]) {
|
|
484
|
-
mergedCategories[cat] = [];
|
|
485
|
-
}
|
|
486
|
-
for (const t of catTokens) {
|
|
487
|
-
// Deduplicate by name
|
|
488
|
-
if (!mergedCategories[cat].some((e) => e.name === t.name)) {
|
|
489
|
-
// Use resolved value from the combined parse if available
|
|
490
|
-
const combinedToken = Object.values(parsed.categories)
|
|
491
|
-
.flat()
|
|
492
|
-
.find((ct) => ct.name === t.name);
|
|
493
|
-
const resolvedValue = combinedToken?.resolvedValue ?? t.resolvedValue;
|
|
494
|
-
|
|
495
|
-
mergedCategories[cat].push({
|
|
496
|
-
name: t.name,
|
|
497
|
-
...(resolvedValue
|
|
498
|
-
? { value: resolvedValue }
|
|
499
|
-
: t.value ? { value: t.value } : {}),
|
|
500
|
-
description: t.description,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Sass compilation fallback: resolve tokens the regex parser couldn't handle
|
|
508
|
-
if (total > 0) {
|
|
509
|
-
const allTokens = Object.values(mergedCategories).flat();
|
|
510
|
-
const unresolved = allTokens.filter(
|
|
511
|
-
t => t.value && (t.value.includes('#{') || t.value.includes('$'))
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
if (unresolved.length > 0 && tokenFiles.length > 0) {
|
|
515
|
-
// Determine the tokens directory from the first discovered file
|
|
516
|
-
const tokensDir = resolve(configDir, tokenFiles[0].relativePath, '..');
|
|
517
|
-
const sassResolved = await resolveTokensWithSass(
|
|
518
|
-
unresolved as Array<{ name: string; value: string }>,
|
|
519
|
-
tokensDir,
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// Merge sass-resolved values back into the categories
|
|
523
|
-
if (sassResolved.size > 0) {
|
|
524
|
-
for (const catTokens of Object.values(mergedCategories)) {
|
|
525
|
-
for (const token of catTokens) {
|
|
526
|
-
const resolved = sassResolved.get(token.name);
|
|
527
|
-
if (resolved && token.value && (token.value.includes('#{') || token.value.includes('$'))) {
|
|
528
|
-
token.value = resolved;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
tokens = { prefix, total, categories: mergedCategories };
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
} catch {
|
|
539
|
-
// Token extraction failure is non-fatal
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Read package name for import statements
|
|
543
|
-
let packageName: string | undefined;
|
|
544
|
-
const pkgJsonPath = resolve(configDir, "package.json");
|
|
545
|
-
if (existsSync(pkgJsonPath)) {
|
|
546
|
-
try {
|
|
547
|
-
const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
548
|
-
if (pkg.name) packageName = pkg.name;
|
|
549
|
-
} catch {
|
|
550
|
-
// Non-fatal
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Build component graph for AI structural queries
|
|
555
|
-
// Derive component directory from configDir (typically src/components/)
|
|
556
|
-
const componentDir = resolve(configDir, "src", "components");
|
|
557
|
-
let graphData: ReturnType<typeof serializeGraph> | undefined;
|
|
558
|
-
try {
|
|
559
|
-
const graphResult = await buildComponentGraph(fragments, blocks, componentDir);
|
|
560
|
-
|
|
561
|
-
// Auto-enrich fragments with detected metadata
|
|
562
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
563
|
-
const detected = graphResult.autoDetected.get(name);
|
|
564
|
-
if (!detected) continue;
|
|
565
|
-
|
|
566
|
-
if (!fragment.ai) fragment.ai = {};
|
|
567
|
-
if (!fragment.ai.subComponents && detected.subComponents) {
|
|
568
|
-
fragment.ai.subComponents = detected.subComponents;
|
|
569
|
-
}
|
|
570
|
-
if (!fragment.ai.compositionPattern && detected.compositionPattern) {
|
|
571
|
-
fragment.ai.compositionPattern = detected.compositionPattern;
|
|
572
|
-
}
|
|
573
|
-
if (!fragment.ai.commonPatterns && detected.commonPatterns) {
|
|
574
|
-
fragment.ai.commonPatterns = detected.commonPatterns;
|
|
575
|
-
}
|
|
576
|
-
if (!fragment.ai.requiredChildren && detected.requiredChildren) {
|
|
577
|
-
fragment.ai.requiredChildren = detected.requiredChildren;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Report drift warnings
|
|
582
|
-
for (const w of graphResult.warnings) {
|
|
583
|
-
warnings.push({ file: "graph", warning: w });
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
graphData = serializeGraph(graphResult.graph);
|
|
587
|
-
} catch (error) {
|
|
588
|
-
warnings.push({
|
|
589
|
-
file: "graph",
|
|
590
|
-
warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Measure performance budgets if configured
|
|
595
|
-
let performanceSummary: PerformanceSummary | undefined;
|
|
596
|
-
if (config.performance) {
|
|
597
|
-
try {
|
|
598
|
-
const perfConfig = resolvePerformanceConfig(config.performance);
|
|
599
|
-
const perfResult = await measureBundleSizes(fragments, configDir, {
|
|
600
|
-
perfConfig,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const tiers: Record<string, number> = { lightweight: 0, moderate: 0, heavy: 0 };
|
|
604
|
-
let overBudgetCount = 0;
|
|
605
|
-
|
|
606
|
-
for (const [name, measurement] of perfResult.measurements) {
|
|
607
|
-
const fragment = fragments[name];
|
|
608
|
-
const contractBudget = fragment?.contract?.performanceBudget as number | undefined;
|
|
609
|
-
const perfData = toPerformanceData(measurement, perfConfig, contractBudget);
|
|
610
|
-
fragment.performance = perfData;
|
|
611
|
-
tiers[perfData.complexity]++;
|
|
612
|
-
if (perfData.overBudget) overBudgetCount++;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
performanceSummary = {
|
|
616
|
-
preset: perfConfig.preset,
|
|
617
|
-
budget: perfConfig.budgets.bundleSize,
|
|
618
|
-
total: perfResult.measurements.size,
|
|
619
|
-
overBudget: overBudgetCount,
|
|
620
|
-
tiers,
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
for (const err of perfResult.errors) {
|
|
624
|
-
warnings.push({
|
|
625
|
-
file: 'performance',
|
|
626
|
-
warning: `Could not measure ${err.name}: ${err.error}`,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
} catch (error) {
|
|
630
|
-
warnings.push({
|
|
631
|
-
file: 'performance',
|
|
632
|
-
warning: `Performance measurement failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const output: CompiledFragmentsFile = {
|
|
638
|
-
version: "1.0.0",
|
|
639
|
-
generatedAt: new Date().toISOString(),
|
|
640
|
-
...(packageName && { packageName }),
|
|
641
|
-
fragments,
|
|
642
|
-
...(Object.keys(blocks).length > 0 && { blocks }),
|
|
643
|
-
...(tokens && { tokens }),
|
|
644
|
-
...(graphData && { graph: graphData }),
|
|
645
|
-
...(performanceSummary && { performanceSummary }),
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
649
|
-
await writeFile(outputPath, JSON.stringify(output));
|
|
650
|
-
|
|
651
|
-
return {
|
|
652
|
-
success: errors.length === 0,
|
|
653
|
-
outputPath,
|
|
654
|
-
fragmentCount: Object.keys(fragments).length,
|
|
655
|
-
errors,
|
|
656
|
-
warnings,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Result of building the .fragments directory
|
|
662
|
-
*/
|
|
663
|
-
export interface FragmentsBuildResult {
|
|
664
|
-
success: boolean;
|
|
665
|
-
indexPath: string;
|
|
666
|
-
registryPath: string;
|
|
667
|
-
contextPath: string;
|
|
668
|
-
componentCount: number;
|
|
669
|
-
errors: Array<{ file: string; error: string }>;
|
|
670
|
-
warnings: Array<{ file: string; warning: string }>;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Build the .fragments/ directory with index.json, registry.json, and context.md
|
|
675
|
-
*
|
|
676
|
-
* This generates:
|
|
677
|
-
* - .fragments/index.json - Minimal name → path mapping (tiny, for quick lookups)
|
|
678
|
-
* - .fragments/registry.json - Component index with paths and enrichment references
|
|
679
|
-
* - .fragments/context.md - AI-ready consolidated context file
|
|
680
|
-
*/
|
|
681
|
-
export async function buildFragmentsDir(
|
|
682
|
-
config: FragmentsConfig,
|
|
683
|
-
configDir: string
|
|
684
|
-
): Promise<FragmentsBuildResult> {
|
|
685
|
-
const fragmentsDir = join(configDir, BRAND.dataDir);
|
|
686
|
-
const componentsDir = join(fragmentsDir, BRAND.componentsDir);
|
|
687
|
-
|
|
688
|
-
// Create directories
|
|
689
|
-
await mkdir(fragmentsDir, { recursive: true });
|
|
690
|
-
await mkdir(componentsDir, { recursive: true });
|
|
691
|
-
|
|
692
|
-
// Generate registry with config options
|
|
693
|
-
const registryResult = await generateRegistry({
|
|
694
|
-
projectRoot: configDir,
|
|
695
|
-
componentPatterns: config.components || ["src/**/*.tsx", "src/**/*.ts"],
|
|
696
|
-
storyPatterns: config.include || ["src/**/*.stories.tsx"],
|
|
697
|
-
fragmentsDir,
|
|
698
|
-
registryOptions: config.registry || {},
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
const errors = [...registryResult.errors];
|
|
702
|
-
const warnings = [...registryResult.warnings];
|
|
703
|
-
|
|
704
|
-
// Write index.json (minimal name → path)
|
|
705
|
-
const indexPath = join(fragmentsDir, "index.json");
|
|
706
|
-
await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
|
|
707
|
-
|
|
708
|
-
// Write registry.json (full metadata)
|
|
709
|
-
const registryPath = join(fragmentsDir, BRAND.registryFile);
|
|
710
|
-
await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
|
|
711
|
-
|
|
712
|
-
// Generate context.md - focus on semantic knowledge, skip props (AI can read source)
|
|
713
|
-
const contextResult = generateContextMd(registryResult.registry, {
|
|
714
|
-
format: "markdown",
|
|
715
|
-
compact: false,
|
|
716
|
-
include: {
|
|
717
|
-
props: false, // AI can read TypeScript directly
|
|
718
|
-
relations: true,
|
|
719
|
-
code: false,
|
|
720
|
-
},
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// Write context.md
|
|
724
|
-
const contextPath = join(fragmentsDir, BRAND.contextFile);
|
|
725
|
-
await writeFile(contextPath, contextResult.content);
|
|
726
|
-
|
|
727
|
-
return {
|
|
728
|
-
success: errors.length === 0,
|
|
729
|
-
indexPath,
|
|
730
|
-
registryPath,
|
|
731
|
-
contextPath,
|
|
732
|
-
componentCount: registryResult.registry.componentCount,
|
|
733
|
-
errors,
|
|
734
|
-
warnings,
|
|
735
|
-
};
|
|
736
|
-
}
|