@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.
Files changed (118) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +463 -71
  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-IH4R45GE.js +806 -0
  19. package/dist/create-IH4R45GE.js.map +1 -0
  20. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  21. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  22. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  23. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  24. package/dist/index.d.ts +2 -22
  25. package/dist/index.js +8 -7
  26. package/dist/index.js.map +1 -1
  27. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  28. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  29. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  30. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  31. package/dist/mcp-bin.js +1 -2
  32. package/dist/mcp-bin.js.map +1 -1
  33. package/dist/node-37AUE74M.js +65 -0
  34. package/dist/push-contracts-WY32TFP6.js +84 -0
  35. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  36. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  37. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  38. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  39. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  40. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  41. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  42. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  43. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  44. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  45. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  46. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  47. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  48. package/dist/token-parser-32KOIOFN.js +22 -0
  49. package/dist/token-parser-32KOIOFN.js.map +1 -0
  50. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  51. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  52. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  53. package/dist/tokens-push-HY3KO36V.js +148 -0
  54. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  55. package/package.json +5 -3
  56. package/src/bin.ts +90 -0
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  59. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  60. package/src/commands/__tests__/create.test.ts +71 -0
  61. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  62. package/src/commands/__tests__/govern.test.ts +258 -0
  63. package/src/commands/__tests__/init.test.ts +1 -1
  64. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  65. package/src/commands/build.ts +54 -1
  66. package/src/commands/context.ts +1 -1
  67. package/src/commands/create.ts +536 -0
  68. package/src/commands/doctor.ts +3 -2
  69. package/src/commands/govern-scan.ts +187 -8
  70. package/src/commands/govern.ts +65 -2
  71. package/src/commands/init-cloud.ts +32 -4
  72. package/src/commands/push-contracts.ts +112 -0
  73. package/src/commands/scan-generate.ts +1 -1
  74. package/src/commands/scan.ts +13 -0
  75. package/src/commands/sync.ts +2 -2
  76. package/src/commands/tokens-push.ts +199 -0
  77. package/src/core/__tests__/token-resolver.test.ts +1 -1
  78. package/src/core/component-extractor.test.ts +1 -1
  79. package/src/core/drift-verifier.ts +1 -1
  80. package/src/core/extractor-adapter.ts +1 -1
  81. package/src/index.ts +3 -3
  82. package/src/migrate/fragment-to-contract.ts +2 -2
  83. package/src/service/index.ts +8 -0
  84. package/src/service/tailwind-v4-parser.ts +314 -0
  85. package/src/service/token-parser.ts +56 -0
  86. package/src/setup.ts +10 -39
  87. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  88. package/src/theme/__tests__/serializer.test.ts +1 -1
  89. package/src/theme/generator.ts +16 -1
  90. package/src/theme/schema.ts +8 -0
  91. package/src/theme/serializer.ts +13 -9
  92. package/src/theme/types.ts +8 -0
  93. package/src/validators.ts +1 -2
  94. package/dist/chunk-65WSVDV5.js.map +0 -1
  95. package/dist/chunk-7WHVW72L.js +0 -2664
  96. package/dist/chunk-7WHVW72L.js.map +0 -1
  97. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  98. package/dist/chunk-MN3TJ3D5.js +0 -695
  99. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  100. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  101. package/dist/chunk-Z7EY4VHE.js +0 -50
  102. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  103. package/dist/sass.node-4XJK6YBF.js +0 -130708
  104. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  105. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  106. package/src/build.ts +0 -736
  107. package/src/core/auto-props.ts +0 -464
  108. package/src/core/component-extractor.ts +0 -1121
  109. package/src/core/token-resolver.ts +0 -155
  110. package/src/viewer/preview-adapter.ts +0 -116
  111. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  112. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  113. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  114. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  115. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  116. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  117. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  118. /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
@@ -0,0 +1,1255 @@
1
+ import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
+ import {
3
+ BrowserPool,
4
+ CaptureEngine,
5
+ DiffEngine,
6
+ StorageManager,
7
+ analyzeDesignSystem,
8
+ formatMs,
9
+ generateHtmlReport,
10
+ getGrade
11
+ } from "./chunk-6SQPP47U.js";
12
+ import {
13
+ discoverBlockFiles,
14
+ discoverComponentFiles,
15
+ discoverFragmentFiles,
16
+ extractComponentName,
17
+ loadFragmentFile,
18
+ parseFragmentFile
19
+ } from "./chunk-HQ6A6DTV.js";
20
+ import {
21
+ BRAND,
22
+ DEFAULTS,
23
+ fragmentDefinitionSchema
24
+ } from "./chunk-32LIWN2P.js";
25
+
26
+ // src/service/snippet-validation.ts
27
+ import ts from "typescript";
28
+ import { readFile } from "fs/promises";
29
+ import { existsSync } from "fs";
30
+ import { join } from "path";
31
+ var INTRINSIC_TAGS = /* @__PURE__ */ new Set([
32
+ "div",
33
+ "span",
34
+ "p",
35
+ "h1",
36
+ "h2",
37
+ "h3",
38
+ "h4",
39
+ "h5",
40
+ "h6",
41
+ "main",
42
+ "section",
43
+ "article",
44
+ "aside",
45
+ "nav",
46
+ "header",
47
+ "footer",
48
+ "ul",
49
+ "ol",
50
+ "li",
51
+ "button",
52
+ "input",
53
+ "textarea",
54
+ "label",
55
+ "svg",
56
+ "path"
57
+ ]);
58
+ var JSX_TAG_PATTERN = /<\s*([A-Za-z][A-Za-z0-9.]*)\b/g;
59
+ var STYLE_PATTERN = /\bstyle\s*=\s*\{/;
60
+ var TRANSPILED_PATTERN = /jsxDEV|_jsx|@__PURE__|\bfileName\s*:|\blineNumber\s*:|\bcolumnNumber\s*:/;
61
+ var ALIAS_DRIFT_PATTERN = /<\s*[A-Z][A-Za-z0-9]*(?:Root|2)\b/;
62
+ var HAS_IMPORT_PATTERN = /\bimport\s+[^;]+\s+from\s+['"][^'"]+['"]/;
63
+ var HAS_JSX_PATTERN = /<\s*[A-Za-z][A-Za-z0-9.]*\b/;
64
+ var DEFAULT_POLICY = {
65
+ mode: "warn",
66
+ scope: "snippet+render",
67
+ requireFullSnippet: true,
68
+ allowedExternalModules: /* @__PURE__ */ new Set([
69
+ "@phosphor-icons/react",
70
+ "recharts",
71
+ "react-day-picker"
72
+ ])
73
+ };
74
+ function normalizePolicy(configured, overrides) {
75
+ const fromConfig = {
76
+ mode: configured?.mode ?? DEFAULT_POLICY.mode,
77
+ scope: configured?.scope ?? DEFAULT_POLICY.scope,
78
+ requireFullSnippet: configured?.requireFullSnippet ?? DEFAULT_POLICY.requireFullSnippet,
79
+ allowedExternalModules: new Set(configured?.allowedExternalModules ?? [...DEFAULT_POLICY.allowedExternalModules]),
80
+ componentStart: overrides.componentStart,
81
+ componentLimit: overrides.componentLimit
82
+ };
83
+ if (overrides.mode) fromConfig.mode = overrides.mode;
84
+ if (overrides.scope) fromConfig.scope = overrides.scope;
85
+ if (typeof overrides.requireFullSnippet === "boolean") {
86
+ fromConfig.requireFullSnippet = overrides.requireFullSnippet;
87
+ }
88
+ if (overrides.allowedExternalModules && overrides.allowedExternalModules.length > 0) {
89
+ fromConfig.allowedExternalModules = new Set(overrides.allowedExternalModules);
90
+ }
91
+ return fromConfig;
92
+ }
93
+ function isFragmentsModule(modulePath) {
94
+ return modulePath === "@fragments-sdk/ui" || modulePath === "." || modulePath === ".." || modulePath.startsWith("@/components/") || modulePath.startsWith("@components/") || modulePath.startsWith("./") || modulePath.startsWith("../");
95
+ }
96
+ function collectSourceContext(sourceFile) {
97
+ const imports = /* @__PURE__ */ new Map();
98
+ const localComponents = /* @__PURE__ */ new Set();
99
+ function markLocal(name) {
100
+ if (!name) return;
101
+ if (/^[A-Z]/.test(name)) {
102
+ localComponents.add(name);
103
+ }
104
+ }
105
+ function visit(node) {
106
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
107
+ const modulePath = node.moduleSpecifier.text;
108
+ const clause = node.importClause;
109
+ if (clause?.name) {
110
+ imports.set(clause.name.text, modulePath);
111
+ }
112
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
113
+ for (const item of clause.namedBindings.elements) {
114
+ imports.set(item.name.text, modulePath);
115
+ }
116
+ }
117
+ }
118
+ if (ts.isFunctionDeclaration(node)) {
119
+ markLocal(node.name?.text);
120
+ }
121
+ if (ts.isClassDeclaration(node)) {
122
+ markLocal(node.name?.text);
123
+ }
124
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
125
+ markLocal(node.name.text);
126
+ }
127
+ ts.forEachChild(node, visit);
128
+ }
129
+ visit(sourceFile);
130
+ return { imports, localComponents };
131
+ }
132
+ function getJsxTags(code) {
133
+ const tags = [];
134
+ JSX_TAG_PATTERN.lastIndex = 0;
135
+ let match;
136
+ while ((match = JSX_TAG_PATTERN.exec(code)) !== null) {
137
+ tags.push(match[1]);
138
+ }
139
+ return tags;
140
+ }
141
+ function rootTagName(tag) {
142
+ return tag.split(".")[0];
143
+ }
144
+ function parseSnippetImports(snippet) {
145
+ const sourceFile = ts.createSourceFile(
146
+ "snippet.tsx",
147
+ snippet,
148
+ ts.ScriptTarget.Latest,
149
+ true,
150
+ ts.ScriptKind.TSX
151
+ );
152
+ return collectSourceContext(sourceFile).imports;
153
+ }
154
+ function findDefineCall(sourceFile, name) {
155
+ let result = null;
156
+ function visit(node) {
157
+ if (result) return;
158
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name) {
159
+ result = node;
160
+ return;
161
+ }
162
+ ts.forEachChild(node, visit);
163
+ }
164
+ visit(sourceFile);
165
+ return result;
166
+ }
167
+ function findProperty(obj, propertyName) {
168
+ for (const prop of obj.properties) {
169
+ if (!ts.isPropertyAssignment(prop)) continue;
170
+ if (!ts.isIdentifier(prop.name)) continue;
171
+ if (prop.name.text === propertyName) {
172
+ return prop.initializer;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ function readStaticString(expr) {
178
+ if (!expr) return null;
179
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
180
+ return expr.text;
181
+ }
182
+ return null;
183
+ }
184
+ function readRenderBody(renderExpr, sourceFile) {
185
+ if (!ts.isArrowFunction(renderExpr) && !ts.isFunctionExpression(renderExpr)) {
186
+ return null;
187
+ }
188
+ const body = renderExpr.body;
189
+ const start = body.getStart(sourceFile);
190
+ const end = body.getEnd();
191
+ return sourceFile.text.slice(start, end).trim();
192
+ }
193
+ function report(issues, file, message) {
194
+ issues.push({ file, message });
195
+ }
196
+ function validateRawRules(issues, file, label, code) {
197
+ if (STYLE_PATTERN.test(code)) {
198
+ report(issues, file, `${label}: inline style usage is not allowed; use Box/Stack/Text props.`);
199
+ }
200
+ if (TRANSPILED_PATTERN.test(code)) {
201
+ report(issues, file, `${label}: transpiler output detected (jsxDEV/_jsx/@__PURE__). Use authored snippet source.`);
202
+ }
203
+ if (ALIAS_DRIFT_PATTERN.test(code)) {
204
+ report(issues, file, `${label}: alias drift tag detected (*Root/*2). Use canonical component names.`);
205
+ }
206
+ const tags = getJsxTags(code);
207
+ const intrinsic = tags.map((tag) => rootTagName(tag)).filter((tag) => /^[a-z]/.test(tag)).map((tag) => tag.toLowerCase()).filter((tag) => INTRINSIC_TAGS.has(tag));
208
+ if (intrinsic.length > 0) {
209
+ const names = [...new Set(intrinsic)].sort().join(", ");
210
+ report(issues, file, `${label}: raw HTML tags are not allowed (${names}). Use Fragments primitives.`);
211
+ }
212
+ }
213
+ function validateComponentAllowlist(issues, file, label, code, imports, localComponents, policy) {
214
+ const tags = getJsxTags(code);
215
+ const seen = /* @__PURE__ */ new Set();
216
+ for (const tag of tags) {
217
+ const root = rootTagName(tag);
218
+ if (seen.has(root)) continue;
219
+ seen.add(root);
220
+ if (!/^[A-Z]/.test(root)) {
221
+ continue;
222
+ }
223
+ const modulePath = imports.get(root);
224
+ if (modulePath) {
225
+ if (isFragmentsModule(modulePath)) {
226
+ continue;
227
+ }
228
+ if (policy.allowedExternalModules.has(modulePath)) {
229
+ continue;
230
+ }
231
+ report(
232
+ issues,
233
+ file,
234
+ `${label}: component "${root}" comes from "${modulePath}" and is not in snippets.allowedExternalModules.`
235
+ );
236
+ continue;
237
+ }
238
+ if (localComponents.has(root)) {
239
+ report(
240
+ issues,
241
+ file,
242
+ `${label}: locally defined JSX component "${root}" is not allowed in snippets/renders. Import approved components instead.`
243
+ );
244
+ continue;
245
+ }
246
+ report(
247
+ issues,
248
+ file,
249
+ `${label}: component "${root}" is used without an import and is not allowed.`
250
+ );
251
+ }
252
+ }
253
+ function validateSnippetString(issues, file, label, snippet, policy) {
254
+ validateRawRules(issues, file, label, snippet);
255
+ if (policy.requireFullSnippet) {
256
+ if (!HAS_IMPORT_PATTERN.test(snippet)) {
257
+ report(issues, file, `${label}: full snippet required (missing import statement).`);
258
+ }
259
+ if (!HAS_JSX_PATTERN.test(snippet)) {
260
+ report(issues, file, `${label}: full snippet required (missing JSX usage).`);
261
+ }
262
+ }
263
+ const imports = parseSnippetImports(snippet);
264
+ validateComponentAllowlist(issues, file, label, snippet, imports, /* @__PURE__ */ new Set(), policy);
265
+ }
266
+ function validateFragmentSource(sourceFile, file, policy, issues) {
267
+ const context = collectSourceContext(sourceFile);
268
+ const defineCall = findDefineCall(sourceFile, "defineFragment");
269
+ if (!defineCall) {
270
+ return;
271
+ }
272
+ const arg = defineCall.arguments[0];
273
+ if (!arg || !ts.isObjectLiteralExpression(arg)) {
274
+ return;
275
+ }
276
+ const variantsExpr = findProperty(arg, "variants");
277
+ if (!variantsExpr || !ts.isArrayLiteralExpression(variantsExpr)) {
278
+ return;
279
+ }
280
+ for (const variantExpr of variantsExpr.elements) {
281
+ if (!ts.isObjectLiteralExpression(variantExpr)) continue;
282
+ const name = readStaticString(findProperty(variantExpr, "name")) ?? "Unknown";
283
+ const labelPrefix = `variant "${name}"`;
284
+ const codeExpr = findProperty(variantExpr, "code");
285
+ const snippet = readStaticString(codeExpr);
286
+ if (snippet) {
287
+ validateSnippetString(issues, file, `${labelPrefix} snippet`, snippet, policy);
288
+ } else {
289
+ report(issues, file, `${labelPrefix}: missing explicit code snippet (variant.code).`);
290
+ }
291
+ if (policy.scope === "snippet+render") {
292
+ const renderExpr = findProperty(variantExpr, "render");
293
+ if (renderExpr) {
294
+ const renderBody = readRenderBody(renderExpr, sourceFile);
295
+ if (!renderBody) {
296
+ report(issues, file, `${labelPrefix} render: expected a static render function.`);
297
+ continue;
298
+ }
299
+ validateRawRules(issues, file, `${labelPrefix} render`, renderBody);
300
+ validateComponentAllowlist(
301
+ issues,
302
+ file,
303
+ `${labelPrefix} render`,
304
+ renderBody,
305
+ context.imports,
306
+ context.localComponents,
307
+ policy
308
+ );
309
+ }
310
+ }
311
+ }
312
+ }
313
+ function validateBlockSource(sourceFile, file, policy, issues) {
314
+ const defineCall = findDefineCall(sourceFile, "defineBlock");
315
+ if (!defineCall) {
316
+ return;
317
+ }
318
+ const arg = defineCall.arguments[0];
319
+ if (!arg || !ts.isObjectLiteralExpression(arg)) {
320
+ return;
321
+ }
322
+ const codeExpr = findProperty(arg, "code");
323
+ const snippet = readStaticString(codeExpr);
324
+ if (!snippet) {
325
+ report(issues, file, "block snippet: missing static code string.");
326
+ return;
327
+ }
328
+ validateSnippetString(issues, file, "block snippet", snippet, policy);
329
+ }
330
+ function validateBlockPreviewExamples(sourceFile, file, policy, issues) {
331
+ if (policy.scope !== "snippet+render") {
332
+ return;
333
+ }
334
+ const code = sourceFile.text;
335
+ const context = collectSourceContext(sourceFile);
336
+ validateRawRules(issues, file, "block preview render", code);
337
+ validateComponentAllowlist(
338
+ issues,
339
+ file,
340
+ "block preview render",
341
+ code,
342
+ context.imports,
343
+ context.localComponents,
344
+ policy
345
+ );
346
+ }
347
+ function sourceFileFromText(filePath, content) {
348
+ return ts.createSourceFile(
349
+ filePath,
350
+ content,
351
+ ts.ScriptTarget.Latest,
352
+ true,
353
+ ts.ScriptKind.TSX
354
+ );
355
+ }
356
+ function sortAndFilterBatch(files, componentStart, componentLimit) {
357
+ const getComponentName = (relativePath) => {
358
+ const normalized = relativePath.replace(/\\/g, "/");
359
+ const fileName = normalized.split("/").pop() ?? normalized;
360
+ for (const ext of [BRAND.fileExtension, ".fragment.tsx", ".fragment.ts"]) {
361
+ if (fileName.endsWith(ext)) {
362
+ return fileName.slice(0, -ext.length);
363
+ }
364
+ }
365
+ return extractComponentName(relativePath);
366
+ };
367
+ const sorted = [...files].sort((a, b) => {
368
+ const nameA = getComponentName(a.relativePath).toLowerCase();
369
+ const nameB = getComponentName(b.relativePath).toLowerCase();
370
+ return nameA.localeCompare(nameB);
371
+ });
372
+ if (!componentStart && !componentLimit) {
373
+ return { selected: sorted };
374
+ }
375
+ const startName = componentStart?.toLowerCase();
376
+ let startIndex = 0;
377
+ if (startName) {
378
+ const foundIndex = sorted.findIndex((file) => getComponentName(file.relativePath).toLowerCase() === startName);
379
+ if (foundIndex === -1) {
380
+ return {
381
+ selected: [],
382
+ warning: `Component start "${componentStart}" not found for snippet validation batch.`
383
+ };
384
+ }
385
+ startIndex = foundIndex;
386
+ }
387
+ const limit = componentLimit && componentLimit > 0 ? componentLimit : sorted.length;
388
+ return {
389
+ selected: sorted.slice(startIndex, startIndex + limit)
390
+ };
391
+ }
392
+ async function findBlockPreviewExamplesFile(configDir) {
393
+ const candidates = [
394
+ join(configDir, "apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
395
+ join(configDir, "../apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
396
+ join(configDir, "../../apps/docs/src/app/(docs)/blocks/examples/index.tsx")
397
+ ];
398
+ for (const candidate of candidates) {
399
+ if (existsSync(candidate)) {
400
+ return candidate;
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+ function toValidationResult(policy, issues) {
406
+ if (policy.mode === "error") {
407
+ return {
408
+ errors: issues,
409
+ warnings: []
410
+ };
411
+ }
412
+ return {
413
+ errors: [],
414
+ warnings: issues
415
+ };
416
+ }
417
+ async function validateSnippetPolicy(config, configDir, options = {}) {
418
+ const policy = normalizePolicy(config.snippets, options);
419
+ const issues = [];
420
+ const discovered = await discoverFragmentFiles(config, configDir);
421
+ const fragmentFiles = discovered.filter(
422
+ (file) => file.relativePath.endsWith(".fragment.tsx") || file.relativePath.endsWith(".fragment.ts")
423
+ );
424
+ const batchResult = sortAndFilterBatch(fragmentFiles, policy.componentStart, policy.componentLimit);
425
+ if (batchResult.warning) {
426
+ issues.push({ file: "snippets", message: batchResult.warning });
427
+ }
428
+ for (const file of batchResult.selected) {
429
+ try {
430
+ const content = await readFile(file.absolutePath, "utf-8");
431
+ const sourceFile = sourceFileFromText(file.relativePath, content);
432
+ validateFragmentSource(sourceFile, file.relativePath, policy, issues);
433
+ } catch (error) {
434
+ issues.push({
435
+ file: file.relativePath,
436
+ message: `Failed to validate fragment snippets: ${error instanceof Error ? error.message : String(error)}`
437
+ });
438
+ }
439
+ }
440
+ const isBatchOnly = Boolean(policy.componentStart || policy.componentLimit);
441
+ if (!isBatchOnly) {
442
+ try {
443
+ const blockFiles = await discoverBlockFiles(configDir, config.exclude);
444
+ for (const file of blockFiles) {
445
+ try {
446
+ const content = await readFile(file.absolutePath, "utf-8");
447
+ const sourceFile = sourceFileFromText(file.relativePath, content);
448
+ validateBlockSource(sourceFile, file.relativePath, policy, issues);
449
+ } catch (error) {
450
+ issues.push({
451
+ file: file.relativePath,
452
+ message: `Failed to validate block snippets: ${error instanceof Error ? error.message : String(error)}`
453
+ });
454
+ }
455
+ }
456
+ } catch (error) {
457
+ issues.push({
458
+ file: "blocks",
459
+ message: `Failed to discover block files: ${error instanceof Error ? error.message : String(error)}`
460
+ });
461
+ }
462
+ const blockPreviewFile = await findBlockPreviewExamplesFile(configDir);
463
+ if (blockPreviewFile) {
464
+ try {
465
+ const content = await readFile(blockPreviewFile, "utf-8");
466
+ const sourceFile = sourceFileFromText(blockPreviewFile, content);
467
+ validateBlockPreviewExamples(sourceFile, blockPreviewFile, policy, issues);
468
+ } catch (error) {
469
+ issues.push({
470
+ file: blockPreviewFile,
471
+ message: `Failed to validate block preview examples: ${error instanceof Error ? error.message : String(error)}`
472
+ });
473
+ }
474
+ }
475
+ }
476
+ return toValidationResult(policy, issues);
477
+ }
478
+
479
+ // src/validators.ts
480
+ import { createComponentExtractor, resolveComponentSourcePath } from "@fragments-sdk/extract";
481
+ import { readFile as readFile2 } from "fs/promises";
482
+ async function validateSchema(config, configDir) {
483
+ const files = await discoverFragmentFiles(config, configDir);
484
+ const errors = [];
485
+ const warnings = [];
486
+ for (const file of files) {
487
+ try {
488
+ const fragment = await loadFragmentFile(file.absolutePath);
489
+ if (!fragment) {
490
+ errors.push({
491
+ file: file.relativePath,
492
+ message: "No default export found",
493
+ details: `Fragment files must have a default export from defineFragment()`
494
+ });
495
+ continue;
496
+ }
497
+ const result = fragmentDefinitionSchema.safeParse(fragment);
498
+ if (!result.success) {
499
+ const details = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
500
+ errors.push({
501
+ file: file.relativePath,
502
+ message: "Invalid fragment schema",
503
+ details
504
+ });
505
+ }
506
+ } catch (error) {
507
+ errors.push({
508
+ file: file.relativePath,
509
+ message: "Failed to load fragment file",
510
+ details: error instanceof Error ? error.message : String(error)
511
+ });
512
+ }
513
+ }
514
+ return {
515
+ valid: errors.length === 0,
516
+ errors,
517
+ warnings
518
+ };
519
+ }
520
+ async function validateCoverage(config, configDir) {
521
+ const fragmentFiles = await discoverFragmentFiles(config, configDir);
522
+ const componentFiles = await discoverComponentFiles(config, configDir);
523
+ const errors = [];
524
+ const warnings = [];
525
+ const documentedComponents = /* @__PURE__ */ new Set();
526
+ for (const file of fragmentFiles) {
527
+ try {
528
+ const fragment = await loadFragmentFile(file.absolutePath);
529
+ if (fragment?.meta?.name) {
530
+ documentedComponents.add(fragment.meta.name);
531
+ }
532
+ } catch {
533
+ }
534
+ }
535
+ for (const file of componentFiles) {
536
+ const componentName = extractComponentName(file.relativePath);
537
+ const fragmentPath = file.relativePath.replace(
538
+ /\.(tsx?|jsx?)$/,
539
+ BRAND.fileExtension
540
+ );
541
+ const hasFragmentFile = fragmentFiles.some(
542
+ (s) => s.relativePath === fragmentPath
543
+ );
544
+ if (!hasFragmentFile && !documentedComponents.has(componentName)) {
545
+ warnings.push({
546
+ file: file.relativePath,
547
+ message: `Component "${componentName}" has no fragment documentation`
548
+ });
549
+ }
550
+ }
551
+ return {
552
+ valid: errors.length === 0,
553
+ errors,
554
+ warnings
555
+ };
556
+ }
557
+ async function validateAll(config, configDir, options = {}) {
558
+ const [schemaResult, coverageResult] = await Promise.all([
559
+ validateSchema(config, configDir),
560
+ validateCoverage(config, configDir)
561
+ ]);
562
+ if (options.snippets === false) {
563
+ return {
564
+ valid: schemaResult.valid && coverageResult.valid,
565
+ errors: [...schemaResult.errors, ...coverageResult.errors],
566
+ warnings: [...schemaResult.warnings, ...coverageResult.warnings]
567
+ };
568
+ }
569
+ const snippetOptions = {
570
+ ...options.snippetMode && { mode: options.snippetMode },
571
+ ...options.componentStart && { componentStart: options.componentStart },
572
+ ...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
573
+ };
574
+ const snippetResult = await validateSnippetPolicy(config, configDir, snippetOptions);
575
+ return {
576
+ valid: schemaResult.valid && coverageResult.valid && snippetResult.errors.length === 0,
577
+ errors: [...schemaResult.errors, ...coverageResult.errors, ...snippetResult.errors],
578
+ warnings: [...schemaResult.warnings, ...coverageResult.warnings, ...snippetResult.warnings]
579
+ };
580
+ }
581
+ async function validateSnippets(config, configDir, options = {}) {
582
+ const snippetResult = await validateSnippetPolicy(config, configDir, {
583
+ ...options.snippetMode && { mode: options.snippetMode },
584
+ ...options.componentStart && { componentStart: options.componentStart },
585
+ ...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
586
+ });
587
+ return {
588
+ valid: snippetResult.errors.length === 0,
589
+ errors: snippetResult.errors,
590
+ warnings: snippetResult.warnings
591
+ };
592
+ }
593
+ async function validateDrift(config, configDir, options = {}) {
594
+ const fragmentFiles = await discoverFragmentFiles(config, configDir);
595
+ const errors = [];
596
+ const warnings = [];
597
+ const reports = [];
598
+ if (fragmentFiles.length === 0) {
599
+ return { valid: true, errors, warnings, reports };
600
+ }
601
+ const extractor = createComponentExtractor(options.tsconfig);
602
+ try {
603
+ for (const file of fragmentFiles) {
604
+ try {
605
+ const fragment = await loadFragmentFile(file.absolutePath);
606
+ if (!fragment?.meta?.name) continue;
607
+ const fileContent = await readFile2(file.absolutePath, "utf-8");
608
+ const parsed = parseFragmentFile(fileContent, file.absolutePath);
609
+ if (!parsed.componentImport) continue;
610
+ const sourcePath = resolveComponentSourcePath(file.absolutePath, parsed.componentImport);
611
+ if (!sourcePath) continue;
612
+ const meta = extractor.extract(sourcePath, fragment.meta.name);
613
+ if (!meta) continue;
614
+ const drifts = diffProps(fragment.props, meta.props);
615
+ let compositionDrift = null;
616
+ const fragmentAi = fragment.ai;
617
+ if (meta.composition && !fragmentAi?.compositionPattern) {
618
+ compositionDrift = `Source has "${meta.composition.pattern}" composition but fragment has no ai.compositionPattern`;
619
+ } else if (!meta.composition && fragmentAi?.compositionPattern) {
620
+ compositionDrift = `Fragment declares "${fragmentAi.compositionPattern}" but source has no compound pattern`;
621
+ } else if (meta.composition && fragmentAi?.compositionPattern && meta.composition.pattern !== fragmentAi.compositionPattern) {
622
+ compositionDrift = `Composition pattern changed: fragment="${fragmentAi.compositionPattern}" source="${meta.composition.pattern}"`;
623
+ }
624
+ if (drifts.length > 0 || compositionDrift) {
625
+ const report2 = {
626
+ component: fragment.meta.name,
627
+ file: file.relativePath,
628
+ drifts,
629
+ compositionDrift
630
+ };
631
+ reports.push(report2);
632
+ for (const drift of drifts) {
633
+ if (drift.kind === "removed") {
634
+ errors.push({
635
+ file: file.relativePath,
636
+ message: `Prop "${drift.prop}" documented in fragment but removed from source`,
637
+ details: `Fragment: ${drift.fragment} | Source: (not found)`
638
+ });
639
+ } else if (drift.kind === "added") {
640
+ warnings.push({
641
+ file: file.relativePath,
642
+ message: `Prop "${drift.prop}" exists in source but not documented in fragment`
643
+ });
644
+ } else {
645
+ warnings.push({
646
+ file: file.relativePath,
647
+ message: `Prop "${drift.prop}" ${drift.kind.replace("_", " ")}: fragment=${drift.fragment} source=${drift.source}`
648
+ });
649
+ }
650
+ }
651
+ if (compositionDrift) {
652
+ warnings.push({
653
+ file: file.relativePath,
654
+ message: compositionDrift
655
+ });
656
+ }
657
+ }
658
+ } catch {
659
+ }
660
+ }
661
+ } finally {
662
+ extractor.dispose();
663
+ }
664
+ return {
665
+ valid: errors.length === 0,
666
+ errors,
667
+ warnings,
668
+ reports
669
+ };
670
+ }
671
+ function diffProps(fragmentProps, sourceProps) {
672
+ const drifts = [];
673
+ const localSourceProps = Object.fromEntries(
674
+ Object.entries(sourceProps).filter(([_, p]) => p.source === "local")
675
+ );
676
+ for (const [name, sourceProp] of Object.entries(localSourceProps)) {
677
+ if (!(name in fragmentProps)) {
678
+ drifts.push({
679
+ prop: name,
680
+ kind: "added",
681
+ source: sourceProp.type,
682
+ fragment: "(not documented)"
683
+ });
684
+ }
685
+ }
686
+ for (const [name, fragProp] of Object.entries(fragmentProps)) {
687
+ if (!(name in localSourceProps)) {
688
+ drifts.push({
689
+ prop: name,
690
+ kind: "removed",
691
+ source: "(not found)",
692
+ fragment: String(fragProp.type ?? "unknown")
693
+ });
694
+ continue;
695
+ }
696
+ const sourceProp = localSourceProps[name];
697
+ if (fragProp.type && fragProp.type !== sourceProp.typeKind) {
698
+ drifts.push({
699
+ prop: name,
700
+ kind: "type_changed",
701
+ source: sourceProp.typeKind,
702
+ fragment: String(fragProp.type)
703
+ });
704
+ }
705
+ if (fragProp.required !== void 0 && fragProp.required !== sourceProp.required) {
706
+ drifts.push({
707
+ prop: name,
708
+ kind: "required_changed",
709
+ source: String(sourceProp.required),
710
+ fragment: String(fragProp.required)
711
+ });
712
+ }
713
+ if (fragProp.values && sourceProp.values) {
714
+ const fragSet = new Set(fragProp.values);
715
+ const srcSet = new Set(sourceProp.values);
716
+ const added = sourceProp.values.filter((v) => !fragSet.has(v));
717
+ const removed = Array.from(fragProp.values).filter((v) => !srcSet.has(v));
718
+ if (added.length > 0 || removed.length > 0) {
719
+ drifts.push({
720
+ prop: name,
721
+ kind: "values_changed",
722
+ source: sourceProp.values.join(", "),
723
+ fragment: Array.from(fragProp.values).join(", ")
724
+ });
725
+ }
726
+ }
727
+ if (fragProp.default !== void 0 && sourceProp.default !== void 0) {
728
+ if (String(fragProp.default) !== sourceProp.default) {
729
+ drifts.push({
730
+ prop: name,
731
+ kind: "default_changed",
732
+ source: sourceProp.default,
733
+ fragment: String(fragProp.default)
734
+ });
735
+ }
736
+ }
737
+ }
738
+ return drifts;
739
+ }
740
+
741
+ // src/screenshot.ts
742
+ import pc from "picocolors";
743
+ async function runScreenshotCommand(config, configDir, options = {}) {
744
+ const startTime = Date.now();
745
+ const errors = [];
746
+ const storage = new StorageManager({
747
+ projectRoot: configDir,
748
+ viewport: options.width && options.height ? { width: options.width, height: options.height } : config.screenshots?.viewport
749
+ });
750
+ await storage.initialize();
751
+ const fragmentFiles = await discoverFragmentFiles(config, configDir);
752
+ if (fragmentFiles.length === 0) {
753
+ console.log(pc.yellow("No fragment files found."));
754
+ return {
755
+ success: true,
756
+ captured: 0,
757
+ skipped: 0,
758
+ errors: [],
759
+ totalTimeMs: Date.now() - startTime
760
+ };
761
+ }
762
+ const fragments = [];
763
+ for (const file of fragmentFiles) {
764
+ try {
765
+ const fragment = await loadFragmentFile(file.absolutePath);
766
+ if (fragment) {
767
+ fragments.push({ path: file.relativePath, fragment });
768
+ }
769
+ } catch (error) {
770
+ errors.push({
771
+ component: file.relativePath,
772
+ variant: "",
773
+ error: error instanceof Error ? error.message : String(error)
774
+ });
775
+ }
776
+ }
777
+ const filteredFragments = options.component ? fragments.filter((s) => s.fragment.meta.name === options.component) : fragments;
778
+ if (options.component && filteredFragments.length === 0) {
779
+ console.log(pc.yellow(`Component "${options.component}" not found.`));
780
+ return {
781
+ success: false,
782
+ captured: 0,
783
+ skipped: 0,
784
+ errors: [],
785
+ totalTimeMs: Date.now() - startTime
786
+ };
787
+ }
788
+ const variantsToCapture = [];
789
+ for (const { fragment } of filteredFragments) {
790
+ const variants = options.variant ? fragment.variants.filter((v) => v.name === options.variant) : fragment.variants;
791
+ for (const variant of variants) {
792
+ variantsToCapture.push({
793
+ component: fragment.meta.name,
794
+ variant: variant.name,
795
+ render: variant.render
796
+ });
797
+ }
798
+ }
799
+ if (variantsToCapture.length === 0) {
800
+ console.log(pc.yellow("No variants to capture."));
801
+ return {
802
+ success: true,
803
+ captured: 0,
804
+ skipped: 0,
805
+ errors: [],
806
+ totalTimeMs: Date.now() - startTime
807
+ };
808
+ }
809
+ const theme = options.theme ?? DEFAULTS.theme;
810
+ const viewport = {
811
+ width: options.width ?? config.screenshots?.viewport?.width ?? DEFAULTS.viewport.width,
812
+ height: options.height ?? config.screenshots?.viewport?.height ?? DEFAULTS.viewport.height
813
+ };
814
+ console.log(pc.cyan(`
815
+ ${BRAND.name} Screenshot
816
+ `));
817
+ console.log(pc.dim(`Capturing variants (theme: ${theme}, viewport: ${viewport.width}x${viewport.height}):
818
+ `));
819
+ const pool = new BrowserPool({
820
+ viewport
821
+ });
822
+ const viewerPort = DEFAULTS.port;
823
+ const baseUrl = `http://localhost:${viewerPort}`;
824
+ const captureEngine = new CaptureEngine(pool, baseUrl);
825
+ let captured = 0;
826
+ let skipped = 0;
827
+ const captureOptions = {
828
+ theme,
829
+ viewport,
830
+ delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
831
+ };
832
+ try {
833
+ console.log(pc.dim("Starting browser..."));
834
+ await pool.warmup();
835
+ console.log(pc.dim("Browser ready.\n"));
836
+ for (const { component, variant } of variantsToCapture) {
837
+ const hasExisting = storage.hasBaseline(component, variant, theme);
838
+ if (hasExisting && !options.update) {
839
+ console.log(` ${pc.dim("\u25CB")} ${component}/${variant} ${pc.dim("(skipped)")}`);
840
+ skipped++;
841
+ continue;
842
+ }
843
+ try {
844
+ const screenshot = await captureEngine.captureVariant(
845
+ component,
846
+ variant,
847
+ captureOptions
848
+ );
849
+ await storage.saveBaseline(screenshot);
850
+ const totalTime = screenshot.metadata.renderTimeMs + screenshot.metadata.captureTimeMs;
851
+ console.log(
852
+ ` ${pc.green("\u2713")} ${component}/${variant} ${pc.dim(formatMs(totalTime))}`
853
+ );
854
+ captured++;
855
+ } catch (error) {
856
+ const errorMsg = error instanceof Error ? error.message : String(error);
857
+ console.log(` ${pc.red("\u2717")} ${component}/${variant} ${pc.dim(errorMsg)}`);
858
+ errors.push({ component, variant, error: errorMsg });
859
+ }
860
+ }
861
+ } finally {
862
+ await pool.shutdown();
863
+ }
864
+ const totalTimeMs = Date.now() - startTime;
865
+ console.log();
866
+ if (errors.length === 0) {
867
+ console.log(pc.green(`\u2713 Captured ${captured} screenshot(s) in ${formatMs(totalTimeMs)}`));
868
+ } else {
869
+ console.log(pc.yellow(`\u26A0 Captured ${captured} screenshot(s) with ${errors.length} error(s)`));
870
+ }
871
+ if (skipped > 0) {
872
+ console.log(pc.dim(` ${skipped} skipped (use --update to recapture)`));
873
+ }
874
+ console.log(pc.dim(` Stored in ${storage.screenshotsDirPath}
875
+ `));
876
+ return {
877
+ success: errors.length === 0,
878
+ captured,
879
+ skipped,
880
+ errors,
881
+ totalTimeMs
882
+ };
883
+ }
884
+
885
+ // src/diff.ts
886
+ import pc2 from "picocolors";
887
+ async function runDiffCommand(config, configDir, options = {}) {
888
+ const startTime = Date.now();
889
+ const results = [];
890
+ const storage = new StorageManager({
891
+ projectRoot: configDir,
892
+ viewport: config.screenshots?.viewport
893
+ });
894
+ await storage.initialize();
895
+ const threshold = options.threshold ?? config.screenshots?.threshold ?? DEFAULTS.diffThreshold;
896
+ const diffEngine = new DiffEngine(threshold);
897
+ const fragmentFiles = await discoverFragmentFiles(config, configDir);
898
+ if (fragmentFiles.length === 0) {
899
+ console.log(pc2.yellow("No fragment files found."));
900
+ return {
901
+ success: true,
902
+ total: 0,
903
+ passed: 0,
904
+ failed: 0,
905
+ missing: 0,
906
+ results: [],
907
+ totalTimeMs: Date.now() - startTime
908
+ };
909
+ }
910
+ const fragments = [];
911
+ for (const file of fragmentFiles) {
912
+ try {
913
+ const fragment = await loadFragmentFile(file.absolutePath);
914
+ if (fragment) {
915
+ fragments.push({ path: file.relativePath, fragment });
916
+ }
917
+ } catch {
918
+ }
919
+ }
920
+ const filteredFragments = options.component ? fragments.filter((s) => s.fragment.meta.name === options.component) : fragments;
921
+ if (options.component && filteredFragments.length === 0) {
922
+ console.log(pc2.yellow(`Component "${options.component}" not found.`));
923
+ return {
924
+ success: false,
925
+ total: 0,
926
+ passed: 0,
927
+ failed: 0,
928
+ missing: 0,
929
+ results: [],
930
+ totalTimeMs: Date.now() - startTime
931
+ };
932
+ }
933
+ const variantsToDiff = [];
934
+ for (const { fragment } of filteredFragments) {
935
+ const variants = options.variant ? fragment.variants.filter((v) => v.name === options.variant) : fragment.variants;
936
+ for (const variant of variants) {
937
+ variantsToDiff.push({
938
+ component: fragment.meta.name,
939
+ variant: variant.name
940
+ });
941
+ }
942
+ }
943
+ if (variantsToDiff.length === 0) {
944
+ console.log(pc2.yellow("No variants to compare."));
945
+ return {
946
+ success: true,
947
+ total: 0,
948
+ passed: 0,
949
+ failed: 0,
950
+ missing: 0,
951
+ results: [],
952
+ totalTimeMs: Date.now() - startTime
953
+ };
954
+ }
955
+ const theme = options.theme ?? DEFAULTS.theme;
956
+ const viewport = config.screenshots?.viewport ?? DEFAULTS.viewport;
957
+ console.log(pc2.cyan(`
958
+ ${BRAND.name} Diff
959
+ `));
960
+ console.log(pc2.dim(`Comparing against baselines (theme: ${theme}, threshold: ${threshold}%):
961
+ `));
962
+ const pool = new BrowserPool({
963
+ viewport
964
+ });
965
+ const viewerPort = DEFAULTS.port;
966
+ const baseUrl = `http://localhost:${viewerPort}`;
967
+ const captureEngine = new CaptureEngine(pool, baseUrl);
968
+ let passed = 0;
969
+ let failed = 0;
970
+ let missing = 0;
971
+ const captureOptions = {
972
+ theme,
973
+ viewport,
974
+ delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
975
+ };
976
+ try {
977
+ await pool.warmup();
978
+ for (const { component, variant } of variantsToDiff) {
979
+ const baseline = await storage.loadBaseline(component, variant, theme);
980
+ if (!baseline) {
981
+ console.log(
982
+ ` ${pc2.yellow("?")} ${component}/${variant} ${pc2.dim("(no baseline)")}`
983
+ );
984
+ missing++;
985
+ continue;
986
+ }
987
+ try {
988
+ const current = await captureEngine.captureVariant(
989
+ component,
990
+ variant,
991
+ captureOptions
992
+ );
993
+ if (diffEngine.areIdentical(current, baseline)) {
994
+ console.log(` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim("0.0%")}`);
995
+ results.push({
996
+ component,
997
+ variant,
998
+ theme,
999
+ result: {
1000
+ matches: true,
1001
+ diffPercentage: 0,
1002
+ diffPixelCount: 0,
1003
+ totalPixels: current.viewport.width * current.viewport.height,
1004
+ changedRegions: [],
1005
+ diffTimeMs: 0
1006
+ }
1007
+ });
1008
+ passed++;
1009
+ continue;
1010
+ }
1011
+ const diffResult = diffEngine.compare(current, baseline, { threshold });
1012
+ if (diffResult.matches) {
1013
+ console.log(
1014
+ ` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim(`${diffResult.diffPercentage}%`)}`
1015
+ );
1016
+ passed++;
1017
+ } else {
1018
+ let diffImagePath;
1019
+ if (diffResult.diffImage) {
1020
+ diffImagePath = await storage.saveDiff(
1021
+ component,
1022
+ variant,
1023
+ theme,
1024
+ diffResult.diffImage
1025
+ );
1026
+ }
1027
+ console.log(
1028
+ ` ${pc2.red("\u2717")} ${component}/${variant} ${pc2.yellow(`${diffResult.diffPercentage}%`)}` + (diffImagePath ? pc2.dim(` \u2192 ${diffImagePath}`) : "")
1029
+ );
1030
+ failed++;
1031
+ results.push({
1032
+ component,
1033
+ variant,
1034
+ theme,
1035
+ result: diffResult,
1036
+ diffImagePath
1037
+ });
1038
+ continue;
1039
+ }
1040
+ results.push({
1041
+ component,
1042
+ variant,
1043
+ theme,
1044
+ result: diffResult
1045
+ });
1046
+ } catch (error) {
1047
+ const errorMsg = error instanceof Error ? error.message : String(error);
1048
+ console.log(` ${pc2.red("!")} ${component}/${variant} ${pc2.dim(errorMsg)}`);
1049
+ failed++;
1050
+ }
1051
+ }
1052
+ } finally {
1053
+ await pool.shutdown();
1054
+ }
1055
+ const totalTimeMs = Date.now() - startTime;
1056
+ const total = passed + failed + missing;
1057
+ console.log();
1058
+ if (failed === 0 && missing === 0) {
1059
+ console.log(pc2.green(`\u2713 All ${passed} variant(s) match baselines`));
1060
+ } else if (failed > 0) {
1061
+ console.log(pc2.red(`\u2717 ${failed} variant(s) differ from baselines`));
1062
+ }
1063
+ if (missing > 0) {
1064
+ console.log(pc2.yellow(` ${missing} variant(s) have no baseline (run \`${BRAND.cliCommand} screenshot\`)`));
1065
+ }
1066
+ console.log(pc2.dim(` Completed in ${formatMs(totalTimeMs)}
1067
+ `));
1068
+ const success = failed === 0;
1069
+ return {
1070
+ success,
1071
+ total,
1072
+ passed,
1073
+ failed,
1074
+ missing,
1075
+ results,
1076
+ totalTimeMs
1077
+ };
1078
+ }
1079
+
1080
+ // src/analyze.ts
1081
+ import { existsSync as existsSync2 } from "fs";
1082
+ import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
1083
+ import { join as join2, dirname } from "path";
1084
+ import pc3 from "picocolors";
1085
+ async function runAnalyzeCommand(config, configDir, options = {}) {
1086
+ const format = options.format ?? "html";
1087
+ const minScore = options.minScore ?? 0;
1088
+ console.log(pc3.cyan(`
1089
+ ${BRAND.name} Analyzer
1090
+ `));
1091
+ const fragmentsPath = join2(configDir, config.outFile ?? "fragments.json");
1092
+ if (!existsSync2(fragmentsPath)) {
1093
+ console.log(pc3.red(`\u2717 No fragments.json found. Run \`${BRAND.cliCommand} build\` first.
1094
+ `));
1095
+ return {
1096
+ success: false,
1097
+ analytics: createEmptyAnalytics()
1098
+ };
1099
+ }
1100
+ console.log(pc3.dim("Analyzing design system...\n"));
1101
+ const content = await readFile3(fragmentsPath, "utf-8");
1102
+ const data = JSON.parse(content);
1103
+ const analytics = analyzeDesignSystem(data);
1104
+ printConsoleSummary(analytics);
1105
+ let outputPath;
1106
+ if (format === "html" || format === "json") {
1107
+ outputPath = options.output ?? getDefaultOutputPath(format, configDir);
1108
+ await mkdir(dirname(outputPath), { recursive: true });
1109
+ if (format === "html") {
1110
+ const html = generateHtmlReport(analytics);
1111
+ await writeFile(outputPath, html);
1112
+ console.log(pc3.green(`\u2713 Report generated: ${outputPath}
1113
+ `));
1114
+ } else {
1115
+ await writeFile(outputPath, JSON.stringify(analytics, null, 2));
1116
+ console.log(pc3.green(`\u2713 JSON report generated: ${outputPath}
1117
+ `));
1118
+ }
1119
+ if (options.open && format === "html") {
1120
+ await openInBrowser(outputPath);
1121
+ }
1122
+ }
1123
+ const passedCi = analytics.summary.overallScore >= minScore;
1124
+ if (options.ci) {
1125
+ if (passedCi) {
1126
+ console.log(
1127
+ pc3.green(`\u2713 Score ${analytics.summary.overallScore} meets minimum threshold ${minScore}
1128
+ `)
1129
+ );
1130
+ } else {
1131
+ console.log(
1132
+ pc3.red(
1133
+ `\u2717 Score ${analytics.summary.overallScore} below minimum threshold ${minScore}
1134
+ `
1135
+ )
1136
+ );
1137
+ }
1138
+ }
1139
+ return {
1140
+ success: !options.ci || passedCi,
1141
+ analytics,
1142
+ outputPath
1143
+ };
1144
+ }
1145
+ function printConsoleSummary(analytics) {
1146
+ const { summary, coverage, recommendations } = analytics;
1147
+ const grade = getGrade(summary.overallScore);
1148
+ console.log(
1149
+ pc3.bold(
1150
+ `Overall Score: ${colorizeScore(summary.overallScore)} (${grade})
1151
+ `
1152
+ )
1153
+ );
1154
+ console.log(pc3.dim("Summary"));
1155
+ console.log(` Components: ${pc3.white(summary.totalComponents.toString())}`);
1156
+ console.log(` Variants: ${pc3.white(summary.totalVariants.toString())}`);
1157
+ console.log(` Props: ${pc3.white(summary.totalProps.toString())}`);
1158
+ console.log(` Categories: ${pc3.white(summary.categories.join(", "))}`);
1159
+ console.log();
1160
+ console.log(pc3.dim("Coverage"));
1161
+ console.log(` Description: ${formatCoverage(coverage.fields.description)}`);
1162
+ console.log(` Usage when: ${formatCoverage(coverage.fields.usageWhen)}`);
1163
+ console.log(` Usage whenNot:${formatCoverage(coverage.fields.usageWhenNot)}`);
1164
+ console.log(` Guidelines: ${formatCoverage(coverage.fields.guidelines)}`);
1165
+ console.log(` Relations: ${formatCoverage(coverage.fields.relations)}`);
1166
+ console.log();
1167
+ if (recommendations.length > 0) {
1168
+ console.log(pc3.dim("Top Recommendations"));
1169
+ for (const rec of recommendations.slice(0, 3)) {
1170
+ const priority = rec.priority === "high" ? pc3.red(`[${rec.priority}]`) : rec.priority === "medium" ? pc3.yellow(`[${rec.priority}]`) : pc3.dim(`[${rec.priority}]`);
1171
+ console.log(` ${priority} ${rec.title}`);
1172
+ }
1173
+ console.log();
1174
+ }
1175
+ }
1176
+ function formatCoverage(field) {
1177
+ const pct = colorizeScore(field.percentage);
1178
+ return `${pct} (${field.covered}/${field.total})`;
1179
+ }
1180
+ function colorizeScore(score) {
1181
+ if (score >= 80) return pc3.green(`${score}%`);
1182
+ if (score >= 60) return pc3.yellow(`${score}%`);
1183
+ return pc3.red(`${score}%`);
1184
+ }
1185
+ function getDefaultOutputPath(format, configDir) {
1186
+ const filename = format === "html" ? "fragments-report.html" : "fragments-report.json";
1187
+ return join2(configDir, filename);
1188
+ }
1189
+ async function openInBrowser(path) {
1190
+ const { platform } = await import("os");
1191
+ const { exec } = await import("child_process");
1192
+ const os = platform();
1193
+ const cmd = os === "darwin" ? `open "${path}"` : os === "win32" ? `start "" "${path}"` : `xdg-open "${path}"`;
1194
+ exec(cmd);
1195
+ }
1196
+ function createEmptyAnalytics() {
1197
+ return {
1198
+ analyzedAt: /* @__PURE__ */ new Date(),
1199
+ summary: {
1200
+ totalComponents: 0,
1201
+ totalVariants: 0,
1202
+ totalProps: 0,
1203
+ categories: [],
1204
+ overallScore: 0
1205
+ },
1206
+ inventory: {
1207
+ byCategory: {},
1208
+ byStatus: {},
1209
+ byVariantCount: [],
1210
+ byPropCount: []
1211
+ },
1212
+ coverage: {
1213
+ overall: 0,
1214
+ fields: {
1215
+ description: { covered: 0, total: 0, percentage: 0 },
1216
+ usageWhen: { covered: 0, total: 0, percentage: 0 },
1217
+ usageWhenNot: { covered: 0, total: 0, percentage: 0 },
1218
+ guidelines: { covered: 0, total: 0, percentage: 0 },
1219
+ accessibility: { covered: 0, total: 0, percentage: 0 },
1220
+ relations: { covered: 0, total: 0, percentage: 0 },
1221
+ propDescriptions: { covered: 0, total: 0, percentage: 0 },
1222
+ propConstraints: { covered: 0, total: 0, percentage: 0 }
1223
+ },
1224
+ incomplete: []
1225
+ },
1226
+ quality: {
1227
+ missingWhenNot: [],
1228
+ isolated: [],
1229
+ deprecated: [],
1230
+ fewVariants: [],
1231
+ undocumentedProps: [],
1232
+ unconstrainedProps: []
1233
+ },
1234
+ distribution: {
1235
+ variantsPerComponent: [],
1236
+ propsPerComponent: [],
1237
+ componentsPerCategory: [],
1238
+ statusDistribution: [],
1239
+ tagFrequency: []
1240
+ },
1241
+ recommendations: []
1242
+ };
1243
+ }
1244
+
1245
+ export {
1246
+ validateSchema,
1247
+ validateCoverage,
1248
+ validateAll,
1249
+ validateSnippets,
1250
+ validateDrift,
1251
+ runScreenshotCommand,
1252
+ runDiffCommand,
1253
+ runAnalyzeCommand
1254
+ };
1255
+ //# sourceMappingURL=chunk-5JF26E55.js.map