@deneb-ui/cli 2.0.51 → 2.0.53
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/bin/index.js +12 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +96 -11
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +127 -28
- package/src/arc/learning.cjs +19 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +2 -1
- package/src/arc/printer.cjs +9 -0
- package/src/arc/residual.cjs +400 -0
- package/src/arc/scanner.cjs +46 -2
- package/src/arc/semantic.cjs +8 -4
- package/src/arc/transformer.cjs +321 -28
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
package/src/arc/transformer.cjs
CHANGED
|
@@ -159,18 +159,23 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
159
159
|
b.jsxAttribute(b.jsxIdentifier('type'), b.stringLiteral('submit'))
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
|
-
if (
|
|
162
|
+
if (transform.labelField) {
|
|
163
|
+
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
164
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
165
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
169
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
170
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
174
|
+
} else if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
163
175
|
node.openingElement.attributes.push(
|
|
164
176
|
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-button'))
|
|
165
177
|
);
|
|
166
178
|
}
|
|
167
|
-
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
168
|
-
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
169
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
173
|
-
wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
|
|
174
179
|
return;
|
|
175
180
|
}
|
|
176
181
|
if (transform.operation === 'split-action-contract') {
|
|
@@ -206,18 +211,17 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
214
|
+
// Fivora Strict Action/Label Contract:
|
|
215
|
+
// Bind the URL action marker directly to the outer <a> element (never hidden)
|
|
216
|
+
replaceAttrValue(node, 'data-preview-field-path', b.stringLiteral(transform.urlField));
|
|
217
|
+
// Remove data-preview-static if present to prevent static/field collisions
|
|
218
|
+
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
215
219
|
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
216
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-
|
|
220
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
217
221
|
);
|
|
218
222
|
}
|
|
223
|
+
// Bind the label to the inner text child
|
|
219
224
|
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
220
|
-
wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
|
|
221
225
|
return;
|
|
222
226
|
}
|
|
223
227
|
if (transform.operation === 'extract-url') {
|
|
@@ -255,11 +259,29 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
255
259
|
return;
|
|
256
260
|
}
|
|
257
261
|
if (transform.operation === 'style-bind') {
|
|
258
|
-
if (transform.styleKind === 'grid'
|
|
262
|
+
if (transform.styleKind === 'grid') {
|
|
263
|
+
const container = findAncestorJsxElement(pathNode) || node;
|
|
264
|
+
ensureStyleAttrs(container, transform.stylePath, 'grid');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (transform.styleKind === 'card') {
|
|
268
|
+
ensureStyleAttrs(node, transform.stylePath, 'card');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
259
271
|
ensureStyleAttrs(node, transform.stylePath, transform.styleKind || 'text');
|
|
260
272
|
}
|
|
261
273
|
}
|
|
262
274
|
|
|
275
|
+
function findAncestorJsxElement(pathNode) {
|
|
276
|
+
let current = pathNode.parent;
|
|
277
|
+
while (current) {
|
|
278
|
+
const candidate = current.node || current.value;
|
|
279
|
+
if (candidate?.type === 'JSXElement') return candidate;
|
|
280
|
+
current = current.parentPath || current.parent;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
263
285
|
function inferButtonKind(tag) {
|
|
264
286
|
if (tag === 'button' || tag === 'Button') return 'button';
|
|
265
287
|
return 'text';
|
|
@@ -335,6 +357,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
335
357
|
}
|
|
336
358
|
|
|
337
359
|
markItemFields(callback, { listPath, binding, indexName });
|
|
360
|
+
markComponentRefItemsStatic(callback, binding);
|
|
338
361
|
|
|
339
362
|
const container = findListContainer(mapCall);
|
|
340
363
|
if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
|
|
@@ -351,7 +374,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
351
374
|
);
|
|
352
375
|
}
|
|
353
376
|
|
|
354
|
-
return bindArrayDeclaration(ast, mapCall, listPath, isClient);
|
|
377
|
+
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef));
|
|
355
378
|
}
|
|
356
379
|
|
|
357
380
|
function findMapCall(ast, loc) {
|
|
@@ -410,9 +433,14 @@ function markItemFields(callback, { listPath, binding, indexName }) {
|
|
|
410
433
|
);
|
|
411
434
|
if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
412
435
|
const property = itemMemberName(textChild.expression, binding);
|
|
413
|
-
node
|
|
414
|
-
|
|
415
|
-
|
|
436
|
+
const tagName = getJsxName(node);
|
|
437
|
+
if (BROAD_CONTENT_CONTAINERS.has(tagName) || BROAD_CONTENT_CONTAINERS.has(String(tagName).toLowerCase())) {
|
|
438
|
+
wrapItemFieldInSpan(node, textChild, listPath, indexName, property);
|
|
439
|
+
} else {
|
|
440
|
+
node.openingElement.attributes.push(
|
|
441
|
+
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
416
444
|
}
|
|
417
445
|
|
|
418
446
|
this.traverse(pathNode);
|
|
@@ -420,6 +448,51 @@ function markItemFields(callback, { listPath, binding, indexName }) {
|
|
|
420
448
|
});
|
|
421
449
|
}
|
|
422
450
|
|
|
451
|
+
function wrapItemFieldInSpan(node, textChild, listPath, indexName, property) {
|
|
452
|
+
const nextChildren = [];
|
|
453
|
+
for (const child of node.children || []) {
|
|
454
|
+
if (child === textChild) {
|
|
455
|
+
nextChildren.push(
|
|
456
|
+
b.jsxElement(
|
|
457
|
+
b.jsxOpeningElement(
|
|
458
|
+
b.jsxIdentifier('span'),
|
|
459
|
+
[jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)],
|
|
460
|
+
false
|
|
461
|
+
),
|
|
462
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
463
|
+
[child],
|
|
464
|
+
false
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
nextChildren.push(child);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
node.children = nextChildren;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function markComponentRefItemsStatic(callback, binding) {
|
|
475
|
+
recast.types.visit(callback, {
|
|
476
|
+
visitJSXElement(pathNode) {
|
|
477
|
+
const name = pathNode.node.openingElement && pathNode.node.openingElement.name;
|
|
478
|
+
if (
|
|
479
|
+
name &&
|
|
480
|
+
name.type === 'JSXMemberExpression' &&
|
|
481
|
+
name.object &&
|
|
482
|
+
(name.object.type === 'Identifier' || name.object.type === 'JSXIdentifier') &&
|
|
483
|
+
name.object.name === binding
|
|
484
|
+
) {
|
|
485
|
+
if (!hasJsxAttribute(pathNode.node, 'data-preview-static') && !hasJsxAttribute(pathNode.node, 'data-preview-field-path')) {
|
|
486
|
+
pathNode.node.openingElement.attributes.push(
|
|
487
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('component-ref'))
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this.traverse(pathNode);
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
423
496
|
function itemMemberName(expr, binding) {
|
|
424
497
|
if (!expr || expr.type !== 'MemberExpression' || expr.computed) return null;
|
|
425
498
|
if (expr.object?.type !== 'Identifier' || expr.object.name !== binding) return null;
|
|
@@ -470,7 +543,7 @@ function findEnclosingFunction(pathNode) {
|
|
|
470
543
|
return null;
|
|
471
544
|
}
|
|
472
545
|
|
|
473
|
-
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
546
|
+
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false) {
|
|
474
547
|
const arrayName = mapCallPath.node.callee.object?.name;
|
|
475
548
|
if (!arrayName) return false;
|
|
476
549
|
let bound = false;
|
|
@@ -498,7 +571,9 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
|
498
571
|
const localDecl = b.variableDeclaration('const', [
|
|
499
572
|
b.variableDeclarator(
|
|
500
573
|
b.identifier(arrayName),
|
|
501
|
-
|
|
574
|
+
mergeDefaultRefs
|
|
575
|
+
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
576
|
+
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
502
577
|
),
|
|
503
578
|
]);
|
|
504
579
|
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
@@ -522,6 +597,28 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
|
522
597
|
return bound;
|
|
523
598
|
}
|
|
524
599
|
|
|
600
|
+
function mergeDefaultItemRefsBinding(pathParts, defaultName) {
|
|
601
|
+
const listExpr = siteDataListBinding(pathParts, b.identifier(defaultName));
|
|
602
|
+
return b.callExpression(
|
|
603
|
+
b.memberExpression(listExpr, b.identifier('map'), false),
|
|
604
|
+
[
|
|
605
|
+
b.arrowFunctionExpression(
|
|
606
|
+
[b.identifier('item'), b.identifier('index')],
|
|
607
|
+
b.objectExpression([
|
|
608
|
+
b.spreadElement(
|
|
609
|
+
b.logicalExpression(
|
|
610
|
+
'??',
|
|
611
|
+
b.memberExpression(b.identifier(defaultName), b.identifier('index'), true),
|
|
612
|
+
b.objectExpression([])
|
|
613
|
+
)
|
|
614
|
+
),
|
|
615
|
+
b.spreadElement(b.identifier('item')),
|
|
616
|
+
])
|
|
617
|
+
),
|
|
618
|
+
]
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
525
622
|
function fileAlreadyUsesSiteDataHook(ast) {
|
|
526
623
|
let found = false;
|
|
527
624
|
recast.types.visit(ast, {
|
|
@@ -710,6 +807,9 @@ function applyFilePlan(filePlan, profile) {
|
|
|
710
807
|
|
|
711
808
|
// Sanitize any conflicting data-preview-static on elements with editable markers
|
|
712
809
|
sanitizeContradictoryMarkers(ast);
|
|
810
|
+
healBroadContainerMarkers(ast);
|
|
811
|
+
healEmptyStateConditionals(ast);
|
|
812
|
+
healHiddenPreviewMarkers(ast);
|
|
713
813
|
|
|
714
814
|
// Page keys are stamped in a separate route-driven pass so App Router and
|
|
715
815
|
// Pages Router projects are handled by the same logic.
|
|
@@ -994,8 +1094,7 @@ function sanitizeContradictoryMarkers(ast) {
|
|
|
994
1094
|
}
|
|
995
1095
|
|
|
996
1096
|
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
997
|
-
if (!code.includes('data-preview-static')
|
|
998
|
-
if (!code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path')) {
|
|
1097
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path')) {
|
|
999
1098
|
return { code, updated: false };
|
|
1000
1099
|
}
|
|
1001
1100
|
let ast;
|
|
@@ -1004,9 +1103,200 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
|
1004
1103
|
} catch {
|
|
1005
1104
|
return { code, updated: false };
|
|
1006
1105
|
}
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1106
|
+
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
1107
|
+
const healedBroad = healBroadContainerMarkers(ast);
|
|
1108
|
+
const healedEmpty = healEmptyStateConditionals(ast);
|
|
1109
|
+
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
1110
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0) return { code, updated: false };
|
|
1111
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function healBroadContainerMarkers(ast) {
|
|
1115
|
+
let healed = 0;
|
|
1116
|
+
recast.types.visit(ast, {
|
|
1117
|
+
visitJSXElement(pathNode) {
|
|
1118
|
+
const node = pathNode.node;
|
|
1119
|
+
const tag = getJsxName(node);
|
|
1120
|
+
const lower = String(tag || '').toLowerCase();
|
|
1121
|
+
if (!BROAD_CONTENT_CONTAINERS.has(tag) && !BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
1122
|
+
this.traverse(pathNode);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const fieldAttr = findJsxAttribute(node, 'data-preview-field-path');
|
|
1126
|
+
if (!fieldAttr) {
|
|
1127
|
+
this.traverse(pathNode);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const fieldValue = fieldAttr.value;
|
|
1132
|
+
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
1133
|
+
(attr) =>
|
|
1134
|
+
!(
|
|
1135
|
+
attr.type === 'JSXAttribute' &&
|
|
1136
|
+
attr.name &&
|
|
1137
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1138
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
1139
|
+
attr.name.name === 'data-preview-style-type')
|
|
1140
|
+
)
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
const leaf = findFirstLeafChild(node);
|
|
1144
|
+
if (leaf && !hasJsxAttribute(leaf, 'data-preview-field-path')) {
|
|
1145
|
+
leaf.openingElement.attributes.push(
|
|
1146
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)
|
|
1147
|
+
);
|
|
1148
|
+
healed++;
|
|
1149
|
+
} else {
|
|
1150
|
+
const span = b.jsxElement(
|
|
1151
|
+
b.jsxOpeningElement(
|
|
1152
|
+
b.jsxIdentifier('span'),
|
|
1153
|
+
[b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)],
|
|
1154
|
+
false
|
|
1155
|
+
),
|
|
1156
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
1157
|
+
node.children || [],
|
|
1158
|
+
false
|
|
1159
|
+
);
|
|
1160
|
+
node.children = [span];
|
|
1161
|
+
healed++;
|
|
1162
|
+
}
|
|
1163
|
+
this.traverse(pathNode);
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
return healed;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function findFirstLeafChild(node) {
|
|
1170
|
+
const leafTags = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'img', 'button', 'a', 'strong', 'em', 'small']);
|
|
1171
|
+
for (const child of node.children || []) {
|
|
1172
|
+
if (child && child.type === 'JSXElement') {
|
|
1173
|
+
const name = getJsxName(child);
|
|
1174
|
+
if (leafTags.has(name) || leafTags.has(String(name).toLowerCase())) return child;
|
|
1175
|
+
const nested = findFirstLeafChild(child);
|
|
1176
|
+
if (nested) return nested;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function healEmptyStateConditionals(ast) {
|
|
1183
|
+
let healed = 0;
|
|
1184
|
+
|
|
1185
|
+
function jsxHasPreviewMarker(node) {
|
|
1186
|
+
if (!node) return false;
|
|
1187
|
+
if (node.type === 'JSXElement') {
|
|
1188
|
+
if (
|
|
1189
|
+
hasJsxAttribute(node, 'data-preview-field-path') ||
|
|
1190
|
+
hasJsxAttribute(node, 'data-preview-list-path') ||
|
|
1191
|
+
hasJsxAttribute(node, 'data-preview-item-path')
|
|
1192
|
+
) {
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
1196
|
+
}
|
|
1197
|
+
if (node.type === 'JSXFragment') {
|
|
1198
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
1199
|
+
}
|
|
1200
|
+
if (node.type === 'ParenthesizedExpression') return jsxHasPreviewMarker(node.expression);
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
recast.types.visit(ast, {
|
|
1205
|
+
visitLogicalExpression(pathNode) {
|
|
1206
|
+
const expr = pathNode.node;
|
|
1207
|
+
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1208
|
+
pathNode.replace(expr.right);
|
|
1209
|
+
healed++;
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
this.traverse(pathNode);
|
|
1213
|
+
},
|
|
1214
|
+
visitJSXExpressionContainer(pathNode) {
|
|
1215
|
+
const expr = pathNode.node.expression;
|
|
1216
|
+
if (!expr) {
|
|
1217
|
+
this.traverse(pathNode);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1221
|
+
pathNode.node.expression = expr.right;
|
|
1222
|
+
healed++;
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
if (
|
|
1226
|
+
expr.type === 'ConditionalExpression' &&
|
|
1227
|
+
jsxHasPreviewMarker(expr.consequent) &&
|
|
1228
|
+
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
1229
|
+
) {
|
|
1230
|
+
pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
|
|
1231
|
+
? expr.consequent
|
|
1232
|
+
: expr.alternate;
|
|
1233
|
+
healed++;
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
this.traverse(pathNode);
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
return healed;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function isNullish(node) {
|
|
1243
|
+
if (!node) return true;
|
|
1244
|
+
if (node.type === 'NullLiteral') return true;
|
|
1245
|
+
if (node.type === 'Literal' && (node.value === null || node.value === false || node.value === undefined)) return true;
|
|
1246
|
+
if (node.type === 'BooleanLiteral' && node.value === false) return true;
|
|
1247
|
+
if (node.type === 'Identifier' && (node.name === 'undefined' || node.name === 'null')) return true;
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function healHiddenPreviewMarkers(ast) {
|
|
1252
|
+
let healed = 0;
|
|
1253
|
+
recast.types.visit(ast, {
|
|
1254
|
+
visitJSXOpeningElement(pathNode) {
|
|
1255
|
+
const attrs = pathNode.node.attributes || [];
|
|
1256
|
+
const hasEditable = attrs.some(
|
|
1257
|
+
(attr) =>
|
|
1258
|
+
attr.type === 'JSXAttribute' &&
|
|
1259
|
+
attr.name &&
|
|
1260
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1261
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
1262
|
+
attr.name.name === 'data-preview-item-path')
|
|
1263
|
+
);
|
|
1264
|
+
if (!hasEditable) {
|
|
1265
|
+
this.traverse(pathNode);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const classAttr = attrs.find((attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'className' || attr.name.name === 'class'));
|
|
1269
|
+
let classText = '';
|
|
1270
|
+
if (classAttr && classAttr.value) {
|
|
1271
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') classText = String(classAttr.value.value || '');
|
|
1272
|
+
else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression) {
|
|
1273
|
+
const expr = classAttr.value.expression;
|
|
1274
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal') classText = String(expr.value || '');
|
|
1275
|
+
if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const hiddenAttr = attrs.some(
|
|
1279
|
+
(attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'hidden' || (attr.name.name === 'aria-hidden' && recast.print(attr).code.includes('true')))
|
|
1280
|
+
);
|
|
1281
|
+
if (hiddenAttr || /(^|\s)hidden(\s|$)/.test(classText)) {
|
|
1282
|
+
pathNode.node.attributes = attrs.filter(
|
|
1283
|
+
(attr) =>
|
|
1284
|
+
!(
|
|
1285
|
+
attr.type === 'JSXAttribute' &&
|
|
1286
|
+
attr.name &&
|
|
1287
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1288
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
1289
|
+
attr.name.name === 'data-preview-item-path' ||
|
|
1290
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
1291
|
+
attr.name.name === 'data-preview-style-type')
|
|
1292
|
+
)
|
|
1293
|
+
);
|
|
1294
|
+
healed++;
|
|
1295
|
+
}
|
|
1296
|
+
this.traverse(pathNode);
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
return healed;
|
|
1010
1300
|
}
|
|
1011
1301
|
|
|
1012
1302
|
module.exports = {
|
|
@@ -1020,4 +1310,7 @@ module.exports = {
|
|
|
1020
1310
|
inferPageKey,
|
|
1021
1311
|
sanitizeContradictoryMarkers,
|
|
1022
1312
|
sanitizeContradictoryMarkersInSource,
|
|
1313
|
+
healBroadContainerMarkers,
|
|
1314
|
+
healEmptyStateConditionals,
|
|
1315
|
+
healHiddenPreviewMarkers,
|
|
1023
1316
|
};
|
package/src/arc/validator.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const { parseSource } = require('./ast.cjs');
|
|
|
6
6
|
const { collectDesignSnapshot } = require('./semantic.cjs');
|
|
7
7
|
const { getDeep } = require('./manifest.cjs');
|
|
8
8
|
const { walkFiles, isJsxFile, rel } = require('./fs-utils.cjs');
|
|
9
|
+
const { enumerateContentPaths, isControlOnly, wildcardPath } = require('./fivora-contract.cjs');
|
|
9
10
|
|
|
10
11
|
function validateAstFiles(files) {
|
|
11
12
|
const results = [];
|
|
@@ -143,18 +144,57 @@ function countOverlap(a, b) {
|
|
|
143
144
|
return n;
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function coverageMetrics({
|
|
147
|
+
function coverageMetrics({
|
|
148
|
+
analyses,
|
|
149
|
+
plan,
|
|
150
|
+
appliedCount,
|
|
151
|
+
skippedDynamic,
|
|
152
|
+
alreadyEditable,
|
|
153
|
+
content,
|
|
154
|
+
controlOnlyPaths,
|
|
155
|
+
pathCoverage,
|
|
156
|
+
uncoveredVisibleText,
|
|
157
|
+
}) {
|
|
147
158
|
const detected = analyses.reduce((n, a) => n + (a.candidates || []).filter((c) => c.kind !== 'decoration' && c.kind !== 'already-editable').length, 0);
|
|
148
159
|
const skippedLow = plan.skipped.filter((s) => s.reason === 'low-confidence' || (s.confidence || 0) < 0.6).length;
|
|
149
160
|
const transformed = appliedCount;
|
|
150
|
-
const
|
|
161
|
+
const transformCoverage = detected === 0 ? 100 : Math.round((transformed / detected) * 1000) / 10;
|
|
162
|
+
|
|
163
|
+
const inventory = content ? enumerateContentPaths(content) : { concreteFields: new Set() };
|
|
164
|
+
const fieldMarkers = pathCoverage?.fieldMarkers || new Set();
|
|
165
|
+
const covers = (markerSet, path) =>
|
|
166
|
+
markerSet.has(path) || [...markerSet].some((marker) => wildcardPath(marker) === wildcardPath(path));
|
|
167
|
+
|
|
168
|
+
let visualRequired = 0;
|
|
169
|
+
let visualCovered = 0;
|
|
170
|
+
for (const path of inventory.concreteFields) {
|
|
171
|
+
if (isControlOnly(path, controlOnlyPaths || [])) continue;
|
|
172
|
+
visualRequired += 1;
|
|
173
|
+
if (covers(fieldMarkers, path)) visualCovered += 1;
|
|
174
|
+
}
|
|
175
|
+
const uncovered = Array.isArray(uncoveredVisibleText) ? uncoveredVisibleText.length : Number(uncoveredVisibleText) || 0;
|
|
176
|
+
let visualCoverage = 100;
|
|
177
|
+
if (visualRequired === 0) {
|
|
178
|
+
visualCoverage = uncovered === 0 ? 100 : Math.max(0, Math.round((1 / (1 + uncovered)) * 1000) / 10);
|
|
179
|
+
} else {
|
|
180
|
+
visualCoverage = Math.round((visualCovered / visualRequired) * 1000) / 10;
|
|
181
|
+
if (uncovered > 0) {
|
|
182
|
+
visualCoverage = Math.min(visualCoverage, Math.round((visualCovered / (visualRequired + uncovered)) * 1000) / 10);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
return {
|
|
152
187
|
detectedEditableCandidates: detected,
|
|
153
188
|
safelyTransformed: transformed,
|
|
154
189
|
alreadyEditable,
|
|
155
190
|
skippedDynamic,
|
|
156
191
|
skippedLowConfidence: skippedLow,
|
|
157
|
-
|
|
192
|
+
transformCoverage: Math.min(100, transformCoverage),
|
|
193
|
+
visualCovered,
|
|
194
|
+
visualRequired,
|
|
195
|
+
uncoveredVisibleText: uncovered,
|
|
196
|
+
visualCoverage: Math.min(100, visualCoverage),
|
|
197
|
+
editableCoverage: Math.min(100, visualCoverage),
|
|
158
198
|
};
|
|
159
199
|
}
|
|
160
200
|
|
package/src/arc/version.cjs
CHANGED