@fragments-sdk/cli 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +463 -71
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
- package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
- package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
- package/dist/core/index.js +0 -1
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
- package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +2 -22
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
- package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
- package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
- package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +1 -2
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
- package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
- package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
- package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
- package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
- package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
- package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
- package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
- package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +5 -3
- package/src/bin.ts +90 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +1 -1
- package/src/commands/__tests__/scan-generate.test.ts +1 -1
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/govern-scan.ts +187 -8
- package/src/commands/govern.ts +65 -2
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +1 -1
- package/src/commands/scan.ts +13 -0
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-push.ts +199 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +1 -1
- package/src/core/drift-verifier.ts +1 -1
- package/src/core/extractor-adapter.ts +1 -1
- package/src/index.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +2 -2
- package/src/service/index.ts +8 -0
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/dist/chunk-65WSVDV5.js.map +0 -1
- package/dist/chunk-7WHVW72L.js +0 -2664
- package/dist/chunk-7WHVW72L.js.map +0 -1
- package/dist/chunk-CZD3AD4Q.js.map +0 -1
- package/dist/chunk-MN3TJ3D5.js +0 -695
- package/dist/chunk-MN3TJ3D5.js.map +0 -1
- package/dist/chunk-XJQ5BIWI.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/govern-scan-UCBZR6D6.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-generate-TWRHNU5M.js.map +0 -1
- package/src/build.ts +0 -736
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1121
- package/src/core/token-resolver.ts +0 -155
- package/src/viewer/preview-adapter.ts +0 -116
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
- /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
- /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
package/dist/chunk-7WHVW72L.js
DELETED
|
@@ -1,2664 +0,0 @@
|
|
|
1
|
-
import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
|
|
2
|
-
import {
|
|
3
|
-
createComponentExtractor
|
|
4
|
-
} from "./chunk-MN3TJ3D5.js";
|
|
5
|
-
import {
|
|
6
|
-
discoverBlockFiles,
|
|
7
|
-
discoverComponentFiles,
|
|
8
|
-
discoverFragmentFiles,
|
|
9
|
-
discoverTokenFiles,
|
|
10
|
-
extractComponentName,
|
|
11
|
-
generateContextMd,
|
|
12
|
-
generateRegistry,
|
|
13
|
-
loadFragmentFile,
|
|
14
|
-
parseFragmentFile
|
|
15
|
-
} from "./chunk-65WSVDV5.js";
|
|
16
|
-
import {
|
|
17
|
-
BrowserPool,
|
|
18
|
-
CaptureEngine,
|
|
19
|
-
DiffEngine,
|
|
20
|
-
StorageManager,
|
|
21
|
-
analyzeDesignSystem,
|
|
22
|
-
formatMs,
|
|
23
|
-
generateHtmlReport,
|
|
24
|
-
getGrade
|
|
25
|
-
} from "./chunk-XJQ5BIWI.js";
|
|
26
|
-
import {
|
|
27
|
-
BRAND,
|
|
28
|
-
DEFAULTS,
|
|
29
|
-
classifyComplexity,
|
|
30
|
-
compileBlock,
|
|
31
|
-
fragmentDefinitionSchema,
|
|
32
|
-
isContractFile,
|
|
33
|
-
isDTCGFile,
|
|
34
|
-
parseComponentContract,
|
|
35
|
-
parseDTCGFile,
|
|
36
|
-
parseTokenFile,
|
|
37
|
-
resolvePerformanceConfig
|
|
38
|
-
} from "./chunk-32LIWN2P.js";
|
|
39
|
-
|
|
40
|
-
// src/service/snippet-validation.ts
|
|
41
|
-
import ts from "typescript";
|
|
42
|
-
import { readFile } from "fs/promises";
|
|
43
|
-
import { existsSync } from "fs";
|
|
44
|
-
import { join } from "path";
|
|
45
|
-
var INTRINSIC_TAGS = /* @__PURE__ */ new Set([
|
|
46
|
-
"div",
|
|
47
|
-
"span",
|
|
48
|
-
"p",
|
|
49
|
-
"h1",
|
|
50
|
-
"h2",
|
|
51
|
-
"h3",
|
|
52
|
-
"h4",
|
|
53
|
-
"h5",
|
|
54
|
-
"h6",
|
|
55
|
-
"main",
|
|
56
|
-
"section",
|
|
57
|
-
"article",
|
|
58
|
-
"aside",
|
|
59
|
-
"nav",
|
|
60
|
-
"header",
|
|
61
|
-
"footer",
|
|
62
|
-
"ul",
|
|
63
|
-
"ol",
|
|
64
|
-
"li",
|
|
65
|
-
"button",
|
|
66
|
-
"input",
|
|
67
|
-
"textarea",
|
|
68
|
-
"label",
|
|
69
|
-
"svg",
|
|
70
|
-
"path"
|
|
71
|
-
]);
|
|
72
|
-
var JSX_TAG_PATTERN = /<\s*([A-Za-z][A-Za-z0-9.]*)\b/g;
|
|
73
|
-
var STYLE_PATTERN = /\bstyle\s*=\s*\{/;
|
|
74
|
-
var TRANSPILED_PATTERN = /jsxDEV|_jsx|@__PURE__|\bfileName\s*:|\blineNumber\s*:|\bcolumnNumber\s*:/;
|
|
75
|
-
var ALIAS_DRIFT_PATTERN = /<\s*[A-Z][A-Za-z0-9]*(?:Root|2)\b/;
|
|
76
|
-
var HAS_IMPORT_PATTERN = /\bimport\s+[^;]+\s+from\s+['"][^'"]+['"]/;
|
|
77
|
-
var HAS_JSX_PATTERN = /<\s*[A-Za-z][A-Za-z0-9.]*\b/;
|
|
78
|
-
var DEFAULT_POLICY = {
|
|
79
|
-
mode: "warn",
|
|
80
|
-
scope: "snippet+render",
|
|
81
|
-
requireFullSnippet: true,
|
|
82
|
-
allowedExternalModules: /* @__PURE__ */ new Set([
|
|
83
|
-
"@phosphor-icons/react",
|
|
84
|
-
"recharts",
|
|
85
|
-
"react-day-picker"
|
|
86
|
-
])
|
|
87
|
-
};
|
|
88
|
-
function normalizePolicy(configured, overrides) {
|
|
89
|
-
const fromConfig = {
|
|
90
|
-
mode: configured?.mode ?? DEFAULT_POLICY.mode,
|
|
91
|
-
scope: configured?.scope ?? DEFAULT_POLICY.scope,
|
|
92
|
-
requireFullSnippet: configured?.requireFullSnippet ?? DEFAULT_POLICY.requireFullSnippet,
|
|
93
|
-
allowedExternalModules: new Set(configured?.allowedExternalModules ?? [...DEFAULT_POLICY.allowedExternalModules]),
|
|
94
|
-
componentStart: overrides.componentStart,
|
|
95
|
-
componentLimit: overrides.componentLimit
|
|
96
|
-
};
|
|
97
|
-
if (overrides.mode) fromConfig.mode = overrides.mode;
|
|
98
|
-
if (overrides.scope) fromConfig.scope = overrides.scope;
|
|
99
|
-
if (typeof overrides.requireFullSnippet === "boolean") {
|
|
100
|
-
fromConfig.requireFullSnippet = overrides.requireFullSnippet;
|
|
101
|
-
}
|
|
102
|
-
if (overrides.allowedExternalModules && overrides.allowedExternalModules.length > 0) {
|
|
103
|
-
fromConfig.allowedExternalModules = new Set(overrides.allowedExternalModules);
|
|
104
|
-
}
|
|
105
|
-
return fromConfig;
|
|
106
|
-
}
|
|
107
|
-
function isFragmentsModule(modulePath) {
|
|
108
|
-
return modulePath === "@fragments-sdk/ui" || modulePath === "." || modulePath === ".." || modulePath.startsWith("@/components/") || modulePath.startsWith("@components/") || modulePath.startsWith("./") || modulePath.startsWith("../");
|
|
109
|
-
}
|
|
110
|
-
function collectSourceContext(sourceFile) {
|
|
111
|
-
const imports = /* @__PURE__ */ new Map();
|
|
112
|
-
const localComponents = /* @__PURE__ */ new Set();
|
|
113
|
-
function markLocal(name) {
|
|
114
|
-
if (!name) return;
|
|
115
|
-
if (/^[A-Z]/.test(name)) {
|
|
116
|
-
localComponents.add(name);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function visit(node) {
|
|
120
|
-
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
121
|
-
const modulePath = node.moduleSpecifier.text;
|
|
122
|
-
const clause = node.importClause;
|
|
123
|
-
if (clause?.name) {
|
|
124
|
-
imports.set(clause.name.text, modulePath);
|
|
125
|
-
}
|
|
126
|
-
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
127
|
-
for (const item of clause.namedBindings.elements) {
|
|
128
|
-
imports.set(item.name.text, modulePath);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (ts.isFunctionDeclaration(node)) {
|
|
133
|
-
markLocal(node.name?.text);
|
|
134
|
-
}
|
|
135
|
-
if (ts.isClassDeclaration(node)) {
|
|
136
|
-
markLocal(node.name?.text);
|
|
137
|
-
}
|
|
138
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
139
|
-
markLocal(node.name.text);
|
|
140
|
-
}
|
|
141
|
-
ts.forEachChild(node, visit);
|
|
142
|
-
}
|
|
143
|
-
visit(sourceFile);
|
|
144
|
-
return { imports, localComponents };
|
|
145
|
-
}
|
|
146
|
-
function getJsxTags(code) {
|
|
147
|
-
const tags = [];
|
|
148
|
-
JSX_TAG_PATTERN.lastIndex = 0;
|
|
149
|
-
let match;
|
|
150
|
-
while ((match = JSX_TAG_PATTERN.exec(code)) !== null) {
|
|
151
|
-
tags.push(match[1]);
|
|
152
|
-
}
|
|
153
|
-
return tags;
|
|
154
|
-
}
|
|
155
|
-
function rootTagName(tag) {
|
|
156
|
-
return tag.split(".")[0];
|
|
157
|
-
}
|
|
158
|
-
function parseSnippetImports(snippet) {
|
|
159
|
-
const sourceFile = ts.createSourceFile(
|
|
160
|
-
"snippet.tsx",
|
|
161
|
-
snippet,
|
|
162
|
-
ts.ScriptTarget.Latest,
|
|
163
|
-
true,
|
|
164
|
-
ts.ScriptKind.TSX
|
|
165
|
-
);
|
|
166
|
-
return collectSourceContext(sourceFile).imports;
|
|
167
|
-
}
|
|
168
|
-
function findDefineCall(sourceFile, name) {
|
|
169
|
-
let result = null;
|
|
170
|
-
function visit(node) {
|
|
171
|
-
if (result) return;
|
|
172
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name) {
|
|
173
|
-
result = node;
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
ts.forEachChild(node, visit);
|
|
177
|
-
}
|
|
178
|
-
visit(sourceFile);
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
function findProperty(obj, propertyName) {
|
|
182
|
-
for (const prop of obj.properties) {
|
|
183
|
-
if (!ts.isPropertyAssignment(prop)) continue;
|
|
184
|
-
if (!ts.isIdentifier(prop.name)) continue;
|
|
185
|
-
if (prop.name.text === propertyName) {
|
|
186
|
-
return prop.initializer;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
function readStaticString(expr) {
|
|
192
|
-
if (!expr) return null;
|
|
193
|
-
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
194
|
-
return expr.text;
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
function readRenderBody(renderExpr, sourceFile) {
|
|
199
|
-
if (!ts.isArrowFunction(renderExpr) && !ts.isFunctionExpression(renderExpr)) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
const body = renderExpr.body;
|
|
203
|
-
const start = body.getStart(sourceFile);
|
|
204
|
-
const end = body.getEnd();
|
|
205
|
-
return sourceFile.text.slice(start, end).trim();
|
|
206
|
-
}
|
|
207
|
-
function report(issues, file, message) {
|
|
208
|
-
issues.push({ file, message });
|
|
209
|
-
}
|
|
210
|
-
function validateRawRules(issues, file, label, code) {
|
|
211
|
-
if (STYLE_PATTERN.test(code)) {
|
|
212
|
-
report(issues, file, `${label}: inline style usage is not allowed; use Box/Stack/Text props.`);
|
|
213
|
-
}
|
|
214
|
-
if (TRANSPILED_PATTERN.test(code)) {
|
|
215
|
-
report(issues, file, `${label}: transpiler output detected (jsxDEV/_jsx/@__PURE__). Use authored snippet source.`);
|
|
216
|
-
}
|
|
217
|
-
if (ALIAS_DRIFT_PATTERN.test(code)) {
|
|
218
|
-
report(issues, file, `${label}: alias drift tag detected (*Root/*2). Use canonical component names.`);
|
|
219
|
-
}
|
|
220
|
-
const tags = getJsxTags(code);
|
|
221
|
-
const intrinsic = tags.map((tag) => rootTagName(tag)).filter((tag) => /^[a-z]/.test(tag)).map((tag) => tag.toLowerCase()).filter((tag) => INTRINSIC_TAGS.has(tag));
|
|
222
|
-
if (intrinsic.length > 0) {
|
|
223
|
-
const names = [...new Set(intrinsic)].sort().join(", ");
|
|
224
|
-
report(issues, file, `${label}: raw HTML tags are not allowed (${names}). Use Fragments primitives.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function validateComponentAllowlist(issues, file, label, code, imports, localComponents, policy) {
|
|
228
|
-
const tags = getJsxTags(code);
|
|
229
|
-
const seen = /* @__PURE__ */ new Set();
|
|
230
|
-
for (const tag of tags) {
|
|
231
|
-
const root = rootTagName(tag);
|
|
232
|
-
if (seen.has(root)) continue;
|
|
233
|
-
seen.add(root);
|
|
234
|
-
if (!/^[A-Z]/.test(root)) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const modulePath = imports.get(root);
|
|
238
|
-
if (modulePath) {
|
|
239
|
-
if (isFragmentsModule(modulePath)) {
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (policy.allowedExternalModules.has(modulePath)) {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
report(
|
|
246
|
-
issues,
|
|
247
|
-
file,
|
|
248
|
-
`${label}: component "${root}" comes from "${modulePath}" and is not in snippets.allowedExternalModules.`
|
|
249
|
-
);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (localComponents.has(root)) {
|
|
253
|
-
report(
|
|
254
|
-
issues,
|
|
255
|
-
file,
|
|
256
|
-
`${label}: locally defined JSX component "${root}" is not allowed in snippets/renders. Import approved components instead.`
|
|
257
|
-
);
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
report(
|
|
261
|
-
issues,
|
|
262
|
-
file,
|
|
263
|
-
`${label}: component "${root}" is used without an import and is not allowed.`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function validateSnippetString(issues, file, label, snippet, policy) {
|
|
268
|
-
validateRawRules(issues, file, label, snippet);
|
|
269
|
-
if (policy.requireFullSnippet) {
|
|
270
|
-
if (!HAS_IMPORT_PATTERN.test(snippet)) {
|
|
271
|
-
report(issues, file, `${label}: full snippet required (missing import statement).`);
|
|
272
|
-
}
|
|
273
|
-
if (!HAS_JSX_PATTERN.test(snippet)) {
|
|
274
|
-
report(issues, file, `${label}: full snippet required (missing JSX usage).`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const imports = parseSnippetImports(snippet);
|
|
278
|
-
validateComponentAllowlist(issues, file, label, snippet, imports, /* @__PURE__ */ new Set(), policy);
|
|
279
|
-
}
|
|
280
|
-
function validateFragmentSource(sourceFile, file, policy, issues) {
|
|
281
|
-
const context = collectSourceContext(sourceFile);
|
|
282
|
-
const defineCall = findDefineCall(sourceFile, "defineFragment");
|
|
283
|
-
if (!defineCall) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const arg = defineCall.arguments[0];
|
|
287
|
-
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const variantsExpr = findProperty(arg, "variants");
|
|
291
|
-
if (!variantsExpr || !ts.isArrayLiteralExpression(variantsExpr)) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
for (const variantExpr of variantsExpr.elements) {
|
|
295
|
-
if (!ts.isObjectLiteralExpression(variantExpr)) continue;
|
|
296
|
-
const name = readStaticString(findProperty(variantExpr, "name")) ?? "Unknown";
|
|
297
|
-
const labelPrefix = `variant "${name}"`;
|
|
298
|
-
const codeExpr = findProperty(variantExpr, "code");
|
|
299
|
-
const snippet = readStaticString(codeExpr);
|
|
300
|
-
if (snippet) {
|
|
301
|
-
validateSnippetString(issues, file, `${labelPrefix} snippet`, snippet, policy);
|
|
302
|
-
} else {
|
|
303
|
-
report(issues, file, `${labelPrefix}: missing explicit code snippet (variant.code).`);
|
|
304
|
-
}
|
|
305
|
-
if (policy.scope === "snippet+render") {
|
|
306
|
-
const renderExpr = findProperty(variantExpr, "render");
|
|
307
|
-
if (renderExpr) {
|
|
308
|
-
const renderBody = readRenderBody(renderExpr, sourceFile);
|
|
309
|
-
if (!renderBody) {
|
|
310
|
-
report(issues, file, `${labelPrefix} render: expected a static render function.`);
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
validateRawRules(issues, file, `${labelPrefix} render`, renderBody);
|
|
314
|
-
validateComponentAllowlist(
|
|
315
|
-
issues,
|
|
316
|
-
file,
|
|
317
|
-
`${labelPrefix} render`,
|
|
318
|
-
renderBody,
|
|
319
|
-
context.imports,
|
|
320
|
-
context.localComponents,
|
|
321
|
-
policy
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function validateBlockSource(sourceFile, file, policy, issues) {
|
|
328
|
-
const defineCall = findDefineCall(sourceFile, "defineBlock");
|
|
329
|
-
if (!defineCall) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
const arg = defineCall.arguments[0];
|
|
333
|
-
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const codeExpr = findProperty(arg, "code");
|
|
337
|
-
const snippet = readStaticString(codeExpr);
|
|
338
|
-
if (!snippet) {
|
|
339
|
-
report(issues, file, "block snippet: missing static code string.");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
validateSnippetString(issues, file, "block snippet", snippet, policy);
|
|
343
|
-
}
|
|
344
|
-
function validateBlockPreviewExamples(sourceFile, file, policy, issues) {
|
|
345
|
-
if (policy.scope !== "snippet+render") {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const code = sourceFile.text;
|
|
349
|
-
const context = collectSourceContext(sourceFile);
|
|
350
|
-
validateRawRules(issues, file, "block preview render", code);
|
|
351
|
-
validateComponentAllowlist(
|
|
352
|
-
issues,
|
|
353
|
-
file,
|
|
354
|
-
"block preview render",
|
|
355
|
-
code,
|
|
356
|
-
context.imports,
|
|
357
|
-
context.localComponents,
|
|
358
|
-
policy
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
function sourceFileFromText(filePath, content) {
|
|
362
|
-
return ts.createSourceFile(
|
|
363
|
-
filePath,
|
|
364
|
-
content,
|
|
365
|
-
ts.ScriptTarget.Latest,
|
|
366
|
-
true,
|
|
367
|
-
ts.ScriptKind.TSX
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
function sortAndFilterBatch(files, componentStart, componentLimit) {
|
|
371
|
-
const getComponentName = (relativePath) => {
|
|
372
|
-
const normalized = relativePath.replace(/\\/g, "/");
|
|
373
|
-
const fileName = normalized.split("/").pop() ?? normalized;
|
|
374
|
-
for (const ext of [BRAND.fileExtension, ".fragment.tsx", ".fragment.ts"]) {
|
|
375
|
-
if (fileName.endsWith(ext)) {
|
|
376
|
-
return fileName.slice(0, -ext.length);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return extractComponentName(relativePath);
|
|
380
|
-
};
|
|
381
|
-
const sorted = [...files].sort((a, b) => {
|
|
382
|
-
const nameA = getComponentName(a.relativePath).toLowerCase();
|
|
383
|
-
const nameB = getComponentName(b.relativePath).toLowerCase();
|
|
384
|
-
return nameA.localeCompare(nameB);
|
|
385
|
-
});
|
|
386
|
-
if (!componentStart && !componentLimit) {
|
|
387
|
-
return { selected: sorted };
|
|
388
|
-
}
|
|
389
|
-
const startName = componentStart?.toLowerCase();
|
|
390
|
-
let startIndex = 0;
|
|
391
|
-
if (startName) {
|
|
392
|
-
const foundIndex = sorted.findIndex((file) => getComponentName(file.relativePath).toLowerCase() === startName);
|
|
393
|
-
if (foundIndex === -1) {
|
|
394
|
-
return {
|
|
395
|
-
selected: [],
|
|
396
|
-
warning: `Component start "${componentStart}" not found for snippet validation batch.`
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
startIndex = foundIndex;
|
|
400
|
-
}
|
|
401
|
-
const limit = componentLimit && componentLimit > 0 ? componentLimit : sorted.length;
|
|
402
|
-
return {
|
|
403
|
-
selected: sorted.slice(startIndex, startIndex + limit)
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
async function findBlockPreviewExamplesFile(configDir) {
|
|
407
|
-
const candidates = [
|
|
408
|
-
join(configDir, "apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
|
|
409
|
-
join(configDir, "../apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
|
|
410
|
-
join(configDir, "../../apps/docs/src/app/(docs)/blocks/examples/index.tsx")
|
|
411
|
-
];
|
|
412
|
-
for (const candidate of candidates) {
|
|
413
|
-
if (existsSync(candidate)) {
|
|
414
|
-
return candidate;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
function toValidationResult(policy, issues) {
|
|
420
|
-
if (policy.mode === "error") {
|
|
421
|
-
return {
|
|
422
|
-
errors: issues,
|
|
423
|
-
warnings: []
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
errors: [],
|
|
428
|
-
warnings: issues
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
async function validateSnippetPolicy(config, configDir, options = {}) {
|
|
432
|
-
const policy = normalizePolicy(config.snippets, options);
|
|
433
|
-
const issues = [];
|
|
434
|
-
const discovered = await discoverFragmentFiles(config, configDir);
|
|
435
|
-
const fragmentFiles = discovered.filter(
|
|
436
|
-
(file) => file.relativePath.endsWith(".fragment.tsx") || file.relativePath.endsWith(".fragment.ts")
|
|
437
|
-
);
|
|
438
|
-
const batchResult = sortAndFilterBatch(fragmentFiles, policy.componentStart, policy.componentLimit);
|
|
439
|
-
if (batchResult.warning) {
|
|
440
|
-
issues.push({ file: "snippets", message: batchResult.warning });
|
|
441
|
-
}
|
|
442
|
-
for (const file of batchResult.selected) {
|
|
443
|
-
try {
|
|
444
|
-
const content = await readFile(file.absolutePath, "utf-8");
|
|
445
|
-
const sourceFile = sourceFileFromText(file.relativePath, content);
|
|
446
|
-
validateFragmentSource(sourceFile, file.relativePath, policy, issues);
|
|
447
|
-
} catch (error) {
|
|
448
|
-
issues.push({
|
|
449
|
-
file: file.relativePath,
|
|
450
|
-
message: `Failed to validate fragment snippets: ${error instanceof Error ? error.message : String(error)}`
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const isBatchOnly = Boolean(policy.componentStart || policy.componentLimit);
|
|
455
|
-
if (!isBatchOnly) {
|
|
456
|
-
try {
|
|
457
|
-
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
458
|
-
for (const file of blockFiles) {
|
|
459
|
-
try {
|
|
460
|
-
const content = await readFile(file.absolutePath, "utf-8");
|
|
461
|
-
const sourceFile = sourceFileFromText(file.relativePath, content);
|
|
462
|
-
validateBlockSource(sourceFile, file.relativePath, policy, issues);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
issues.push({
|
|
465
|
-
file: file.relativePath,
|
|
466
|
-
message: `Failed to validate block snippets: ${error instanceof Error ? error.message : String(error)}`
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
} catch (error) {
|
|
471
|
-
issues.push({
|
|
472
|
-
file: "blocks",
|
|
473
|
-
message: `Failed to discover block files: ${error instanceof Error ? error.message : String(error)}`
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
const blockPreviewFile = await findBlockPreviewExamplesFile(configDir);
|
|
477
|
-
if (blockPreviewFile) {
|
|
478
|
-
try {
|
|
479
|
-
const content = await readFile(blockPreviewFile, "utf-8");
|
|
480
|
-
const sourceFile = sourceFileFromText(blockPreviewFile, content);
|
|
481
|
-
validateBlockPreviewExamples(sourceFile, blockPreviewFile, policy, issues);
|
|
482
|
-
} catch (error) {
|
|
483
|
-
issues.push({
|
|
484
|
-
file: blockPreviewFile,
|
|
485
|
-
message: `Failed to validate block preview examples: ${error instanceof Error ? error.message : String(error)}`
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return toValidationResult(policy, issues);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// src/core/auto-props.ts
|
|
494
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
495
|
-
import { dirname, extname, join as join2, resolve } from "path";
|
|
496
|
-
import ts2 from "typescript";
|
|
497
|
-
function isFile(filePath) {
|
|
498
|
-
if (!existsSync2(filePath)) return false;
|
|
499
|
-
try {
|
|
500
|
-
return statSync(filePath).isFile();
|
|
501
|
-
} catch {
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
function resolveModulePath(basePath) {
|
|
506
|
-
const candidates = [];
|
|
507
|
-
const extension = extname(basePath);
|
|
508
|
-
if (extension) {
|
|
509
|
-
candidates.push(basePath);
|
|
510
|
-
} else {
|
|
511
|
-
candidates.push(
|
|
512
|
-
`${basePath}.tsx`,
|
|
513
|
-
`${basePath}.ts`,
|
|
514
|
-
`${basePath}.jsx`,
|
|
515
|
-
`${basePath}.js`,
|
|
516
|
-
join2(basePath, "index.tsx"),
|
|
517
|
-
join2(basePath, "index.ts"),
|
|
518
|
-
join2(basePath, "index.jsx"),
|
|
519
|
-
join2(basePath, "index.js")
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
for (const candidate of candidates) {
|
|
523
|
-
if (isFile(candidate)) {
|
|
524
|
-
return resolve(candidate);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
function resolveComponentSourcePath(fragmentFileAbsolutePath, componentImportPath) {
|
|
530
|
-
if (!componentImportPath) return null;
|
|
531
|
-
if (!componentImportPath.startsWith(".")) return null;
|
|
532
|
-
const fragmentDir = dirname(fragmentFileAbsolutePath);
|
|
533
|
-
const basePath = resolve(fragmentDir, componentImportPath);
|
|
534
|
-
return resolveModulePath(basePath);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// src/validators.ts
|
|
538
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
539
|
-
async function validateSchema(config, configDir) {
|
|
540
|
-
const files = await discoverFragmentFiles(config, configDir);
|
|
541
|
-
const errors = [];
|
|
542
|
-
const warnings = [];
|
|
543
|
-
for (const file of files) {
|
|
544
|
-
try {
|
|
545
|
-
const fragment = await loadFragmentFile(file.absolutePath);
|
|
546
|
-
if (!fragment) {
|
|
547
|
-
errors.push({
|
|
548
|
-
file: file.relativePath,
|
|
549
|
-
message: "No default export found",
|
|
550
|
-
details: `Fragment files must have a default export from defineFragment()`
|
|
551
|
-
});
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
const result = fragmentDefinitionSchema.safeParse(fragment);
|
|
555
|
-
if (!result.success) {
|
|
556
|
-
const details = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
557
|
-
errors.push({
|
|
558
|
-
file: file.relativePath,
|
|
559
|
-
message: "Invalid fragment schema",
|
|
560
|
-
details
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
} catch (error) {
|
|
564
|
-
errors.push({
|
|
565
|
-
file: file.relativePath,
|
|
566
|
-
message: "Failed to load fragment file",
|
|
567
|
-
details: error instanceof Error ? error.message : String(error)
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
valid: errors.length === 0,
|
|
573
|
-
errors,
|
|
574
|
-
warnings
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
async function validateCoverage(config, configDir) {
|
|
578
|
-
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
579
|
-
const componentFiles = await discoverComponentFiles(config, configDir);
|
|
580
|
-
const errors = [];
|
|
581
|
-
const warnings = [];
|
|
582
|
-
const documentedComponents = /* @__PURE__ */ new Set();
|
|
583
|
-
for (const file of fragmentFiles) {
|
|
584
|
-
try {
|
|
585
|
-
const fragment = await loadFragmentFile(file.absolutePath);
|
|
586
|
-
if (fragment?.meta?.name) {
|
|
587
|
-
documentedComponents.add(fragment.meta.name);
|
|
588
|
-
}
|
|
589
|
-
} catch {
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
for (const file of componentFiles) {
|
|
593
|
-
const componentName = extractComponentName(file.relativePath);
|
|
594
|
-
const fragmentPath = file.relativePath.replace(
|
|
595
|
-
/\.(tsx?|jsx?)$/,
|
|
596
|
-
BRAND.fileExtension
|
|
597
|
-
);
|
|
598
|
-
const hasFragmentFile = fragmentFiles.some(
|
|
599
|
-
(s) => s.relativePath === fragmentPath
|
|
600
|
-
);
|
|
601
|
-
if (!hasFragmentFile && !documentedComponents.has(componentName)) {
|
|
602
|
-
warnings.push({
|
|
603
|
-
file: file.relativePath,
|
|
604
|
-
message: `Component "${componentName}" has no fragment documentation`
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return {
|
|
609
|
-
valid: errors.length === 0,
|
|
610
|
-
errors,
|
|
611
|
-
warnings
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
async function validateAll(config, configDir, options = {}) {
|
|
615
|
-
const [schemaResult, coverageResult] = await Promise.all([
|
|
616
|
-
validateSchema(config, configDir),
|
|
617
|
-
validateCoverage(config, configDir)
|
|
618
|
-
]);
|
|
619
|
-
if (options.snippets === false) {
|
|
620
|
-
return {
|
|
621
|
-
valid: schemaResult.valid && coverageResult.valid,
|
|
622
|
-
errors: [...schemaResult.errors, ...coverageResult.errors],
|
|
623
|
-
warnings: [...schemaResult.warnings, ...coverageResult.warnings]
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
const snippetOptions = {
|
|
627
|
-
...options.snippetMode && { mode: options.snippetMode },
|
|
628
|
-
...options.componentStart && { componentStart: options.componentStart },
|
|
629
|
-
...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
|
|
630
|
-
};
|
|
631
|
-
const snippetResult = await validateSnippetPolicy(config, configDir, snippetOptions);
|
|
632
|
-
return {
|
|
633
|
-
valid: schemaResult.valid && coverageResult.valid && snippetResult.errors.length === 0,
|
|
634
|
-
errors: [...schemaResult.errors, ...coverageResult.errors, ...snippetResult.errors],
|
|
635
|
-
warnings: [...schemaResult.warnings, ...coverageResult.warnings, ...snippetResult.warnings]
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
async function validateSnippets(config, configDir, options = {}) {
|
|
639
|
-
const snippetResult = await validateSnippetPolicy(config, configDir, {
|
|
640
|
-
...options.snippetMode && { mode: options.snippetMode },
|
|
641
|
-
...options.componentStart && { componentStart: options.componentStart },
|
|
642
|
-
...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
|
|
643
|
-
});
|
|
644
|
-
return {
|
|
645
|
-
valid: snippetResult.errors.length === 0,
|
|
646
|
-
errors: snippetResult.errors,
|
|
647
|
-
warnings: snippetResult.warnings
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
async function validateDrift(config, configDir, options = {}) {
|
|
651
|
-
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
652
|
-
const errors = [];
|
|
653
|
-
const warnings = [];
|
|
654
|
-
const reports = [];
|
|
655
|
-
if (fragmentFiles.length === 0) {
|
|
656
|
-
return { valid: true, errors, warnings, reports };
|
|
657
|
-
}
|
|
658
|
-
const extractor = createComponentExtractor(options.tsconfig);
|
|
659
|
-
try {
|
|
660
|
-
for (const file of fragmentFiles) {
|
|
661
|
-
try {
|
|
662
|
-
const fragment = await loadFragmentFile(file.absolutePath);
|
|
663
|
-
if (!fragment?.meta?.name) continue;
|
|
664
|
-
const fileContent = await readFile2(file.absolutePath, "utf-8");
|
|
665
|
-
const parsed = parseFragmentFile(fileContent, file.absolutePath);
|
|
666
|
-
if (!parsed.componentImport) continue;
|
|
667
|
-
const sourcePath = resolveComponentSourcePath(file.absolutePath, parsed.componentImport);
|
|
668
|
-
if (!sourcePath) continue;
|
|
669
|
-
const meta = extractor.extract(sourcePath, fragment.meta.name);
|
|
670
|
-
if (!meta) continue;
|
|
671
|
-
const drifts = diffProps(fragment.props, meta.props);
|
|
672
|
-
let compositionDrift = null;
|
|
673
|
-
const fragmentAi = fragment.ai;
|
|
674
|
-
if (meta.composition && !fragmentAi?.compositionPattern) {
|
|
675
|
-
compositionDrift = `Source has "${meta.composition.pattern}" composition but fragment has no ai.compositionPattern`;
|
|
676
|
-
} else if (!meta.composition && fragmentAi?.compositionPattern) {
|
|
677
|
-
compositionDrift = `Fragment declares "${fragmentAi.compositionPattern}" but source has no compound pattern`;
|
|
678
|
-
} else if (meta.composition && fragmentAi?.compositionPattern && meta.composition.pattern !== fragmentAi.compositionPattern) {
|
|
679
|
-
compositionDrift = `Composition pattern changed: fragment="${fragmentAi.compositionPattern}" source="${meta.composition.pattern}"`;
|
|
680
|
-
}
|
|
681
|
-
if (drifts.length > 0 || compositionDrift) {
|
|
682
|
-
const report2 = {
|
|
683
|
-
component: fragment.meta.name,
|
|
684
|
-
file: file.relativePath,
|
|
685
|
-
drifts,
|
|
686
|
-
compositionDrift
|
|
687
|
-
};
|
|
688
|
-
reports.push(report2);
|
|
689
|
-
for (const drift of drifts) {
|
|
690
|
-
if (drift.kind === "removed") {
|
|
691
|
-
errors.push({
|
|
692
|
-
file: file.relativePath,
|
|
693
|
-
message: `Prop "${drift.prop}" documented in fragment but removed from source`,
|
|
694
|
-
details: `Fragment: ${drift.fragment} | Source: (not found)`
|
|
695
|
-
});
|
|
696
|
-
} else if (drift.kind === "added") {
|
|
697
|
-
warnings.push({
|
|
698
|
-
file: file.relativePath,
|
|
699
|
-
message: `Prop "${drift.prop}" exists in source but not documented in fragment`
|
|
700
|
-
});
|
|
701
|
-
} else {
|
|
702
|
-
warnings.push({
|
|
703
|
-
file: file.relativePath,
|
|
704
|
-
message: `Prop "${drift.prop}" ${drift.kind.replace("_", " ")}: fragment=${drift.fragment} source=${drift.source}`
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (compositionDrift) {
|
|
709
|
-
warnings.push({
|
|
710
|
-
file: file.relativePath,
|
|
711
|
-
message: compositionDrift
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
} catch {
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
} finally {
|
|
719
|
-
extractor.dispose();
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
valid: errors.length === 0,
|
|
723
|
-
errors,
|
|
724
|
-
warnings,
|
|
725
|
-
reports
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
function diffProps(fragmentProps, sourceProps) {
|
|
729
|
-
const drifts = [];
|
|
730
|
-
const localSourceProps = Object.fromEntries(
|
|
731
|
-
Object.entries(sourceProps).filter(([_, p]) => p.source === "local")
|
|
732
|
-
);
|
|
733
|
-
for (const [name, sourceProp] of Object.entries(localSourceProps)) {
|
|
734
|
-
if (!(name in fragmentProps)) {
|
|
735
|
-
drifts.push({
|
|
736
|
-
prop: name,
|
|
737
|
-
kind: "added",
|
|
738
|
-
source: sourceProp.type,
|
|
739
|
-
fragment: "(not documented)"
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
for (const [name, fragProp] of Object.entries(fragmentProps)) {
|
|
744
|
-
if (!(name in localSourceProps)) {
|
|
745
|
-
drifts.push({
|
|
746
|
-
prop: name,
|
|
747
|
-
kind: "removed",
|
|
748
|
-
source: "(not found)",
|
|
749
|
-
fragment: String(fragProp.type ?? "unknown")
|
|
750
|
-
});
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
const sourceProp = localSourceProps[name];
|
|
754
|
-
if (fragProp.type && fragProp.type !== sourceProp.typeKind) {
|
|
755
|
-
drifts.push({
|
|
756
|
-
prop: name,
|
|
757
|
-
kind: "type_changed",
|
|
758
|
-
source: sourceProp.typeKind,
|
|
759
|
-
fragment: String(fragProp.type)
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
if (fragProp.required !== void 0 && fragProp.required !== sourceProp.required) {
|
|
763
|
-
drifts.push({
|
|
764
|
-
prop: name,
|
|
765
|
-
kind: "required_changed",
|
|
766
|
-
source: String(sourceProp.required),
|
|
767
|
-
fragment: String(fragProp.required)
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
if (fragProp.values && sourceProp.values) {
|
|
771
|
-
const fragSet = new Set(fragProp.values);
|
|
772
|
-
const srcSet = new Set(sourceProp.values);
|
|
773
|
-
const added = sourceProp.values.filter((v) => !fragSet.has(v));
|
|
774
|
-
const removed = Array.from(fragProp.values).filter((v) => !srcSet.has(v));
|
|
775
|
-
if (added.length > 0 || removed.length > 0) {
|
|
776
|
-
drifts.push({
|
|
777
|
-
prop: name,
|
|
778
|
-
kind: "values_changed",
|
|
779
|
-
source: sourceProp.values.join(", "),
|
|
780
|
-
fragment: Array.from(fragProp.values).join(", ")
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (fragProp.default !== void 0 && sourceProp.default !== void 0) {
|
|
785
|
-
if (String(fragProp.default) !== sourceProp.default) {
|
|
786
|
-
drifts.push({
|
|
787
|
-
prop: name,
|
|
788
|
-
kind: "default_changed",
|
|
789
|
-
source: sourceProp.default,
|
|
790
|
-
fragment: String(fragProp.default)
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return drifts;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// src/build.ts
|
|
799
|
-
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
800
|
-
import { resolve as resolve5, join as join5 } from "path";
|
|
801
|
-
import { existsSync as existsSync6 } from "fs";
|
|
802
|
-
|
|
803
|
-
// src/core/token-resolver.ts
|
|
804
|
-
import { resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
805
|
-
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
806
|
-
function roundRgbValues(value) {
|
|
807
|
-
return value.replace(
|
|
808
|
-
/rgb\(([^)]+)\)/g,
|
|
809
|
-
(_full, inner) => {
|
|
810
|
-
const parts = inner.split(",").map((p) => p.trim());
|
|
811
|
-
const rounded = parts.map((p) => {
|
|
812
|
-
const num = parseFloat(p);
|
|
813
|
-
return isNaN(num) ? p : String(Math.round(num));
|
|
814
|
-
});
|
|
815
|
-
return `rgb(${rounded.join(", ")})`;
|
|
816
|
-
}
|
|
817
|
-
).replace(
|
|
818
|
-
/rgba\(([^)]+)\)/g,
|
|
819
|
-
(_full, inner) => {
|
|
820
|
-
const parts = inner.split(",").map((p) => p.trim());
|
|
821
|
-
const rounded = parts.map((p, i) => {
|
|
822
|
-
if (i >= 3) return p;
|
|
823
|
-
const num = parseFloat(p);
|
|
824
|
-
return isNaN(num) ? p : String(Math.round(num));
|
|
825
|
-
});
|
|
826
|
-
return `rgba(${rounded.join(", ")})`;
|
|
827
|
-
}
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
async function resolveTokensWithSass(unresolvedTokens, tokensDir) {
|
|
831
|
-
const resolvedMap = /* @__PURE__ */ new Map();
|
|
832
|
-
const needsResolution = unresolvedTokens.filter(
|
|
833
|
-
(t) => t.value.includes("#{") || t.value.includes("$")
|
|
834
|
-
);
|
|
835
|
-
if (needsResolution.length === 0) {
|
|
836
|
-
return resolvedMap;
|
|
837
|
-
}
|
|
838
|
-
try {
|
|
839
|
-
const sass = await import("./sass.node-4XJK6YBF.js");
|
|
840
|
-
const variablesPath = findVariablesFile(tokensDir);
|
|
841
|
-
if (!variablesPath) {
|
|
842
|
-
return resolvedMap;
|
|
843
|
-
}
|
|
844
|
-
const fileName = basename(variablesPath);
|
|
845
|
-
const moduleName = fileName.replace(/^_/, "").replace(/\.scss$/, "");
|
|
846
|
-
const scssSource = `
|
|
847
|
-
@use '${moduleName}' as vars;
|
|
848
|
-
:root { @include vars.fui-css-variables; }
|
|
849
|
-
`;
|
|
850
|
-
const compiled = sass.compileString(scssSource, {
|
|
851
|
-
loadPaths: [tokensDir, dirname2(tokensDir)],
|
|
852
|
-
style: "expanded",
|
|
853
|
-
// Suppress sass deprecation warnings during build
|
|
854
|
-
logger: { warn() {
|
|
855
|
-
}, debug() {
|
|
856
|
-
} }
|
|
857
|
-
});
|
|
858
|
-
const cssVarRegex = /(--[\w-]+)\s*:\s*([^;]+)/g;
|
|
859
|
-
let match;
|
|
860
|
-
const allResolved = /* @__PURE__ */ new Map();
|
|
861
|
-
while ((match = cssVarRegex.exec(compiled.css)) !== null) {
|
|
862
|
-
allResolved.set(match[1], roundRgbValues(match[2].trim()));
|
|
863
|
-
}
|
|
864
|
-
for (const token of needsResolution) {
|
|
865
|
-
const value = allResolved.get(token.name);
|
|
866
|
-
if (value !== void 0) {
|
|
867
|
-
resolvedMap.set(token.name, value);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
} catch {
|
|
871
|
-
}
|
|
872
|
-
return resolvedMap;
|
|
873
|
-
}
|
|
874
|
-
function findVariablesFile(tokensDir) {
|
|
875
|
-
const candidates = ["_variables.scss", "variables.scss"];
|
|
876
|
-
for (const name of candidates) {
|
|
877
|
-
const path = resolve2(tokensDir, name);
|
|
878
|
-
if (existsSync3(path)) {
|
|
879
|
-
return path;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
try {
|
|
883
|
-
const files = readdirSync(tokensDir).filter((f) => f.endsWith(".scss"));
|
|
884
|
-
for (const file of files) {
|
|
885
|
-
const path = resolve2(tokensDir, file);
|
|
886
|
-
if (file.includes("variables") || file.includes("tokens")) {
|
|
887
|
-
return path;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
} catch {
|
|
891
|
-
}
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// src/core/extractor-adapter.ts
|
|
896
|
-
var ReactExtractorAdapter = class {
|
|
897
|
-
extractor;
|
|
898
|
-
verificationLevel = "full";
|
|
899
|
-
constructor(tsconfigPath) {
|
|
900
|
-
this.extractor = createComponentExtractor(tsconfigPath);
|
|
901
|
-
}
|
|
902
|
-
canHandle(framework) {
|
|
903
|
-
return framework === "react";
|
|
904
|
-
}
|
|
905
|
-
extract(sourcePath, exportName) {
|
|
906
|
-
return this.extractor.extract(sourcePath, exportName);
|
|
907
|
-
}
|
|
908
|
-
dispose() {
|
|
909
|
-
this.extractor.dispose();
|
|
910
|
-
}
|
|
911
|
-
};
|
|
912
|
-
var NoopExtractorAdapter = class {
|
|
913
|
-
verificationLevel = "none";
|
|
914
|
-
canHandle() {
|
|
915
|
-
return true;
|
|
916
|
-
}
|
|
917
|
-
extract() {
|
|
918
|
-
return null;
|
|
919
|
-
}
|
|
920
|
-
dispose() {
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
function createExtractorAdapter(framework, tsconfigPath) {
|
|
924
|
-
if (framework === "react") {
|
|
925
|
-
return new ReactExtractorAdapter(tsconfigPath);
|
|
926
|
-
}
|
|
927
|
-
return new NoopExtractorAdapter();
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// src/core/drift-verifier.ts
|
|
931
|
-
function verifyContractDrift(contract, extracted) {
|
|
932
|
-
const contractPropNames = new Set(Object.keys(contract.props));
|
|
933
|
-
const sourcePropNames = new Set(
|
|
934
|
-
Object.entries(extracted.props).filter(([, p]) => p.source === "local").map(([name]) => name)
|
|
935
|
-
);
|
|
936
|
-
const removedProps = [];
|
|
937
|
-
const undocumentedProps = [];
|
|
938
|
-
const typeMismatches = [];
|
|
939
|
-
const defaultMismatches = [];
|
|
940
|
-
for (const name of contractPropNames) {
|
|
941
|
-
if (!sourcePropNames.has(name)) {
|
|
942
|
-
removedProps.push(name);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
for (const name of sourcePropNames) {
|
|
946
|
-
if (!contractPropNames.has(name)) {
|
|
947
|
-
undocumentedProps.push(name);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
for (const name of contractPropNames) {
|
|
951
|
-
const contractProp = contract.props[name];
|
|
952
|
-
const sourceProp = extracted.props[name];
|
|
953
|
-
if (!sourceProp || sourceProp.source !== "local") continue;
|
|
954
|
-
if (contractProp.type !== sourceProp.typeKind) {
|
|
955
|
-
typeMismatches.push({
|
|
956
|
-
prop: name,
|
|
957
|
-
contract: contractProp.type,
|
|
958
|
-
source: sourceProp.typeKind
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
const contractDefault = contractProp.default;
|
|
962
|
-
const sourceDefault = sourceProp.default;
|
|
963
|
-
if (contractDefault !== void 0 && sourceDefault !== void 0) {
|
|
964
|
-
if (String(contractDefault) !== String(sourceDefault)) {
|
|
965
|
-
defaultMismatches.push({
|
|
966
|
-
prop: name,
|
|
967
|
-
contract: contractDefault,
|
|
968
|
-
source: sourceDefault
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
const isClean = removedProps.length === 0 && undocumentedProps.length === 0 && typeMismatches.length === 0 && defaultMismatches.length === 0;
|
|
974
|
-
return {
|
|
975
|
-
componentName: contract.meta.name,
|
|
976
|
-
removedProps,
|
|
977
|
-
undocumentedProps,
|
|
978
|
-
typeMismatches,
|
|
979
|
-
defaultMismatches,
|
|
980
|
-
isClean
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
function formatDriftReport(report2) {
|
|
984
|
-
const lines = [`Drift detected for ${report2.componentName}:`];
|
|
985
|
-
if (report2.removedProps.length > 0) {
|
|
986
|
-
lines.push(` Removed from source: ${report2.removedProps.join(", ")}`);
|
|
987
|
-
}
|
|
988
|
-
if (report2.undocumentedProps.length > 0) {
|
|
989
|
-
lines.push(` Missing from contract: ${report2.undocumentedProps.join(", ")}`);
|
|
990
|
-
}
|
|
991
|
-
for (const m of report2.typeMismatches) {
|
|
992
|
-
lines.push(` Type mismatch: ${m.prop} (contract: ${m.contract}, source: ${m.source})`);
|
|
993
|
-
}
|
|
994
|
-
for (const m of report2.defaultMismatches) {
|
|
995
|
-
lines.push(` Default mismatch: ${m.prop} (contract: ${String(m.contract)}, source: ${String(m.source)})`);
|
|
996
|
-
}
|
|
997
|
-
return lines.join("\n");
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// src/core/graph-extractor.ts
|
|
1001
|
-
import ts3 from "typescript";
|
|
1002
|
-
import { readFileSync, existsSync as existsSync4 } from "fs";
|
|
1003
|
-
import { join as join3 } from "path";
|
|
1004
|
-
import { readdirSync as readdirSync2 } from "fs";
|
|
1005
|
-
import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from "@fragments-sdk/context/graph";
|
|
1006
|
-
async function buildComponentGraph(fragments, blocks, componentDir, options) {
|
|
1007
|
-
const knownComponents = new Set(Object.keys(fragments));
|
|
1008
|
-
const allEdges = [];
|
|
1009
|
-
const autoDetected = /* @__PURE__ */ new Map();
|
|
1010
|
-
const warnings = [];
|
|
1011
|
-
if (!options?.skipSourceAnalysis) {
|
|
1012
|
-
const sourceEdges = extractImportAndHookEdges(componentDir, knownComponents);
|
|
1013
|
-
allEdges.push(...sourceEdges);
|
|
1014
|
-
const subComponentResults = extractSubComponents(componentDir, knownComponents);
|
|
1015
|
-
for (const [name, subs] of subComponentResults) {
|
|
1016
|
-
autoDetected.set(name, {
|
|
1017
|
-
...autoDetected.get(name),
|
|
1018
|
-
subComponents: subs,
|
|
1019
|
-
compositionPattern: subs.length > 0 ? "compound" : "simple"
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
const jsxEdges = extractJsxUsageEdges(fragments, knownComponents);
|
|
1024
|
-
allEdges.push(...jsxEdges);
|
|
1025
|
-
const blockEdges = extractBlockEdges(blocks);
|
|
1026
|
-
allEdges.push(...blockEdges);
|
|
1027
|
-
const relationEdges = extractRelationEdges(fragments);
|
|
1028
|
-
allEdges.push(...relationEdges);
|
|
1029
|
-
const requiredChildrenMap = inferRequiredChildren(fragments, autoDetected);
|
|
1030
|
-
for (const [name, children] of requiredChildrenMap) {
|
|
1031
|
-
const existing = autoDetected.get(name) ?? {};
|
|
1032
|
-
autoDetected.set(name, { ...existing, requiredChildren: children });
|
|
1033
|
-
}
|
|
1034
|
-
const patternsMap = generateCommonPatterns(fragments, autoDetected);
|
|
1035
|
-
for (const [name, patterns] of patternsMap) {
|
|
1036
|
-
const existing = autoDetected.get(name) ?? {};
|
|
1037
|
-
autoDetected.set(name, { ...existing, commonPatterns: patterns });
|
|
1038
|
-
}
|
|
1039
|
-
const mergedEdges = mergeAndDeduplicate(allEdges);
|
|
1040
|
-
const nodes = Object.entries(fragments).map(([name, fragment]) => {
|
|
1041
|
-
const detected = autoDetected.get(name);
|
|
1042
|
-
return {
|
|
1043
|
-
name,
|
|
1044
|
-
category: fragment.meta.category,
|
|
1045
|
-
status: fragment.meta.status ?? "stable",
|
|
1046
|
-
compositionPattern: fragment.ai?.compositionPattern ?? detected?.compositionPattern,
|
|
1047
|
-
subComponents: fragment.ai?.subComponents ?? detected?.subComponents
|
|
1048
|
-
};
|
|
1049
|
-
});
|
|
1050
|
-
const blockIndex = /* @__PURE__ */ new Map();
|
|
1051
|
-
for (const [blockName, block] of Object.entries(blocks)) {
|
|
1052
|
-
for (const comp of block.components) {
|
|
1053
|
-
const existing = blockIndex.get(comp);
|
|
1054
|
-
if (existing) existing.push(blockName);
|
|
1055
|
-
else blockIndex.set(comp, [blockName]);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
const health = computeHealthFromData(nodes, mergedEdges, blockIndex);
|
|
1059
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1060
|
-
const detected = autoDetected.get(name);
|
|
1061
|
-
if (!detected) continue;
|
|
1062
|
-
if (fragment.ai?.subComponents && detected.subComponents) {
|
|
1063
|
-
const declared = new Set(fragment.ai.subComponents);
|
|
1064
|
-
const found = new Set(detected.subComponents);
|
|
1065
|
-
const missing = detected.subComponents.filter((s) => !declared.has(s));
|
|
1066
|
-
const extra = fragment.ai.subComponents.filter((s) => !found.has(s));
|
|
1067
|
-
if (missing.length > 0) {
|
|
1068
|
-
warnings.push(
|
|
1069
|
-
`${name}: declares ${declared.size} subComponents but code has ${found.size}. Missing from declaration: ${missing.join(", ")}`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
if (extra.length > 0) {
|
|
1073
|
-
warnings.push(
|
|
1074
|
-
`${name}: declares subComponents [${extra.join(", ")}] not found in Object.assign`
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
return {
|
|
1080
|
-
graph: { nodes, edges: mergedEdges, health },
|
|
1081
|
-
autoDetected,
|
|
1082
|
-
warnings
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
1086
|
-
const edges = [];
|
|
1087
|
-
for (const componentName of knownComponents) {
|
|
1088
|
-
const indexPath = findComponentIndex(componentDir, componentName);
|
|
1089
|
-
if (!indexPath) continue;
|
|
1090
|
-
let sourceText;
|
|
1091
|
-
try {
|
|
1092
|
-
sourceText = readFileSync(indexPath, "utf-8");
|
|
1093
|
-
} catch {
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
const sourceFile = ts3.createSourceFile(
|
|
1097
|
-
indexPath,
|
|
1098
|
-
sourceText,
|
|
1099
|
-
ts3.ScriptTarget.Latest,
|
|
1100
|
-
true,
|
|
1101
|
-
indexPath.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS
|
|
1102
|
-
);
|
|
1103
|
-
const visitNode = (node) => {
|
|
1104
|
-
if (ts3.isImportDeclaration(node)) {
|
|
1105
|
-
const moduleSpecifier = node.moduleSpecifier;
|
|
1106
|
-
if (ts3.isStringLiteral(moduleSpecifier)) {
|
|
1107
|
-
const importPath = moduleSpecifier.text;
|
|
1108
|
-
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
1109
|
-
const clause = node.importClause;
|
|
1110
|
-
if (clause) {
|
|
1111
|
-
if (clause.name && isPascalCase(clause.name.text) && knownComponents.has(clause.name.text)) {
|
|
1112
|
-
edges.push({
|
|
1113
|
-
source: componentName,
|
|
1114
|
-
target: clause.name.text,
|
|
1115
|
-
type: "imports",
|
|
1116
|
-
weight: EDGE_TYPE_WEIGHTS["imports"],
|
|
1117
|
-
provenance: `source:${componentName}/index.tsx`
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
if (clause.namedBindings && ts3.isNamedImports(clause.namedBindings)) {
|
|
1121
|
-
for (const element of clause.namedBindings.elements) {
|
|
1122
|
-
const name = element.name.text;
|
|
1123
|
-
if (isPascalCase(name) && knownComponents.has(name) && name !== componentName) {
|
|
1124
|
-
edges.push({
|
|
1125
|
-
source: componentName,
|
|
1126
|
-
target: name,
|
|
1127
|
-
type: "imports",
|
|
1128
|
-
weight: EDGE_TYPE_WEIGHTS["imports"],
|
|
1129
|
-
provenance: `source:${componentName}/index.tsx`
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (ts3.isCallExpression(node) && ts3.isIdentifier(node.expression)) {
|
|
1139
|
-
const callName = node.expression.text;
|
|
1140
|
-
const hookMatch = callName.match(/^use([A-Z][a-zA-Z]*)$/);
|
|
1141
|
-
if (hookMatch) {
|
|
1142
|
-
const hookTarget = hookMatch[1];
|
|
1143
|
-
if (knownComponents.has(hookTarget) && hookTarget !== componentName) {
|
|
1144
|
-
edges.push({
|
|
1145
|
-
source: componentName,
|
|
1146
|
-
target: hookTarget,
|
|
1147
|
-
type: "hook-depends",
|
|
1148
|
-
weight: EDGE_TYPE_WEIGHTS["hook-depends"],
|
|
1149
|
-
provenance: `source:${componentName}/index.tsx`
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
ts3.forEachChild(node, visitNode);
|
|
1155
|
-
};
|
|
1156
|
-
ts3.forEachChild(sourceFile, visitNode);
|
|
1157
|
-
}
|
|
1158
|
-
return edges;
|
|
1159
|
-
}
|
|
1160
|
-
function extractSubComponents(componentDir, knownComponents) {
|
|
1161
|
-
const result = /* @__PURE__ */ new Map();
|
|
1162
|
-
for (const componentName of knownComponents) {
|
|
1163
|
-
const indexPath = findComponentIndex(componentDir, componentName);
|
|
1164
|
-
if (!indexPath) continue;
|
|
1165
|
-
let sourceText;
|
|
1166
|
-
try {
|
|
1167
|
-
sourceText = readFileSync(indexPath, "utf-8");
|
|
1168
|
-
} catch {
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (!sourceText.includes("Object.assign")) continue;
|
|
1172
|
-
const sourceFile = ts3.createSourceFile(
|
|
1173
|
-
indexPath,
|
|
1174
|
-
sourceText,
|
|
1175
|
-
ts3.ScriptTarget.Latest,
|
|
1176
|
-
true,
|
|
1177
|
-
indexPath.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS
|
|
1178
|
-
);
|
|
1179
|
-
const subComponents = [];
|
|
1180
|
-
const visitNode = (node) => {
|
|
1181
|
-
if (ts3.isCallExpression(node) && ts3.isPropertyAccessExpression(node.expression) && ts3.isIdentifier(node.expression.expression) && node.expression.expression.text === "Object" && node.expression.name.text === "assign" && node.arguments.length >= 2) {
|
|
1182
|
-
const propsArg = node.arguments[1];
|
|
1183
|
-
if (ts3.isObjectLiteralExpression(propsArg)) {
|
|
1184
|
-
for (const prop of propsArg.properties) {
|
|
1185
|
-
if (ts3.isShorthandPropertyAssignment(prop)) {
|
|
1186
|
-
subComponents.push(prop.name.text);
|
|
1187
|
-
} else if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.name)) {
|
|
1188
|
-
subComponents.push(prop.name.text);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
ts3.forEachChild(node, visitNode);
|
|
1194
|
-
};
|
|
1195
|
-
ts3.forEachChild(sourceFile, visitNode);
|
|
1196
|
-
if (subComponents.length > 0) {
|
|
1197
|
-
result.set(componentName, subComponents);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
return result;
|
|
1201
|
-
}
|
|
1202
|
-
function extractJsxUsageEdges(fragments, knownComponents) {
|
|
1203
|
-
const edges = [];
|
|
1204
|
-
const jsxTagRegex = /<([A-Z][a-zA-Z]*(?:\.[A-Z][a-zA-Z]*)?)/g;
|
|
1205
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1206
|
-
const usedComponents = /* @__PURE__ */ new Set();
|
|
1207
|
-
for (const variant of fragment.variants) {
|
|
1208
|
-
if (!variant.code) continue;
|
|
1209
|
-
let match;
|
|
1210
|
-
jsxTagRegex.lastIndex = 0;
|
|
1211
|
-
while ((match = jsxTagRegex.exec(variant.code)) !== null) {
|
|
1212
|
-
let tagName = match[1];
|
|
1213
|
-
if (tagName.includes(".")) {
|
|
1214
|
-
tagName = tagName.split(".")[0];
|
|
1215
|
-
}
|
|
1216
|
-
if (knownComponents.has(tagName) && tagName !== name) {
|
|
1217
|
-
usedComponents.add(tagName);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
for (const target of usedComponents) {
|
|
1222
|
-
edges.push({
|
|
1223
|
-
source: name,
|
|
1224
|
-
target,
|
|
1225
|
-
type: "renders",
|
|
1226
|
-
weight: EDGE_TYPE_WEIGHTS["renders"],
|
|
1227
|
-
provenance: `variant:${name}`
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return edges;
|
|
1232
|
-
}
|
|
1233
|
-
function extractBlockEdges(blocks) {
|
|
1234
|
-
const edges = [];
|
|
1235
|
-
for (const [blockName, block] of Object.entries(blocks)) {
|
|
1236
|
-
const components = block.components;
|
|
1237
|
-
for (let i = 0; i < components.length; i++) {
|
|
1238
|
-
for (let j = i + 1; j < components.length; j++) {
|
|
1239
|
-
edges.push({
|
|
1240
|
-
source: components[i],
|
|
1241
|
-
target: components[j],
|
|
1242
|
-
type: "composes",
|
|
1243
|
-
weight: EDGE_TYPE_WEIGHTS["composes"],
|
|
1244
|
-
provenance: `block:${blockName}`
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
return edges;
|
|
1250
|
-
}
|
|
1251
|
-
function extractRelationEdges(fragments) {
|
|
1252
|
-
const edges = [];
|
|
1253
|
-
const relationToEdgeType = {
|
|
1254
|
-
parent: "parent-of",
|
|
1255
|
-
child: "parent-of",
|
|
1256
|
-
// reversed: if A declares child B, edge is A parent-of B
|
|
1257
|
-
composition: "composes",
|
|
1258
|
-
alternative: "alternative-to",
|
|
1259
|
-
sibling: "sibling-of"
|
|
1260
|
-
};
|
|
1261
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1262
|
-
if (!fragment.relations) continue;
|
|
1263
|
-
for (const rel of fragment.relations) {
|
|
1264
|
-
const edgeType = relationToEdgeType[rel.relationship];
|
|
1265
|
-
if (!edgeType) continue;
|
|
1266
|
-
let source;
|
|
1267
|
-
let target;
|
|
1268
|
-
if (rel.relationship === "parent") {
|
|
1269
|
-
source = rel.component;
|
|
1270
|
-
target = name;
|
|
1271
|
-
} else {
|
|
1272
|
-
source = name;
|
|
1273
|
-
target = rel.component;
|
|
1274
|
-
}
|
|
1275
|
-
edges.push({
|
|
1276
|
-
source,
|
|
1277
|
-
target,
|
|
1278
|
-
type: edgeType,
|
|
1279
|
-
weight: EDGE_TYPE_WEIGHTS[edgeType],
|
|
1280
|
-
note: rel.note,
|
|
1281
|
-
provenance: "relation"
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
return edges;
|
|
1286
|
-
}
|
|
1287
|
-
function inferRequiredChildren(fragments, autoDetected) {
|
|
1288
|
-
const result = /* @__PURE__ */ new Map();
|
|
1289
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1290
|
-
const detected = autoDetected.get(name);
|
|
1291
|
-
const subs = detected?.subComponents ?? fragment.ai?.subComponents;
|
|
1292
|
-
if (!subs || subs.length === 0) continue;
|
|
1293
|
-
const variantsWithCode = fragment.variants.filter((v) => v.code);
|
|
1294
|
-
if (variantsWithCode.length === 0) continue;
|
|
1295
|
-
const required = [];
|
|
1296
|
-
for (const sub of subs) {
|
|
1297
|
-
const inAll = variantsWithCode.every((v) => {
|
|
1298
|
-
const patterns = [
|
|
1299
|
-
new RegExp(`<${name}\\.${sub}[\\s/>]`),
|
|
1300
|
-
new RegExp(`<${sub}[\\s/>]`)
|
|
1301
|
-
];
|
|
1302
|
-
return patterns.some((p) => p.test(v.code));
|
|
1303
|
-
});
|
|
1304
|
-
if (inAll) required.push(sub);
|
|
1305
|
-
}
|
|
1306
|
-
if (required.length > 0) {
|
|
1307
|
-
result.set(name, required);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
return result;
|
|
1311
|
-
}
|
|
1312
|
-
function generateCommonPatterns(fragments, autoDetected) {
|
|
1313
|
-
const result = /* @__PURE__ */ new Map();
|
|
1314
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1315
|
-
const detected = autoDetected.get(name);
|
|
1316
|
-
const subs = detected?.subComponents ?? fragment.ai?.subComponents;
|
|
1317
|
-
if (!subs || subs.length === 0) continue;
|
|
1318
|
-
const firstVariant = fragment.variants.find((v) => v.code);
|
|
1319
|
-
if (!firstVariant?.code) continue;
|
|
1320
|
-
const usedSubs = [];
|
|
1321
|
-
for (const sub of subs) {
|
|
1322
|
-
const patterns = [
|
|
1323
|
-
new RegExp(`<${name}\\.${sub}`),
|
|
1324
|
-
new RegExp(`<${sub}[\\s/>]`)
|
|
1325
|
-
];
|
|
1326
|
-
if (patterns.some((p) => p.test(firstVariant.code))) {
|
|
1327
|
-
usedSubs.push(sub);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
if (usedSubs.length > 0) {
|
|
1331
|
-
const pattern = `<${name}>
|
|
1332
|
-
${usedSubs.map((s) => ` <${name}.${s}>...</${name}.${s}>`).join("\n")}
|
|
1333
|
-
</${name}>`;
|
|
1334
|
-
result.set(name, [pattern]);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
return result;
|
|
1338
|
-
}
|
|
1339
|
-
function mergeAndDeduplicate(edges) {
|
|
1340
|
-
const edgeMap = /* @__PURE__ */ new Map();
|
|
1341
|
-
for (const edge of edges) {
|
|
1342
|
-
const key = `${edge.source}\u2192${edge.target}:${edge.type}`;
|
|
1343
|
-
const existing = edgeMap.get(key);
|
|
1344
|
-
if (!existing || edge.weight > existing.weight) {
|
|
1345
|
-
edgeMap.set(key, edge);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
return [...edgeMap.values()];
|
|
1349
|
-
}
|
|
1350
|
-
function isPascalCase(name) {
|
|
1351
|
-
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
1352
|
-
}
|
|
1353
|
-
function findComponentIndex(componentDir, componentName) {
|
|
1354
|
-
const candidates = [
|
|
1355
|
-
join3(componentDir, componentName, "index.tsx"),
|
|
1356
|
-
join3(componentDir, componentName, "index.ts"),
|
|
1357
|
-
join3(componentDir, componentName, `${componentName}.tsx`),
|
|
1358
|
-
join3(componentDir, componentName, `${componentName}.ts`)
|
|
1359
|
-
];
|
|
1360
|
-
for (const candidate of candidates) {
|
|
1361
|
-
if (existsSync4(candidate)) {
|
|
1362
|
-
return candidate;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
try {
|
|
1366
|
-
const entries = readdirSync2(componentDir, { withFileTypes: true });
|
|
1367
|
-
for (const entry of entries) {
|
|
1368
|
-
if (entry.isDirectory() && entry.name === componentName) {
|
|
1369
|
-
const subCandidates = [
|
|
1370
|
-
join3(componentDir, entry.name, "index.tsx"),
|
|
1371
|
-
join3(componentDir, entry.name, "index.ts")
|
|
1372
|
-
];
|
|
1373
|
-
for (const sc of subCandidates) {
|
|
1374
|
-
if (existsSync4(sc)) return sc;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
} catch {
|
|
1379
|
-
}
|
|
1380
|
-
return null;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// src/build.ts
|
|
1384
|
-
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
1385
|
-
|
|
1386
|
-
// src/core/bundle-measurer.ts
|
|
1387
|
-
import { build } from "esbuild";
|
|
1388
|
-
import { gzipSync } from "zlib";
|
|
1389
|
-
import { resolve as resolve4, dirname as dirname3, join as join4, basename as basename3 } from "path";
|
|
1390
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1391
|
-
function resolveEntryPoint(fragmentFilePath, configDir) {
|
|
1392
|
-
const absPath = resolve4(configDir, fragmentFilePath);
|
|
1393
|
-
const dir = dirname3(absPath);
|
|
1394
|
-
const candidates = ["index.tsx", "index.ts", "index.jsx", "index.js"];
|
|
1395
|
-
for (const candidate of candidates) {
|
|
1396
|
-
const path = join4(dir, candidate);
|
|
1397
|
-
if (existsSync5(path)) return path;
|
|
1398
|
-
}
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
function labelForPath(filePath) {
|
|
1402
|
-
const lastNmIdx = filePath.lastIndexOf("node_modules/");
|
|
1403
|
-
if (lastNmIdx >= 0) {
|
|
1404
|
-
const afterNm = filePath.slice(lastNmIdx + "node_modules/".length);
|
|
1405
|
-
if (afterNm.startsWith("@")) {
|
|
1406
|
-
const parts = afterNm.split("/");
|
|
1407
|
-
return parts.slice(0, 2).join("/");
|
|
1408
|
-
}
|
|
1409
|
-
return afterNm.split("/")[0];
|
|
1410
|
-
}
|
|
1411
|
-
const componentsIdx = filePath.indexOf("components/");
|
|
1412
|
-
if (componentsIdx >= 0) {
|
|
1413
|
-
const afterComponents = filePath.slice(componentsIdx + "components/".length);
|
|
1414
|
-
const componentName = afterComponents.split("/")[0];
|
|
1415
|
-
return componentName;
|
|
1416
|
-
}
|
|
1417
|
-
const srcIdx = filePath.indexOf("src/");
|
|
1418
|
-
if (srcIdx >= 0) return filePath.slice(srcIdx);
|
|
1419
|
-
return filePath;
|
|
1420
|
-
}
|
|
1421
|
-
function groupImportsByDirectDep(metafile, entryPoint) {
|
|
1422
|
-
const inputs = metafile.inputs;
|
|
1423
|
-
const outputKey = Object.keys(metafile.outputs)[0];
|
|
1424
|
-
const outputMeta = outputKey ? metafile.outputs[outputKey] : void 0;
|
|
1425
|
-
if (!outputMeta?.inputs) return [];
|
|
1426
|
-
const bytesMap = /* @__PURE__ */ new Map();
|
|
1427
|
-
for (const [path, info] of Object.entries(outputMeta.inputs)) {
|
|
1428
|
-
if (info.bytesInOutput > 0) {
|
|
1429
|
-
bytesMap.set(path, info.bytesInOutput);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
let entryKey;
|
|
1433
|
-
for (const key of Object.keys(inputs)) {
|
|
1434
|
-
if (key === entryPoint || entryPoint.endsWith(key) || key.endsWith(basename3(entryPoint))) {
|
|
1435
|
-
const entryDir = dirname3(entryPoint);
|
|
1436
|
-
if (key.includes(basename3(entryDir))) {
|
|
1437
|
-
entryKey = key;
|
|
1438
|
-
break;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
if (!entryKey) {
|
|
1443
|
-
const entryBasename = basename3(dirname3(entryPoint));
|
|
1444
|
-
for (const key of Object.keys(inputs)) {
|
|
1445
|
-
if (key.includes(`/${entryBasename}/index.`)) {
|
|
1446
|
-
entryKey = key;
|
|
1447
|
-
break;
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
if (!entryKey || !inputs[entryKey]) {
|
|
1452
|
-
return groupByPackage(bytesMap);
|
|
1453
|
-
}
|
|
1454
|
-
const directImports = inputs[entryKey].imports.map((imp) => imp.path).filter((p) => inputs[p]);
|
|
1455
|
-
const claimed = /* @__PURE__ */ new Set();
|
|
1456
|
-
claimed.add(entryKey);
|
|
1457
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
1458
|
-
for (const directPath of directImports) {
|
|
1459
|
-
if (claimed.has(directPath)) continue;
|
|
1460
|
-
const queue = [directPath];
|
|
1461
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
1462
|
-
while (queue.length > 0) {
|
|
1463
|
-
const current = queue.pop();
|
|
1464
|
-
if (reachable.has(current) || claimed.has(current)) continue;
|
|
1465
|
-
reachable.add(current);
|
|
1466
|
-
claimed.add(current);
|
|
1467
|
-
const entry = inputs[current];
|
|
1468
|
-
if (entry?.imports) {
|
|
1469
|
-
for (const imp of entry.imports) {
|
|
1470
|
-
if (inputs[imp.path] && !claimed.has(imp.path)) {
|
|
1471
|
-
queue.push(imp.path);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
let totalBytes = 0;
|
|
1477
|
-
for (const path of reachable) {
|
|
1478
|
-
totalBytes += bytesMap.get(path) ?? 0;
|
|
1479
|
-
}
|
|
1480
|
-
if (totalBytes > 0) {
|
|
1481
|
-
const label = labelForPath(directPath);
|
|
1482
|
-
const existing = groupMap.get(label);
|
|
1483
|
-
if (existing) {
|
|
1484
|
-
existing.bytes += totalBytes;
|
|
1485
|
-
} else {
|
|
1486
|
-
const entry = { path: label, bytes: totalBytes };
|
|
1487
|
-
groupMap.set(label, entry);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
const entryLabel = labelForPath(entryKey);
|
|
1492
|
-
const selfLabel = entryLabel + " (self)";
|
|
1493
|
-
let selfBytes = bytesMap.get(entryKey) ?? 0;
|
|
1494
|
-
const siblingGroup = groupMap.get(entryLabel);
|
|
1495
|
-
if (siblingGroup) {
|
|
1496
|
-
selfBytes += siblingGroup.bytes;
|
|
1497
|
-
groupMap.delete(entryLabel);
|
|
1498
|
-
}
|
|
1499
|
-
if (selfBytes > 0) {
|
|
1500
|
-
groupMap.set(selfLabel, { path: selfLabel, bytes: selfBytes });
|
|
1501
|
-
}
|
|
1502
|
-
let unclaimedBytes = 0;
|
|
1503
|
-
for (const [path, bytes] of bytesMap) {
|
|
1504
|
-
if (!claimed.has(path)) unclaimedBytes += bytes;
|
|
1505
|
-
}
|
|
1506
|
-
if (unclaimedBytes > 0) {
|
|
1507
|
-
groupMap.set("(other)", { path: "(other)", bytes: unclaimedBytes });
|
|
1508
|
-
}
|
|
1509
|
-
return [...groupMap.values()].sort((a, b) => b.bytes - a.bytes);
|
|
1510
|
-
}
|
|
1511
|
-
function groupByPackage(bytesMap) {
|
|
1512
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1513
|
-
for (const [path, bytes] of bytesMap) {
|
|
1514
|
-
const label = labelForPath(path);
|
|
1515
|
-
const key = label.includes("/") && !label.startsWith("components/") ? label.split("/").slice(0, label.startsWith("@") ? 2 : 1).join("/") : label;
|
|
1516
|
-
groups.set(key, (groups.get(key) ?? 0) + bytes);
|
|
1517
|
-
}
|
|
1518
|
-
return [...groups.entries()].map(([path, bytes]) => ({ path, bytes })).sort((a, b) => b.bytes - a.bytes);
|
|
1519
|
-
}
|
|
1520
|
-
async function measureSingleComponent(entryPoint, name) {
|
|
1521
|
-
const result = await build({
|
|
1522
|
-
entryPoints: [entryPoint],
|
|
1523
|
-
bundle: true,
|
|
1524
|
-
write: false,
|
|
1525
|
-
minify: true,
|
|
1526
|
-
metafile: true,
|
|
1527
|
-
format: "esm",
|
|
1528
|
-
target: "es2020",
|
|
1529
|
-
platform: "browser",
|
|
1530
|
-
treeShaking: true,
|
|
1531
|
-
external: [
|
|
1532
|
-
"react",
|
|
1533
|
-
"react-dom",
|
|
1534
|
-
"react/jsx-runtime",
|
|
1535
|
-
"react/jsx-dev-runtime",
|
|
1536
|
-
// Optional peer deps — excluded from measurement
|
|
1537
|
-
"recharts",
|
|
1538
|
-
"shiki",
|
|
1539
|
-
"react-day-picker",
|
|
1540
|
-
"@tanstack/react-table",
|
|
1541
|
-
"date-fns",
|
|
1542
|
-
"@base-ui-components/*",
|
|
1543
|
-
"@base-ui/react/*"
|
|
1544
|
-
],
|
|
1545
|
-
loader: {
|
|
1546
|
-
".scss": "empty",
|
|
1547
|
-
".css": "empty",
|
|
1548
|
-
".svg": "empty",
|
|
1549
|
-
".png": "empty",
|
|
1550
|
-
".jpg": "empty",
|
|
1551
|
-
".gif": "empty",
|
|
1552
|
-
".woff": "empty",
|
|
1553
|
-
".woff2": "empty",
|
|
1554
|
-
".ttf": "empty",
|
|
1555
|
-
".eot": "empty"
|
|
1556
|
-
},
|
|
1557
|
-
logLevel: "silent"
|
|
1558
|
-
});
|
|
1559
|
-
const output = result.outputFiles[0];
|
|
1560
|
-
const rawBytes = output.contents.byteLength;
|
|
1561
|
-
const gzipBytes = gzipSync(output.contents).byteLength;
|
|
1562
|
-
const imports = result.metafile ? groupImportsByDirectDep(result.metafile, entryPoint) : void 0;
|
|
1563
|
-
return { name, rawBytes, gzipBytes, imports };
|
|
1564
|
-
}
|
|
1565
|
-
async function measureBundleSizes(fragments, configDir, options = {}) {
|
|
1566
|
-
const concurrency = options.concurrency ?? 4;
|
|
1567
|
-
const measurements = /* @__PURE__ */ new Map();
|
|
1568
|
-
const errors = [];
|
|
1569
|
-
const start = Date.now();
|
|
1570
|
-
const entries = [];
|
|
1571
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
1572
|
-
const entryPoint = resolveEntryPoint(fragment.filePath, configDir);
|
|
1573
|
-
if (entryPoint) {
|
|
1574
|
-
entries.push({ name, entryPoint });
|
|
1575
|
-
} else {
|
|
1576
|
-
errors.push({
|
|
1577
|
-
name,
|
|
1578
|
-
error: `Could not resolve entry point from ${fragment.filePath}`
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
let completed = 0;
|
|
1583
|
-
for (let i = 0; i < entries.length; i += concurrency) {
|
|
1584
|
-
const batch = entries.slice(i, i + concurrency);
|
|
1585
|
-
const results = await Promise.allSettled(
|
|
1586
|
-
batch.map(
|
|
1587
|
-
({ name, entryPoint }) => measureSingleComponent(entryPoint, name)
|
|
1588
|
-
)
|
|
1589
|
-
);
|
|
1590
|
-
for (let j = 0; j < results.length; j++) {
|
|
1591
|
-
const result = results[j];
|
|
1592
|
-
const { name } = batch[j];
|
|
1593
|
-
completed++;
|
|
1594
|
-
if (result.status === "fulfilled") {
|
|
1595
|
-
measurements.set(name, result.value);
|
|
1596
|
-
} else {
|
|
1597
|
-
errors.push({
|
|
1598
|
-
name,
|
|
1599
|
-
error: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
options.onProgress?.(completed, entries.length, name);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
return {
|
|
1606
|
-
measurements,
|
|
1607
|
-
errors,
|
|
1608
|
-
elapsed: Date.now() - start
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
function toPerformanceData(measurement, config, contractBudget) {
|
|
1612
|
-
const budget = contractBudget ?? config.budgets.bundleSize;
|
|
1613
|
-
const budgetPercent = Math.round(measurement.gzipBytes / budget * 100);
|
|
1614
|
-
const imports = measurement.imports?.slice(0, 10).map((imp) => ({
|
|
1615
|
-
path: imp.path,
|
|
1616
|
-
bytes: imp.bytes,
|
|
1617
|
-
percent: Math.round(imp.bytes / measurement.rawBytes * 100)
|
|
1618
|
-
}));
|
|
1619
|
-
return {
|
|
1620
|
-
bundleSize: measurement.gzipBytes,
|
|
1621
|
-
rawSize: measurement.rawBytes,
|
|
1622
|
-
complexity: classifyComplexity(measurement.gzipBytes),
|
|
1623
|
-
budgetPercent,
|
|
1624
|
-
overBudget: budgetPercent > 100,
|
|
1625
|
-
measuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1626
|
-
...imports && imports.length > 0 ? { imports } : {}
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// src/build.ts
|
|
1631
|
-
function normalizeParsedProps(parsedProps) {
|
|
1632
|
-
return Object.fromEntries(
|
|
1633
|
-
Object.entries(parsedProps).map(([name, prop]) => [
|
|
1634
|
-
name,
|
|
1635
|
-
{
|
|
1636
|
-
type: prop.type ?? "custom",
|
|
1637
|
-
description: prop.description ?? "",
|
|
1638
|
-
default: prop.default,
|
|
1639
|
-
required: prop.required,
|
|
1640
|
-
values: prop.values,
|
|
1641
|
-
constraints: prop.constraints
|
|
1642
|
-
}
|
|
1643
|
-
])
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
function mergeDocumentedAndAutoProps(documentedProps, autoProps) {
|
|
1647
|
-
return Object.fromEntries(
|
|
1648
|
-
Object.keys(autoProps).filter((name) => autoProps[name].source === "local" || name in documentedProps).map((name) => {
|
|
1649
|
-
const documented = documentedProps[name];
|
|
1650
|
-
const auto = autoProps[name];
|
|
1651
|
-
return [
|
|
1652
|
-
name,
|
|
1653
|
-
{
|
|
1654
|
-
type: auto.typeKind,
|
|
1655
|
-
description: documented?.description ?? auto.description ?? "",
|
|
1656
|
-
default: auto.default !== void 0 ? auto.default : documented?.default,
|
|
1657
|
-
required: auto.required,
|
|
1658
|
-
values: auto.values ?? documented?.values,
|
|
1659
|
-
constraints: documented?.constraints
|
|
1660
|
-
}
|
|
1661
|
-
];
|
|
1662
|
-
})
|
|
1663
|
-
);
|
|
1664
|
-
}
|
|
1665
|
-
function compilePropsSummary(props) {
|
|
1666
|
-
return Object.entries(props).filter(([_, p]) => p.source === "local").map(([name, prop]) => {
|
|
1667
|
-
let summary = name + ": ";
|
|
1668
|
-
if (prop.values && prop.values.length > 0) {
|
|
1669
|
-
summary += prop.values.join("|");
|
|
1670
|
-
} else {
|
|
1671
|
-
summary += prop.typeKind;
|
|
1672
|
-
}
|
|
1673
|
-
if (prop.default !== void 0) {
|
|
1674
|
-
summary += ` (default: ${prop.default})`;
|
|
1675
|
-
}
|
|
1676
|
-
if (prop.required) {
|
|
1677
|
-
summary += " (required)";
|
|
1678
|
-
}
|
|
1679
|
-
return summary;
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
async function buildFragments(config, configDir) {
|
|
1683
|
-
const files = await discoverFragmentFiles(config, configDir);
|
|
1684
|
-
const errors = [];
|
|
1685
|
-
const warnings = [];
|
|
1686
|
-
const fragments = {};
|
|
1687
|
-
const contractSourcedNames = /* @__PURE__ */ new Set();
|
|
1688
|
-
const tsconfigCandidates = [
|
|
1689
|
-
resolve5(configDir, "tsconfig.json"),
|
|
1690
|
-
resolve5(configDir, "..", "tsconfig.json")
|
|
1691
|
-
];
|
|
1692
|
-
const tsconfigPath = tsconfigCandidates.find((p) => existsSync6(p));
|
|
1693
|
-
const extractor = createComponentExtractor(tsconfigPath);
|
|
1694
|
-
for (const file of files) {
|
|
1695
|
-
try {
|
|
1696
|
-
const content = await readFile3(file.absolutePath, "utf-8");
|
|
1697
|
-
if (isContractFile(file.absolutePath)) {
|
|
1698
|
-
try {
|
|
1699
|
-
const parsed2 = parseComponentContract(content, file.relativePath);
|
|
1700
|
-
const framework = parsed2.framework ?? config.framework ?? "react";
|
|
1701
|
-
const contractAdapter = createExtractorAdapter(framework, tsconfigPath);
|
|
1702
|
-
let extractedMeta2 = null;
|
|
1703
|
-
if (parsed2.sourcePath && parsed2.exportName && contractAdapter.canHandle(framework)) {
|
|
1704
|
-
try {
|
|
1705
|
-
const absSourcePath = resolve5(configDir, parsed2.sourcePath);
|
|
1706
|
-
extractedMeta2 = contractAdapter.extract(absSourcePath, parsed2.exportName);
|
|
1707
|
-
} catch {
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
let mergedProps2 = parsed2.props;
|
|
1711
|
-
if (extractedMeta2 && Object.keys(extractedMeta2.props).length > 0) {
|
|
1712
|
-
mergedProps2 = mergeDocumentedAndAutoProps(mergedProps2, extractedMeta2.props);
|
|
1713
|
-
}
|
|
1714
|
-
let contractPropsSummary = parsed2.propsSummary;
|
|
1715
|
-
if ((!contractPropsSummary || contractPropsSummary.length === 0) && extractedMeta2) {
|
|
1716
|
-
contractPropsSummary = compilePropsSummary(extractedMeta2.props);
|
|
1717
|
-
}
|
|
1718
|
-
let ai2 = parsed2.ai;
|
|
1719
|
-
if (extractedMeta2?.composition) {
|
|
1720
|
-
const comp = extractedMeta2.composition;
|
|
1721
|
-
ai2 = {
|
|
1722
|
-
compositionPattern: comp.pattern,
|
|
1723
|
-
subComponents: comp.parts.map((p) => p.name),
|
|
1724
|
-
...ai2
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
if (parsed2.provenance?.verified && extractedMeta2) {
|
|
1728
|
-
const drift = verifyContractDrift(parsed2, extractedMeta2);
|
|
1729
|
-
if (!drift.isClean) {
|
|
1730
|
-
errors.push({
|
|
1731
|
-
file: file.relativePath,
|
|
1732
|
-
error: formatDriftReport(drift)
|
|
1733
|
-
});
|
|
1734
|
-
contractAdapter.dispose();
|
|
1735
|
-
continue;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
const compiled2 = {
|
|
1739
|
-
filePath: file.relativePath,
|
|
1740
|
-
meta: {
|
|
1741
|
-
name: parsed2.meta.name,
|
|
1742
|
-
description: parsed2.meta.description,
|
|
1743
|
-
category: parsed2.meta.category,
|
|
1744
|
-
tags: parsed2.meta.tags,
|
|
1745
|
-
status: parsed2.meta.status,
|
|
1746
|
-
figma: parsed2.meta.figma
|
|
1747
|
-
},
|
|
1748
|
-
usage: parsed2.usage,
|
|
1749
|
-
props: mergedProps2,
|
|
1750
|
-
relations: parsed2.relations,
|
|
1751
|
-
variants: parsed2.variants,
|
|
1752
|
-
...parsed2.contract && { contract: parsed2.contract },
|
|
1753
|
-
...ai2 && { ai: ai2 },
|
|
1754
|
-
framework: parsed2.framework,
|
|
1755
|
-
propsSummary: contractPropsSummary,
|
|
1756
|
-
sourcePath: parsed2.sourcePath,
|
|
1757
|
-
exportName: parsed2.exportName,
|
|
1758
|
-
provenance: parsed2.provenance
|
|
1759
|
-
};
|
|
1760
|
-
fragments[compiled2.meta.name] = compiled2;
|
|
1761
|
-
contractSourcedNames.add(compiled2.meta.name);
|
|
1762
|
-
contractAdapter.dispose();
|
|
1763
|
-
} catch (error) {
|
|
1764
|
-
errors.push({
|
|
1765
|
-
file: file.relativePath,
|
|
1766
|
-
error: `Contract parse error: ${error instanceof Error ? error.message : String(error)}`
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
continue;
|
|
1770
|
-
}
|
|
1771
|
-
if (file.absolutePath.endsWith(".fragment.tsx") || file.absolutePath.endsWith(".fragment.ts")) {
|
|
1772
|
-
warnings.push({
|
|
1773
|
-
file: file.relativePath,
|
|
1774
|
-
warning: "Deprecated format: .fragment.tsx \u2014 run `fragments migrate-contract` to generate .contract.json"
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
if (!content.includes("defineFragment")) {
|
|
1778
|
-
warnings.push({
|
|
1779
|
-
file: file.relativePath,
|
|
1780
|
-
warning: "No defineFragment() call found"
|
|
1781
|
-
});
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
const parsed = parseFragmentFile(content, file.relativePath);
|
|
1785
|
-
for (const warning of parsed.warnings) {
|
|
1786
|
-
warnings.push({ file: file.relativePath, warning });
|
|
1787
|
-
}
|
|
1788
|
-
if (!parsed.meta.name) {
|
|
1789
|
-
warnings.push({
|
|
1790
|
-
file: file.relativePath,
|
|
1791
|
-
warning: "Missing meta.name in fragment definition \u2014 skipped"
|
|
1792
|
-
});
|
|
1793
|
-
continue;
|
|
1794
|
-
}
|
|
1795
|
-
const documentedProps = normalizeParsedProps(parsed.props);
|
|
1796
|
-
let mergedProps = documentedProps;
|
|
1797
|
-
const componentExportName = parsed.componentName ?? parsed.meta.name;
|
|
1798
|
-
const componentSourcePath = resolveComponentSourcePath(
|
|
1799
|
-
file.absolutePath,
|
|
1800
|
-
parsed.componentImport
|
|
1801
|
-
);
|
|
1802
|
-
let extractedMeta = null;
|
|
1803
|
-
if (componentExportName && componentSourcePath) {
|
|
1804
|
-
try {
|
|
1805
|
-
extractedMeta = extractor.extract(componentSourcePath, componentExportName);
|
|
1806
|
-
} catch {
|
|
1807
|
-
}
|
|
1808
|
-
if (extractedMeta) {
|
|
1809
|
-
const autoProps = extractedMeta.props;
|
|
1810
|
-
const hasAutoProps = Object.keys(autoProps).length > 0;
|
|
1811
|
-
if (hasAutoProps) {
|
|
1812
|
-
const removedDocumentedProps = Object.keys(documentedProps).filter(
|
|
1813
|
-
(propName) => !(propName in autoProps)
|
|
1814
|
-
);
|
|
1815
|
-
if (removedDocumentedProps.length > 0) {
|
|
1816
|
-
warnings.push({
|
|
1817
|
-
file: file.relativePath,
|
|
1818
|
-
warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`
|
|
1819
|
-
});
|
|
1820
|
-
}
|
|
1821
|
-
mergedProps = mergeDocumentedAndAutoProps(documentedProps, autoProps);
|
|
1822
|
-
} else if (Object.keys(documentedProps).length > 0) {
|
|
1823
|
-
warnings.push({
|
|
1824
|
-
file: file.relativePath,
|
|
1825
|
-
warning: "Auto-props extraction returned no props; falling back to documented props"
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
} else if (!componentExportName) {
|
|
1830
|
-
warnings.push({
|
|
1831
|
-
file: file.relativePath,
|
|
1832
|
-
warning: "Unable to resolve component export name for auto-props extraction"
|
|
1833
|
-
});
|
|
1834
|
-
} else if (!componentSourcePath) {
|
|
1835
|
-
warnings.push({
|
|
1836
|
-
file: file.relativePath,
|
|
1837
|
-
warning: `Unable to resolve component source path from import: ${parsed.componentImport ?? "unknown"}`
|
|
1838
|
-
});
|
|
1839
|
-
}
|
|
1840
|
-
let contract = parsed.contract;
|
|
1841
|
-
if (!contract?.propsSummary && extractedMeta) {
|
|
1842
|
-
const summary = compilePropsSummary(extractedMeta.props);
|
|
1843
|
-
if (summary.length > 0) {
|
|
1844
|
-
contract = { ...contract, propsSummary: summary };
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
let ai = parsed.ai;
|
|
1848
|
-
if (extractedMeta?.composition) {
|
|
1849
|
-
const comp = extractedMeta.composition;
|
|
1850
|
-
ai = {
|
|
1851
|
-
compositionPattern: comp.pattern,
|
|
1852
|
-
subComponents: comp.parts.map((p) => p.name),
|
|
1853
|
-
...ai
|
|
1854
|
-
// Manually authored ai fields take precedence
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
const compiled = {
|
|
1858
|
-
filePath: file.relativePath,
|
|
1859
|
-
meta: {
|
|
1860
|
-
name: parsed.meta.name,
|
|
1861
|
-
description: parsed.meta.description ?? "",
|
|
1862
|
-
category: parsed.meta.category ?? "Uncategorized",
|
|
1863
|
-
status: parsed.meta.status,
|
|
1864
|
-
tags: parsed.meta.tags,
|
|
1865
|
-
since: parsed.meta.since,
|
|
1866
|
-
...parsed.meta.dependencies && { dependencies: parsed.meta.dependencies },
|
|
1867
|
-
figma: parsed.meta.figma
|
|
1868
|
-
},
|
|
1869
|
-
usage: {
|
|
1870
|
-
when: parsed.usage.when ?? [],
|
|
1871
|
-
whenNot: parsed.usage.whenNot ?? [],
|
|
1872
|
-
guidelines: parsed.usage.guidelines,
|
|
1873
|
-
accessibility: parsed.usage.accessibility
|
|
1874
|
-
},
|
|
1875
|
-
props: mergedProps,
|
|
1876
|
-
relations: parsed.relations.map((rel) => ({
|
|
1877
|
-
component: rel.component,
|
|
1878
|
-
relationship: rel.relationship,
|
|
1879
|
-
note: rel.note
|
|
1880
|
-
})),
|
|
1881
|
-
variants: parsed.variants.map((v) => ({
|
|
1882
|
-
name: v.name,
|
|
1883
|
-
description: v.description,
|
|
1884
|
-
...v.code && { code: v.code },
|
|
1885
|
-
...v.figma && { figma: v.figma },
|
|
1886
|
-
...v.args && { args: v.args }
|
|
1887
|
-
})),
|
|
1888
|
-
// Include AI metadata (auto-enriched or manual)
|
|
1889
|
-
...ai && { ai },
|
|
1890
|
-
// Include contract metadata (auto-compiled or manual)
|
|
1891
|
-
...contract && { contract },
|
|
1892
|
-
// Provenance from TSX path
|
|
1893
|
-
provenance: {
|
|
1894
|
-
source: extractedMeta ? "extracted" : "manual",
|
|
1895
|
-
verified: !!extractedMeta
|
|
1896
|
-
}
|
|
1897
|
-
};
|
|
1898
|
-
if (contractSourcedNames.has(parsed.meta.name)) {
|
|
1899
|
-
warnings.push({
|
|
1900
|
-
file: file.relativePath,
|
|
1901
|
-
warning: `Duplicate: "${parsed.meta.name}" already loaded from .contract.json \u2014 skipping .fragment.tsx`
|
|
1902
|
-
});
|
|
1903
|
-
} else {
|
|
1904
|
-
fragments[parsed.meta.name] = compiled;
|
|
1905
|
-
}
|
|
1906
|
-
} catch (error) {
|
|
1907
|
-
errors.push({
|
|
1908
|
-
file: file.relativePath,
|
|
1909
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
extractor.dispose();
|
|
1914
|
-
const blocks = {};
|
|
1915
|
-
try {
|
|
1916
|
-
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
1917
|
-
for (const file of blockFiles) {
|
|
1918
|
-
try {
|
|
1919
|
-
let raw = await loadFragmentFile(file.absolutePath);
|
|
1920
|
-
if (raw && "default" in raw && typeof raw.default === "object") {
|
|
1921
|
-
raw = raw.default;
|
|
1922
|
-
}
|
|
1923
|
-
const def = raw;
|
|
1924
|
-
if (def && typeof def === "object" && "name" in def && "code" in def && "components" in def) {
|
|
1925
|
-
const compiled = compileBlock(def, file.relativePath);
|
|
1926
|
-
blocks[compiled.name] = compiled;
|
|
1927
|
-
}
|
|
1928
|
-
} catch (error) {
|
|
1929
|
-
warnings.push({
|
|
1930
|
-
file: file.relativePath,
|
|
1931
|
-
warning: `Failed to load block: ${error instanceof Error ? error.message : String(error)}`
|
|
1932
|
-
});
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
} catch {
|
|
1936
|
-
}
|
|
1937
|
-
let tokens;
|
|
1938
|
-
try {
|
|
1939
|
-
const tokenPatterns = config.tokens?.include;
|
|
1940
|
-
const tokenFiles = await discoverTokenFiles(configDir, tokenPatterns, config.exclude);
|
|
1941
|
-
if (tokenFiles.length > 0) {
|
|
1942
|
-
const mergedCategories = {};
|
|
1943
|
-
let prefix = "--";
|
|
1944
|
-
let total = 0;
|
|
1945
|
-
const fileContents = [];
|
|
1946
|
-
for (const file of tokenFiles) {
|
|
1947
|
-
const content = await readFile3(file.absolutePath, "utf-8");
|
|
1948
|
-
fileContents.push({ content, path: file.relativePath });
|
|
1949
|
-
}
|
|
1950
|
-
const allContent = fileContents.map((f) => f.content).join("\n");
|
|
1951
|
-
for (const { content, path } of fileContents) {
|
|
1952
|
-
let fileParsed;
|
|
1953
|
-
let parsed;
|
|
1954
|
-
if (isDTCGFile(path)) {
|
|
1955
|
-
fileParsed = parseDTCGFile(content, path);
|
|
1956
|
-
parsed = fileParsed;
|
|
1957
|
-
} else {
|
|
1958
|
-
parsed = parseTokenFile(allContent, path);
|
|
1959
|
-
fileParsed = parseTokenFile(content, path);
|
|
1960
|
-
}
|
|
1961
|
-
prefix = fileParsed.prefix;
|
|
1962
|
-
total += fileParsed.total;
|
|
1963
|
-
for (const [cat, catTokens] of Object.entries(fileParsed.categories)) {
|
|
1964
|
-
if (!mergedCategories[cat]) {
|
|
1965
|
-
mergedCategories[cat] = [];
|
|
1966
|
-
}
|
|
1967
|
-
for (const t of catTokens) {
|
|
1968
|
-
if (!mergedCategories[cat].some((e) => e.name === t.name)) {
|
|
1969
|
-
const combinedToken = Object.values(parsed.categories).flat().find((ct) => ct.name === t.name);
|
|
1970
|
-
const resolvedValue = combinedToken?.resolvedValue ?? t.resolvedValue;
|
|
1971
|
-
mergedCategories[cat].push({
|
|
1972
|
-
name: t.name,
|
|
1973
|
-
...resolvedValue ? { value: resolvedValue } : t.value ? { value: t.value } : {},
|
|
1974
|
-
description: t.description
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
if (total > 0) {
|
|
1981
|
-
const allTokens = Object.values(mergedCategories).flat();
|
|
1982
|
-
const unresolved = allTokens.filter(
|
|
1983
|
-
(t) => t.value && (t.value.includes("#{") || t.value.includes("$"))
|
|
1984
|
-
);
|
|
1985
|
-
if (unresolved.length > 0 && tokenFiles.length > 0) {
|
|
1986
|
-
const tokensDir = resolve5(configDir, tokenFiles[0].relativePath, "..");
|
|
1987
|
-
const sassResolved = await resolveTokensWithSass(
|
|
1988
|
-
unresolved,
|
|
1989
|
-
tokensDir
|
|
1990
|
-
);
|
|
1991
|
-
if (sassResolved.size > 0) {
|
|
1992
|
-
for (const catTokens of Object.values(mergedCategories)) {
|
|
1993
|
-
for (const token of catTokens) {
|
|
1994
|
-
const resolved = sassResolved.get(token.name);
|
|
1995
|
-
if (resolved && token.value && (token.value.includes("#{") || token.value.includes("$"))) {
|
|
1996
|
-
token.value = resolved;
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
tokens = { prefix, total, categories: mergedCategories };
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
} catch {
|
|
2006
|
-
}
|
|
2007
|
-
let packageName;
|
|
2008
|
-
const pkgJsonPath = resolve5(configDir, "package.json");
|
|
2009
|
-
if (existsSync6(pkgJsonPath)) {
|
|
2010
|
-
try {
|
|
2011
|
-
const pkg = JSON.parse(await readFile3(pkgJsonPath, "utf-8"));
|
|
2012
|
-
if (pkg.name) packageName = pkg.name;
|
|
2013
|
-
} catch {
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
const componentDir = resolve5(configDir, "src", "components");
|
|
2017
|
-
let graphData;
|
|
2018
|
-
try {
|
|
2019
|
-
const graphResult = await buildComponentGraph(fragments, blocks, componentDir);
|
|
2020
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
2021
|
-
const detected = graphResult.autoDetected.get(name);
|
|
2022
|
-
if (!detected) continue;
|
|
2023
|
-
if (!fragment.ai) fragment.ai = {};
|
|
2024
|
-
if (!fragment.ai.subComponents && detected.subComponents) {
|
|
2025
|
-
fragment.ai.subComponents = detected.subComponents;
|
|
2026
|
-
}
|
|
2027
|
-
if (!fragment.ai.compositionPattern && detected.compositionPattern) {
|
|
2028
|
-
fragment.ai.compositionPattern = detected.compositionPattern;
|
|
2029
|
-
}
|
|
2030
|
-
if (!fragment.ai.commonPatterns && detected.commonPatterns) {
|
|
2031
|
-
fragment.ai.commonPatterns = detected.commonPatterns;
|
|
2032
|
-
}
|
|
2033
|
-
if (!fragment.ai.requiredChildren && detected.requiredChildren) {
|
|
2034
|
-
fragment.ai.requiredChildren = detected.requiredChildren;
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
for (const w of graphResult.warnings) {
|
|
2038
|
-
warnings.push({ file: "graph", warning: w });
|
|
2039
|
-
}
|
|
2040
|
-
graphData = serializeGraph(graphResult.graph);
|
|
2041
|
-
} catch (error) {
|
|
2042
|
-
warnings.push({
|
|
2043
|
-
file: "graph",
|
|
2044
|
-
warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2045
|
-
});
|
|
2046
|
-
}
|
|
2047
|
-
let performanceSummary;
|
|
2048
|
-
if (config.performance) {
|
|
2049
|
-
try {
|
|
2050
|
-
const perfConfig = resolvePerformanceConfig(config.performance);
|
|
2051
|
-
const perfResult = await measureBundleSizes(fragments, configDir, {
|
|
2052
|
-
perfConfig
|
|
2053
|
-
});
|
|
2054
|
-
const tiers = { lightweight: 0, moderate: 0, heavy: 0 };
|
|
2055
|
-
let overBudgetCount = 0;
|
|
2056
|
-
for (const [name, measurement] of perfResult.measurements) {
|
|
2057
|
-
const fragment = fragments[name];
|
|
2058
|
-
const contractBudget = fragment?.contract?.performanceBudget;
|
|
2059
|
-
const perfData = toPerformanceData(measurement, perfConfig, contractBudget);
|
|
2060
|
-
fragment.performance = perfData;
|
|
2061
|
-
tiers[perfData.complexity]++;
|
|
2062
|
-
if (perfData.overBudget) overBudgetCount++;
|
|
2063
|
-
}
|
|
2064
|
-
performanceSummary = {
|
|
2065
|
-
preset: perfConfig.preset,
|
|
2066
|
-
budget: perfConfig.budgets.bundleSize,
|
|
2067
|
-
total: perfResult.measurements.size,
|
|
2068
|
-
overBudget: overBudgetCount,
|
|
2069
|
-
tiers
|
|
2070
|
-
};
|
|
2071
|
-
for (const err of perfResult.errors) {
|
|
2072
|
-
warnings.push({
|
|
2073
|
-
file: "performance",
|
|
2074
|
-
warning: `Could not measure ${err.name}: ${err.error}`
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
} catch (error) {
|
|
2078
|
-
warnings.push({
|
|
2079
|
-
file: "performance",
|
|
2080
|
-
warning: `Performance measurement failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
const output = {
|
|
2085
|
-
version: "1.0.0",
|
|
2086
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2087
|
-
...packageName && { packageName },
|
|
2088
|
-
fragments,
|
|
2089
|
-
...Object.keys(blocks).length > 0 && { blocks },
|
|
2090
|
-
...tokens && { tokens },
|
|
2091
|
-
...graphData && { graph: graphData },
|
|
2092
|
-
...performanceSummary && { performanceSummary }
|
|
2093
|
-
};
|
|
2094
|
-
const outputPath = resolve5(configDir, config.outFile ?? BRAND.outFile);
|
|
2095
|
-
await writeFile(outputPath, JSON.stringify(output));
|
|
2096
|
-
return {
|
|
2097
|
-
success: errors.length === 0,
|
|
2098
|
-
outputPath,
|
|
2099
|
-
fragmentCount: Object.keys(fragments).length,
|
|
2100
|
-
errors,
|
|
2101
|
-
warnings
|
|
2102
|
-
};
|
|
2103
|
-
}
|
|
2104
|
-
async function buildFragmentsDir(config, configDir) {
|
|
2105
|
-
const fragmentsDir = join5(configDir, BRAND.dataDir);
|
|
2106
|
-
const componentsDir = join5(fragmentsDir, BRAND.componentsDir);
|
|
2107
|
-
await mkdir(fragmentsDir, { recursive: true });
|
|
2108
|
-
await mkdir(componentsDir, { recursive: true });
|
|
2109
|
-
const registryResult = await generateRegistry({
|
|
2110
|
-
projectRoot: configDir,
|
|
2111
|
-
componentPatterns: config.components || ["src/**/*.tsx", "src/**/*.ts"],
|
|
2112
|
-
storyPatterns: config.include || ["src/**/*.stories.tsx"],
|
|
2113
|
-
fragmentsDir,
|
|
2114
|
-
registryOptions: config.registry || {}
|
|
2115
|
-
});
|
|
2116
|
-
const errors = [...registryResult.errors];
|
|
2117
|
-
const warnings = [...registryResult.warnings];
|
|
2118
|
-
const indexPath = join5(fragmentsDir, "index.json");
|
|
2119
|
-
await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
|
|
2120
|
-
const registryPath = join5(fragmentsDir, BRAND.registryFile);
|
|
2121
|
-
await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
|
|
2122
|
-
const contextResult = generateContextMd(registryResult.registry, {
|
|
2123
|
-
format: "markdown",
|
|
2124
|
-
compact: false,
|
|
2125
|
-
include: {
|
|
2126
|
-
props: false,
|
|
2127
|
-
// AI can read TypeScript directly
|
|
2128
|
-
relations: true,
|
|
2129
|
-
code: false
|
|
2130
|
-
}
|
|
2131
|
-
});
|
|
2132
|
-
const contextPath = join5(fragmentsDir, BRAND.contextFile);
|
|
2133
|
-
await writeFile(contextPath, contextResult.content);
|
|
2134
|
-
return {
|
|
2135
|
-
success: errors.length === 0,
|
|
2136
|
-
indexPath,
|
|
2137
|
-
registryPath,
|
|
2138
|
-
contextPath,
|
|
2139
|
-
componentCount: registryResult.registry.componentCount,
|
|
2140
|
-
errors,
|
|
2141
|
-
warnings
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// src/screenshot.ts
|
|
2146
|
-
import pc from "picocolors";
|
|
2147
|
-
async function runScreenshotCommand(config, configDir, options = {}) {
|
|
2148
|
-
const startTime = Date.now();
|
|
2149
|
-
const errors = [];
|
|
2150
|
-
const storage = new StorageManager({
|
|
2151
|
-
projectRoot: configDir,
|
|
2152
|
-
viewport: options.width && options.height ? { width: options.width, height: options.height } : config.screenshots?.viewport
|
|
2153
|
-
});
|
|
2154
|
-
await storage.initialize();
|
|
2155
|
-
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
2156
|
-
if (fragmentFiles.length === 0) {
|
|
2157
|
-
console.log(pc.yellow("No fragment files found."));
|
|
2158
|
-
return {
|
|
2159
|
-
success: true,
|
|
2160
|
-
captured: 0,
|
|
2161
|
-
skipped: 0,
|
|
2162
|
-
errors: [],
|
|
2163
|
-
totalTimeMs: Date.now() - startTime
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
const fragments = [];
|
|
2167
|
-
for (const file of fragmentFiles) {
|
|
2168
|
-
try {
|
|
2169
|
-
const fragment = await loadFragmentFile(file.absolutePath);
|
|
2170
|
-
if (fragment) {
|
|
2171
|
-
fragments.push({ path: file.relativePath, fragment });
|
|
2172
|
-
}
|
|
2173
|
-
} catch (error) {
|
|
2174
|
-
errors.push({
|
|
2175
|
-
component: file.relativePath,
|
|
2176
|
-
variant: "",
|
|
2177
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
const filteredFragments = options.component ? fragments.filter((s) => s.fragment.meta.name === options.component) : fragments;
|
|
2182
|
-
if (options.component && filteredFragments.length === 0) {
|
|
2183
|
-
console.log(pc.yellow(`Component "${options.component}" not found.`));
|
|
2184
|
-
return {
|
|
2185
|
-
success: false,
|
|
2186
|
-
captured: 0,
|
|
2187
|
-
skipped: 0,
|
|
2188
|
-
errors: [],
|
|
2189
|
-
totalTimeMs: Date.now() - startTime
|
|
2190
|
-
};
|
|
2191
|
-
}
|
|
2192
|
-
const variantsToCapture = [];
|
|
2193
|
-
for (const { fragment } of filteredFragments) {
|
|
2194
|
-
const variants = options.variant ? fragment.variants.filter((v) => v.name === options.variant) : fragment.variants;
|
|
2195
|
-
for (const variant of variants) {
|
|
2196
|
-
variantsToCapture.push({
|
|
2197
|
-
component: fragment.meta.name,
|
|
2198
|
-
variant: variant.name,
|
|
2199
|
-
render: variant.render
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
if (variantsToCapture.length === 0) {
|
|
2204
|
-
console.log(pc.yellow("No variants to capture."));
|
|
2205
|
-
return {
|
|
2206
|
-
success: true,
|
|
2207
|
-
captured: 0,
|
|
2208
|
-
skipped: 0,
|
|
2209
|
-
errors: [],
|
|
2210
|
-
totalTimeMs: Date.now() - startTime
|
|
2211
|
-
};
|
|
2212
|
-
}
|
|
2213
|
-
const theme = options.theme ?? DEFAULTS.theme;
|
|
2214
|
-
const viewport = {
|
|
2215
|
-
width: options.width ?? config.screenshots?.viewport?.width ?? DEFAULTS.viewport.width,
|
|
2216
|
-
height: options.height ?? config.screenshots?.viewport?.height ?? DEFAULTS.viewport.height
|
|
2217
|
-
};
|
|
2218
|
-
console.log(pc.cyan(`
|
|
2219
|
-
${BRAND.name} Screenshot
|
|
2220
|
-
`));
|
|
2221
|
-
console.log(pc.dim(`Capturing variants (theme: ${theme}, viewport: ${viewport.width}x${viewport.height}):
|
|
2222
|
-
`));
|
|
2223
|
-
const pool = new BrowserPool({
|
|
2224
|
-
viewport
|
|
2225
|
-
});
|
|
2226
|
-
const viewerPort = DEFAULTS.port;
|
|
2227
|
-
const baseUrl = `http://localhost:${viewerPort}`;
|
|
2228
|
-
const captureEngine = new CaptureEngine(pool, baseUrl);
|
|
2229
|
-
let captured = 0;
|
|
2230
|
-
let skipped = 0;
|
|
2231
|
-
const captureOptions = {
|
|
2232
|
-
theme,
|
|
2233
|
-
viewport,
|
|
2234
|
-
delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
|
|
2235
|
-
};
|
|
2236
|
-
try {
|
|
2237
|
-
console.log(pc.dim("Starting browser..."));
|
|
2238
|
-
await pool.warmup();
|
|
2239
|
-
console.log(pc.dim("Browser ready.\n"));
|
|
2240
|
-
for (const { component, variant } of variantsToCapture) {
|
|
2241
|
-
const hasExisting = storage.hasBaseline(component, variant, theme);
|
|
2242
|
-
if (hasExisting && !options.update) {
|
|
2243
|
-
console.log(` ${pc.dim("\u25CB")} ${component}/${variant} ${pc.dim("(skipped)")}`);
|
|
2244
|
-
skipped++;
|
|
2245
|
-
continue;
|
|
2246
|
-
}
|
|
2247
|
-
try {
|
|
2248
|
-
const screenshot = await captureEngine.captureVariant(
|
|
2249
|
-
component,
|
|
2250
|
-
variant,
|
|
2251
|
-
captureOptions
|
|
2252
|
-
);
|
|
2253
|
-
await storage.saveBaseline(screenshot);
|
|
2254
|
-
const totalTime = screenshot.metadata.renderTimeMs + screenshot.metadata.captureTimeMs;
|
|
2255
|
-
console.log(
|
|
2256
|
-
` ${pc.green("\u2713")} ${component}/${variant} ${pc.dim(formatMs(totalTime))}`
|
|
2257
|
-
);
|
|
2258
|
-
captured++;
|
|
2259
|
-
} catch (error) {
|
|
2260
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2261
|
-
console.log(` ${pc.red("\u2717")} ${component}/${variant} ${pc.dim(errorMsg)}`);
|
|
2262
|
-
errors.push({ component, variant, error: errorMsg });
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
} finally {
|
|
2266
|
-
await pool.shutdown();
|
|
2267
|
-
}
|
|
2268
|
-
const totalTimeMs = Date.now() - startTime;
|
|
2269
|
-
console.log();
|
|
2270
|
-
if (errors.length === 0) {
|
|
2271
|
-
console.log(pc.green(`\u2713 Captured ${captured} screenshot(s) in ${formatMs(totalTimeMs)}`));
|
|
2272
|
-
} else {
|
|
2273
|
-
console.log(pc.yellow(`\u26A0 Captured ${captured} screenshot(s) with ${errors.length} error(s)`));
|
|
2274
|
-
}
|
|
2275
|
-
if (skipped > 0) {
|
|
2276
|
-
console.log(pc.dim(` ${skipped} skipped (use --update to recapture)`));
|
|
2277
|
-
}
|
|
2278
|
-
console.log(pc.dim(` Stored in ${storage.screenshotsDirPath}
|
|
2279
|
-
`));
|
|
2280
|
-
return {
|
|
2281
|
-
success: errors.length === 0,
|
|
2282
|
-
captured,
|
|
2283
|
-
skipped,
|
|
2284
|
-
errors,
|
|
2285
|
-
totalTimeMs
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
// src/diff.ts
|
|
2290
|
-
import pc2 from "picocolors";
|
|
2291
|
-
async function runDiffCommand(config, configDir, options = {}) {
|
|
2292
|
-
const startTime = Date.now();
|
|
2293
|
-
const results = [];
|
|
2294
|
-
const storage = new StorageManager({
|
|
2295
|
-
projectRoot: configDir,
|
|
2296
|
-
viewport: config.screenshots?.viewport
|
|
2297
|
-
});
|
|
2298
|
-
await storage.initialize();
|
|
2299
|
-
const threshold = options.threshold ?? config.screenshots?.threshold ?? DEFAULTS.diffThreshold;
|
|
2300
|
-
const diffEngine = new DiffEngine(threshold);
|
|
2301
|
-
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
2302
|
-
if (fragmentFiles.length === 0) {
|
|
2303
|
-
console.log(pc2.yellow("No fragment files found."));
|
|
2304
|
-
return {
|
|
2305
|
-
success: true,
|
|
2306
|
-
total: 0,
|
|
2307
|
-
passed: 0,
|
|
2308
|
-
failed: 0,
|
|
2309
|
-
missing: 0,
|
|
2310
|
-
results: [],
|
|
2311
|
-
totalTimeMs: Date.now() - startTime
|
|
2312
|
-
};
|
|
2313
|
-
}
|
|
2314
|
-
const fragments = [];
|
|
2315
|
-
for (const file of fragmentFiles) {
|
|
2316
|
-
try {
|
|
2317
|
-
const fragment = await loadFragmentFile(file.absolutePath);
|
|
2318
|
-
if (fragment) {
|
|
2319
|
-
fragments.push({ path: file.relativePath, fragment });
|
|
2320
|
-
}
|
|
2321
|
-
} catch {
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
const filteredFragments = options.component ? fragments.filter((s) => s.fragment.meta.name === options.component) : fragments;
|
|
2325
|
-
if (options.component && filteredFragments.length === 0) {
|
|
2326
|
-
console.log(pc2.yellow(`Component "${options.component}" not found.`));
|
|
2327
|
-
return {
|
|
2328
|
-
success: false,
|
|
2329
|
-
total: 0,
|
|
2330
|
-
passed: 0,
|
|
2331
|
-
failed: 0,
|
|
2332
|
-
missing: 0,
|
|
2333
|
-
results: [],
|
|
2334
|
-
totalTimeMs: Date.now() - startTime
|
|
2335
|
-
};
|
|
2336
|
-
}
|
|
2337
|
-
const variantsToDiff = [];
|
|
2338
|
-
for (const { fragment } of filteredFragments) {
|
|
2339
|
-
const variants = options.variant ? fragment.variants.filter((v) => v.name === options.variant) : fragment.variants;
|
|
2340
|
-
for (const variant of variants) {
|
|
2341
|
-
variantsToDiff.push({
|
|
2342
|
-
component: fragment.meta.name,
|
|
2343
|
-
variant: variant.name
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
if (variantsToDiff.length === 0) {
|
|
2348
|
-
console.log(pc2.yellow("No variants to compare."));
|
|
2349
|
-
return {
|
|
2350
|
-
success: true,
|
|
2351
|
-
total: 0,
|
|
2352
|
-
passed: 0,
|
|
2353
|
-
failed: 0,
|
|
2354
|
-
missing: 0,
|
|
2355
|
-
results: [],
|
|
2356
|
-
totalTimeMs: Date.now() - startTime
|
|
2357
|
-
};
|
|
2358
|
-
}
|
|
2359
|
-
const theme = options.theme ?? DEFAULTS.theme;
|
|
2360
|
-
const viewport = config.screenshots?.viewport ?? DEFAULTS.viewport;
|
|
2361
|
-
console.log(pc2.cyan(`
|
|
2362
|
-
${BRAND.name} Diff
|
|
2363
|
-
`));
|
|
2364
|
-
console.log(pc2.dim(`Comparing against baselines (theme: ${theme}, threshold: ${threshold}%):
|
|
2365
|
-
`));
|
|
2366
|
-
const pool = new BrowserPool({
|
|
2367
|
-
viewport
|
|
2368
|
-
});
|
|
2369
|
-
const viewerPort = DEFAULTS.port;
|
|
2370
|
-
const baseUrl = `http://localhost:${viewerPort}`;
|
|
2371
|
-
const captureEngine = new CaptureEngine(pool, baseUrl);
|
|
2372
|
-
let passed = 0;
|
|
2373
|
-
let failed = 0;
|
|
2374
|
-
let missing = 0;
|
|
2375
|
-
const captureOptions = {
|
|
2376
|
-
theme,
|
|
2377
|
-
viewport,
|
|
2378
|
-
delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
|
|
2379
|
-
};
|
|
2380
|
-
try {
|
|
2381
|
-
await pool.warmup();
|
|
2382
|
-
for (const { component, variant } of variantsToDiff) {
|
|
2383
|
-
const baseline = await storage.loadBaseline(component, variant, theme);
|
|
2384
|
-
if (!baseline) {
|
|
2385
|
-
console.log(
|
|
2386
|
-
` ${pc2.yellow("?")} ${component}/${variant} ${pc2.dim("(no baseline)")}`
|
|
2387
|
-
);
|
|
2388
|
-
missing++;
|
|
2389
|
-
continue;
|
|
2390
|
-
}
|
|
2391
|
-
try {
|
|
2392
|
-
const current = await captureEngine.captureVariant(
|
|
2393
|
-
component,
|
|
2394
|
-
variant,
|
|
2395
|
-
captureOptions
|
|
2396
|
-
);
|
|
2397
|
-
if (diffEngine.areIdentical(current, baseline)) {
|
|
2398
|
-
console.log(` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim("0.0%")}`);
|
|
2399
|
-
results.push({
|
|
2400
|
-
component,
|
|
2401
|
-
variant,
|
|
2402
|
-
theme,
|
|
2403
|
-
result: {
|
|
2404
|
-
matches: true,
|
|
2405
|
-
diffPercentage: 0,
|
|
2406
|
-
diffPixelCount: 0,
|
|
2407
|
-
totalPixels: current.viewport.width * current.viewport.height,
|
|
2408
|
-
changedRegions: [],
|
|
2409
|
-
diffTimeMs: 0
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2412
|
-
passed++;
|
|
2413
|
-
continue;
|
|
2414
|
-
}
|
|
2415
|
-
const diffResult = diffEngine.compare(current, baseline, { threshold });
|
|
2416
|
-
if (diffResult.matches) {
|
|
2417
|
-
console.log(
|
|
2418
|
-
` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim(`${diffResult.diffPercentage}%`)}`
|
|
2419
|
-
);
|
|
2420
|
-
passed++;
|
|
2421
|
-
} else {
|
|
2422
|
-
let diffImagePath;
|
|
2423
|
-
if (diffResult.diffImage) {
|
|
2424
|
-
diffImagePath = await storage.saveDiff(
|
|
2425
|
-
component,
|
|
2426
|
-
variant,
|
|
2427
|
-
theme,
|
|
2428
|
-
diffResult.diffImage
|
|
2429
|
-
);
|
|
2430
|
-
}
|
|
2431
|
-
console.log(
|
|
2432
|
-
` ${pc2.red("\u2717")} ${component}/${variant} ${pc2.yellow(`${diffResult.diffPercentage}%`)}` + (diffImagePath ? pc2.dim(` \u2192 ${diffImagePath}`) : "")
|
|
2433
|
-
);
|
|
2434
|
-
failed++;
|
|
2435
|
-
results.push({
|
|
2436
|
-
component,
|
|
2437
|
-
variant,
|
|
2438
|
-
theme,
|
|
2439
|
-
result: diffResult,
|
|
2440
|
-
diffImagePath
|
|
2441
|
-
});
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
results.push({
|
|
2445
|
-
component,
|
|
2446
|
-
variant,
|
|
2447
|
-
theme,
|
|
2448
|
-
result: diffResult
|
|
2449
|
-
});
|
|
2450
|
-
} catch (error) {
|
|
2451
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2452
|
-
console.log(` ${pc2.red("!")} ${component}/${variant} ${pc2.dim(errorMsg)}`);
|
|
2453
|
-
failed++;
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
} finally {
|
|
2457
|
-
await pool.shutdown();
|
|
2458
|
-
}
|
|
2459
|
-
const totalTimeMs = Date.now() - startTime;
|
|
2460
|
-
const total = passed + failed + missing;
|
|
2461
|
-
console.log();
|
|
2462
|
-
if (failed === 0 && missing === 0) {
|
|
2463
|
-
console.log(pc2.green(`\u2713 All ${passed} variant(s) match baselines`));
|
|
2464
|
-
} else if (failed > 0) {
|
|
2465
|
-
console.log(pc2.red(`\u2717 ${failed} variant(s) differ from baselines`));
|
|
2466
|
-
}
|
|
2467
|
-
if (missing > 0) {
|
|
2468
|
-
console.log(pc2.yellow(` ${missing} variant(s) have no baseline (run \`${BRAND.cliCommand} screenshot\`)`));
|
|
2469
|
-
}
|
|
2470
|
-
console.log(pc2.dim(` Completed in ${formatMs(totalTimeMs)}
|
|
2471
|
-
`));
|
|
2472
|
-
const success = failed === 0;
|
|
2473
|
-
return {
|
|
2474
|
-
success,
|
|
2475
|
-
total,
|
|
2476
|
-
passed,
|
|
2477
|
-
failed,
|
|
2478
|
-
missing,
|
|
2479
|
-
results,
|
|
2480
|
-
totalTimeMs
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
// src/analyze.ts
|
|
2485
|
-
import { existsSync as existsSync7 } from "fs";
|
|
2486
|
-
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2487
|
-
import { join as join6, dirname as dirname4 } from "path";
|
|
2488
|
-
import pc3 from "picocolors";
|
|
2489
|
-
async function runAnalyzeCommand(config, configDir, options = {}) {
|
|
2490
|
-
const format = options.format ?? "html";
|
|
2491
|
-
const minScore = options.minScore ?? 0;
|
|
2492
|
-
console.log(pc3.cyan(`
|
|
2493
|
-
${BRAND.name} Analyzer
|
|
2494
|
-
`));
|
|
2495
|
-
const fragmentsPath = join6(configDir, config.outFile ?? "fragments.json");
|
|
2496
|
-
if (!existsSync7(fragmentsPath)) {
|
|
2497
|
-
console.log(pc3.red(`\u2717 No fragments.json found. Run \`${BRAND.cliCommand} build\` first.
|
|
2498
|
-
`));
|
|
2499
|
-
return {
|
|
2500
|
-
success: false,
|
|
2501
|
-
analytics: createEmptyAnalytics()
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
console.log(pc3.dim("Analyzing design system...\n"));
|
|
2505
|
-
const content = await readFile4(fragmentsPath, "utf-8");
|
|
2506
|
-
const data = JSON.parse(content);
|
|
2507
|
-
const analytics = analyzeDesignSystem(data);
|
|
2508
|
-
printConsoleSummary(analytics);
|
|
2509
|
-
let outputPath;
|
|
2510
|
-
if (format === "html" || format === "json") {
|
|
2511
|
-
outputPath = options.output ?? getDefaultOutputPath(format, configDir);
|
|
2512
|
-
await mkdir2(dirname4(outputPath), { recursive: true });
|
|
2513
|
-
if (format === "html") {
|
|
2514
|
-
const html = generateHtmlReport(analytics);
|
|
2515
|
-
await writeFile2(outputPath, html);
|
|
2516
|
-
console.log(pc3.green(`\u2713 Report generated: ${outputPath}
|
|
2517
|
-
`));
|
|
2518
|
-
} else {
|
|
2519
|
-
await writeFile2(outputPath, JSON.stringify(analytics, null, 2));
|
|
2520
|
-
console.log(pc3.green(`\u2713 JSON report generated: ${outputPath}
|
|
2521
|
-
`));
|
|
2522
|
-
}
|
|
2523
|
-
if (options.open && format === "html") {
|
|
2524
|
-
await openInBrowser(outputPath);
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
const passedCi = analytics.summary.overallScore >= minScore;
|
|
2528
|
-
if (options.ci) {
|
|
2529
|
-
if (passedCi) {
|
|
2530
|
-
console.log(
|
|
2531
|
-
pc3.green(`\u2713 Score ${analytics.summary.overallScore} meets minimum threshold ${minScore}
|
|
2532
|
-
`)
|
|
2533
|
-
);
|
|
2534
|
-
} else {
|
|
2535
|
-
console.log(
|
|
2536
|
-
pc3.red(
|
|
2537
|
-
`\u2717 Score ${analytics.summary.overallScore} below minimum threshold ${minScore}
|
|
2538
|
-
`
|
|
2539
|
-
)
|
|
2540
|
-
);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
return {
|
|
2544
|
-
success: !options.ci || passedCi,
|
|
2545
|
-
analytics,
|
|
2546
|
-
outputPath
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
function printConsoleSummary(analytics) {
|
|
2550
|
-
const { summary, coverage, recommendations } = analytics;
|
|
2551
|
-
const grade = getGrade(summary.overallScore);
|
|
2552
|
-
console.log(
|
|
2553
|
-
pc3.bold(
|
|
2554
|
-
`Overall Score: ${colorizeScore(summary.overallScore)} (${grade})
|
|
2555
|
-
`
|
|
2556
|
-
)
|
|
2557
|
-
);
|
|
2558
|
-
console.log(pc3.dim("Summary"));
|
|
2559
|
-
console.log(` Components: ${pc3.white(summary.totalComponents.toString())}`);
|
|
2560
|
-
console.log(` Variants: ${pc3.white(summary.totalVariants.toString())}`);
|
|
2561
|
-
console.log(` Props: ${pc3.white(summary.totalProps.toString())}`);
|
|
2562
|
-
console.log(` Categories: ${pc3.white(summary.categories.join(", "))}`);
|
|
2563
|
-
console.log();
|
|
2564
|
-
console.log(pc3.dim("Coverage"));
|
|
2565
|
-
console.log(` Description: ${formatCoverage(coverage.fields.description)}`);
|
|
2566
|
-
console.log(` Usage when: ${formatCoverage(coverage.fields.usageWhen)}`);
|
|
2567
|
-
console.log(` Usage whenNot:${formatCoverage(coverage.fields.usageWhenNot)}`);
|
|
2568
|
-
console.log(` Guidelines: ${formatCoverage(coverage.fields.guidelines)}`);
|
|
2569
|
-
console.log(` Relations: ${formatCoverage(coverage.fields.relations)}`);
|
|
2570
|
-
console.log();
|
|
2571
|
-
if (recommendations.length > 0) {
|
|
2572
|
-
console.log(pc3.dim("Top Recommendations"));
|
|
2573
|
-
for (const rec of recommendations.slice(0, 3)) {
|
|
2574
|
-
const priority = rec.priority === "high" ? pc3.red(`[${rec.priority}]`) : rec.priority === "medium" ? pc3.yellow(`[${rec.priority}]`) : pc3.dim(`[${rec.priority}]`);
|
|
2575
|
-
console.log(` ${priority} ${rec.title}`);
|
|
2576
|
-
}
|
|
2577
|
-
console.log();
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
function formatCoverage(field) {
|
|
2581
|
-
const pct = colorizeScore(field.percentage);
|
|
2582
|
-
return `${pct} (${field.covered}/${field.total})`;
|
|
2583
|
-
}
|
|
2584
|
-
function colorizeScore(score) {
|
|
2585
|
-
if (score >= 80) return pc3.green(`${score}%`);
|
|
2586
|
-
if (score >= 60) return pc3.yellow(`${score}%`);
|
|
2587
|
-
return pc3.red(`${score}%`);
|
|
2588
|
-
}
|
|
2589
|
-
function getDefaultOutputPath(format, configDir) {
|
|
2590
|
-
const filename = format === "html" ? "fragments-report.html" : "fragments-report.json";
|
|
2591
|
-
return join6(configDir, filename);
|
|
2592
|
-
}
|
|
2593
|
-
async function openInBrowser(path) {
|
|
2594
|
-
const { platform } = await import("os");
|
|
2595
|
-
const { exec } = await import("child_process");
|
|
2596
|
-
const os = platform();
|
|
2597
|
-
const cmd = os === "darwin" ? `open "${path}"` : os === "win32" ? `start "" "${path}"` : `xdg-open "${path}"`;
|
|
2598
|
-
exec(cmd);
|
|
2599
|
-
}
|
|
2600
|
-
function createEmptyAnalytics() {
|
|
2601
|
-
return {
|
|
2602
|
-
analyzedAt: /* @__PURE__ */ new Date(),
|
|
2603
|
-
summary: {
|
|
2604
|
-
totalComponents: 0,
|
|
2605
|
-
totalVariants: 0,
|
|
2606
|
-
totalProps: 0,
|
|
2607
|
-
categories: [],
|
|
2608
|
-
overallScore: 0
|
|
2609
|
-
},
|
|
2610
|
-
inventory: {
|
|
2611
|
-
byCategory: {},
|
|
2612
|
-
byStatus: {},
|
|
2613
|
-
byVariantCount: [],
|
|
2614
|
-
byPropCount: []
|
|
2615
|
-
},
|
|
2616
|
-
coverage: {
|
|
2617
|
-
overall: 0,
|
|
2618
|
-
fields: {
|
|
2619
|
-
description: { covered: 0, total: 0, percentage: 0 },
|
|
2620
|
-
usageWhen: { covered: 0, total: 0, percentage: 0 },
|
|
2621
|
-
usageWhenNot: { covered: 0, total: 0, percentage: 0 },
|
|
2622
|
-
guidelines: { covered: 0, total: 0, percentage: 0 },
|
|
2623
|
-
accessibility: { covered: 0, total: 0, percentage: 0 },
|
|
2624
|
-
relations: { covered: 0, total: 0, percentage: 0 },
|
|
2625
|
-
propDescriptions: { covered: 0, total: 0, percentage: 0 },
|
|
2626
|
-
propConstraints: { covered: 0, total: 0, percentage: 0 }
|
|
2627
|
-
},
|
|
2628
|
-
incomplete: []
|
|
2629
|
-
},
|
|
2630
|
-
quality: {
|
|
2631
|
-
missingWhenNot: [],
|
|
2632
|
-
isolated: [],
|
|
2633
|
-
deprecated: [],
|
|
2634
|
-
fewVariants: [],
|
|
2635
|
-
undocumentedProps: [],
|
|
2636
|
-
unconstrainedProps: []
|
|
2637
|
-
},
|
|
2638
|
-
distribution: {
|
|
2639
|
-
variantsPerComponent: [],
|
|
2640
|
-
propsPerComponent: [],
|
|
2641
|
-
componentsPerCategory: [],
|
|
2642
|
-
statusDistribution: [],
|
|
2643
|
-
tagFrequency: []
|
|
2644
|
-
},
|
|
2645
|
-
recommendations: []
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
export {
|
|
2650
|
-
resolveComponentSourcePath,
|
|
2651
|
-
validateSchema,
|
|
2652
|
-
validateCoverage,
|
|
2653
|
-
validateAll,
|
|
2654
|
-
validateSnippets,
|
|
2655
|
-
validateDrift,
|
|
2656
|
-
measureBundleSizes,
|
|
2657
|
-
toPerformanceData,
|
|
2658
|
-
buildFragments,
|
|
2659
|
-
buildFragmentsDir,
|
|
2660
|
-
runScreenshotCommand,
|
|
2661
|
-
runDiffCommand,
|
|
2662
|
-
runAnalyzeCommand
|
|
2663
|
-
};
|
|
2664
|
-
//# sourceMappingURL=chunk-7WHVW72L.js.map
|