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