@fragments-sdk/cli 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +77 -14
  2. package/dist/bin.js +22 -18
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-D34Q6A7S.js +266 -0
  5. package/dist/chunk-D34Q6A7S.js.map +1 -0
  6. package/dist/chunk-EKLMXTWU.js +80 -0
  7. package/dist/chunk-EKLMXTWU.js.map +1 -0
  8. package/dist/{chunk-GHYYFAQN.js → chunk-P33AKQJW.js} +1 -76
  9. package/dist/chunk-P33AKQJW.js.map +1 -0
  10. package/dist/{chunk-U6VTHBNI.js → chunk-QPY4DUFB.js} +177 -46
  11. package/dist/chunk-QPY4DUFB.js.map +1 -0
  12. package/dist/{chunk-32VIEOQY.js → chunk-R2YH7NLN.js} +9 -7
  13. package/dist/{chunk-32VIEOQY.js.map → chunk-R2YH7NLN.js.map} +1 -1
  14. package/dist/{chunk-5ITIP3ES.js → chunk-R6IZZSE7.js} +44 -278
  15. package/dist/chunk-R6IZZSE7.js.map +1 -0
  16. package/dist/{chunk-DQHWLAUV.js → chunk-TOIE7VXF.js} +2 -2
  17. package/dist/{chunk-GCZMFLDI.js → chunk-UXLGIGSX.js} +60 -3
  18. package/dist/chunk-UXLGIGSX.js.map +1 -0
  19. package/dist/{chunk-GKX2HPZ6.js → chunk-YMPGYEWK.js} +9 -3
  20. package/dist/chunk-YMPGYEWK.js.map +1 -0
  21. package/dist/chunk-Z7EY4VHE.js +50 -0
  22. package/dist/{core-SFHPYR5H.js → core-3NMNCLFW.js} +8 -5
  23. package/dist/discovery-AKGA6CJD.js +28 -0
  24. package/dist/{generate-54GJAWUY.js → generate-JAUEHKK7.js} +7 -4
  25. package/dist/{generate-54GJAWUY.js.map → generate-JAUEHKK7.js.map} +1 -1
  26. package/dist/index.js +15 -11
  27. package/dist/index.js.map +1 -1
  28. package/dist/{init-EIM5WNMP.js → init-DZQOT54X.js} +6 -4
  29. package/dist/{init-EIM5WNMP.js.map → init-DZQOT54X.js.map} +1 -1
  30. package/dist/mcp-bin.js +5 -3
  31. package/dist/mcp-bin.js.map +1 -1
  32. package/dist/sass.node-4XJK6YBF.js +130708 -0
  33. package/dist/sass.node-4XJK6YBF.js.map +1 -0
  34. package/dist/scan-OJRCVKK2.js +15 -0
  35. package/dist/{service-ED2LNCTU.js → service-CFFBHW4X.js} +6 -4
  36. package/dist/service-CFFBHW4X.js.map +1 -0
  37. package/dist/{static-viewer-Q4F4QP5M.js → static-viewer-VA2JXSCX.js} +6 -4
  38. package/dist/static-viewer-VA2JXSCX.js.map +1 -0
  39. package/dist/{test-6VN2DA3S.js → test-O7DZNKDC.js} +8 -4
  40. package/dist/{test-6VN2DA3S.js.map → test-O7DZNKDC.js.map} +1 -1
  41. package/dist/{tokens-P2B7ZAM3.js → tokens-N7THFD6J.js} +10 -7
  42. package/dist/{tokens-P2B7ZAM3.js.map → tokens-N7THFD6J.js.map} +1 -1
  43. package/dist/{viewer-GM7IQPPB.js → viewer-QTR7QJMM.js} +390 -25
  44. package/dist/viewer-QTR7QJMM.js.map +1 -0
  45. package/package.json +13 -2
  46. package/src/build.ts +60 -6
  47. package/src/commands/graph.ts +2 -2
  48. package/src/core/__tests__/token-resolver.test.ts +82 -0
  49. package/src/core/loader.ts +0 -3
  50. package/src/core/parser.ts +41 -1
  51. package/src/core/token-parser.ts +111 -1
  52. package/src/core/token-resolver.ts +155 -0
  53. package/src/service/__tests__/patch-generator.test.ts +2 -2
  54. package/src/service/patch-generator.ts +8 -1
  55. package/src/viewer/render-utils.ts +141 -0
  56. package/src/viewer/vite-plugin.ts +381 -23
  57. package/dist/chunk-5ITIP3ES.js.map +0 -1
  58. package/dist/chunk-GCZMFLDI.js.map +0 -1
  59. package/dist/chunk-GHYYFAQN.js.map +0 -1
  60. package/dist/chunk-GKX2HPZ6.js.map +0 -1
  61. package/dist/chunk-U6VTHBNI.js.map +0 -1
  62. package/dist/scan-KQBKUS64.js +0 -12
  63. package/dist/viewer-GM7IQPPB.js.map +0 -1
  64. /package/dist/{chunk-DQHWLAUV.js.map → chunk-TOIE7VXF.js.map} +0 -0
  65. /package/dist/{core-SFHPYR5H.js.map → chunk-Z7EY4VHE.js.map} +0 -0
  66. /package/dist/{scan-KQBKUS64.js.map → core-3NMNCLFW.js.map} +0 -0
  67. /package/dist/{service-ED2LNCTU.js.map → discovery-AKGA6CJD.js.map} +0 -0
  68. /package/dist/{static-viewer-Q4F4QP5M.js.map → scan-OJRCVKK2.js.map} +0 -0
@@ -28,6 +28,8 @@ import {
28
28
  import svgr from "vite-plugin-svgr";
29
29
  import {
30
30
  generateRenderScript,
31
+ generateVariantRenderScript,
32
+ generateA11yRenderScript,
31
33
  findFragmentByName,
32
34
  getAvailableComponents,
33
35
  type RenderRequest,
@@ -216,7 +218,7 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
216
218
  try {
217
219
  // Parse JSON body
218
220
  const body = await parseJsonBody(req);
219
- const { component, props = {}, viewport } = body as RenderRequest;
221
+ const { component, props = {}, viewport, variant } = body as RenderRequest;
220
222
 
221
223
  if (!component) {
222
224
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -258,12 +260,18 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
258
260
  return;
259
261
  }
260
262
 
261
- // Generate render script
262
- const renderScript = generateRenderScript(
263
- fragmentFile.absolutePath,
264
- fragmentInfo.name,
265
- props
266
- );
263
+ // Generate render script — use variant render if specified, otherwise props
264
+ const renderScript = variant
265
+ ? generateVariantRenderScript(
266
+ fragmentFile.absolutePath,
267
+ fragmentInfo.name,
268
+ variant
269
+ )
270
+ : generateRenderScript(
271
+ fragmentFile.absolutePath,
272
+ fragmentInfo.name,
273
+ props
274
+ );
267
275
 
268
276
  // Store the render request for the render page to pick up
269
277
  const requestId =
@@ -1022,14 +1030,32 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1022
1030
  return;
1023
1031
  }
1024
1032
 
1025
- // Check if tokens are configured
1033
+ // Auto-discover tokens when config.tokens is missing
1026
1034
  if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
1027
- res.writeHead(400, { "Content-Type": "application/json" });
1028
- res.end(JSON.stringify({
1029
- error: "No token configuration found",
1030
- suggestion: "Add 'tokens' config to fragments.config.ts to enable fix generation",
1031
- }));
1032
- return;
1035
+ try {
1036
+ const { discoverTokenFiles } = await import("../core/discovery.js");
1037
+ const discovered = await discoverTokenFiles(projectRoot);
1038
+ if (discovered.length > 0) {
1039
+ config.tokens = {
1040
+ ...config.tokens,
1041
+ include: discovered.map((f) => f.relativePath),
1042
+ };
1043
+ } else {
1044
+ res.writeHead(400, { "Content-Type": "application/json" });
1045
+ res.end(JSON.stringify({
1046
+ error: "No token files found",
1047
+ suggestion: "Add 'tokens' config to fragments.config.ts or add token files matching default patterns (_variables.scss, tokens.scss, etc.)",
1048
+ }));
1049
+ return;
1050
+ }
1051
+ } catch {
1052
+ res.writeHead(400, { "Content-Type": "application/json" });
1053
+ res.end(JSON.stringify({
1054
+ error: "No token configuration found and auto-discovery failed",
1055
+ suggestion: "Add 'tokens' config to fragments.config.ts to enable fix generation",
1056
+ }));
1057
+ return;
1058
+ }
1033
1059
  }
1034
1060
 
1035
1061
  // Load fragment data
@@ -1057,23 +1083,81 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1057
1083
  await registry.initialize(config.tokens, projectRoot);
1058
1084
  }
1059
1085
 
1060
- // For now, we generate patches based on style diff data
1061
- // In a full implementation, we would:
1062
- // 1. Render the component and get computed styles
1063
- // 2. Compare with Figma styles to find hardcoded values
1064
- // 3. Generate patches for each hardcoded value
1065
-
1066
1086
  // Get source file path from fragment
1067
1087
  const fragmentFile = fragmentFiles.find(
1068
1088
  (f) => f.relativePath === fragmentInfo.path
1069
1089
  );
1070
1090
  const sourceFile = fragmentFile?.relativePath || `${component}.tsx`;
1071
1091
 
1072
- // For demonstration, we'll create a placeholder response
1073
- // In production, this would use style comparison + AST patching
1092
+ // Render the component and extract computed styles
1093
+ let styleDiffs: Array<{
1094
+ property: string;
1095
+ figma: string;
1096
+ rendered: string;
1097
+ match: boolean;
1098
+ }> = [];
1099
+
1100
+ if (fragmentFile) {
1101
+ try {
1102
+ const renderScript = generateRenderScript(
1103
+ fragmentFile.absolutePath,
1104
+ fragmentInfo.name,
1105
+ {}
1106
+ );
1107
+
1108
+ const requestId =
1109
+ Date.now().toString(36) + Math.random().toString(36).slice(2);
1110
+ pendingRenders.set(requestId, {
1111
+ script: renderScript,
1112
+ viewport: { width: 800, height: 600 },
1113
+ });
1114
+
1115
+ const address = _server.httpServer?.address();
1116
+ const port =
1117
+ typeof address === "object" && address ? address.port : 6006;
1118
+
1119
+ const { computedStyles } = await captureRenderWithStyles(
1120
+ `http://localhost:${port}/fragments/__render__/${requestId}`,
1121
+ { width: 800, height: 600 },
1122
+ true
1123
+ );
1124
+ pendingRenders.delete(requestId);
1125
+
1126
+ if (computedStyles) {
1127
+ // Build token value lookup from registry
1128
+ const tokenValues = new Map<string, string>();
1129
+ const allTokens = registry.getAllTokens();
1130
+ for (const t of allTokens) {
1131
+ if (t.resolvedValue) {
1132
+ tokenValues.set(t.resolvedValue, t.name);
1133
+ }
1134
+ }
1135
+
1136
+ // For each computed style property, check if it uses a token
1137
+ for (const [prop, value] of Object.entries(computedStyles)) {
1138
+ if (!value || value === "transparent" || value === "rgba(0, 0, 0, 0)") continue;
1139
+
1140
+ // Check if the value matches any token
1141
+ const matchesToken = tokenValues.has(value);
1142
+ if (!matchesToken) {
1143
+ styleDiffs.push({
1144
+ property: prop,
1145
+ figma: value, // Using rendered as "expected" since we have no Figma
1146
+ rendered: value,
1147
+ match: false,
1148
+ });
1149
+ }
1150
+ }
1151
+ }
1152
+ } catch (renderErr) {
1153
+ // If rendering fails, continue with empty diffs
1154
+ console.warn("[Fragments] Could not render for style extraction:", renderErr);
1155
+ }
1156
+ }
1157
+
1074
1158
  const result = generateTokenPatches(
1075
1159
  component,
1076
- [], // Would be populated by actual style diffs
1160
+ styleDiffs,
1077
1161
  registry,
1078
1162
  { sourceFile }
1079
1163
  );
@@ -1095,6 +1179,205 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1095
1179
  return;
1096
1180
  }
1097
1181
 
1182
+ // Handle /fragments/a11y endpoint for accessibility auditing
1183
+ if (req.url === "/fragments/a11y" && req.method === "POST") {
1184
+ try {
1185
+ const body = (await parseJsonBody(req)) as {
1186
+ component: string;
1187
+ variant?: string;
1188
+ standard?: "AA" | "AAA";
1189
+ };
1190
+
1191
+ const { component, variant: variantName, standard = "AA" } = body;
1192
+
1193
+ if (!component) {
1194
+ res.writeHead(400, { "Content-Type": "application/json" });
1195
+ res.end(
1196
+ JSON.stringify({ error: "Missing required field: component" })
1197
+ );
1198
+ return;
1199
+ }
1200
+
1201
+ // Load fragments to find the component
1202
+ const loadedFragments = await loadFragmentsForRender(
1203
+ fragmentFiles,
1204
+ projectRoot
1205
+ );
1206
+ const fragmentInfo = findFragmentByName(component, loadedFragments);
1207
+
1208
+ if (!fragmentInfo) {
1209
+ const available = getAvailableComponents(loadedFragments);
1210
+ res.writeHead(400, { "Content-Type": "application/json" });
1211
+ res.end(
1212
+ JSON.stringify({
1213
+ error: `Component '${component}' not found. Available: ${available.join(", ")}`,
1214
+ })
1215
+ );
1216
+ return;
1217
+ }
1218
+
1219
+ const fragmentFile = fragmentFiles.find(
1220
+ (f) => f.relativePath === fragmentInfo.path
1221
+ );
1222
+ if (!fragmentFile) {
1223
+ res.writeHead(500, { "Content-Type": "application/json" });
1224
+ res.end(
1225
+ JSON.stringify({ error: "Could not resolve fragment file path" })
1226
+ );
1227
+ return;
1228
+ }
1229
+
1230
+ // Determine which variants to audit
1231
+ const variantNames: string[] = [];
1232
+ if (variantName) {
1233
+ variantNames.push(variantName);
1234
+ } else {
1235
+ // Load full fragment data to get variant names
1236
+ const fullData = await loadFullFragmentData(projectRoot);
1237
+ const fragmentData = fullData
1238
+ ? Object.values(fullData.fragments).find(
1239
+ (f) => f.meta.name.toLowerCase() === component.toLowerCase()
1240
+ )
1241
+ : null;
1242
+
1243
+ if (fragmentData && fragmentData.variants?.length > 0) {
1244
+ for (const v of fragmentData.variants) {
1245
+ variantNames.push(v.name);
1246
+ }
1247
+ } else {
1248
+ // Fallback: audit default render (no variant)
1249
+ variantNames.push("Default");
1250
+ }
1251
+ }
1252
+
1253
+ // Get server address
1254
+ const address = _server.httpServer?.address();
1255
+ const port =
1256
+ typeof address === "object" && address ? address.port : 6006;
1257
+
1258
+ // Audit each variant
1259
+ const results: Array<{
1260
+ variant: string;
1261
+ violations: number;
1262
+ passes: number;
1263
+ incomplete: number;
1264
+ summary: {
1265
+ total: number;
1266
+ critical: number;
1267
+ serious: number;
1268
+ moderate: number;
1269
+ minor: number;
1270
+ };
1271
+ violationDetails?: Array<{
1272
+ id: string;
1273
+ impact: string | undefined;
1274
+ description: string;
1275
+ helpUrl: string;
1276
+ nodes: number;
1277
+ }>;
1278
+ }> = [];
1279
+
1280
+ for (const vName of variantNames) {
1281
+ // Generate a11y render script for this variant
1282
+ const a11yScript = generateA11yRenderScript(
1283
+ fragmentFile.absolutePath,
1284
+ fragmentInfo.name,
1285
+ vName === "Default" && !variantName ? undefined : vName
1286
+ );
1287
+
1288
+ const requestId =
1289
+ Date.now().toString(36) + Math.random().toString(36).slice(2);
1290
+ pendingRenders.set(requestId, {
1291
+ script: a11yScript,
1292
+ viewport: { width: 800, height: 600 },
1293
+ });
1294
+
1295
+ try {
1296
+ const auditResult = await captureA11yAudit(
1297
+ `http://localhost:${port}/fragments/__render__/${requestId}`,
1298
+ { width: 800, height: 600 }
1299
+ );
1300
+
1301
+ // Transform axe results into the expected shape
1302
+ let critical = 0;
1303
+ let serious = 0;
1304
+ let moderate = 0;
1305
+ let minor = 0;
1306
+
1307
+ for (const violation of auditResult.violations ?? []) {
1308
+ switch (violation.impact) {
1309
+ case "critical":
1310
+ critical++;
1311
+ break;
1312
+ case "serious":
1313
+ serious++;
1314
+ break;
1315
+ case "moderate":
1316
+ moderate++;
1317
+ break;
1318
+ case "minor":
1319
+ minor++;
1320
+ break;
1321
+ }
1322
+ }
1323
+
1324
+ results.push({
1325
+ variant: vName,
1326
+ violations: auditResult.violations?.length ?? 0,
1327
+ passes: auditResult.passes?.length ?? 0,
1328
+ incomplete: auditResult.incomplete?.length ?? 0,
1329
+ summary: {
1330
+ total: critical + serious + moderate + minor,
1331
+ critical,
1332
+ serious,
1333
+ moderate,
1334
+ minor,
1335
+ },
1336
+ violationDetails: (auditResult.violations ?? []).map(v => ({
1337
+ id: v.id,
1338
+ impact: v.impact,
1339
+ description: v.description,
1340
+ helpUrl: v.helpUrl,
1341
+ nodes: v.nodes.length,
1342
+ })),
1343
+ });
1344
+ } catch (err) {
1345
+ // If a single variant fails, report it as a result with error info
1346
+ results.push({
1347
+ variant: vName,
1348
+ violations: 0,
1349
+ passes: 0,
1350
+ incomplete: 0,
1351
+ summary: {
1352
+ total: 0,
1353
+ critical: 0,
1354
+ serious: 0,
1355
+ moderate: 0,
1356
+ minor: 0,
1357
+ },
1358
+ });
1359
+ } finally {
1360
+ pendingRenders.delete(requestId);
1361
+ }
1362
+ }
1363
+
1364
+ res.setHeader("Content-Type", "application/json");
1365
+ res.end(JSON.stringify({ results }));
1366
+ } catch (error) {
1367
+ console.error("[Fragments] Error running a11y audit:", error);
1368
+ res.writeHead(500, { "Content-Type": "application/json" });
1369
+ res.end(
1370
+ JSON.stringify({
1371
+ error:
1372
+ error instanceof Error
1373
+ ? error.message
1374
+ : "A11y audit failed",
1375
+ })
1376
+ );
1377
+ }
1378
+ return;
1379
+ }
1380
+
1098
1381
  // Handle /fragments/preview/ - isolated iframe for component previews
1099
1382
  if (req.url?.startsWith("/fragments/preview")) {
1100
1383
  // Redirect to trailing slash
@@ -1673,6 +1956,81 @@ async function loadFragmentsForRender(
1673
1956
  });
1674
1957
  }
1675
1958
 
1959
+ /**
1960
+ * Load full fragment data from fragments.json (includes variants).
1961
+ * Used by the a11y endpoint to enumerate all variants.
1962
+ */
1963
+ async function loadFullFragmentData(
1964
+ configDir: string
1965
+ ): Promise<{
1966
+ fragments: Record<
1967
+ string,
1968
+ {
1969
+ filePath: string;
1970
+ meta: { name: string };
1971
+ variants: Array<{ name: string; description?: string; code?: string }>;
1972
+ }
1973
+ >;
1974
+ } | null> {
1975
+ const { join } = await import("node:path");
1976
+ const fragmentsJsonPath = join(configDir, BRAND.outFile);
1977
+
1978
+ try {
1979
+ const content = await readFile(fragmentsJsonPath, "utf-8");
1980
+ return JSON.parse(content);
1981
+ } catch {
1982
+ return null;
1983
+ }
1984
+ }
1985
+
1986
+ /**
1987
+ * Capture an accessibility audit using the shared browser pool.
1988
+ * Navigates to the render page (which imports axe-core and runs the audit),
1989
+ * then extracts the axe results from the page context.
1990
+ */
1991
+ async function captureA11yAudit(
1992
+ url: string,
1993
+ viewport: { width: number; height: number }
1994
+ ): Promise<{
1995
+ violations: Array<{ impact?: string; id: string; description: string; helpUrl: string; nodes: Array<unknown> }>;
1996
+ passes: Array<{ id: string }>;
1997
+ incomplete: Array<{ id: string }>;
1998
+ }> {
1999
+ const { pool } = await getSharedRenderPool();
2000
+
2001
+ const ctx = await pool.acquire();
2002
+ const page = await ctx.newPage();
2003
+
2004
+ try {
2005
+ await page.setViewportSize(viewport);
2006
+ await page.goto(url, { waitUntil: "networkidle" });
2007
+
2008
+ // Wait for the render + axe audit to complete
2009
+ await page.waitForFunction(
2010
+ () => (window as any).__RENDER_READY__ === true,
2011
+ { timeout: 15000 }
2012
+ );
2013
+
2014
+ // Check for error
2015
+ const error = await page.evaluate(() => (window as any).__AXE_ERROR__);
2016
+ if (error) {
2017
+ throw new Error(`A11y audit error: ${error}`);
2018
+ }
2019
+
2020
+ // Extract axe results
2021
+ const results = await page.evaluate(() => (window as any).__AXE_RESULTS__);
2022
+
2023
+ if (!results) {
2024
+ throw new Error("Axe results not available — axe-core may not be installed");
2025
+ }
2026
+
2027
+ return results;
2028
+ } finally {
2029
+ await page.close();
2030
+ pool.release(ctx);
2031
+ }
2032
+ }
2033
+
1676
2034
  /**
1677
2035
  * Serve the render HTML page for AI preview.
1678
2036
  */