@fragments-sdk/cli 0.7.2 → 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.
- package/dist/bin.js +20 -16
- package/dist/bin.js.map +1 -1
- package/dist/chunk-D34Q6A7S.js +266 -0
- package/dist/chunk-D34Q6A7S.js.map +1 -0
- package/dist/chunk-EKLMXTWU.js +80 -0
- package/dist/chunk-EKLMXTWU.js.map +1 -0
- package/dist/{chunk-GHYYFAQN.js → chunk-P33AKQJW.js} +1 -76
- package/dist/chunk-P33AKQJW.js.map +1 -0
- package/dist/{chunk-7KUSBMI4.js → chunk-QPY4DUFB.js} +174 -45
- package/dist/chunk-QPY4DUFB.js.map +1 -0
- package/dist/{chunk-DH4ETVSM.js → chunk-R2YH7NLN.js} +9 -7
- package/dist/{chunk-DH4ETVSM.js.map → chunk-R2YH7NLN.js.map} +1 -1
- package/dist/{chunk-3T6QL7IY.js → chunk-R6IZZSE7.js} +23 -275
- package/dist/chunk-R6IZZSE7.js.map +1 -0
- package/dist/{chunk-DQHWLAUV.js → chunk-TOIE7VXF.js} +2 -2
- package/dist/{chunk-OOGTG5FM.js → chunk-UXLGIGSX.js} +56 -2
- package/dist/chunk-UXLGIGSX.js.map +1 -0
- package/dist/{chunk-GKX2HPZ6.js → chunk-YMPGYEWK.js} +9 -3
- package/dist/chunk-YMPGYEWK.js.map +1 -0
- package/dist/chunk-Z7EY4VHE.js +50 -0
- package/dist/{core-UQXZTBFZ.js → core-3NMNCLFW.js} +8 -5
- package/dist/discovery-AKGA6CJD.js +28 -0
- package/dist/{generate-GP6ZLAQB.js → generate-JAUEHKK7.js} +7 -4
- package/dist/{generate-GP6ZLAQB.js.map → generate-JAUEHKK7.js.map} +1 -1
- package/dist/index.js +15 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-W72WBSU2.js → init-DZQOT54X.js} +6 -4
- package/dist/{init-W72WBSU2.js.map → init-DZQOT54X.js.map} +1 -1
- package/dist/mcp-bin.js +5 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/sass.node-4XJK6YBF.js +130708 -0
- package/dist/sass.node-4XJK6YBF.js.map +1 -0
- package/dist/scan-OJRCVKK2.js +15 -0
- package/dist/{service-PVGTYUKX.js → service-CFFBHW4X.js} +6 -4
- package/dist/service-CFFBHW4X.js.map +1 -0
- package/dist/{static-viewer-KILKIVN7.js → static-viewer-VA2JXSCX.js} +6 -4
- package/dist/static-viewer-VA2JXSCX.js.map +1 -0
- package/dist/{test-3YRYQRGV.js → test-O7DZNKDC.js} +8 -4
- package/dist/{test-3YRYQRGV.js.map → test-O7DZNKDC.js.map} +1 -1
- package/dist/{tokens-IXSQHPQK.js → tokens-N7THFD6J.js} +10 -7
- package/dist/{tokens-IXSQHPQK.js.map → tokens-N7THFD6J.js.map} +1 -1
- package/dist/{viewer-K42REJU2.js → viewer-QTR7QJMM.js} +390 -25
- package/dist/viewer-QTR7QJMM.js.map +1 -0
- package/package.json +1 -1
- package/src/build.ts +57 -5
- package/src/core/__tests__/token-resolver.test.ts +82 -0
- package/src/core/token-parser.ts +102 -0
- package/src/core/token-resolver.ts +155 -0
- package/src/service/__tests__/patch-generator.test.ts +2 -2
- package/src/service/patch-generator.ts +8 -1
- package/src/viewer/render-utils.ts +141 -0
- package/src/viewer/vite-plugin.ts +381 -23
- package/dist/chunk-3T6QL7IY.js.map +0 -1
- package/dist/chunk-7KUSBMI4.js.map +0 -1
- package/dist/chunk-GHYYFAQN.js.map +0 -1
- package/dist/chunk-GKX2HPZ6.js.map +0 -1
- package/dist/chunk-OOGTG5FM.js.map +0 -1
- package/dist/scan-V54HWRDY.js +0 -12
- package/dist/viewer-K42REJU2.js.map +0 -1
- /package/dist/{chunk-DQHWLAUV.js.map → chunk-TOIE7VXF.js.map} +0 -0
- /package/dist/{core-UQXZTBFZ.js.map → chunk-Z7EY4VHE.js.map} +0 -0
- /package/dist/{scan-V54HWRDY.js.map → core-3NMNCLFW.js.map} +0 -0
- /package/dist/{service-PVGTYUKX.js.map → discovery-AKGA6CJD.js.map} +0 -0
- /package/dist/{static-viewer-KILKIVN7.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 =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
//
|
|
1033
|
+
// Auto-discover tokens when config.tokens is missing
|
|
1026
1034
|
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
//
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
*/
|