@fragments-sdk/cli 0.15.0 → 0.15.2

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.
Files changed (120) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +565 -548
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-5JF26E55.js +1255 -0
  5. package/dist/chunk-5JF26E55.js.map +1 -0
  6. package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
  7. package/dist/chunk-6SQPP47U.js.map +1 -0
  8. package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
  9. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  10. package/dist/chunk-MHIBEEW4.js +511 -0
  11. package/dist/chunk-MHIBEEW4.js.map +1 -0
  12. package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
  13. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  14. package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
  15. package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
  16. package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
  17. package/dist/core/index.js +0 -1
  18. package/dist/create-JVAU3YKN.js +852 -0
  19. package/dist/create-JVAU3YKN.js.map +1 -0
  20. package/dist/doctor-BDPMYYE6.js +385 -0
  21. package/dist/doctor-BDPMYYE6.js.map +1 -0
  22. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  23. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  24. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  25. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  26. package/dist/index.d.ts +2 -22
  27. package/dist/index.js +8 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  30. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  31. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  32. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  33. package/dist/mcp-bin.js +1 -2
  34. package/dist/mcp-bin.js.map +1 -1
  35. package/dist/node-37AUE74M.js +65 -0
  36. package/dist/push-contracts-WY32TFP6.js +84 -0
  37. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  38. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  39. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  40. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  41. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  42. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  43. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  44. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  45. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  46. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  47. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  48. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  49. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  50. package/dist/token-parser-32KOIOFN.js +22 -0
  51. package/dist/token-parser-32KOIOFN.js.map +1 -0
  52. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  53. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  54. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  55. package/dist/tokens-push-HY3KO36V.js +148 -0
  56. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  57. package/package.json +18 -16
  58. package/src/bin.ts +94 -1
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  61. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  62. package/src/commands/__tests__/create.test.ts +71 -0
  63. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  64. package/src/commands/__tests__/govern.test.ts +258 -0
  65. package/src/commands/__tests__/init.test.ts +9 -1
  66. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  67. package/src/commands/build.ts +54 -1
  68. package/src/commands/context.ts +1 -1
  69. package/src/commands/create.ts +590 -0
  70. package/src/commands/doctor.ts +3 -2
  71. package/src/commands/govern-scan.ts +187 -8
  72. package/src/commands/govern.ts +65 -2
  73. package/src/commands/init-cloud.ts +32 -4
  74. package/src/commands/push-contracts.ts +112 -0
  75. package/src/commands/scan-generate.ts +1 -1
  76. package/src/commands/scan.ts +13 -0
  77. package/src/commands/sync.ts +2 -2
  78. package/src/commands/tokens-push.ts +199 -0
  79. package/src/core/__tests__/token-resolver.test.ts +1 -1
  80. package/src/core/component-extractor.test.ts +1 -1
  81. package/src/core/drift-verifier.ts +1 -1
  82. package/src/core/extractor-adapter.ts +1 -1
  83. package/src/index.ts +3 -3
  84. package/src/migrate/fragment-to-contract.ts +2 -2
  85. package/src/service/index.ts +8 -0
  86. package/src/service/tailwind-v4-parser.ts +314 -0
  87. package/src/service/token-parser.ts +56 -0
  88. package/src/setup.ts +10 -39
  89. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  90. package/src/theme/__tests__/serializer.test.ts +1 -1
  91. package/src/theme/generator.ts +30 -1
  92. package/src/theme/schema.ts +8 -0
  93. package/src/theme/serializer.ts +13 -9
  94. package/src/theme/types.ts +8 -0
  95. package/src/validators.ts +1 -2
  96. package/dist/chunk-65WSVDV5.js.map +0 -1
  97. package/dist/chunk-7WHVW72L.js +0 -2664
  98. package/dist/chunk-7WHVW72L.js.map +0 -1
  99. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  100. package/dist/chunk-MN3TJ3D5.js +0 -695
  101. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  102. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  103. package/dist/chunk-Z7EY4VHE.js +0 -50
  104. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  105. package/dist/sass.node-4XJK6YBF.js +0 -130708
  106. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  107. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  108. package/src/build.ts +0 -736
  109. package/src/core/auto-props.ts +0 -464
  110. package/src/core/component-extractor.ts +0 -1121
  111. package/src/core/token-resolver.ts +0 -155
  112. package/src/viewer/preview-adapter.ts +0 -116
  113. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  114. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  115. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  116. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  117. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  118. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  119. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  120. /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
- }