@fragments-sdk/cli 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
|
@@ -0,0 +1,1625 @@
|
|
|
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-NWQ4CJOQ.js";
|
|
12
|
+
import {
|
|
13
|
+
discoverBlockFiles,
|
|
14
|
+
discoverComponentFiles,
|
|
15
|
+
discoverSegmentFiles,
|
|
16
|
+
discoverTokenFiles,
|
|
17
|
+
extractComponentName,
|
|
18
|
+
generateContextMd,
|
|
19
|
+
generateRegistry,
|
|
20
|
+
loadSegmentFile,
|
|
21
|
+
parseSegmentFile
|
|
22
|
+
} from "./chunk-CVXKXVOY.js";
|
|
23
|
+
import {
|
|
24
|
+
compileBlock,
|
|
25
|
+
parseTokenFile
|
|
26
|
+
} from "./chunk-TJ34N7C7.js";
|
|
27
|
+
import {
|
|
28
|
+
BRAND,
|
|
29
|
+
DEFAULTS,
|
|
30
|
+
segmentDefinitionSchema
|
|
31
|
+
} from "./chunk-6JBGU74P.js";
|
|
32
|
+
|
|
33
|
+
// src/validators.ts
|
|
34
|
+
async function validateSchema(config, configDir) {
|
|
35
|
+
const files = await discoverSegmentFiles(config, configDir);
|
|
36
|
+
const errors = [];
|
|
37
|
+
const warnings = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
try {
|
|
40
|
+
const segment = await loadSegmentFile(file.absolutePath);
|
|
41
|
+
if (!segment) {
|
|
42
|
+
errors.push({
|
|
43
|
+
file: file.relativePath,
|
|
44
|
+
message: "No default export found",
|
|
45
|
+
details: `Segment files must have a default export from defineSegment()`
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const result = segmentDefinitionSchema.safeParse(segment);
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
const details = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
52
|
+
errors.push({
|
|
53
|
+
file: file.relativePath,
|
|
54
|
+
message: "Invalid segment schema",
|
|
55
|
+
details
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
errors.push({
|
|
60
|
+
file: file.relativePath,
|
|
61
|
+
message: "Failed to load segment file",
|
|
62
|
+
details: error instanceof Error ? error.message : String(error)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: errors.length === 0,
|
|
68
|
+
errors,
|
|
69
|
+
warnings
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function validateCoverage(config, configDir) {
|
|
73
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
74
|
+
const componentFiles = await discoverComponentFiles(config, configDir);
|
|
75
|
+
const errors = [];
|
|
76
|
+
const warnings = [];
|
|
77
|
+
const documentedComponents = /* @__PURE__ */ new Set();
|
|
78
|
+
for (const file of segmentFiles) {
|
|
79
|
+
try {
|
|
80
|
+
const segment = await loadSegmentFile(file.absolutePath);
|
|
81
|
+
if (segment?.meta?.name) {
|
|
82
|
+
documentedComponents.add(segment.meta.name);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const file of componentFiles) {
|
|
88
|
+
const componentName = extractComponentName(file.relativePath);
|
|
89
|
+
const segmentPath = file.relativePath.replace(
|
|
90
|
+
/\.(tsx?|jsx?)$/,
|
|
91
|
+
BRAND.fileExtension
|
|
92
|
+
);
|
|
93
|
+
const hasSegmentFile = segmentFiles.some(
|
|
94
|
+
(s) => s.relativePath === segmentPath
|
|
95
|
+
);
|
|
96
|
+
if (!hasSegmentFile && !documentedComponents.has(componentName)) {
|
|
97
|
+
warnings.push({
|
|
98
|
+
file: file.relativePath,
|
|
99
|
+
message: `Component "${componentName}" has no segment documentation`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
valid: errors.length === 0,
|
|
105
|
+
errors,
|
|
106
|
+
warnings
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function validateAll(config, configDir) {
|
|
110
|
+
const [schemaResult, coverageResult] = await Promise.all([
|
|
111
|
+
validateSchema(config, configDir),
|
|
112
|
+
validateCoverage(config, configDir)
|
|
113
|
+
]);
|
|
114
|
+
return {
|
|
115
|
+
valid: schemaResult.valid && coverageResult.valid,
|
|
116
|
+
errors: [...schemaResult.errors, ...coverageResult.errors],
|
|
117
|
+
warnings: [...schemaResult.warnings, ...coverageResult.warnings]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/build.ts
|
|
122
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
123
|
+
import { resolve as resolve3, join as join3 } from "path";
|
|
124
|
+
import { existsSync as existsSync3 } from "fs";
|
|
125
|
+
|
|
126
|
+
// src/core/auto-props.ts
|
|
127
|
+
import { existsSync, statSync } from "fs";
|
|
128
|
+
import { dirname, extname, join, resolve } from "path";
|
|
129
|
+
import ts from "typescript";
|
|
130
|
+
function toPosixPath(filePath) {
|
|
131
|
+
return filePath.replace(/\\/g, "/");
|
|
132
|
+
}
|
|
133
|
+
function isFile(filePath) {
|
|
134
|
+
if (!existsSync(filePath)) return false;
|
|
135
|
+
try {
|
|
136
|
+
return statSync(filePath).isFile();
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function resolveModulePath(basePath) {
|
|
142
|
+
const candidates = [];
|
|
143
|
+
const extension = extname(basePath);
|
|
144
|
+
if (extension) {
|
|
145
|
+
candidates.push(basePath);
|
|
146
|
+
} else {
|
|
147
|
+
candidates.push(
|
|
148
|
+
`${basePath}.tsx`,
|
|
149
|
+
`${basePath}.ts`,
|
|
150
|
+
`${basePath}.jsx`,
|
|
151
|
+
`${basePath}.js`,
|
|
152
|
+
join(basePath, "index.tsx"),
|
|
153
|
+
join(basePath, "index.ts"),
|
|
154
|
+
join(basePath, "index.jsx"),
|
|
155
|
+
join(basePath, "index.js")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
for (const candidate of candidates) {
|
|
159
|
+
if (isFile(candidate)) {
|
|
160
|
+
return resolve(candidate);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function resolveComponentSourcePath(segmentFileAbsolutePath, componentImportPath) {
|
|
166
|
+
if (!componentImportPath) return null;
|
|
167
|
+
if (!componentImportPath.startsWith(".")) return null;
|
|
168
|
+
const segmentDir = dirname(segmentFileAbsolutePath);
|
|
169
|
+
const basePath = resolve(segmentDir, componentImportPath);
|
|
170
|
+
return resolveModulePath(basePath);
|
|
171
|
+
}
|
|
172
|
+
function collectTopLevelDeclarations(sourceFile) {
|
|
173
|
+
const typeDeclarations = /* @__PURE__ */ new Map();
|
|
174
|
+
const functionDeclarations = /* @__PURE__ */ new Map();
|
|
175
|
+
const variableDeclarations = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const node of sourceFile.statements) {
|
|
177
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
178
|
+
typeDeclarations.set(node.name.text, node);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
182
|
+
typeDeclarations.set(node.name.text, node);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
186
|
+
functionDeclarations.set(node.name.text, node);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ts.isVariableStatement(node)) {
|
|
190
|
+
for (const declaration of node.declarationList.declarations) {
|
|
191
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
192
|
+
variableDeclarations.set(declaration.name.text, declaration);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { typeDeclarations, functionDeclarations, variableDeclarations };
|
|
198
|
+
}
|
|
199
|
+
function readDefaultValue(expression) {
|
|
200
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
201
|
+
return expression.text;
|
|
202
|
+
}
|
|
203
|
+
if (ts.isNumericLiteral(expression)) {
|
|
204
|
+
return Number(expression.text);
|
|
205
|
+
}
|
|
206
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
207
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
208
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
209
|
+
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expression.operand)) {
|
|
210
|
+
return -Number(expression.operand.text);
|
|
211
|
+
}
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
function extractDefaultValues(componentNode) {
|
|
215
|
+
const defaults = {};
|
|
216
|
+
if (!componentNode?.parameters?.length) return defaults;
|
|
217
|
+
const firstParam = componentNode.parameters[0];
|
|
218
|
+
if (!ts.isObjectBindingPattern(firstParam.name)) return defaults;
|
|
219
|
+
for (const element of firstParam.name.elements) {
|
|
220
|
+
let propName = null;
|
|
221
|
+
if (element.propertyName) {
|
|
222
|
+
if (ts.isIdentifier(element.propertyName) || ts.isStringLiteral(element.propertyName)) {
|
|
223
|
+
propName = element.propertyName.text;
|
|
224
|
+
}
|
|
225
|
+
} else if (ts.isIdentifier(element.name)) {
|
|
226
|
+
propName = element.name.text;
|
|
227
|
+
}
|
|
228
|
+
if (!propName || !element.initializer) continue;
|
|
229
|
+
const value = readDefaultValue(element.initializer);
|
|
230
|
+
if (value !== void 0) {
|
|
231
|
+
defaults[propName] = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return defaults;
|
|
235
|
+
}
|
|
236
|
+
function isNullishType(type) {
|
|
237
|
+
return (type.flags & ts.TypeFlags.Null) !== 0 || (type.flags & ts.TypeFlags.Undefined) !== 0 || (type.flags & ts.TypeFlags.Void) !== 0;
|
|
238
|
+
}
|
|
239
|
+
function isBooleanLikeType(type) {
|
|
240
|
+
return (type.flags & ts.TypeFlags.BooleanLike) !== 0 || type.flags === ts.TypeFlags.BooleanLiteral;
|
|
241
|
+
}
|
|
242
|
+
function inferPropType(type, checker) {
|
|
243
|
+
const typeText = checker.typeToString(type, void 0, ts.TypeFormatFlags.NoTruncation);
|
|
244
|
+
if (typeText.includes("ReactNode")) {
|
|
245
|
+
return { type: "node" };
|
|
246
|
+
}
|
|
247
|
+
if (typeText.includes("ReactElement") || typeText.includes("JSX.Element")) {
|
|
248
|
+
return { type: "element" };
|
|
249
|
+
}
|
|
250
|
+
if (type.getCallSignatures().length > 0) {
|
|
251
|
+
return { type: "function" };
|
|
252
|
+
}
|
|
253
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
254
|
+
return { type: "array" };
|
|
255
|
+
}
|
|
256
|
+
if (type.isUnion()) {
|
|
257
|
+
const nonNullableTypes = type.types.filter((unionType) => !isNullishType(unionType));
|
|
258
|
+
if (nonNullableTypes.length === 1) {
|
|
259
|
+
return inferPropType(nonNullableTypes[0], checker);
|
|
260
|
+
}
|
|
261
|
+
const stringLiteralValues = nonNullableTypes.filter((unionType) => (unionType.flags & ts.TypeFlags.StringLiteral) !== 0).map((unionType) => unionType.value);
|
|
262
|
+
if (stringLiteralValues.length > 0 && stringLiteralValues.length === nonNullableTypes.length) {
|
|
263
|
+
return { type: "enum", values: stringLiteralValues };
|
|
264
|
+
}
|
|
265
|
+
if (nonNullableTypes.every((unionType) => isBooleanLikeType(unionType))) {
|
|
266
|
+
return { type: "boolean" };
|
|
267
|
+
}
|
|
268
|
+
return { type: "union" };
|
|
269
|
+
}
|
|
270
|
+
if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
|
|
271
|
+
return { type: "string" };
|
|
272
|
+
}
|
|
273
|
+
if ((type.flags & ts.TypeFlags.NumberLike) !== 0) {
|
|
274
|
+
return { type: "number" };
|
|
275
|
+
}
|
|
276
|
+
if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) {
|
|
277
|
+
return { type: "boolean" };
|
|
278
|
+
}
|
|
279
|
+
if ((type.flags & ts.TypeFlags.Object) !== 0) {
|
|
280
|
+
return { type: "object" };
|
|
281
|
+
}
|
|
282
|
+
return { type: "custom" };
|
|
283
|
+
}
|
|
284
|
+
function resolveComponentSignature(exportName, declarations, sourceFile) {
|
|
285
|
+
const visitedNames = /* @__PURE__ */ new Set();
|
|
286
|
+
const typeNodeFromFunction = (node) => ({
|
|
287
|
+
propsTypeNode: node.parameters[0]?.type ?? null,
|
|
288
|
+
componentNode: node
|
|
289
|
+
});
|
|
290
|
+
const resolveFromExpression = (expression) => {
|
|
291
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
292
|
+
return resolveFromExpression(expression.expression);
|
|
293
|
+
}
|
|
294
|
+
if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
|
|
295
|
+
return resolveFromExpression(expression.expression);
|
|
296
|
+
}
|
|
297
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
298
|
+
return typeNodeFromFunction(expression);
|
|
299
|
+
}
|
|
300
|
+
if (ts.isIdentifier(expression)) {
|
|
301
|
+
return resolveFromIdentifier(expression.text);
|
|
302
|
+
}
|
|
303
|
+
if (ts.isCallExpression(expression)) {
|
|
304
|
+
if (ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "forwardRef") {
|
|
305
|
+
const forwardRefPropsType = expression.typeArguments?.[1] ?? null;
|
|
306
|
+
const innerArg = expression.arguments[0];
|
|
307
|
+
const inner = innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg)) ? typeNodeFromFunction(innerArg) : innerArg && ts.isIdentifier(innerArg) ? resolveFromIdentifier(innerArg.text) : { propsTypeNode: null, componentNode: null };
|
|
308
|
+
return {
|
|
309
|
+
propsTypeNode: forwardRefPropsType ?? inner.propsTypeNode,
|
|
310
|
+
componentNode: inner.componentNode
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "memo" && expression.arguments[0]) {
|
|
314
|
+
return resolveFromExpression(expression.arguments[0]);
|
|
315
|
+
}
|
|
316
|
+
if (ts.isPropertyAccessExpression(expression.expression) && expression.expression.expression.getText(sourceFile) === "Object" && expression.expression.name.text === "assign" && expression.arguments[0]) {
|
|
317
|
+
return resolveFromExpression(expression.arguments[0]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { propsTypeNode: null, componentNode: null };
|
|
321
|
+
};
|
|
322
|
+
const resolveFromVariable = (declaration) => {
|
|
323
|
+
if (declaration.type && ts.isTypeReferenceNode(declaration.type) && declaration.type.typeArguments?.length) {
|
|
324
|
+
const typeName = declaration.type.typeName.getText(sourceFile);
|
|
325
|
+
if (typeName.includes("FC") || typeName.includes("FunctionComponent")) {
|
|
326
|
+
const componentNode = declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) ? declaration.initializer : null;
|
|
327
|
+
return {
|
|
328
|
+
propsTypeNode: declaration.type.typeArguments[0] ?? null,
|
|
329
|
+
componentNode
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (declaration.initializer) {
|
|
334
|
+
return resolveFromExpression(declaration.initializer);
|
|
335
|
+
}
|
|
336
|
+
return { propsTypeNode: null, componentNode: null };
|
|
337
|
+
};
|
|
338
|
+
const resolveFromIdentifier = (name) => {
|
|
339
|
+
if (!name || visitedNames.has(name)) {
|
|
340
|
+
return { propsTypeNode: null, componentNode: null };
|
|
341
|
+
}
|
|
342
|
+
visitedNames.add(name);
|
|
343
|
+
const functionDeclaration = declarations.functionDeclarations.get(name);
|
|
344
|
+
if (functionDeclaration) {
|
|
345
|
+
return typeNodeFromFunction(functionDeclaration);
|
|
346
|
+
}
|
|
347
|
+
const variableDeclaration = declarations.variableDeclarations.get(name);
|
|
348
|
+
if (variableDeclaration) {
|
|
349
|
+
return resolveFromVariable(variableDeclaration);
|
|
350
|
+
}
|
|
351
|
+
return { propsTypeNode: null, componentNode: null };
|
|
352
|
+
};
|
|
353
|
+
return resolveFromIdentifier(exportName);
|
|
354
|
+
}
|
|
355
|
+
function extractCustomPropsFromComponentFile(componentFilePath, exportName) {
|
|
356
|
+
const warnings = [];
|
|
357
|
+
const resolvedPath = resolve(componentFilePath);
|
|
358
|
+
if (!existsSync(resolvedPath)) {
|
|
359
|
+
return {
|
|
360
|
+
props: {},
|
|
361
|
+
warnings: [`Component file not found: ${resolvedPath}`],
|
|
362
|
+
resolved: false
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const compilerOptions = {
|
|
366
|
+
target: ts.ScriptTarget.ESNext,
|
|
367
|
+
module: ts.ModuleKind.ESNext,
|
|
368
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
369
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
370
|
+
allowSyntheticDefaultImports: true,
|
|
371
|
+
esModuleInterop: true,
|
|
372
|
+
skipLibCheck: true,
|
|
373
|
+
strict: false,
|
|
374
|
+
noEmit: true
|
|
375
|
+
};
|
|
376
|
+
const program = ts.createProgram([resolvedPath], compilerOptions);
|
|
377
|
+
const sourceFile = program.getSourceFile(resolvedPath);
|
|
378
|
+
if (!sourceFile) {
|
|
379
|
+
return {
|
|
380
|
+
props: {},
|
|
381
|
+
warnings: [`Unable to parse component source: ${resolvedPath}`],
|
|
382
|
+
resolved: false
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const checker = program.getTypeChecker();
|
|
386
|
+
const declarations = collectTopLevelDeclarations(sourceFile);
|
|
387
|
+
const signature = resolveComponentSignature(exportName, declarations, sourceFile);
|
|
388
|
+
if (!signature.propsTypeNode) {
|
|
389
|
+
return {
|
|
390
|
+
props: {},
|
|
391
|
+
warnings: [`Unable to resolve props type for export: ${exportName}`],
|
|
392
|
+
resolved: false
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const propsType = checker.getTypeFromTypeNode(signature.propsTypeNode);
|
|
396
|
+
const defaultValues = extractDefaultValues(signature.componentNode);
|
|
397
|
+
const sourceFilePath = toPosixPath(sourceFile.fileName);
|
|
398
|
+
const extractedProps = {};
|
|
399
|
+
for (const symbol of checker.getPropertiesOfType(propsType)) {
|
|
400
|
+
const propName = symbol.getName();
|
|
401
|
+
if (propName.startsWith("_") || propName.startsWith("$")) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const declarationsForSymbol = symbol.getDeclarations() ?? [];
|
|
405
|
+
const localDeclarations = declarationsForSymbol.filter(
|
|
406
|
+
(declaration) => toPosixPath(declaration.getSourceFile().fileName) === sourceFilePath
|
|
407
|
+
);
|
|
408
|
+
if (localDeclarations.length === 0) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const referenceNode = localDeclarations[0];
|
|
412
|
+
const inferredType = inferPropType(checker.getTypeOfSymbolAtLocation(symbol, referenceNode), checker);
|
|
413
|
+
const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
414
|
+
extractedProps[propName] = {
|
|
415
|
+
type: inferredType.type,
|
|
416
|
+
description,
|
|
417
|
+
required: (symbol.getFlags() & ts.SymbolFlags.Optional) === 0,
|
|
418
|
+
...inferredType.values && { values: inferredType.values },
|
|
419
|
+
...defaultValues[propName] !== void 0 && { default: defaultValues[propName] }
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (Object.keys(extractedProps).length === 0) {
|
|
423
|
+
warnings.push(`Resolved props type for ${exportName}, but no local custom props were found`);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
props: extractedProps,
|
|
427
|
+
warnings,
|
|
428
|
+
resolved: true
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/core/graph-extractor.ts
|
|
433
|
+
import ts2 from "typescript";
|
|
434
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
435
|
+
import { join as join2 } from "path";
|
|
436
|
+
import { readdirSync } from "fs";
|
|
437
|
+
import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from "@fragments-sdk/context/graph";
|
|
438
|
+
async function buildComponentGraph(segments, blocks, componentDir, options) {
|
|
439
|
+
const knownComponents = new Set(Object.keys(segments));
|
|
440
|
+
const allEdges = [];
|
|
441
|
+
const autoDetected = /* @__PURE__ */ new Map();
|
|
442
|
+
const warnings = [];
|
|
443
|
+
if (!options?.skipSourceAnalysis) {
|
|
444
|
+
const sourceEdges = extractImportAndHookEdges(componentDir, knownComponents);
|
|
445
|
+
allEdges.push(...sourceEdges);
|
|
446
|
+
const subComponentResults = extractSubComponents(componentDir, knownComponents);
|
|
447
|
+
for (const [name, subs] of subComponentResults) {
|
|
448
|
+
autoDetected.set(name, {
|
|
449
|
+
...autoDetected.get(name),
|
|
450
|
+
subComponents: subs,
|
|
451
|
+
compositionPattern: subs.length > 0 ? "compound" : "simple"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const jsxEdges = extractJsxUsageEdges(segments, knownComponents);
|
|
456
|
+
allEdges.push(...jsxEdges);
|
|
457
|
+
const blockEdges = extractBlockEdges(blocks);
|
|
458
|
+
allEdges.push(...blockEdges);
|
|
459
|
+
const relationEdges = extractRelationEdges(segments);
|
|
460
|
+
allEdges.push(...relationEdges);
|
|
461
|
+
const requiredChildrenMap = inferRequiredChildren(segments, autoDetected);
|
|
462
|
+
for (const [name, children] of requiredChildrenMap) {
|
|
463
|
+
const existing = autoDetected.get(name) ?? {};
|
|
464
|
+
autoDetected.set(name, { ...existing, requiredChildren: children });
|
|
465
|
+
}
|
|
466
|
+
const patternsMap = generateCommonPatterns(segments, autoDetected);
|
|
467
|
+
for (const [name, patterns] of patternsMap) {
|
|
468
|
+
const existing = autoDetected.get(name) ?? {};
|
|
469
|
+
autoDetected.set(name, { ...existing, commonPatterns: patterns });
|
|
470
|
+
}
|
|
471
|
+
const mergedEdges = mergeAndDeduplicate(allEdges);
|
|
472
|
+
const nodes = Object.entries(segments).map(([name, segment]) => {
|
|
473
|
+
const detected = autoDetected.get(name);
|
|
474
|
+
return {
|
|
475
|
+
name,
|
|
476
|
+
category: segment.meta.category,
|
|
477
|
+
status: segment.meta.status ?? "stable",
|
|
478
|
+
compositionPattern: segment.ai?.compositionPattern ?? detected?.compositionPattern,
|
|
479
|
+
subComponents: segment.ai?.subComponents ?? detected?.subComponents
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
const blockIndex = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const [blockName, block] of Object.entries(blocks)) {
|
|
484
|
+
for (const comp of block.components) {
|
|
485
|
+
const existing = blockIndex.get(comp);
|
|
486
|
+
if (existing) existing.push(blockName);
|
|
487
|
+
else blockIndex.set(comp, [blockName]);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const health = computeHealthFromData(nodes, mergedEdges, blockIndex);
|
|
491
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
492
|
+
const detected = autoDetected.get(name);
|
|
493
|
+
if (!detected) continue;
|
|
494
|
+
if (segment.ai?.subComponents && detected.subComponents) {
|
|
495
|
+
const declared = new Set(segment.ai.subComponents);
|
|
496
|
+
const found = new Set(detected.subComponents);
|
|
497
|
+
const missing = detected.subComponents.filter((s) => !declared.has(s));
|
|
498
|
+
const extra = segment.ai.subComponents.filter((s) => !found.has(s));
|
|
499
|
+
if (missing.length > 0) {
|
|
500
|
+
warnings.push(
|
|
501
|
+
`${name}: declares ${declared.size} subComponents but code has ${found.size}. Missing from declaration: ${missing.join(", ")}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (extra.length > 0) {
|
|
505
|
+
warnings.push(
|
|
506
|
+
`${name}: declares subComponents [${extra.join(", ")}] not found in Object.assign`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
graph: { nodes, edges: mergedEdges, health },
|
|
513
|
+
autoDetected,
|
|
514
|
+
warnings
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
518
|
+
const edges = [];
|
|
519
|
+
for (const componentName of knownComponents) {
|
|
520
|
+
const indexPath = findComponentIndex(componentDir, componentName);
|
|
521
|
+
if (!indexPath) continue;
|
|
522
|
+
let sourceText;
|
|
523
|
+
try {
|
|
524
|
+
sourceText = readFileSync(indexPath, "utf-8");
|
|
525
|
+
} catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const sourceFile = ts2.createSourceFile(
|
|
529
|
+
indexPath,
|
|
530
|
+
sourceText,
|
|
531
|
+
ts2.ScriptTarget.Latest,
|
|
532
|
+
true,
|
|
533
|
+
indexPath.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS
|
|
534
|
+
);
|
|
535
|
+
const visitNode = (node) => {
|
|
536
|
+
if (ts2.isImportDeclaration(node)) {
|
|
537
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
538
|
+
if (ts2.isStringLiteral(moduleSpecifier)) {
|
|
539
|
+
const importPath = moduleSpecifier.text;
|
|
540
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
541
|
+
const clause = node.importClause;
|
|
542
|
+
if (clause) {
|
|
543
|
+
if (clause.name && isPascalCase(clause.name.text) && knownComponents.has(clause.name.text)) {
|
|
544
|
+
edges.push({
|
|
545
|
+
source: componentName,
|
|
546
|
+
target: clause.name.text,
|
|
547
|
+
type: "imports",
|
|
548
|
+
weight: EDGE_TYPE_WEIGHTS["imports"],
|
|
549
|
+
provenance: `source:${componentName}/index.tsx`
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (clause.namedBindings && ts2.isNamedImports(clause.namedBindings)) {
|
|
553
|
+
for (const element of clause.namedBindings.elements) {
|
|
554
|
+
const name = element.name.text;
|
|
555
|
+
if (isPascalCase(name) && knownComponents.has(name) && name !== componentName) {
|
|
556
|
+
edges.push({
|
|
557
|
+
source: componentName,
|
|
558
|
+
target: name,
|
|
559
|
+
type: "imports",
|
|
560
|
+
weight: EDGE_TYPE_WEIGHTS["imports"],
|
|
561
|
+
provenance: `source:${componentName}/index.tsx`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (ts2.isCallExpression(node) && ts2.isIdentifier(node.expression)) {
|
|
571
|
+
const callName = node.expression.text;
|
|
572
|
+
const hookMatch = callName.match(/^use([A-Z][a-zA-Z]*)$/);
|
|
573
|
+
if (hookMatch) {
|
|
574
|
+
const hookTarget = hookMatch[1];
|
|
575
|
+
if (knownComponents.has(hookTarget) && hookTarget !== componentName) {
|
|
576
|
+
edges.push({
|
|
577
|
+
source: componentName,
|
|
578
|
+
target: hookTarget,
|
|
579
|
+
type: "hook-depends",
|
|
580
|
+
weight: EDGE_TYPE_WEIGHTS["hook-depends"],
|
|
581
|
+
provenance: `source:${componentName}/index.tsx`
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
ts2.forEachChild(node, visitNode);
|
|
587
|
+
};
|
|
588
|
+
ts2.forEachChild(sourceFile, visitNode);
|
|
589
|
+
}
|
|
590
|
+
return edges;
|
|
591
|
+
}
|
|
592
|
+
function extractSubComponents(componentDir, knownComponents) {
|
|
593
|
+
const result = /* @__PURE__ */ new Map();
|
|
594
|
+
for (const componentName of knownComponents) {
|
|
595
|
+
const indexPath = findComponentIndex(componentDir, componentName);
|
|
596
|
+
if (!indexPath) continue;
|
|
597
|
+
let sourceText;
|
|
598
|
+
try {
|
|
599
|
+
sourceText = readFileSync(indexPath, "utf-8");
|
|
600
|
+
} catch {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (!sourceText.includes("Object.assign")) continue;
|
|
604
|
+
const sourceFile = ts2.createSourceFile(
|
|
605
|
+
indexPath,
|
|
606
|
+
sourceText,
|
|
607
|
+
ts2.ScriptTarget.Latest,
|
|
608
|
+
true,
|
|
609
|
+
indexPath.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS
|
|
610
|
+
);
|
|
611
|
+
const subComponents = [];
|
|
612
|
+
const visitNode = (node) => {
|
|
613
|
+
if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression) && ts2.isIdentifier(node.expression.expression) && node.expression.expression.text === "Object" && node.expression.name.text === "assign" && node.arguments.length >= 2) {
|
|
614
|
+
const propsArg = node.arguments[1];
|
|
615
|
+
if (ts2.isObjectLiteralExpression(propsArg)) {
|
|
616
|
+
for (const prop of propsArg.properties) {
|
|
617
|
+
if (ts2.isShorthandPropertyAssignment(prop)) {
|
|
618
|
+
subComponents.push(prop.name.text);
|
|
619
|
+
} else if (ts2.isPropertyAssignment(prop) && ts2.isIdentifier(prop.name)) {
|
|
620
|
+
subComponents.push(prop.name.text);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
ts2.forEachChild(node, visitNode);
|
|
626
|
+
};
|
|
627
|
+
ts2.forEachChild(sourceFile, visitNode);
|
|
628
|
+
if (subComponents.length > 0) {
|
|
629
|
+
result.set(componentName, subComponents);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
function extractJsxUsageEdges(segments, knownComponents) {
|
|
635
|
+
const edges = [];
|
|
636
|
+
const jsxTagRegex = /<([A-Z][a-zA-Z]*(?:\.[A-Z][a-zA-Z]*)?)/g;
|
|
637
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
638
|
+
const usedComponents = /* @__PURE__ */ new Set();
|
|
639
|
+
for (const variant of segment.variants) {
|
|
640
|
+
if (!variant.code) continue;
|
|
641
|
+
let match;
|
|
642
|
+
jsxTagRegex.lastIndex = 0;
|
|
643
|
+
while ((match = jsxTagRegex.exec(variant.code)) !== null) {
|
|
644
|
+
let tagName = match[1];
|
|
645
|
+
if (tagName.includes(".")) {
|
|
646
|
+
tagName = tagName.split(".")[0];
|
|
647
|
+
}
|
|
648
|
+
if (knownComponents.has(tagName) && tagName !== name) {
|
|
649
|
+
usedComponents.add(tagName);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
for (const target of usedComponents) {
|
|
654
|
+
edges.push({
|
|
655
|
+
source: name,
|
|
656
|
+
target,
|
|
657
|
+
type: "renders",
|
|
658
|
+
weight: EDGE_TYPE_WEIGHTS["renders"],
|
|
659
|
+
provenance: `variant:${name}`
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return edges;
|
|
664
|
+
}
|
|
665
|
+
function extractBlockEdges(blocks) {
|
|
666
|
+
const edges = [];
|
|
667
|
+
for (const [blockName, block] of Object.entries(blocks)) {
|
|
668
|
+
const components = block.components;
|
|
669
|
+
for (let i = 0; i < components.length; i++) {
|
|
670
|
+
for (let j = i + 1; j < components.length; j++) {
|
|
671
|
+
edges.push({
|
|
672
|
+
source: components[i],
|
|
673
|
+
target: components[j],
|
|
674
|
+
type: "composes",
|
|
675
|
+
weight: EDGE_TYPE_WEIGHTS["composes"],
|
|
676
|
+
provenance: `block:${blockName}`
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return edges;
|
|
682
|
+
}
|
|
683
|
+
function extractRelationEdges(segments) {
|
|
684
|
+
const edges = [];
|
|
685
|
+
const relationToEdgeType = {
|
|
686
|
+
parent: "parent-of",
|
|
687
|
+
child: "parent-of",
|
|
688
|
+
// reversed: if A declares child B, edge is A parent-of B
|
|
689
|
+
composition: "composes",
|
|
690
|
+
alternative: "alternative-to",
|
|
691
|
+
sibling: "sibling-of"
|
|
692
|
+
};
|
|
693
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
694
|
+
if (!segment.relations) continue;
|
|
695
|
+
for (const rel of segment.relations) {
|
|
696
|
+
const edgeType = relationToEdgeType[rel.relationship];
|
|
697
|
+
if (!edgeType) continue;
|
|
698
|
+
let source;
|
|
699
|
+
let target;
|
|
700
|
+
if (rel.relationship === "parent") {
|
|
701
|
+
source = rel.component;
|
|
702
|
+
target = name;
|
|
703
|
+
} else {
|
|
704
|
+
source = name;
|
|
705
|
+
target = rel.component;
|
|
706
|
+
}
|
|
707
|
+
edges.push({
|
|
708
|
+
source,
|
|
709
|
+
target,
|
|
710
|
+
type: edgeType,
|
|
711
|
+
weight: EDGE_TYPE_WEIGHTS[edgeType],
|
|
712
|
+
note: rel.note,
|
|
713
|
+
provenance: "relation"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return edges;
|
|
718
|
+
}
|
|
719
|
+
function inferRequiredChildren(segments, autoDetected) {
|
|
720
|
+
const result = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
722
|
+
const detected = autoDetected.get(name);
|
|
723
|
+
const subs = detected?.subComponents ?? segment.ai?.subComponents;
|
|
724
|
+
if (!subs || subs.length === 0) continue;
|
|
725
|
+
const variantsWithCode = segment.variants.filter((v) => v.code);
|
|
726
|
+
if (variantsWithCode.length === 0) continue;
|
|
727
|
+
const required = [];
|
|
728
|
+
for (const sub of subs) {
|
|
729
|
+
const inAll = variantsWithCode.every((v) => {
|
|
730
|
+
const patterns = [
|
|
731
|
+
new RegExp(`<${name}\\.${sub}[\\s/>]`),
|
|
732
|
+
new RegExp(`<${sub}[\\s/>]`)
|
|
733
|
+
];
|
|
734
|
+
return patterns.some((p) => p.test(v.code));
|
|
735
|
+
});
|
|
736
|
+
if (inAll) required.push(sub);
|
|
737
|
+
}
|
|
738
|
+
if (required.length > 0) {
|
|
739
|
+
result.set(name, required);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
744
|
+
function generateCommonPatterns(segments, autoDetected) {
|
|
745
|
+
const result = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
747
|
+
const detected = autoDetected.get(name);
|
|
748
|
+
const subs = detected?.subComponents ?? segment.ai?.subComponents;
|
|
749
|
+
if (!subs || subs.length === 0) continue;
|
|
750
|
+
const firstVariant = segment.variants.find((v) => v.code);
|
|
751
|
+
if (!firstVariant?.code) continue;
|
|
752
|
+
const usedSubs = [];
|
|
753
|
+
for (const sub of subs) {
|
|
754
|
+
const patterns = [
|
|
755
|
+
new RegExp(`<${name}\\.${sub}`),
|
|
756
|
+
new RegExp(`<${sub}[\\s/>]`)
|
|
757
|
+
];
|
|
758
|
+
if (patterns.some((p) => p.test(firstVariant.code))) {
|
|
759
|
+
usedSubs.push(sub);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (usedSubs.length > 0) {
|
|
763
|
+
const pattern = `<${name}>
|
|
764
|
+
${usedSubs.map((s) => ` <${name}.${s}>...</${name}.${s}>`).join("\n")}
|
|
765
|
+
</${name}>`;
|
|
766
|
+
result.set(name, [pattern]);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function mergeAndDeduplicate(edges) {
|
|
772
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const edge of edges) {
|
|
774
|
+
const key = `${edge.source}\u2192${edge.target}:${edge.type}`;
|
|
775
|
+
const existing = edgeMap.get(key);
|
|
776
|
+
if (!existing || edge.weight > existing.weight) {
|
|
777
|
+
edgeMap.set(key, edge);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return [...edgeMap.values()];
|
|
781
|
+
}
|
|
782
|
+
function isPascalCase(name) {
|
|
783
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
784
|
+
}
|
|
785
|
+
function findComponentIndex(componentDir, componentName) {
|
|
786
|
+
const candidates = [
|
|
787
|
+
join2(componentDir, componentName, "index.tsx"),
|
|
788
|
+
join2(componentDir, componentName, "index.ts"),
|
|
789
|
+
join2(componentDir, componentName, `${componentName}.tsx`),
|
|
790
|
+
join2(componentDir, componentName, `${componentName}.ts`)
|
|
791
|
+
];
|
|
792
|
+
for (const candidate of candidates) {
|
|
793
|
+
if (existsSync2(candidate)) {
|
|
794
|
+
return candidate;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const entries = readdirSync(componentDir, { withFileTypes: true });
|
|
799
|
+
for (const entry of entries) {
|
|
800
|
+
if (entry.isDirectory() && entry.name === componentName) {
|
|
801
|
+
const subCandidates = [
|
|
802
|
+
join2(componentDir, entry.name, "index.tsx"),
|
|
803
|
+
join2(componentDir, entry.name, "index.ts")
|
|
804
|
+
];
|
|
805
|
+
for (const sc of subCandidates) {
|
|
806
|
+
if (existsSync2(sc)) return sc;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/build.ts
|
|
816
|
+
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
817
|
+
function normalizeParsedProps(parsedProps) {
|
|
818
|
+
return Object.fromEntries(
|
|
819
|
+
Object.entries(parsedProps).map(([name, prop]) => [
|
|
820
|
+
name,
|
|
821
|
+
{
|
|
822
|
+
type: prop.type ?? "custom",
|
|
823
|
+
description: prop.description ?? "",
|
|
824
|
+
default: prop.default,
|
|
825
|
+
required: prop.required,
|
|
826
|
+
values: prop.values,
|
|
827
|
+
constraints: prop.constraints
|
|
828
|
+
}
|
|
829
|
+
])
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
function mergeDocumentedAndAutoProps(documentedProps, autoProps) {
|
|
833
|
+
return Object.fromEntries(
|
|
834
|
+
Object.keys(autoProps).map((name) => {
|
|
835
|
+
const documented = documentedProps[name];
|
|
836
|
+
const auto = autoProps[name];
|
|
837
|
+
return [
|
|
838
|
+
name,
|
|
839
|
+
{
|
|
840
|
+
type: auto.type,
|
|
841
|
+
description: documented?.description ?? auto.description ?? "",
|
|
842
|
+
default: auto.default !== void 0 ? auto.default : documented?.default,
|
|
843
|
+
required: auto.required,
|
|
844
|
+
values: auto.values ?? documented?.values,
|
|
845
|
+
constraints: documented?.constraints
|
|
846
|
+
}
|
|
847
|
+
];
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
async function buildSegments(config, configDir) {
|
|
852
|
+
const files = await discoverSegmentFiles(config, configDir);
|
|
853
|
+
const errors = [];
|
|
854
|
+
const warnings = [];
|
|
855
|
+
const segments = {};
|
|
856
|
+
for (const file of files) {
|
|
857
|
+
try {
|
|
858
|
+
const content = await readFile(file.absolutePath, "utf-8");
|
|
859
|
+
const parsed = parseSegmentFile(content, file.relativePath);
|
|
860
|
+
for (const warning of parsed.warnings) {
|
|
861
|
+
warnings.push({ file: file.relativePath, warning });
|
|
862
|
+
}
|
|
863
|
+
if (!parsed.meta.name) {
|
|
864
|
+
errors.push({
|
|
865
|
+
file: file.relativePath,
|
|
866
|
+
error: "Missing meta.name in fragment definition"
|
|
867
|
+
});
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const documentedProps = normalizeParsedProps(parsed.props);
|
|
871
|
+
let mergedProps = documentedProps;
|
|
872
|
+
const componentExportName = parsed.componentName ?? parsed.meta.name;
|
|
873
|
+
const componentSourcePath = resolveComponentSourcePath(
|
|
874
|
+
file.absolutePath,
|
|
875
|
+
parsed.componentImport
|
|
876
|
+
);
|
|
877
|
+
if (componentExportName && componentSourcePath) {
|
|
878
|
+
const autoPropsResult = extractCustomPropsFromComponentFile(
|
|
879
|
+
componentSourcePath,
|
|
880
|
+
componentExportName
|
|
881
|
+
);
|
|
882
|
+
for (const warning of autoPropsResult.warnings) {
|
|
883
|
+
warnings.push({ file: file.relativePath, warning });
|
|
884
|
+
}
|
|
885
|
+
const hasAutoProps = Object.keys(autoPropsResult.props).length > 0;
|
|
886
|
+
if (autoPropsResult.resolved && hasAutoProps) {
|
|
887
|
+
const removedDocumentedProps = Object.keys(documentedProps).filter(
|
|
888
|
+
(propName) => !(propName in autoPropsResult.props)
|
|
889
|
+
);
|
|
890
|
+
if (removedDocumentedProps.length > 0) {
|
|
891
|
+
warnings.push({
|
|
892
|
+
file: file.relativePath,
|
|
893
|
+
warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
mergedProps = mergeDocumentedAndAutoProps(
|
|
897
|
+
documentedProps,
|
|
898
|
+
autoPropsResult.props
|
|
899
|
+
);
|
|
900
|
+
} else if (autoPropsResult.resolved && !hasAutoProps && Object.keys(documentedProps).length > 0) {
|
|
901
|
+
warnings.push({
|
|
902
|
+
file: file.relativePath,
|
|
903
|
+
warning: "Auto-props extraction returned no custom props; falling back to documented props"
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
} else if (!componentExportName) {
|
|
907
|
+
warnings.push({
|
|
908
|
+
file: file.relativePath,
|
|
909
|
+
warning: "Unable to resolve component export name for auto-props extraction"
|
|
910
|
+
});
|
|
911
|
+
} else if (!componentSourcePath) {
|
|
912
|
+
warnings.push({
|
|
913
|
+
file: file.relativePath,
|
|
914
|
+
warning: `Unable to resolve component source path from import: ${parsed.componentImport ?? "unknown"}`
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
const compiled = {
|
|
918
|
+
filePath: file.relativePath,
|
|
919
|
+
meta: {
|
|
920
|
+
name: parsed.meta.name,
|
|
921
|
+
description: parsed.meta.description ?? "",
|
|
922
|
+
category: parsed.meta.category ?? "Uncategorized",
|
|
923
|
+
status: parsed.meta.status,
|
|
924
|
+
tags: parsed.meta.tags,
|
|
925
|
+
since: parsed.meta.since,
|
|
926
|
+
figma: parsed.meta.figma
|
|
927
|
+
},
|
|
928
|
+
usage: {
|
|
929
|
+
when: parsed.usage.when ?? [],
|
|
930
|
+
whenNot: parsed.usage.whenNot ?? [],
|
|
931
|
+
guidelines: parsed.usage.guidelines,
|
|
932
|
+
accessibility: parsed.usage.accessibility
|
|
933
|
+
},
|
|
934
|
+
props: mergedProps,
|
|
935
|
+
relations: parsed.relations.map((rel) => ({
|
|
936
|
+
component: rel.component,
|
|
937
|
+
relationship: rel.relationship,
|
|
938
|
+
note: rel.note
|
|
939
|
+
})),
|
|
940
|
+
variants: parsed.variants.map((v) => ({
|
|
941
|
+
name: v.name,
|
|
942
|
+
description: v.description,
|
|
943
|
+
...v.code && { code: v.code },
|
|
944
|
+
...v.figma && { figma: v.figma },
|
|
945
|
+
...v.args && { args: v.args }
|
|
946
|
+
})),
|
|
947
|
+
// Include AI metadata if present
|
|
948
|
+
...parsed.ai && { ai: parsed.ai }
|
|
949
|
+
};
|
|
950
|
+
segments[parsed.meta.name] = compiled;
|
|
951
|
+
} catch (error) {
|
|
952
|
+
errors.push({
|
|
953
|
+
file: file.relativePath,
|
|
954
|
+
error: error instanceof Error ? error.message : String(error)
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const blocks = {};
|
|
959
|
+
try {
|
|
960
|
+
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
961
|
+
for (const file of blockFiles) {
|
|
962
|
+
try {
|
|
963
|
+
let raw = await loadSegmentFile(file.absolutePath);
|
|
964
|
+
if (raw && "default" in raw && typeof raw.default === "object") {
|
|
965
|
+
raw = raw.default;
|
|
966
|
+
}
|
|
967
|
+
const def = raw;
|
|
968
|
+
if (def && typeof def === "object" && "name" in def && "code" in def && "components" in def) {
|
|
969
|
+
const compiled = compileBlock(def, file.relativePath);
|
|
970
|
+
blocks[compiled.name] = compiled;
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
warnings.push({
|
|
974
|
+
file: file.relativePath,
|
|
975
|
+
warning: `Failed to load block: ${error instanceof Error ? error.message : String(error)}`
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
let tokens;
|
|
982
|
+
try {
|
|
983
|
+
const tokenPatterns = config.tokens?.include;
|
|
984
|
+
const tokenFiles = await discoverTokenFiles(configDir, tokenPatterns, config.exclude);
|
|
985
|
+
if (tokenFiles.length > 0) {
|
|
986
|
+
const mergedCategories = {};
|
|
987
|
+
let prefix = "--";
|
|
988
|
+
let total = 0;
|
|
989
|
+
for (const file of tokenFiles) {
|
|
990
|
+
const content = await readFile(file.absolutePath, "utf-8");
|
|
991
|
+
const parsed = parseTokenFile(content, file.relativePath);
|
|
992
|
+
prefix = parsed.prefix;
|
|
993
|
+
total += parsed.total;
|
|
994
|
+
for (const [cat, catTokens] of Object.entries(parsed.categories)) {
|
|
995
|
+
if (!mergedCategories[cat]) {
|
|
996
|
+
mergedCategories[cat] = [];
|
|
997
|
+
}
|
|
998
|
+
for (const t of catTokens) {
|
|
999
|
+
if (!mergedCategories[cat].some((e) => e.name === t.name)) {
|
|
1000
|
+
mergedCategories[cat].push({ name: t.name, description: t.description });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (total > 0) {
|
|
1006
|
+
tokens = { prefix, total, categories: mergedCategories };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
let packageName;
|
|
1012
|
+
const pkgJsonPath = resolve3(configDir, "package.json");
|
|
1013
|
+
if (existsSync3(pkgJsonPath)) {
|
|
1014
|
+
try {
|
|
1015
|
+
const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
1016
|
+
if (pkg.name) packageName = pkg.name;
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const componentDir = resolve3(configDir, "src", "components");
|
|
1021
|
+
let graphData;
|
|
1022
|
+
try {
|
|
1023
|
+
const graphResult = await buildComponentGraph(segments, blocks, componentDir);
|
|
1024
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
1025
|
+
const detected = graphResult.autoDetected.get(name);
|
|
1026
|
+
if (!detected) continue;
|
|
1027
|
+
if (!segment.ai) segment.ai = {};
|
|
1028
|
+
if (!segment.ai.subComponents && detected.subComponents) {
|
|
1029
|
+
segment.ai.subComponents = detected.subComponents;
|
|
1030
|
+
}
|
|
1031
|
+
if (!segment.ai.compositionPattern && detected.compositionPattern) {
|
|
1032
|
+
segment.ai.compositionPattern = detected.compositionPattern;
|
|
1033
|
+
}
|
|
1034
|
+
if (!segment.ai.commonPatterns && detected.commonPatterns) {
|
|
1035
|
+
segment.ai.commonPatterns = detected.commonPatterns;
|
|
1036
|
+
}
|
|
1037
|
+
if (!segment.ai.requiredChildren && detected.requiredChildren) {
|
|
1038
|
+
segment.ai.requiredChildren = detected.requiredChildren;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
for (const w of graphResult.warnings) {
|
|
1042
|
+
warnings.push({ file: "graph", warning: w });
|
|
1043
|
+
}
|
|
1044
|
+
graphData = serializeGraph(graphResult.graph);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
warnings.push({
|
|
1047
|
+
file: "graph",
|
|
1048
|
+
warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
const output = {
|
|
1052
|
+
version: "1.0.0",
|
|
1053
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1054
|
+
...packageName && { packageName },
|
|
1055
|
+
segments,
|
|
1056
|
+
...Object.keys(blocks).length > 0 && { blocks },
|
|
1057
|
+
...tokens && { tokens },
|
|
1058
|
+
...graphData && { graph: graphData }
|
|
1059
|
+
};
|
|
1060
|
+
const outputPath = resolve3(configDir, config.outFile ?? BRAND.outFile);
|
|
1061
|
+
await writeFile(outputPath, JSON.stringify(output));
|
|
1062
|
+
return {
|
|
1063
|
+
success: errors.length === 0,
|
|
1064
|
+
outputPath,
|
|
1065
|
+
segmentCount: Object.keys(segments).length,
|
|
1066
|
+
errors,
|
|
1067
|
+
warnings
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
async function buildFragmentsDir(config, configDir) {
|
|
1071
|
+
const fragmentsDir = join3(configDir, BRAND.dataDir);
|
|
1072
|
+
const componentsDir = join3(fragmentsDir, BRAND.componentsDir);
|
|
1073
|
+
await mkdir(fragmentsDir, { recursive: true });
|
|
1074
|
+
await mkdir(componentsDir, { recursive: true });
|
|
1075
|
+
const registryResult = await generateRegistry({
|
|
1076
|
+
projectRoot: configDir,
|
|
1077
|
+
componentPatterns: config.components || ["src/**/*.tsx", "src/**/*.ts"],
|
|
1078
|
+
storyPatterns: config.include || ["src/**/*.stories.tsx"],
|
|
1079
|
+
fragmentsDir,
|
|
1080
|
+
registryOptions: config.registry || {}
|
|
1081
|
+
});
|
|
1082
|
+
const errors = [...registryResult.errors];
|
|
1083
|
+
const warnings = [...registryResult.warnings];
|
|
1084
|
+
const indexPath = join3(fragmentsDir, "index.json");
|
|
1085
|
+
await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
|
|
1086
|
+
const registryPath = join3(fragmentsDir, BRAND.registryFile);
|
|
1087
|
+
await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
|
|
1088
|
+
const contextResult = generateContextMd(registryResult.registry, {
|
|
1089
|
+
format: "markdown",
|
|
1090
|
+
compact: false,
|
|
1091
|
+
include: {
|
|
1092
|
+
props: false,
|
|
1093
|
+
// AI can read TypeScript directly
|
|
1094
|
+
relations: true,
|
|
1095
|
+
code: false
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
const contextPath = join3(fragmentsDir, BRAND.contextFile);
|
|
1099
|
+
await writeFile(contextPath, contextResult.content);
|
|
1100
|
+
return {
|
|
1101
|
+
success: errors.length === 0,
|
|
1102
|
+
indexPath,
|
|
1103
|
+
registryPath,
|
|
1104
|
+
contextPath,
|
|
1105
|
+
componentCount: registryResult.registry.componentCount,
|
|
1106
|
+
errors,
|
|
1107
|
+
warnings
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/screenshot.ts
|
|
1112
|
+
import pc from "picocolors";
|
|
1113
|
+
async function runScreenshotCommand(config, configDir, options = {}) {
|
|
1114
|
+
const startTime = Date.now();
|
|
1115
|
+
const errors = [];
|
|
1116
|
+
const storage = new StorageManager({
|
|
1117
|
+
projectRoot: configDir,
|
|
1118
|
+
viewport: options.width && options.height ? { width: options.width, height: options.height } : config.screenshots?.viewport
|
|
1119
|
+
});
|
|
1120
|
+
await storage.initialize();
|
|
1121
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
1122
|
+
if (segmentFiles.length === 0) {
|
|
1123
|
+
console.log(pc.yellow("No segment files found."));
|
|
1124
|
+
return {
|
|
1125
|
+
success: true,
|
|
1126
|
+
captured: 0,
|
|
1127
|
+
skipped: 0,
|
|
1128
|
+
errors: [],
|
|
1129
|
+
totalTimeMs: Date.now() - startTime
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
const segments = [];
|
|
1133
|
+
for (const file of segmentFiles) {
|
|
1134
|
+
try {
|
|
1135
|
+
const segment = await loadSegmentFile(file.absolutePath);
|
|
1136
|
+
if (segment) {
|
|
1137
|
+
segments.push({ path: file.relativePath, segment });
|
|
1138
|
+
}
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
errors.push({
|
|
1141
|
+
component: file.relativePath,
|
|
1142
|
+
variant: "",
|
|
1143
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const filteredSegments = options.component ? segments.filter((s) => s.segment.meta.name === options.component) : segments;
|
|
1148
|
+
if (options.component && filteredSegments.length === 0) {
|
|
1149
|
+
console.log(pc.yellow(`Component "${options.component}" not found.`));
|
|
1150
|
+
return {
|
|
1151
|
+
success: false,
|
|
1152
|
+
captured: 0,
|
|
1153
|
+
skipped: 0,
|
|
1154
|
+
errors: [],
|
|
1155
|
+
totalTimeMs: Date.now() - startTime
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const variantsToCapture = [];
|
|
1159
|
+
for (const { segment } of filteredSegments) {
|
|
1160
|
+
const variants = options.variant ? segment.variants.filter((v) => v.name === options.variant) : segment.variants;
|
|
1161
|
+
for (const variant of variants) {
|
|
1162
|
+
variantsToCapture.push({
|
|
1163
|
+
component: segment.meta.name,
|
|
1164
|
+
variant: variant.name,
|
|
1165
|
+
render: variant.render
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (variantsToCapture.length === 0) {
|
|
1170
|
+
console.log(pc.yellow("No variants to capture."));
|
|
1171
|
+
return {
|
|
1172
|
+
success: true,
|
|
1173
|
+
captured: 0,
|
|
1174
|
+
skipped: 0,
|
|
1175
|
+
errors: [],
|
|
1176
|
+
totalTimeMs: Date.now() - startTime
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const theme = options.theme ?? DEFAULTS.theme;
|
|
1180
|
+
const viewport = {
|
|
1181
|
+
width: options.width ?? config.screenshots?.viewport?.width ?? DEFAULTS.viewport.width,
|
|
1182
|
+
height: options.height ?? config.screenshots?.viewport?.height ?? DEFAULTS.viewport.height
|
|
1183
|
+
};
|
|
1184
|
+
console.log(pc.cyan(`
|
|
1185
|
+
${BRAND.name} Screenshot
|
|
1186
|
+
`));
|
|
1187
|
+
console.log(pc.dim(`Capturing variants (theme: ${theme}, viewport: ${viewport.width}x${viewport.height}):
|
|
1188
|
+
`));
|
|
1189
|
+
const pool = new BrowserPool({
|
|
1190
|
+
viewport
|
|
1191
|
+
});
|
|
1192
|
+
const viewerPort = DEFAULTS.port;
|
|
1193
|
+
const baseUrl = `http://localhost:${viewerPort}`;
|
|
1194
|
+
const captureEngine = new CaptureEngine(pool, baseUrl);
|
|
1195
|
+
let captured = 0;
|
|
1196
|
+
let skipped = 0;
|
|
1197
|
+
const captureOptions = {
|
|
1198
|
+
theme,
|
|
1199
|
+
viewport,
|
|
1200
|
+
delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
|
|
1201
|
+
};
|
|
1202
|
+
try {
|
|
1203
|
+
console.log(pc.dim("Starting browser..."));
|
|
1204
|
+
await pool.warmup();
|
|
1205
|
+
console.log(pc.dim("Browser ready.\n"));
|
|
1206
|
+
for (const { component, variant } of variantsToCapture) {
|
|
1207
|
+
const hasExisting = storage.hasBaseline(component, variant, theme);
|
|
1208
|
+
if (hasExisting && !options.update) {
|
|
1209
|
+
console.log(` ${pc.dim("\u25CB")} ${component}/${variant} ${pc.dim("(skipped)")}`);
|
|
1210
|
+
skipped++;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const screenshot = await captureEngine.captureVariant(
|
|
1215
|
+
component,
|
|
1216
|
+
variant,
|
|
1217
|
+
captureOptions
|
|
1218
|
+
);
|
|
1219
|
+
await storage.saveBaseline(screenshot);
|
|
1220
|
+
const totalTime = screenshot.metadata.renderTimeMs + screenshot.metadata.captureTimeMs;
|
|
1221
|
+
console.log(
|
|
1222
|
+
` ${pc.green("\u2713")} ${component}/${variant} ${pc.dim(formatMs(totalTime))}`
|
|
1223
|
+
);
|
|
1224
|
+
captured++;
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1227
|
+
console.log(` ${pc.red("\u2717")} ${component}/${variant} ${pc.dim(errorMsg)}`);
|
|
1228
|
+
errors.push({ component, variant, error: errorMsg });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
} finally {
|
|
1232
|
+
await pool.shutdown();
|
|
1233
|
+
}
|
|
1234
|
+
const totalTimeMs = Date.now() - startTime;
|
|
1235
|
+
console.log();
|
|
1236
|
+
if (errors.length === 0) {
|
|
1237
|
+
console.log(pc.green(`\u2713 Captured ${captured} screenshot(s) in ${formatMs(totalTimeMs)}`));
|
|
1238
|
+
} else {
|
|
1239
|
+
console.log(pc.yellow(`\u26A0 Captured ${captured} screenshot(s) with ${errors.length} error(s)`));
|
|
1240
|
+
}
|
|
1241
|
+
if (skipped > 0) {
|
|
1242
|
+
console.log(pc.dim(` ${skipped} skipped (use --update to recapture)`));
|
|
1243
|
+
}
|
|
1244
|
+
console.log(pc.dim(` Stored in ${storage.screenshotsDirPath}
|
|
1245
|
+
`));
|
|
1246
|
+
return {
|
|
1247
|
+
success: errors.length === 0,
|
|
1248
|
+
captured,
|
|
1249
|
+
skipped,
|
|
1250
|
+
errors,
|
|
1251
|
+
totalTimeMs
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/diff.ts
|
|
1256
|
+
import pc2 from "picocolors";
|
|
1257
|
+
async function runDiffCommand(config, configDir, options = {}) {
|
|
1258
|
+
const startTime = Date.now();
|
|
1259
|
+
const results = [];
|
|
1260
|
+
const storage = new StorageManager({
|
|
1261
|
+
projectRoot: configDir,
|
|
1262
|
+
viewport: config.screenshots?.viewport
|
|
1263
|
+
});
|
|
1264
|
+
await storage.initialize();
|
|
1265
|
+
const threshold = options.threshold ?? config.screenshots?.threshold ?? DEFAULTS.diffThreshold;
|
|
1266
|
+
const diffEngine = new DiffEngine(threshold);
|
|
1267
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
1268
|
+
if (segmentFiles.length === 0) {
|
|
1269
|
+
console.log(pc2.yellow("No segment files found."));
|
|
1270
|
+
return {
|
|
1271
|
+
success: true,
|
|
1272
|
+
total: 0,
|
|
1273
|
+
passed: 0,
|
|
1274
|
+
failed: 0,
|
|
1275
|
+
missing: 0,
|
|
1276
|
+
results: [],
|
|
1277
|
+
totalTimeMs: Date.now() - startTime
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const segments = [];
|
|
1281
|
+
for (const file of segmentFiles) {
|
|
1282
|
+
try {
|
|
1283
|
+
const segment = await loadSegmentFile(file.absolutePath);
|
|
1284
|
+
if (segment) {
|
|
1285
|
+
segments.push({ path: file.relativePath, segment });
|
|
1286
|
+
}
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const filteredSegments = options.component ? segments.filter((s) => s.segment.meta.name === options.component) : segments;
|
|
1291
|
+
if (options.component && filteredSegments.length === 0) {
|
|
1292
|
+
console.log(pc2.yellow(`Component "${options.component}" not found.`));
|
|
1293
|
+
return {
|
|
1294
|
+
success: false,
|
|
1295
|
+
total: 0,
|
|
1296
|
+
passed: 0,
|
|
1297
|
+
failed: 0,
|
|
1298
|
+
missing: 0,
|
|
1299
|
+
results: [],
|
|
1300
|
+
totalTimeMs: Date.now() - startTime
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
const variantsToDiff = [];
|
|
1304
|
+
for (const { segment } of filteredSegments) {
|
|
1305
|
+
const variants = options.variant ? segment.variants.filter((v) => v.name === options.variant) : segment.variants;
|
|
1306
|
+
for (const variant of variants) {
|
|
1307
|
+
variantsToDiff.push({
|
|
1308
|
+
component: segment.meta.name,
|
|
1309
|
+
variant: variant.name
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (variantsToDiff.length === 0) {
|
|
1314
|
+
console.log(pc2.yellow("No variants to compare."));
|
|
1315
|
+
return {
|
|
1316
|
+
success: true,
|
|
1317
|
+
total: 0,
|
|
1318
|
+
passed: 0,
|
|
1319
|
+
failed: 0,
|
|
1320
|
+
missing: 0,
|
|
1321
|
+
results: [],
|
|
1322
|
+
totalTimeMs: Date.now() - startTime
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const theme = options.theme ?? DEFAULTS.theme;
|
|
1326
|
+
const viewport = config.screenshots?.viewport ?? DEFAULTS.viewport;
|
|
1327
|
+
console.log(pc2.cyan(`
|
|
1328
|
+
${BRAND.name} Diff
|
|
1329
|
+
`));
|
|
1330
|
+
console.log(pc2.dim(`Comparing against baselines (theme: ${theme}, threshold: ${threshold}%):
|
|
1331
|
+
`));
|
|
1332
|
+
const pool = new BrowserPool({
|
|
1333
|
+
viewport
|
|
1334
|
+
});
|
|
1335
|
+
const viewerPort = DEFAULTS.port;
|
|
1336
|
+
const baseUrl = `http://localhost:${viewerPort}`;
|
|
1337
|
+
const captureEngine = new CaptureEngine(pool, baseUrl);
|
|
1338
|
+
let passed = 0;
|
|
1339
|
+
let failed = 0;
|
|
1340
|
+
let missing = 0;
|
|
1341
|
+
const captureOptions = {
|
|
1342
|
+
theme,
|
|
1343
|
+
viewport,
|
|
1344
|
+
delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
|
|
1345
|
+
};
|
|
1346
|
+
try {
|
|
1347
|
+
await pool.warmup();
|
|
1348
|
+
for (const { component, variant } of variantsToDiff) {
|
|
1349
|
+
const baseline = await storage.loadBaseline(component, variant, theme);
|
|
1350
|
+
if (!baseline) {
|
|
1351
|
+
console.log(
|
|
1352
|
+
` ${pc2.yellow("?")} ${component}/${variant} ${pc2.dim("(no baseline)")}`
|
|
1353
|
+
);
|
|
1354
|
+
missing++;
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
const current = await captureEngine.captureVariant(
|
|
1359
|
+
component,
|
|
1360
|
+
variant,
|
|
1361
|
+
captureOptions
|
|
1362
|
+
);
|
|
1363
|
+
if (diffEngine.areIdentical(current, baseline)) {
|
|
1364
|
+
console.log(` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim("0.0%")}`);
|
|
1365
|
+
results.push({
|
|
1366
|
+
component,
|
|
1367
|
+
variant,
|
|
1368
|
+
theme,
|
|
1369
|
+
result: {
|
|
1370
|
+
matches: true,
|
|
1371
|
+
diffPercentage: 0,
|
|
1372
|
+
diffPixelCount: 0,
|
|
1373
|
+
totalPixels: current.viewport.width * current.viewport.height,
|
|
1374
|
+
changedRegions: [],
|
|
1375
|
+
diffTimeMs: 0
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
passed++;
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const diffResult = diffEngine.compare(current, baseline, { threshold });
|
|
1382
|
+
if (diffResult.matches) {
|
|
1383
|
+
console.log(
|
|
1384
|
+
` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim(`${diffResult.diffPercentage}%`)}`
|
|
1385
|
+
);
|
|
1386
|
+
passed++;
|
|
1387
|
+
} else {
|
|
1388
|
+
let diffImagePath;
|
|
1389
|
+
if (diffResult.diffImage) {
|
|
1390
|
+
diffImagePath = await storage.saveDiff(
|
|
1391
|
+
component,
|
|
1392
|
+
variant,
|
|
1393
|
+
theme,
|
|
1394
|
+
diffResult.diffImage
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
console.log(
|
|
1398
|
+
` ${pc2.red("\u2717")} ${component}/${variant} ${pc2.yellow(`${diffResult.diffPercentage}%`)}` + (diffImagePath ? pc2.dim(` \u2192 ${diffImagePath}`) : "")
|
|
1399
|
+
);
|
|
1400
|
+
failed++;
|
|
1401
|
+
results.push({
|
|
1402
|
+
component,
|
|
1403
|
+
variant,
|
|
1404
|
+
theme,
|
|
1405
|
+
result: diffResult,
|
|
1406
|
+
diffImagePath
|
|
1407
|
+
});
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
results.push({
|
|
1411
|
+
component,
|
|
1412
|
+
variant,
|
|
1413
|
+
theme,
|
|
1414
|
+
result: diffResult
|
|
1415
|
+
});
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1418
|
+
console.log(` ${pc2.red("!")} ${component}/${variant} ${pc2.dim(errorMsg)}`);
|
|
1419
|
+
failed++;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
} finally {
|
|
1423
|
+
await pool.shutdown();
|
|
1424
|
+
}
|
|
1425
|
+
const totalTimeMs = Date.now() - startTime;
|
|
1426
|
+
const total = passed + failed + missing;
|
|
1427
|
+
console.log();
|
|
1428
|
+
if (failed === 0 && missing === 0) {
|
|
1429
|
+
console.log(pc2.green(`\u2713 All ${passed} variant(s) match baselines`));
|
|
1430
|
+
} else if (failed > 0) {
|
|
1431
|
+
console.log(pc2.red(`\u2717 ${failed} variant(s) differ from baselines`));
|
|
1432
|
+
}
|
|
1433
|
+
if (missing > 0) {
|
|
1434
|
+
console.log(pc2.yellow(` ${missing} variant(s) have no baseline (run \`${BRAND.cliCommand} screenshot\`)`));
|
|
1435
|
+
}
|
|
1436
|
+
console.log(pc2.dim(` Completed in ${formatMs(totalTimeMs)}
|
|
1437
|
+
`));
|
|
1438
|
+
const success = failed === 0;
|
|
1439
|
+
return {
|
|
1440
|
+
success,
|
|
1441
|
+
total,
|
|
1442
|
+
passed,
|
|
1443
|
+
failed,
|
|
1444
|
+
missing,
|
|
1445
|
+
results,
|
|
1446
|
+
totalTimeMs
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/analyze.ts
|
|
1451
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1452
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1453
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1454
|
+
import pc3 from "picocolors";
|
|
1455
|
+
async function runAnalyzeCommand(config, configDir, options = {}) {
|
|
1456
|
+
const format = options.format ?? "html";
|
|
1457
|
+
const minScore = options.minScore ?? 0;
|
|
1458
|
+
console.log(pc3.cyan(`
|
|
1459
|
+
${BRAND.name} Analyzer
|
|
1460
|
+
`));
|
|
1461
|
+
const segmentsPath = join4(configDir, config.outFile ?? "segments.json");
|
|
1462
|
+
if (!existsSync4(segmentsPath)) {
|
|
1463
|
+
console.log(pc3.red(`\u2717 No segments.json found. Run \`${BRAND.cliCommand} build\` first.
|
|
1464
|
+
`));
|
|
1465
|
+
return {
|
|
1466
|
+
success: false,
|
|
1467
|
+
analytics: createEmptyAnalytics()
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
console.log(pc3.dim("Analyzing design system...\n"));
|
|
1471
|
+
const content = await readFile2(segmentsPath, "utf-8");
|
|
1472
|
+
const data = JSON.parse(content);
|
|
1473
|
+
const analytics = analyzeDesignSystem(data);
|
|
1474
|
+
printConsoleSummary(analytics);
|
|
1475
|
+
let outputPath;
|
|
1476
|
+
if (format === "html" || format === "json") {
|
|
1477
|
+
outputPath = options.output ?? getDefaultOutputPath(format, configDir);
|
|
1478
|
+
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
1479
|
+
if (format === "html") {
|
|
1480
|
+
const html = generateHtmlReport(analytics);
|
|
1481
|
+
await writeFile2(outputPath, html);
|
|
1482
|
+
console.log(pc3.green(`\u2713 Report generated: ${outputPath}
|
|
1483
|
+
`));
|
|
1484
|
+
} else {
|
|
1485
|
+
await writeFile2(outputPath, JSON.stringify(analytics, null, 2));
|
|
1486
|
+
console.log(pc3.green(`\u2713 JSON report generated: ${outputPath}
|
|
1487
|
+
`));
|
|
1488
|
+
}
|
|
1489
|
+
if (options.open && format === "html") {
|
|
1490
|
+
await openInBrowser(outputPath);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const passedCi = analytics.summary.overallScore >= minScore;
|
|
1494
|
+
if (options.ci) {
|
|
1495
|
+
if (passedCi) {
|
|
1496
|
+
console.log(
|
|
1497
|
+
pc3.green(`\u2713 Score ${analytics.summary.overallScore} meets minimum threshold ${minScore}
|
|
1498
|
+
`)
|
|
1499
|
+
);
|
|
1500
|
+
} else {
|
|
1501
|
+
console.log(
|
|
1502
|
+
pc3.red(
|
|
1503
|
+
`\u2717 Score ${analytics.summary.overallScore} below minimum threshold ${minScore}
|
|
1504
|
+
`
|
|
1505
|
+
)
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
success: !options.ci || passedCi,
|
|
1511
|
+
analytics,
|
|
1512
|
+
outputPath
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function printConsoleSummary(analytics) {
|
|
1516
|
+
const { summary, coverage, recommendations } = analytics;
|
|
1517
|
+
const grade = getGrade(summary.overallScore);
|
|
1518
|
+
console.log(
|
|
1519
|
+
pc3.bold(
|
|
1520
|
+
`Overall Score: ${colorizeScore(summary.overallScore)} (${grade})
|
|
1521
|
+
`
|
|
1522
|
+
)
|
|
1523
|
+
);
|
|
1524
|
+
console.log(pc3.dim("Summary"));
|
|
1525
|
+
console.log(` Components: ${pc3.white(summary.totalComponents.toString())}`);
|
|
1526
|
+
console.log(` Variants: ${pc3.white(summary.totalVariants.toString())}`);
|
|
1527
|
+
console.log(` Props: ${pc3.white(summary.totalProps.toString())}`);
|
|
1528
|
+
console.log(` Categories: ${pc3.white(summary.categories.join(", "))}`);
|
|
1529
|
+
console.log();
|
|
1530
|
+
console.log(pc3.dim("Coverage"));
|
|
1531
|
+
console.log(` Description: ${formatCoverage(coverage.fields.description)}`);
|
|
1532
|
+
console.log(` Usage when: ${formatCoverage(coverage.fields.usageWhen)}`);
|
|
1533
|
+
console.log(` Usage whenNot:${formatCoverage(coverage.fields.usageWhenNot)}`);
|
|
1534
|
+
console.log(` Guidelines: ${formatCoverage(coverage.fields.guidelines)}`);
|
|
1535
|
+
console.log(` Relations: ${formatCoverage(coverage.fields.relations)}`);
|
|
1536
|
+
console.log();
|
|
1537
|
+
if (recommendations.length > 0) {
|
|
1538
|
+
console.log(pc3.dim("Top Recommendations"));
|
|
1539
|
+
for (const rec of recommendations.slice(0, 3)) {
|
|
1540
|
+
const priority = rec.priority === "high" ? pc3.red(`[${rec.priority}]`) : rec.priority === "medium" ? pc3.yellow(`[${rec.priority}]`) : pc3.dim(`[${rec.priority}]`);
|
|
1541
|
+
console.log(` ${priority} ${rec.title}`);
|
|
1542
|
+
}
|
|
1543
|
+
console.log();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function formatCoverage(field) {
|
|
1547
|
+
const pct = colorizeScore(field.percentage);
|
|
1548
|
+
return `${pct} (${field.covered}/${field.total})`;
|
|
1549
|
+
}
|
|
1550
|
+
function colorizeScore(score) {
|
|
1551
|
+
if (score >= 80) return pc3.green(`${score}%`);
|
|
1552
|
+
if (score >= 60) return pc3.yellow(`${score}%`);
|
|
1553
|
+
return pc3.red(`${score}%`);
|
|
1554
|
+
}
|
|
1555
|
+
function getDefaultOutputPath(format, configDir) {
|
|
1556
|
+
const filename = format === "html" ? "segments-report.html" : "segments-report.json";
|
|
1557
|
+
return join4(configDir, filename);
|
|
1558
|
+
}
|
|
1559
|
+
async function openInBrowser(path) {
|
|
1560
|
+
const { platform } = await import("os");
|
|
1561
|
+
const { exec } = await import("child_process");
|
|
1562
|
+
const os = platform();
|
|
1563
|
+
const cmd = os === "darwin" ? `open "${path}"` : os === "win32" ? `start "" "${path}"` : `xdg-open "${path}"`;
|
|
1564
|
+
exec(cmd);
|
|
1565
|
+
}
|
|
1566
|
+
function createEmptyAnalytics() {
|
|
1567
|
+
return {
|
|
1568
|
+
analyzedAt: /* @__PURE__ */ new Date(),
|
|
1569
|
+
summary: {
|
|
1570
|
+
totalComponents: 0,
|
|
1571
|
+
totalVariants: 0,
|
|
1572
|
+
totalProps: 0,
|
|
1573
|
+
categories: [],
|
|
1574
|
+
overallScore: 0
|
|
1575
|
+
},
|
|
1576
|
+
inventory: {
|
|
1577
|
+
byCategory: {},
|
|
1578
|
+
byStatus: {},
|
|
1579
|
+
byVariantCount: [],
|
|
1580
|
+
byPropCount: []
|
|
1581
|
+
},
|
|
1582
|
+
coverage: {
|
|
1583
|
+
overall: 0,
|
|
1584
|
+
fields: {
|
|
1585
|
+
description: { covered: 0, total: 0, percentage: 0 },
|
|
1586
|
+
usageWhen: { covered: 0, total: 0, percentage: 0 },
|
|
1587
|
+
usageWhenNot: { covered: 0, total: 0, percentage: 0 },
|
|
1588
|
+
guidelines: { covered: 0, total: 0, percentage: 0 },
|
|
1589
|
+
accessibility: { covered: 0, total: 0, percentage: 0 },
|
|
1590
|
+
relations: { covered: 0, total: 0, percentage: 0 },
|
|
1591
|
+
propDescriptions: { covered: 0, total: 0, percentage: 0 },
|
|
1592
|
+
propConstraints: { covered: 0, total: 0, percentage: 0 }
|
|
1593
|
+
},
|
|
1594
|
+
incomplete: []
|
|
1595
|
+
},
|
|
1596
|
+
quality: {
|
|
1597
|
+
missingWhenNot: [],
|
|
1598
|
+
isolated: [],
|
|
1599
|
+
deprecated: [],
|
|
1600
|
+
fewVariants: [],
|
|
1601
|
+
undocumentedProps: [],
|
|
1602
|
+
unconstrainedProps: []
|
|
1603
|
+
},
|
|
1604
|
+
distribution: {
|
|
1605
|
+
variantsPerComponent: [],
|
|
1606
|
+
propsPerComponent: [],
|
|
1607
|
+
componentsPerCategory: [],
|
|
1608
|
+
statusDistribution: [],
|
|
1609
|
+
tagFrequency: []
|
|
1610
|
+
},
|
|
1611
|
+
recommendations: []
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
export {
|
|
1616
|
+
validateSchema,
|
|
1617
|
+
validateCoverage,
|
|
1618
|
+
validateAll,
|
|
1619
|
+
buildSegments,
|
|
1620
|
+
buildFragmentsDir,
|
|
1621
|
+
runScreenshotCommand,
|
|
1622
|
+
runDiffCommand,
|
|
1623
|
+
runAnalyzeCommand
|
|
1624
|
+
};
|
|
1625
|
+
//# sourceMappingURL=chunk-7OPWMLOE.js.map
|