@fragments-sdk/cli 0.7.4 → 0.7.5
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/LICENSE +1 -4
- package/dist/bin.js +33 -14
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-NEJ2FBTN.js → chunk-CR3XHBGM.js} +2 -2
- package/dist/{chunk-S56I5FST.js → chunk-EFQ7SIBX.js} +582 -107
- package/dist/chunk-EFQ7SIBX.js.map +1 -0
- package/dist/{chunk-UXLGIGSX.js → chunk-GIC3I2KZ.js} +2 -2
- package/dist/{chunk-R6IZZSE7.js → chunk-JZNATKQA.js} +9 -3
- package/dist/chunk-JZNATKQA.js.map +1 -0
- package/dist/{chunk-P33AKQJW.js → chunk-SFWZ4K7C.js} +8 -2
- package/dist/{chunk-P33AKQJW.js.map → chunk-SFWZ4K7C.js.map} +1 -1
- package/dist/{core-3NMNCLFW.js → core-T7BDYEGO.js} +3 -3
- package/dist/{generate-23VLX7QN.js → generate-C2DKFCFJ.js} +4 -4
- package/dist/index.d.ts +28 -2
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/{init-VYVYMVHH.js → init-O3FCHEPN.js} +22 -6
- package/dist/init-O3FCHEPN.js.map +1 -0
- package/dist/mcp-bin.js +3 -3
- package/dist/{scan-FZR6YVI5.js → scan-IYTZDUKG.js} +5 -5
- package/dist/{service-CFFBHW4X.js → service-VA6XKADO.js} +3 -3
- package/dist/{static-viewer-VA2JXSCX.js → static-viewer-5N42MBDR.js} +3 -3
- package/dist/{test-VTD7R6G2.js → test-OMMDWL2W.js} +3 -3
- package/dist/{tokens-7JA5CPDL.js → tokens-6VJAHFIG.js} +4 -4
- package/dist/{viewer-WXTDDQGK.js → viewer-IVP5XC7U.js} +22 -14
- package/dist/viewer-IVP5XC7U.js.map +1 -0
- package/package.json +4 -2
- package/src/bin.ts +4 -0
- package/src/commands/add.ts +6 -0
- package/src/commands/init.ts +18 -2
- package/src/commands/validate.ts +24 -2
- package/src/core/config.ts +6 -0
- package/src/core/index.ts +1 -0
- package/src/core/schema.ts +6 -0
- package/src/core/types.ts +21 -0
- package/src/index.ts +2 -1
- package/src/service/snippet-validation.test.ts +209 -0
- package/src/service/snippet-validation.ts +635 -0
- package/src/validators.ts +53 -5
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -0
- package/src/viewer/components/CodePanel.naming.test.tsx +60 -0
- package/src/viewer/components/CodePanel.tsx +76 -468
- package/src/viewer/components/Layout.tsx +1 -1
- package/src/viewer/utils/a11y-fixes.ts +24 -9
- package/src/viewer/vite-plugin.ts +9 -1
- package/dist/chunk-R6IZZSE7.js.map +0 -1
- package/dist/chunk-S56I5FST.js.map +0 -1
- package/dist/init-VYVYMVHH.js.map +0 -1
- package/dist/viewer-WXTDDQGK.js.map +0 -1
- /package/dist/{chunk-NEJ2FBTN.js.map → chunk-CR3XHBGM.js.map} +0 -0
- /package/dist/{chunk-UXLGIGSX.js.map → chunk-GIC3I2KZ.js.map} +0 -0
- /package/dist/{core-3NMNCLFW.js.map → core-T7BDYEGO.js.map} +0 -0
- /package/dist/{generate-23VLX7QN.js.map → generate-C2DKFCFJ.js.map} +0 -0
- /package/dist/{scan-FZR6YVI5.js.map → scan-IYTZDUKG.js.map} +0 -0
- /package/dist/{service-CFFBHW4X.js.map → service-VA6XKADO.js.map} +0 -0
- /package/dist/{static-viewer-VA2JXSCX.js.map → static-viewer-5N42MBDR.js.map} +0 -0
- /package/dist/{test-VTD7R6G2.js.map → test-OMMDWL2W.js.map} +0 -0
- /package/dist/{tokens-7JA5CPDL.js.map → tokens-6VJAHFIG.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
generateRegistry,
|
|
5
5
|
loadFragmentFile,
|
|
6
6
|
parseFragmentFile
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-JZNATKQA.js";
|
|
8
8
|
import {
|
|
9
9
|
discoverBlockFiles,
|
|
10
10
|
discoverComponentFiles,
|
|
@@ -25,15 +25,464 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
compileBlock,
|
|
27
27
|
parseTokenFile
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-GIC3I2KZ.js";
|
|
29
29
|
import {
|
|
30
30
|
fragmentDefinitionSchema
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-SFWZ4K7C.js";
|
|
32
32
|
import {
|
|
33
33
|
BRAND,
|
|
34
34
|
DEFAULTS
|
|
35
35
|
} from "./chunk-EKLMXTWU.js";
|
|
36
36
|
|
|
37
|
+
// src/service/snippet-validation.ts
|
|
38
|
+
import ts from "typescript";
|
|
39
|
+
import { readFile } from "fs/promises";
|
|
40
|
+
import { existsSync } from "fs";
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
var INTRINSIC_TAGS = /* @__PURE__ */ new Set([
|
|
43
|
+
"div",
|
|
44
|
+
"span",
|
|
45
|
+
"p",
|
|
46
|
+
"h1",
|
|
47
|
+
"h2",
|
|
48
|
+
"h3",
|
|
49
|
+
"h4",
|
|
50
|
+
"h5",
|
|
51
|
+
"h6",
|
|
52
|
+
"main",
|
|
53
|
+
"section",
|
|
54
|
+
"article",
|
|
55
|
+
"aside",
|
|
56
|
+
"nav",
|
|
57
|
+
"header",
|
|
58
|
+
"footer",
|
|
59
|
+
"ul",
|
|
60
|
+
"ol",
|
|
61
|
+
"li",
|
|
62
|
+
"button",
|
|
63
|
+
"input",
|
|
64
|
+
"textarea",
|
|
65
|
+
"label",
|
|
66
|
+
"svg",
|
|
67
|
+
"path"
|
|
68
|
+
]);
|
|
69
|
+
var JSX_TAG_PATTERN = /<\s*([A-Za-z][A-Za-z0-9.]*)\b/g;
|
|
70
|
+
var STYLE_PATTERN = /\bstyle\s*=\s*\{/;
|
|
71
|
+
var TRANSPILED_PATTERN = /jsxDEV|_jsx|@__PURE__|\bfileName\s*:|\blineNumber\s*:|\bcolumnNumber\s*:/;
|
|
72
|
+
var ALIAS_DRIFT_PATTERN = /<\s*[A-Z][A-Za-z0-9]*(?:Root|2)\b/;
|
|
73
|
+
var HAS_IMPORT_PATTERN = /\bimport\s+[^;]+\s+from\s+['"][^'"]+['"]/;
|
|
74
|
+
var HAS_JSX_PATTERN = /<\s*[A-Za-z][A-Za-z0-9.]*\b/;
|
|
75
|
+
var DEFAULT_POLICY = {
|
|
76
|
+
mode: "warn",
|
|
77
|
+
scope: "snippet+render",
|
|
78
|
+
requireFullSnippet: true,
|
|
79
|
+
allowedExternalModules: /* @__PURE__ */ new Set([
|
|
80
|
+
"@phosphor-icons/react",
|
|
81
|
+
"recharts",
|
|
82
|
+
"react-day-picker"
|
|
83
|
+
])
|
|
84
|
+
};
|
|
85
|
+
function normalizePolicy(configured, overrides) {
|
|
86
|
+
const fromConfig = {
|
|
87
|
+
mode: configured?.mode ?? DEFAULT_POLICY.mode,
|
|
88
|
+
scope: configured?.scope ?? DEFAULT_POLICY.scope,
|
|
89
|
+
requireFullSnippet: configured?.requireFullSnippet ?? DEFAULT_POLICY.requireFullSnippet,
|
|
90
|
+
allowedExternalModules: new Set(configured?.allowedExternalModules ?? [...DEFAULT_POLICY.allowedExternalModules]),
|
|
91
|
+
componentStart: overrides.componentStart,
|
|
92
|
+
componentLimit: overrides.componentLimit
|
|
93
|
+
};
|
|
94
|
+
if (overrides.mode) fromConfig.mode = overrides.mode;
|
|
95
|
+
if (overrides.scope) fromConfig.scope = overrides.scope;
|
|
96
|
+
if (typeof overrides.requireFullSnippet === "boolean") {
|
|
97
|
+
fromConfig.requireFullSnippet = overrides.requireFullSnippet;
|
|
98
|
+
}
|
|
99
|
+
if (overrides.allowedExternalModules && overrides.allowedExternalModules.length > 0) {
|
|
100
|
+
fromConfig.allowedExternalModules = new Set(overrides.allowedExternalModules);
|
|
101
|
+
}
|
|
102
|
+
return fromConfig;
|
|
103
|
+
}
|
|
104
|
+
function isFragmentsModule(modulePath) {
|
|
105
|
+
return modulePath === "@fragments-sdk/ui" || modulePath === "@fragments/ui" || modulePath === "." || modulePath === ".." || modulePath.startsWith("@/components/") || modulePath.startsWith("@components/") || modulePath.startsWith("./") || modulePath.startsWith("../");
|
|
106
|
+
}
|
|
107
|
+
function collectSourceContext(sourceFile) {
|
|
108
|
+
const imports = /* @__PURE__ */ new Map();
|
|
109
|
+
const localComponents = /* @__PURE__ */ new Set();
|
|
110
|
+
function markLocal(name) {
|
|
111
|
+
if (!name) return;
|
|
112
|
+
if (/^[A-Z]/.test(name)) {
|
|
113
|
+
localComponents.add(name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function visit(node) {
|
|
117
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
118
|
+
const modulePath = node.moduleSpecifier.text;
|
|
119
|
+
const clause = node.importClause;
|
|
120
|
+
if (clause?.name) {
|
|
121
|
+
imports.set(clause.name.text, modulePath);
|
|
122
|
+
}
|
|
123
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
124
|
+
for (const item of clause.namedBindings.elements) {
|
|
125
|
+
imports.set(item.name.text, modulePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
130
|
+
markLocal(node.name?.text);
|
|
131
|
+
}
|
|
132
|
+
if (ts.isClassDeclaration(node)) {
|
|
133
|
+
markLocal(node.name?.text);
|
|
134
|
+
}
|
|
135
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
136
|
+
markLocal(node.name.text);
|
|
137
|
+
}
|
|
138
|
+
ts.forEachChild(node, visit);
|
|
139
|
+
}
|
|
140
|
+
visit(sourceFile);
|
|
141
|
+
return { imports, localComponents };
|
|
142
|
+
}
|
|
143
|
+
function getJsxTags(code) {
|
|
144
|
+
const tags = [];
|
|
145
|
+
JSX_TAG_PATTERN.lastIndex = 0;
|
|
146
|
+
let match;
|
|
147
|
+
while ((match = JSX_TAG_PATTERN.exec(code)) !== null) {
|
|
148
|
+
tags.push(match[1]);
|
|
149
|
+
}
|
|
150
|
+
return tags;
|
|
151
|
+
}
|
|
152
|
+
function rootTagName(tag) {
|
|
153
|
+
return tag.split(".")[0];
|
|
154
|
+
}
|
|
155
|
+
function parseSnippetImports(snippet) {
|
|
156
|
+
const sourceFile = ts.createSourceFile(
|
|
157
|
+
"snippet.tsx",
|
|
158
|
+
snippet,
|
|
159
|
+
ts.ScriptTarget.Latest,
|
|
160
|
+
true,
|
|
161
|
+
ts.ScriptKind.TSX
|
|
162
|
+
);
|
|
163
|
+
return collectSourceContext(sourceFile).imports;
|
|
164
|
+
}
|
|
165
|
+
function findDefineCall(sourceFile, name) {
|
|
166
|
+
let result = null;
|
|
167
|
+
function visit(node) {
|
|
168
|
+
if (result) return;
|
|
169
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name) {
|
|
170
|
+
result = node;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
ts.forEachChild(node, visit);
|
|
174
|
+
}
|
|
175
|
+
visit(sourceFile);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
function findProperty(obj, propertyName) {
|
|
179
|
+
for (const prop of obj.properties) {
|
|
180
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
181
|
+
if (!ts.isIdentifier(prop.name)) continue;
|
|
182
|
+
if (prop.name.text === propertyName) {
|
|
183
|
+
return prop.initializer;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function readStaticString(expr) {
|
|
189
|
+
if (!expr) return null;
|
|
190
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
191
|
+
return expr.text;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function readRenderBody(renderExpr, sourceFile) {
|
|
196
|
+
if (!ts.isArrowFunction(renderExpr) && !ts.isFunctionExpression(renderExpr)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const body = renderExpr.body;
|
|
200
|
+
const start = body.getStart(sourceFile);
|
|
201
|
+
const end = body.getEnd();
|
|
202
|
+
return sourceFile.text.slice(start, end).trim();
|
|
203
|
+
}
|
|
204
|
+
function report(issues, file, message) {
|
|
205
|
+
issues.push({ file, message });
|
|
206
|
+
}
|
|
207
|
+
function validateRawRules(issues, file, label, code) {
|
|
208
|
+
if (STYLE_PATTERN.test(code)) {
|
|
209
|
+
report(issues, file, `${label}: inline style usage is not allowed; use Box/Stack/Text props.`);
|
|
210
|
+
}
|
|
211
|
+
if (TRANSPILED_PATTERN.test(code)) {
|
|
212
|
+
report(issues, file, `${label}: transpiler output detected (jsxDEV/_jsx/@__PURE__). Use authored snippet source.`);
|
|
213
|
+
}
|
|
214
|
+
if (ALIAS_DRIFT_PATTERN.test(code)) {
|
|
215
|
+
report(issues, file, `${label}: alias drift tag detected (*Root/*2). Use canonical component names.`);
|
|
216
|
+
}
|
|
217
|
+
const tags = getJsxTags(code);
|
|
218
|
+
const intrinsic = tags.map((tag) => rootTagName(tag)).filter((tag) => /^[a-z]/.test(tag)).map((tag) => tag.toLowerCase()).filter((tag) => INTRINSIC_TAGS.has(tag));
|
|
219
|
+
if (intrinsic.length > 0) {
|
|
220
|
+
const names = [...new Set(intrinsic)].sort().join(", ");
|
|
221
|
+
report(issues, file, `${label}: raw HTML tags are not allowed (${names}). Use Fragments primitives.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function validateComponentAllowlist(issues, file, label, code, imports, localComponents, policy) {
|
|
225
|
+
const tags = getJsxTags(code);
|
|
226
|
+
const seen = /* @__PURE__ */ new Set();
|
|
227
|
+
for (const tag of tags) {
|
|
228
|
+
const root = rootTagName(tag);
|
|
229
|
+
if (seen.has(root)) continue;
|
|
230
|
+
seen.add(root);
|
|
231
|
+
if (!/^[A-Z]/.test(root)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const modulePath = imports.get(root);
|
|
235
|
+
if (modulePath) {
|
|
236
|
+
if (isFragmentsModule(modulePath)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (policy.allowedExternalModules.has(modulePath)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
report(
|
|
243
|
+
issues,
|
|
244
|
+
file,
|
|
245
|
+
`${label}: component "${root}" comes from "${modulePath}" and is not in snippets.allowedExternalModules.`
|
|
246
|
+
);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (localComponents.has(root)) {
|
|
250
|
+
report(
|
|
251
|
+
issues,
|
|
252
|
+
file,
|
|
253
|
+
`${label}: locally defined JSX component "${root}" is not allowed in snippets/renders. Import approved components instead.`
|
|
254
|
+
);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
report(
|
|
258
|
+
issues,
|
|
259
|
+
file,
|
|
260
|
+
`${label}: component "${root}" is used without an import and is not allowed.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function validateSnippetString(issues, file, label, snippet, policy) {
|
|
265
|
+
validateRawRules(issues, file, label, snippet);
|
|
266
|
+
if (policy.requireFullSnippet) {
|
|
267
|
+
if (!HAS_IMPORT_PATTERN.test(snippet)) {
|
|
268
|
+
report(issues, file, `${label}: full snippet required (missing import statement).`);
|
|
269
|
+
}
|
|
270
|
+
if (!HAS_JSX_PATTERN.test(snippet)) {
|
|
271
|
+
report(issues, file, `${label}: full snippet required (missing JSX usage).`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const imports = parseSnippetImports(snippet);
|
|
275
|
+
validateComponentAllowlist(issues, file, label, snippet, imports, /* @__PURE__ */ new Set(), policy);
|
|
276
|
+
}
|
|
277
|
+
function validateFragmentSource(sourceFile, file, policy, issues) {
|
|
278
|
+
const context = collectSourceContext(sourceFile);
|
|
279
|
+
const defineCall = findDefineCall(sourceFile, "defineFragment");
|
|
280
|
+
if (!defineCall) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const arg = defineCall.arguments[0];
|
|
284
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const variantsExpr = findProperty(arg, "variants");
|
|
288
|
+
if (!variantsExpr || !ts.isArrayLiteralExpression(variantsExpr)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
for (const variantExpr of variantsExpr.elements) {
|
|
292
|
+
if (!ts.isObjectLiteralExpression(variantExpr)) continue;
|
|
293
|
+
const name = readStaticString(findProperty(variantExpr, "name")) ?? "Unknown";
|
|
294
|
+
const labelPrefix = `variant "${name}"`;
|
|
295
|
+
const codeExpr = findProperty(variantExpr, "code");
|
|
296
|
+
const snippet = readStaticString(codeExpr);
|
|
297
|
+
if (snippet) {
|
|
298
|
+
validateSnippetString(issues, file, `${labelPrefix} snippet`, snippet, policy);
|
|
299
|
+
} else {
|
|
300
|
+
report(issues, file, `${labelPrefix}: missing explicit code snippet (variant.code).`);
|
|
301
|
+
}
|
|
302
|
+
if (policy.scope === "snippet+render") {
|
|
303
|
+
const renderExpr = findProperty(variantExpr, "render");
|
|
304
|
+
if (renderExpr) {
|
|
305
|
+
const renderBody = readRenderBody(renderExpr, sourceFile);
|
|
306
|
+
if (!renderBody) {
|
|
307
|
+
report(issues, file, `${labelPrefix} render: expected a static render function.`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
validateRawRules(issues, file, `${labelPrefix} render`, renderBody);
|
|
311
|
+
validateComponentAllowlist(
|
|
312
|
+
issues,
|
|
313
|
+
file,
|
|
314
|
+
`${labelPrefix} render`,
|
|
315
|
+
renderBody,
|
|
316
|
+
context.imports,
|
|
317
|
+
context.localComponents,
|
|
318
|
+
policy
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function validateBlockSource(sourceFile, file, policy, issues) {
|
|
325
|
+
const defineCall = findDefineCall(sourceFile, "defineBlock");
|
|
326
|
+
if (!defineCall) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const arg = defineCall.arguments[0];
|
|
330
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const codeExpr = findProperty(arg, "code");
|
|
334
|
+
const snippet = readStaticString(codeExpr);
|
|
335
|
+
if (!snippet) {
|
|
336
|
+
report(issues, file, "block snippet: missing static code string.");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
validateSnippetString(issues, file, "block snippet", snippet, policy);
|
|
340
|
+
}
|
|
341
|
+
function validateBlockPreviewExamples(sourceFile, file, policy, issues) {
|
|
342
|
+
if (policy.scope !== "snippet+render") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const code = sourceFile.text;
|
|
346
|
+
const context = collectSourceContext(sourceFile);
|
|
347
|
+
validateRawRules(issues, file, "block preview render", code);
|
|
348
|
+
validateComponentAllowlist(
|
|
349
|
+
issues,
|
|
350
|
+
file,
|
|
351
|
+
"block preview render",
|
|
352
|
+
code,
|
|
353
|
+
context.imports,
|
|
354
|
+
context.localComponents,
|
|
355
|
+
policy
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
function sourceFileFromText(filePath, content) {
|
|
359
|
+
return ts.createSourceFile(
|
|
360
|
+
filePath,
|
|
361
|
+
content,
|
|
362
|
+
ts.ScriptTarget.Latest,
|
|
363
|
+
true,
|
|
364
|
+
ts.ScriptKind.TSX
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function sortAndFilterBatch(files, componentStart, componentLimit) {
|
|
368
|
+
const getComponentName = (relativePath) => {
|
|
369
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
370
|
+
const fileName = normalized.split("/").pop() ?? normalized;
|
|
371
|
+
if (fileName.endsWith(BRAND.fileExtension)) {
|
|
372
|
+
return fileName.slice(0, -BRAND.fileExtension.length);
|
|
373
|
+
}
|
|
374
|
+
return extractComponentName(relativePath);
|
|
375
|
+
};
|
|
376
|
+
const sorted = [...files].sort((a, b) => {
|
|
377
|
+
const nameA = getComponentName(a.relativePath).toLowerCase();
|
|
378
|
+
const nameB = getComponentName(b.relativePath).toLowerCase();
|
|
379
|
+
return nameA.localeCompare(nameB);
|
|
380
|
+
});
|
|
381
|
+
if (!componentStart && !componentLimit) {
|
|
382
|
+
return { selected: sorted };
|
|
383
|
+
}
|
|
384
|
+
const startName = componentStart?.toLowerCase();
|
|
385
|
+
let startIndex = 0;
|
|
386
|
+
if (startName) {
|
|
387
|
+
const foundIndex = sorted.findIndex((file) => getComponentName(file.relativePath).toLowerCase() === startName);
|
|
388
|
+
if (foundIndex === -1) {
|
|
389
|
+
return {
|
|
390
|
+
selected: [],
|
|
391
|
+
warning: `Component start "${componentStart}" not found for snippet validation batch.`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
startIndex = foundIndex;
|
|
395
|
+
}
|
|
396
|
+
const limit = componentLimit && componentLimit > 0 ? componentLimit : sorted.length;
|
|
397
|
+
return {
|
|
398
|
+
selected: sorted.slice(startIndex, startIndex + limit)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function findBlockPreviewExamplesFile(configDir) {
|
|
402
|
+
const candidates = [
|
|
403
|
+
join(configDir, "apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
|
|
404
|
+
join(configDir, "../apps/docs/src/app/(docs)/blocks/examples/index.tsx"),
|
|
405
|
+
join(configDir, "../../apps/docs/src/app/(docs)/blocks/examples/index.tsx")
|
|
406
|
+
];
|
|
407
|
+
for (const candidate of candidates) {
|
|
408
|
+
if (existsSync(candidate)) {
|
|
409
|
+
return candidate;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
function toValidationResult(policy, issues) {
|
|
415
|
+
if (policy.mode === "error") {
|
|
416
|
+
return {
|
|
417
|
+
errors: issues,
|
|
418
|
+
warnings: []
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
errors: [],
|
|
423
|
+
warnings: issues
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function validateSnippetPolicy(config, configDir, options = {}) {
|
|
427
|
+
const policy = normalizePolicy(config.snippets, options);
|
|
428
|
+
const issues = [];
|
|
429
|
+
const discovered = await discoverFragmentFiles(config, configDir);
|
|
430
|
+
const fragmentFiles = discovered.filter((file) => file.relativePath.endsWith(BRAND.fileExtension));
|
|
431
|
+
const batchResult = sortAndFilterBatch(fragmentFiles, policy.componentStart, policy.componentLimit);
|
|
432
|
+
if (batchResult.warning) {
|
|
433
|
+
issues.push({ file: "snippets", message: batchResult.warning });
|
|
434
|
+
}
|
|
435
|
+
for (const file of batchResult.selected) {
|
|
436
|
+
try {
|
|
437
|
+
const content = await readFile(file.absolutePath, "utf-8");
|
|
438
|
+
const sourceFile = sourceFileFromText(file.relativePath, content);
|
|
439
|
+
validateFragmentSource(sourceFile, file.relativePath, policy, issues);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
issues.push({
|
|
442
|
+
file: file.relativePath,
|
|
443
|
+
message: `Failed to validate fragment snippets: ${error instanceof Error ? error.message : String(error)}`
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const isBatchOnly = Boolean(policy.componentStart || policy.componentLimit);
|
|
448
|
+
if (!isBatchOnly) {
|
|
449
|
+
try {
|
|
450
|
+
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
451
|
+
for (const file of blockFiles) {
|
|
452
|
+
try {
|
|
453
|
+
const content = await readFile(file.absolutePath, "utf-8");
|
|
454
|
+
const sourceFile = sourceFileFromText(file.relativePath, content);
|
|
455
|
+
validateBlockSource(sourceFile, file.relativePath, policy, issues);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
issues.push({
|
|
458
|
+
file: file.relativePath,
|
|
459
|
+
message: `Failed to validate block snippets: ${error instanceof Error ? error.message : String(error)}`
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
issues.push({
|
|
465
|
+
file: "blocks",
|
|
466
|
+
message: `Failed to discover block files: ${error instanceof Error ? error.message : String(error)}`
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const blockPreviewFile = await findBlockPreviewExamplesFile(configDir);
|
|
470
|
+
if (blockPreviewFile) {
|
|
471
|
+
try {
|
|
472
|
+
const content = await readFile(blockPreviewFile, "utf-8");
|
|
473
|
+
const sourceFile = sourceFileFromText(blockPreviewFile, content);
|
|
474
|
+
validateBlockPreviewExamples(sourceFile, blockPreviewFile, policy, issues);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
issues.push({
|
|
477
|
+
file: blockPreviewFile,
|
|
478
|
+
message: `Failed to validate block preview examples: ${error instanceof Error ? error.message : String(error)}`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return toValidationResult(policy, issues);
|
|
484
|
+
}
|
|
485
|
+
|
|
37
486
|
// src/validators.ts
|
|
38
487
|
async function validateSchema(config, configDir) {
|
|
39
488
|
const files = await discoverFragmentFiles(config, configDir);
|
|
@@ -110,26 +559,51 @@ async function validateCoverage(config, configDir) {
|
|
|
110
559
|
warnings
|
|
111
560
|
};
|
|
112
561
|
}
|
|
113
|
-
async function validateAll(config, configDir) {
|
|
562
|
+
async function validateAll(config, configDir, options = {}) {
|
|
114
563
|
const [schemaResult, coverageResult] = await Promise.all([
|
|
115
564
|
validateSchema(config, configDir),
|
|
116
565
|
validateCoverage(config, configDir)
|
|
117
566
|
]);
|
|
567
|
+
if (options.snippets === false) {
|
|
568
|
+
return {
|
|
569
|
+
valid: schemaResult.valid && coverageResult.valid,
|
|
570
|
+
errors: [...schemaResult.errors, ...coverageResult.errors],
|
|
571
|
+
warnings: [...schemaResult.warnings, ...coverageResult.warnings]
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const snippetOptions = {
|
|
575
|
+
...options.snippetMode && { mode: options.snippetMode },
|
|
576
|
+
...options.componentStart && { componentStart: options.componentStart },
|
|
577
|
+
...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
|
|
578
|
+
};
|
|
579
|
+
const snippetResult = await validateSnippetPolicy(config, configDir, snippetOptions);
|
|
580
|
+
return {
|
|
581
|
+
valid: schemaResult.valid && coverageResult.valid && snippetResult.errors.length === 0,
|
|
582
|
+
errors: [...schemaResult.errors, ...coverageResult.errors, ...snippetResult.errors],
|
|
583
|
+
warnings: [...schemaResult.warnings, ...coverageResult.warnings, ...snippetResult.warnings]
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function validateSnippets(config, configDir, options = {}) {
|
|
587
|
+
const snippetResult = await validateSnippetPolicy(config, configDir, {
|
|
588
|
+
...options.snippetMode && { mode: options.snippetMode },
|
|
589
|
+
...options.componentStart && { componentStart: options.componentStart },
|
|
590
|
+
...typeof options.componentLimit === "number" ? { componentLimit: options.componentLimit } : {}
|
|
591
|
+
});
|
|
118
592
|
return {
|
|
119
|
-
valid:
|
|
120
|
-
errors:
|
|
121
|
-
warnings:
|
|
593
|
+
valid: snippetResult.errors.length === 0,
|
|
594
|
+
errors: snippetResult.errors,
|
|
595
|
+
warnings: snippetResult.warnings
|
|
122
596
|
};
|
|
123
597
|
}
|
|
124
598
|
|
|
125
599
|
// src/build.ts
|
|
126
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
127
|
-
import { resolve as resolve4, join as
|
|
128
|
-
import { existsSync as
|
|
600
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
601
|
+
import { resolve as resolve4, join as join4 } from "path";
|
|
602
|
+
import { existsSync as existsSync5 } from "fs";
|
|
129
603
|
|
|
130
604
|
// src/core/token-resolver.ts
|
|
131
605
|
import { resolve, dirname, basename } from "path";
|
|
132
|
-
import { existsSync, readdirSync } from "fs";
|
|
606
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
133
607
|
function roundRgbValues(value) {
|
|
134
608
|
return value.replace(
|
|
135
609
|
/rgb\(([^)]+)\)/g,
|
|
@@ -202,7 +676,7 @@ function findVariablesFile(tokensDir) {
|
|
|
202
676
|
const candidates = ["_variables.scss", "variables.scss"];
|
|
203
677
|
for (const name of candidates) {
|
|
204
678
|
const path = resolve(tokensDir, name);
|
|
205
|
-
if (
|
|
679
|
+
if (existsSync2(path)) {
|
|
206
680
|
return path;
|
|
207
681
|
}
|
|
208
682
|
}
|
|
@@ -220,14 +694,14 @@ function findVariablesFile(tokensDir) {
|
|
|
220
694
|
}
|
|
221
695
|
|
|
222
696
|
// src/core/auto-props.ts
|
|
223
|
-
import { existsSync as
|
|
224
|
-
import { dirname as dirname2, extname, join, resolve as resolve2 } from "path";
|
|
225
|
-
import
|
|
697
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
698
|
+
import { dirname as dirname2, extname, join as join2, resolve as resolve2 } from "path";
|
|
699
|
+
import ts2 from "typescript";
|
|
226
700
|
function toPosixPath(filePath) {
|
|
227
701
|
return filePath.replace(/\\/g, "/");
|
|
228
702
|
}
|
|
229
703
|
function isFile(filePath) {
|
|
230
|
-
if (!
|
|
704
|
+
if (!existsSync3(filePath)) return false;
|
|
231
705
|
try {
|
|
232
706
|
return statSync(filePath).isFile();
|
|
233
707
|
} catch {
|
|
@@ -245,10 +719,10 @@ function resolveModulePath(basePath) {
|
|
|
245
719
|
`${basePath}.ts`,
|
|
246
720
|
`${basePath}.jsx`,
|
|
247
721
|
`${basePath}.js`,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
722
|
+
join2(basePath, "index.tsx"),
|
|
723
|
+
join2(basePath, "index.ts"),
|
|
724
|
+
join2(basePath, "index.jsx"),
|
|
725
|
+
join2(basePath, "index.js")
|
|
252
726
|
);
|
|
253
727
|
}
|
|
254
728
|
for (const candidate of candidates) {
|
|
@@ -270,21 +744,21 @@ function collectTopLevelDeclarations(sourceFile) {
|
|
|
270
744
|
const functionDeclarations = /* @__PURE__ */ new Map();
|
|
271
745
|
const variableDeclarations = /* @__PURE__ */ new Map();
|
|
272
746
|
for (const node of sourceFile.statements) {
|
|
273
|
-
if (
|
|
747
|
+
if (ts2.isInterfaceDeclaration(node)) {
|
|
274
748
|
typeDeclarations.set(node.name.text, node);
|
|
275
749
|
continue;
|
|
276
750
|
}
|
|
277
|
-
if (
|
|
751
|
+
if (ts2.isTypeAliasDeclaration(node)) {
|
|
278
752
|
typeDeclarations.set(node.name.text, node);
|
|
279
753
|
continue;
|
|
280
754
|
}
|
|
281
|
-
if (
|
|
755
|
+
if (ts2.isFunctionDeclaration(node) && node.name) {
|
|
282
756
|
functionDeclarations.set(node.name.text, node);
|
|
283
757
|
continue;
|
|
284
758
|
}
|
|
285
|
-
if (
|
|
759
|
+
if (ts2.isVariableStatement(node)) {
|
|
286
760
|
for (const declaration of node.declarationList.declarations) {
|
|
287
|
-
if (
|
|
761
|
+
if (ts2.isIdentifier(declaration.name)) {
|
|
288
762
|
variableDeclarations.set(declaration.name.text, declaration);
|
|
289
763
|
}
|
|
290
764
|
}
|
|
@@ -293,16 +767,16 @@ function collectTopLevelDeclarations(sourceFile) {
|
|
|
293
767
|
return { typeDeclarations, functionDeclarations, variableDeclarations };
|
|
294
768
|
}
|
|
295
769
|
function readDefaultValue(expression) {
|
|
296
|
-
if (
|
|
770
|
+
if (ts2.isStringLiteral(expression) || ts2.isNoSubstitutionTemplateLiteral(expression)) {
|
|
297
771
|
return expression.text;
|
|
298
772
|
}
|
|
299
|
-
if (
|
|
773
|
+
if (ts2.isNumericLiteral(expression)) {
|
|
300
774
|
return Number(expression.text);
|
|
301
775
|
}
|
|
302
|
-
if (expression.kind ===
|
|
303
|
-
if (expression.kind ===
|
|
304
|
-
if (expression.kind ===
|
|
305
|
-
if (
|
|
776
|
+
if (expression.kind === ts2.SyntaxKind.TrueKeyword) return true;
|
|
777
|
+
if (expression.kind === ts2.SyntaxKind.FalseKeyword) return false;
|
|
778
|
+
if (expression.kind === ts2.SyntaxKind.NullKeyword) return null;
|
|
779
|
+
if (ts2.isPrefixUnaryExpression(expression) && expression.operator === ts2.SyntaxKind.MinusToken && ts2.isNumericLiteral(expression.operand)) {
|
|
306
780
|
return -Number(expression.operand.text);
|
|
307
781
|
}
|
|
308
782
|
return void 0;
|
|
@@ -311,14 +785,14 @@ function extractDefaultValues(componentNode) {
|
|
|
311
785
|
const defaults = {};
|
|
312
786
|
if (!componentNode?.parameters?.length) return defaults;
|
|
313
787
|
const firstParam = componentNode.parameters[0];
|
|
314
|
-
if (!
|
|
788
|
+
if (!ts2.isObjectBindingPattern(firstParam.name)) return defaults;
|
|
315
789
|
for (const element of firstParam.name.elements) {
|
|
316
790
|
let propName = null;
|
|
317
791
|
if (element.propertyName) {
|
|
318
|
-
if (
|
|
792
|
+
if (ts2.isIdentifier(element.propertyName) || ts2.isStringLiteral(element.propertyName)) {
|
|
319
793
|
propName = element.propertyName.text;
|
|
320
794
|
}
|
|
321
|
-
} else if (
|
|
795
|
+
} else if (ts2.isIdentifier(element.name)) {
|
|
322
796
|
propName = element.name.text;
|
|
323
797
|
}
|
|
324
798
|
if (!propName || !element.initializer) continue;
|
|
@@ -330,13 +804,13 @@ function extractDefaultValues(componentNode) {
|
|
|
330
804
|
return defaults;
|
|
331
805
|
}
|
|
332
806
|
function isNullishType(type) {
|
|
333
|
-
return (type.flags &
|
|
807
|
+
return (type.flags & ts2.TypeFlags.Null) !== 0 || (type.flags & ts2.TypeFlags.Undefined) !== 0 || (type.flags & ts2.TypeFlags.Void) !== 0;
|
|
334
808
|
}
|
|
335
809
|
function isBooleanLikeType(type) {
|
|
336
|
-
return (type.flags &
|
|
810
|
+
return (type.flags & ts2.TypeFlags.BooleanLike) !== 0 || type.flags === ts2.TypeFlags.BooleanLiteral;
|
|
337
811
|
}
|
|
338
812
|
function inferPropType(type, checker) {
|
|
339
|
-
const typeText = checker.typeToString(type, void 0,
|
|
813
|
+
const typeText = checker.typeToString(type, void 0, ts2.TypeFormatFlags.NoTruncation);
|
|
340
814
|
if (typeText.includes("ReactNode")) {
|
|
341
815
|
return { type: "node" };
|
|
342
816
|
}
|
|
@@ -354,7 +828,7 @@ function inferPropType(type, checker) {
|
|
|
354
828
|
if (nonNullableTypes.length === 1) {
|
|
355
829
|
return inferPropType(nonNullableTypes[0], checker);
|
|
356
830
|
}
|
|
357
|
-
const stringLiteralValues = nonNullableTypes.filter((unionType) => (unionType.flags &
|
|
831
|
+
const stringLiteralValues = nonNullableTypes.filter((unionType) => (unionType.flags & ts2.TypeFlags.StringLiteral) !== 0).map((unionType) => unionType.value);
|
|
358
832
|
if (stringLiteralValues.length > 0 && stringLiteralValues.length === nonNullableTypes.length) {
|
|
359
833
|
return { type: "enum", values: stringLiteralValues };
|
|
360
834
|
}
|
|
@@ -363,16 +837,16 @@ function inferPropType(type, checker) {
|
|
|
363
837
|
}
|
|
364
838
|
return { type: "union" };
|
|
365
839
|
}
|
|
366
|
-
if ((type.flags &
|
|
840
|
+
if ((type.flags & ts2.TypeFlags.StringLike) !== 0) {
|
|
367
841
|
return { type: "string" };
|
|
368
842
|
}
|
|
369
|
-
if ((type.flags &
|
|
843
|
+
if ((type.flags & ts2.TypeFlags.NumberLike) !== 0) {
|
|
370
844
|
return { type: "number" };
|
|
371
845
|
}
|
|
372
|
-
if ((type.flags &
|
|
846
|
+
if ((type.flags & ts2.TypeFlags.BooleanLike) !== 0) {
|
|
373
847
|
return { type: "boolean" };
|
|
374
848
|
}
|
|
375
|
-
if ((type.flags &
|
|
849
|
+
if ((type.flags & ts2.TypeFlags.Object) !== 0) {
|
|
376
850
|
return { type: "object" };
|
|
377
851
|
}
|
|
378
852
|
return { type: "custom" };
|
|
@@ -384,42 +858,42 @@ function resolveComponentSignature(exportName, declarations, sourceFile) {
|
|
|
384
858
|
componentNode: node
|
|
385
859
|
});
|
|
386
860
|
const resolveFromExpression = (expression) => {
|
|
387
|
-
if (
|
|
861
|
+
if (ts2.isParenthesizedExpression(expression)) {
|
|
388
862
|
return resolveFromExpression(expression.expression);
|
|
389
863
|
}
|
|
390
|
-
if (
|
|
864
|
+
if (ts2.isAsExpression(expression) || ts2.isTypeAssertionExpression(expression)) {
|
|
391
865
|
return resolveFromExpression(expression.expression);
|
|
392
866
|
}
|
|
393
|
-
if (
|
|
867
|
+
if (ts2.isArrowFunction(expression) || ts2.isFunctionExpression(expression)) {
|
|
394
868
|
return typeNodeFromFunction(expression);
|
|
395
869
|
}
|
|
396
|
-
if (
|
|
870
|
+
if (ts2.isIdentifier(expression)) {
|
|
397
871
|
return resolveFromIdentifier(expression.text);
|
|
398
872
|
}
|
|
399
|
-
if (
|
|
400
|
-
if (
|
|
873
|
+
if (ts2.isCallExpression(expression)) {
|
|
874
|
+
if (ts2.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "forwardRef") {
|
|
401
875
|
const forwardRefPropsType = expression.typeArguments?.[1] ?? null;
|
|
402
876
|
const innerArg = expression.arguments[0];
|
|
403
|
-
const inner = innerArg && (
|
|
877
|
+
const inner = innerArg && (ts2.isArrowFunction(innerArg) || ts2.isFunctionExpression(innerArg)) ? typeNodeFromFunction(innerArg) : innerArg && ts2.isIdentifier(innerArg) ? resolveFromIdentifier(innerArg.text) : { propsTypeNode: null, componentNode: null };
|
|
404
878
|
return {
|
|
405
879
|
propsTypeNode: forwardRefPropsType ?? inner.propsTypeNode,
|
|
406
880
|
componentNode: inner.componentNode
|
|
407
881
|
};
|
|
408
882
|
}
|
|
409
|
-
if (
|
|
883
|
+
if (ts2.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "memo" && expression.arguments[0]) {
|
|
410
884
|
return resolveFromExpression(expression.arguments[0]);
|
|
411
885
|
}
|
|
412
|
-
if (
|
|
886
|
+
if (ts2.isPropertyAccessExpression(expression.expression) && expression.expression.expression.getText(sourceFile) === "Object" && expression.expression.name.text === "assign" && expression.arguments[0]) {
|
|
413
887
|
return resolveFromExpression(expression.arguments[0]);
|
|
414
888
|
}
|
|
415
889
|
}
|
|
416
890
|
return { propsTypeNode: null, componentNode: null };
|
|
417
891
|
};
|
|
418
892
|
const resolveFromVariable = (declaration) => {
|
|
419
|
-
if (declaration.type &&
|
|
893
|
+
if (declaration.type && ts2.isTypeReferenceNode(declaration.type) && declaration.type.typeArguments?.length) {
|
|
420
894
|
const typeName = declaration.type.typeName.getText(sourceFile);
|
|
421
895
|
if (typeName.includes("FC") || typeName.includes("FunctionComponent")) {
|
|
422
|
-
const componentNode = declaration.initializer && (
|
|
896
|
+
const componentNode = declaration.initializer && (ts2.isArrowFunction(declaration.initializer) || ts2.isFunctionExpression(declaration.initializer)) ? declaration.initializer : null;
|
|
423
897
|
return {
|
|
424
898
|
propsTypeNode: declaration.type.typeArguments[0] ?? null,
|
|
425
899
|
componentNode
|
|
@@ -451,7 +925,7 @@ function resolveComponentSignature(exportName, declarations, sourceFile) {
|
|
|
451
925
|
function extractCustomPropsFromComponentFile(componentFilePath, exportName) {
|
|
452
926
|
const warnings = [];
|
|
453
927
|
const resolvedPath = resolve2(componentFilePath);
|
|
454
|
-
if (!
|
|
928
|
+
if (!existsSync3(resolvedPath)) {
|
|
455
929
|
return {
|
|
456
930
|
props: {},
|
|
457
931
|
warnings: [`Component file not found: ${resolvedPath}`],
|
|
@@ -459,17 +933,17 @@ function extractCustomPropsFromComponentFile(componentFilePath, exportName) {
|
|
|
459
933
|
};
|
|
460
934
|
}
|
|
461
935
|
const compilerOptions = {
|
|
462
|
-
target:
|
|
463
|
-
module:
|
|
464
|
-
moduleResolution:
|
|
465
|
-
jsx:
|
|
936
|
+
target: ts2.ScriptTarget.ESNext,
|
|
937
|
+
module: ts2.ModuleKind.ESNext,
|
|
938
|
+
moduleResolution: ts2.ModuleResolutionKind.Bundler,
|
|
939
|
+
jsx: ts2.JsxEmit.ReactJSX,
|
|
466
940
|
allowSyntheticDefaultImports: true,
|
|
467
941
|
esModuleInterop: true,
|
|
468
942
|
skipLibCheck: true,
|
|
469
943
|
strict: false,
|
|
470
944
|
noEmit: true
|
|
471
945
|
};
|
|
472
|
-
const program =
|
|
946
|
+
const program = ts2.createProgram([resolvedPath], compilerOptions);
|
|
473
947
|
const sourceFile = program.getSourceFile(resolvedPath);
|
|
474
948
|
if (!sourceFile) {
|
|
475
949
|
return {
|
|
@@ -506,11 +980,11 @@ function extractCustomPropsFromComponentFile(componentFilePath, exportName) {
|
|
|
506
980
|
}
|
|
507
981
|
const referenceNode = localDeclarations[0];
|
|
508
982
|
const inferredType = inferPropType(checker.getTypeOfSymbolAtLocation(symbol, referenceNode), checker);
|
|
509
|
-
const description =
|
|
983
|
+
const description = ts2.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
510
984
|
extractedProps[propName] = {
|
|
511
985
|
type: inferredType.type,
|
|
512
986
|
description,
|
|
513
|
-
required: (symbol.getFlags() &
|
|
987
|
+
required: (symbol.getFlags() & ts2.SymbolFlags.Optional) === 0,
|
|
514
988
|
...inferredType.values && { values: inferredType.values },
|
|
515
989
|
...defaultValues[propName] !== void 0 && { default: defaultValues[propName] }
|
|
516
990
|
};
|
|
@@ -526,9 +1000,9 @@ function extractCustomPropsFromComponentFile(componentFilePath, exportName) {
|
|
|
526
1000
|
}
|
|
527
1001
|
|
|
528
1002
|
// src/core/graph-extractor.ts
|
|
529
|
-
import
|
|
530
|
-
import { readFileSync, existsSync as
|
|
531
|
-
import { join as
|
|
1003
|
+
import ts3 from "typescript";
|
|
1004
|
+
import { readFileSync, existsSync as existsSync4 } from "fs";
|
|
1005
|
+
import { join as join3 } from "path";
|
|
532
1006
|
import { readdirSync as readdirSync2 } from "fs";
|
|
533
1007
|
import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from "@fragments-sdk/context/graph";
|
|
534
1008
|
async function buildComponentGraph(fragments, blocks, componentDir, options) {
|
|
@@ -621,17 +1095,17 @@ function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
|
621
1095
|
} catch {
|
|
622
1096
|
continue;
|
|
623
1097
|
}
|
|
624
|
-
const sourceFile =
|
|
1098
|
+
const sourceFile = ts3.createSourceFile(
|
|
625
1099
|
indexPath,
|
|
626
1100
|
sourceText,
|
|
627
|
-
|
|
1101
|
+
ts3.ScriptTarget.Latest,
|
|
628
1102
|
true,
|
|
629
|
-
indexPath.endsWith(".tsx") ?
|
|
1103
|
+
indexPath.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS
|
|
630
1104
|
);
|
|
631
1105
|
const visitNode = (node) => {
|
|
632
|
-
if (
|
|
1106
|
+
if (ts3.isImportDeclaration(node)) {
|
|
633
1107
|
const moduleSpecifier = node.moduleSpecifier;
|
|
634
|
-
if (
|
|
1108
|
+
if (ts3.isStringLiteral(moduleSpecifier)) {
|
|
635
1109
|
const importPath = moduleSpecifier.text;
|
|
636
1110
|
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
637
1111
|
const clause = node.importClause;
|
|
@@ -645,7 +1119,7 @@ function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
|
645
1119
|
provenance: `source:${componentName}/index.tsx`
|
|
646
1120
|
});
|
|
647
1121
|
}
|
|
648
|
-
if (clause.namedBindings &&
|
|
1122
|
+
if (clause.namedBindings && ts3.isNamedImports(clause.namedBindings)) {
|
|
649
1123
|
for (const element of clause.namedBindings.elements) {
|
|
650
1124
|
const name = element.name.text;
|
|
651
1125
|
if (isPascalCase(name) && knownComponents.has(name) && name !== componentName) {
|
|
@@ -663,7 +1137,7 @@ function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
|
663
1137
|
}
|
|
664
1138
|
}
|
|
665
1139
|
}
|
|
666
|
-
if (
|
|
1140
|
+
if (ts3.isCallExpression(node) && ts3.isIdentifier(node.expression)) {
|
|
667
1141
|
const callName = node.expression.text;
|
|
668
1142
|
const hookMatch = callName.match(/^use([A-Z][a-zA-Z]*)$/);
|
|
669
1143
|
if (hookMatch) {
|
|
@@ -679,9 +1153,9 @@ function extractImportAndHookEdges(componentDir, knownComponents) {
|
|
|
679
1153
|
}
|
|
680
1154
|
}
|
|
681
1155
|
}
|
|
682
|
-
|
|
1156
|
+
ts3.forEachChild(node, visitNode);
|
|
683
1157
|
};
|
|
684
|
-
|
|
1158
|
+
ts3.forEachChild(sourceFile, visitNode);
|
|
685
1159
|
}
|
|
686
1160
|
return edges;
|
|
687
1161
|
}
|
|
@@ -697,30 +1171,30 @@ function extractSubComponents(componentDir, knownComponents) {
|
|
|
697
1171
|
continue;
|
|
698
1172
|
}
|
|
699
1173
|
if (!sourceText.includes("Object.assign")) continue;
|
|
700
|
-
const sourceFile =
|
|
1174
|
+
const sourceFile = ts3.createSourceFile(
|
|
701
1175
|
indexPath,
|
|
702
1176
|
sourceText,
|
|
703
|
-
|
|
1177
|
+
ts3.ScriptTarget.Latest,
|
|
704
1178
|
true,
|
|
705
|
-
indexPath.endsWith(".tsx") ?
|
|
1179
|
+
indexPath.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS
|
|
706
1180
|
);
|
|
707
1181
|
const subComponents = [];
|
|
708
1182
|
const visitNode = (node) => {
|
|
709
|
-
if (
|
|
1183
|
+
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) {
|
|
710
1184
|
const propsArg = node.arguments[1];
|
|
711
|
-
if (
|
|
1185
|
+
if (ts3.isObjectLiteralExpression(propsArg)) {
|
|
712
1186
|
for (const prop of propsArg.properties) {
|
|
713
|
-
if (
|
|
1187
|
+
if (ts3.isShorthandPropertyAssignment(prop)) {
|
|
714
1188
|
subComponents.push(prop.name.text);
|
|
715
|
-
} else if (
|
|
1189
|
+
} else if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.name)) {
|
|
716
1190
|
subComponents.push(prop.name.text);
|
|
717
1191
|
}
|
|
718
1192
|
}
|
|
719
1193
|
}
|
|
720
1194
|
}
|
|
721
|
-
|
|
1195
|
+
ts3.forEachChild(node, visitNode);
|
|
722
1196
|
};
|
|
723
|
-
|
|
1197
|
+
ts3.forEachChild(sourceFile, visitNode);
|
|
724
1198
|
if (subComponents.length > 0) {
|
|
725
1199
|
result.set(componentName, subComponents);
|
|
726
1200
|
}
|
|
@@ -880,13 +1354,13 @@ function isPascalCase(name) {
|
|
|
880
1354
|
}
|
|
881
1355
|
function findComponentIndex(componentDir, componentName) {
|
|
882
1356
|
const candidates = [
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1357
|
+
join3(componentDir, componentName, "index.tsx"),
|
|
1358
|
+
join3(componentDir, componentName, "index.ts"),
|
|
1359
|
+
join3(componentDir, componentName, `${componentName}.tsx`),
|
|
1360
|
+
join3(componentDir, componentName, `${componentName}.ts`)
|
|
887
1361
|
];
|
|
888
1362
|
for (const candidate of candidates) {
|
|
889
|
-
if (
|
|
1363
|
+
if (existsSync4(candidate)) {
|
|
890
1364
|
return candidate;
|
|
891
1365
|
}
|
|
892
1366
|
}
|
|
@@ -895,11 +1369,11 @@ function findComponentIndex(componentDir, componentName) {
|
|
|
895
1369
|
for (const entry of entries) {
|
|
896
1370
|
if (entry.isDirectory() && entry.name === componentName) {
|
|
897
1371
|
const subCandidates = [
|
|
898
|
-
|
|
899
|
-
|
|
1372
|
+
join3(componentDir, entry.name, "index.tsx"),
|
|
1373
|
+
join3(componentDir, entry.name, "index.ts")
|
|
900
1374
|
];
|
|
901
1375
|
for (const sc of subCandidates) {
|
|
902
|
-
if (
|
|
1376
|
+
if (existsSync4(sc)) return sc;
|
|
903
1377
|
}
|
|
904
1378
|
}
|
|
905
1379
|
}
|
|
@@ -951,7 +1425,7 @@ async function buildFragments(config, configDir) {
|
|
|
951
1425
|
const fragments = {};
|
|
952
1426
|
for (const file of files) {
|
|
953
1427
|
try {
|
|
954
|
-
const content = await
|
|
1428
|
+
const content = await readFile2(file.absolutePath, "utf-8");
|
|
955
1429
|
const parsed = parseFragmentFile(content, file.relativePath);
|
|
956
1430
|
for (const warning of parsed.warnings) {
|
|
957
1431
|
warnings.push({ file: file.relativePath, warning });
|
|
@@ -1086,7 +1560,7 @@ async function buildFragments(config, configDir) {
|
|
|
1086
1560
|
let total = 0;
|
|
1087
1561
|
const fileContents = [];
|
|
1088
1562
|
for (const file of tokenFiles) {
|
|
1089
|
-
const content = await
|
|
1563
|
+
const content = await readFile2(file.absolutePath, "utf-8");
|
|
1090
1564
|
fileContents.push({ content, path: file.relativePath });
|
|
1091
1565
|
}
|
|
1092
1566
|
const allContent = fileContents.map((f) => f.content).join("\n");
|
|
@@ -1141,9 +1615,9 @@ async function buildFragments(config, configDir) {
|
|
|
1141
1615
|
}
|
|
1142
1616
|
let packageName;
|
|
1143
1617
|
const pkgJsonPath = resolve4(configDir, "package.json");
|
|
1144
|
-
if (
|
|
1618
|
+
if (existsSync5(pkgJsonPath)) {
|
|
1145
1619
|
try {
|
|
1146
|
-
const pkg = JSON.parse(await
|
|
1620
|
+
const pkg = JSON.parse(await readFile2(pkgJsonPath, "utf-8"));
|
|
1147
1621
|
if (pkg.name) packageName = pkg.name;
|
|
1148
1622
|
} catch {
|
|
1149
1623
|
}
|
|
@@ -1199,8 +1673,8 @@ async function buildFragments(config, configDir) {
|
|
|
1199
1673
|
};
|
|
1200
1674
|
}
|
|
1201
1675
|
async function buildFragmentsDir(config, configDir) {
|
|
1202
|
-
const fragmentsDir =
|
|
1203
|
-
const componentsDir =
|
|
1676
|
+
const fragmentsDir = join4(configDir, BRAND.dataDir);
|
|
1677
|
+
const componentsDir = join4(fragmentsDir, BRAND.componentsDir);
|
|
1204
1678
|
await mkdir(fragmentsDir, { recursive: true });
|
|
1205
1679
|
await mkdir(componentsDir, { recursive: true });
|
|
1206
1680
|
const registryResult = await generateRegistry({
|
|
@@ -1212,9 +1686,9 @@ async function buildFragmentsDir(config, configDir) {
|
|
|
1212
1686
|
});
|
|
1213
1687
|
const errors = [...registryResult.errors];
|
|
1214
1688
|
const warnings = [...registryResult.warnings];
|
|
1215
|
-
const indexPath =
|
|
1689
|
+
const indexPath = join4(fragmentsDir, "index.json");
|
|
1216
1690
|
await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
|
|
1217
|
-
const registryPath =
|
|
1691
|
+
const registryPath = join4(fragmentsDir, BRAND.registryFile);
|
|
1218
1692
|
await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
|
|
1219
1693
|
const contextResult = generateContextMd(registryResult.registry, {
|
|
1220
1694
|
format: "markdown",
|
|
@@ -1226,7 +1700,7 @@ async function buildFragmentsDir(config, configDir) {
|
|
|
1226
1700
|
code: false
|
|
1227
1701
|
}
|
|
1228
1702
|
});
|
|
1229
|
-
const contextPath =
|
|
1703
|
+
const contextPath = join4(fragmentsDir, BRAND.contextFile);
|
|
1230
1704
|
await writeFile(contextPath, contextResult.content);
|
|
1231
1705
|
return {
|
|
1232
1706
|
success: errors.length === 0,
|
|
@@ -1579,9 +2053,9 @@ ${BRAND.name} Diff
|
|
|
1579
2053
|
}
|
|
1580
2054
|
|
|
1581
2055
|
// src/analyze.ts
|
|
1582
|
-
import { existsSync as
|
|
1583
|
-
import { readFile as
|
|
1584
|
-
import { join as
|
|
2056
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2057
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2058
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
1585
2059
|
import pc3 from "picocolors";
|
|
1586
2060
|
async function runAnalyzeCommand(config, configDir, options = {}) {
|
|
1587
2061
|
const format = options.format ?? "html";
|
|
@@ -1589,8 +2063,8 @@ async function runAnalyzeCommand(config, configDir, options = {}) {
|
|
|
1589
2063
|
console.log(pc3.cyan(`
|
|
1590
2064
|
${BRAND.name} Analyzer
|
|
1591
2065
|
`));
|
|
1592
|
-
const fragmentsPath =
|
|
1593
|
-
if (!
|
|
2066
|
+
const fragmentsPath = join5(configDir, config.outFile ?? "fragments.json");
|
|
2067
|
+
if (!existsSync6(fragmentsPath)) {
|
|
1594
2068
|
console.log(pc3.red(`\u2717 No fragments.json found. Run \`${BRAND.cliCommand} build\` first.
|
|
1595
2069
|
`));
|
|
1596
2070
|
return {
|
|
@@ -1599,7 +2073,7 @@ ${BRAND.name} Analyzer
|
|
|
1599
2073
|
};
|
|
1600
2074
|
}
|
|
1601
2075
|
console.log(pc3.dim("Analyzing design system...\n"));
|
|
1602
|
-
const content = await
|
|
2076
|
+
const content = await readFile3(fragmentsPath, "utf-8");
|
|
1603
2077
|
const data = JSON.parse(content);
|
|
1604
2078
|
const analytics = analyzeDesignSystem(data);
|
|
1605
2079
|
printConsoleSummary(analytics);
|
|
@@ -1685,7 +2159,7 @@ function colorizeScore(score) {
|
|
|
1685
2159
|
}
|
|
1686
2160
|
function getDefaultOutputPath(format, configDir) {
|
|
1687
2161
|
const filename = format === "html" ? "fragments-report.html" : "fragments-report.json";
|
|
1688
|
-
return
|
|
2162
|
+
return join5(configDir, filename);
|
|
1689
2163
|
}
|
|
1690
2164
|
async function openInBrowser(path) {
|
|
1691
2165
|
const { platform } = await import("os");
|
|
@@ -1747,10 +2221,11 @@ export {
|
|
|
1747
2221
|
validateSchema,
|
|
1748
2222
|
validateCoverage,
|
|
1749
2223
|
validateAll,
|
|
2224
|
+
validateSnippets,
|
|
1750
2225
|
buildFragments,
|
|
1751
2226
|
buildFragmentsDir,
|
|
1752
2227
|
runScreenshotCommand,
|
|
1753
2228
|
runDiffCommand,
|
|
1754
2229
|
runAnalyzeCommand
|
|
1755
2230
|
};
|
|
1756
|
-
//# sourceMappingURL=chunk-
|
|
2231
|
+
//# sourceMappingURL=chunk-EFQ7SIBX.js.map
|