@deneb-ui/cli 2.0.69 → 2.0.71
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 +34 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +160 -2
- package/src/arc/field-paths.cjs +3 -3
- package/src/arc/fivora-contract.cjs +2 -2
- package/src/arc/learning.cjs +9 -1
- package/src/arc/manifest.cjs +21 -5
- package/src/arc/residual.cjs +12 -1
- package/src/arc/semantic.cjs +16 -0
- package/src/arc/transformer.cjs +267 -43
- package/src/common/template-visual-edit-contract.ts +1 -1
- package/src/platform/platform-contract.json +1 -0
- package/src/tools/deneb-doctor.cjs +298 -10
- package/src/tools/deneb-template-validator.cjs +2 -2
package/src/arc/transformer.cjs
CHANGED
|
@@ -313,6 +313,20 @@ function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
|
313
313
|
if (wrapped) node.children = nextChildren;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
function buildCompositeKeyAttribute(binding, indexName) {
|
|
317
|
+
const fields = ['id', 'slug', 'title', 'name'];
|
|
318
|
+
let expr = b.memberExpression(b.identifier(binding), b.identifier(fields[0]), false);
|
|
319
|
+
for (let i = 1; i < fields.length; i++) {
|
|
320
|
+
expr = b.logicalExpression(
|
|
321
|
+
'||',
|
|
322
|
+
expr,
|
|
323
|
+
b.memberExpression(b.identifier(binding), b.identifier(fields[i]), false)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
expr = b.logicalExpression('||', expr, b.identifier(indexName));
|
|
327
|
+
return b.jsxAttribute(b.jsxIdentifier('key'), b.jsxExpressionContainer(expr));
|
|
328
|
+
}
|
|
329
|
+
|
|
316
330
|
/**
|
|
317
331
|
* Converts a literal-array `.map()` render into a Fivora list contract.
|
|
318
332
|
*
|
|
@@ -321,7 +335,7 @@ function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
|
321
335
|
* emitted as JSX template literals (`items[${index}].title`) so added and
|
|
322
336
|
* reordered items stay editable, which is what strict mode requires.
|
|
323
337
|
*/
|
|
324
|
-
function applyCollectionTransform(ast, transform, isClient = false) {
|
|
338
|
+
function applyCollectionTransform(ast, transform, isClient = false, isTypeScript = false) {
|
|
325
339
|
const listPath = transform.listField;
|
|
326
340
|
const binding = transform.itemParam;
|
|
327
341
|
if (!listPath || !binding) return false;
|
|
@@ -335,8 +349,30 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
335
349
|
// The contract needs a concrete index for each item marker.
|
|
336
350
|
let indexName = transform.indexParam;
|
|
337
351
|
if (!indexName) {
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
if (callback.params && callback.params.length >= 2 && callback.params[1]?.type === 'Identifier') {
|
|
353
|
+
indexName = callback.params[1].name;
|
|
354
|
+
} else {
|
|
355
|
+
indexName = callback.params.some((p) => p?.name === 'index') ? 'denebIndex' : 'index';
|
|
356
|
+
const indexId = b.identifier(indexName);
|
|
357
|
+
if (isTypeScript) {
|
|
358
|
+
indexId.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
359
|
+
}
|
|
360
|
+
callback.params.push(indexId);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// In TypeScript files, prevent TS7006 implicit any on callback parameters
|
|
365
|
+
if (isTypeScript && callback.params && callback.params.length > 0) {
|
|
366
|
+
const itemParam = callback.params[0];
|
|
367
|
+
if (itemParam && !itemParam.typeAnnotation) {
|
|
368
|
+
itemParam.typeAnnotation = b.tsTypeAnnotation(b.tsAnyKeyword());
|
|
369
|
+
}
|
|
370
|
+
if (callback.params.length >= 2) {
|
|
371
|
+
const idxParam = callback.params[1];
|
|
372
|
+
if (idxParam && !idxParam.typeAnnotation) {
|
|
373
|
+
idxParam.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
340
376
|
}
|
|
341
377
|
|
|
342
378
|
const itemRoot = findElementByLoc(ast, transform.loc);
|
|
@@ -355,6 +391,11 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
355
391
|
b.jsxAttribute(b.jsxIdentifier('data-preview-style-type'), b.stringLiteral('card'))
|
|
356
392
|
);
|
|
357
393
|
}
|
|
394
|
+
if (!hasJsxAttribute(itemRoot.node, 'key')) {
|
|
395
|
+
itemRoot.node.openingElement.attributes.push(
|
|
396
|
+
buildCompositeKeyAttribute(binding, indexName)
|
|
397
|
+
);
|
|
398
|
+
}
|
|
358
399
|
|
|
359
400
|
markItemFields(callback, { listPath, binding, indexName });
|
|
360
401
|
markComponentRefItemsStatic(callback, binding);
|
|
@@ -374,7 +415,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
374
415
|
);
|
|
375
416
|
}
|
|
376
417
|
|
|
377
|
-
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef));
|
|
418
|
+
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef), isTypeScript);
|
|
378
419
|
}
|
|
379
420
|
|
|
380
421
|
function findMapCall(ast, loc) {
|
|
@@ -395,7 +436,7 @@ function findMapCall(ast, loc) {
|
|
|
395
436
|
hit = true;
|
|
396
437
|
return false;
|
|
397
438
|
}
|
|
398
|
-
|
|
439
|
+
this.traverse(inner);
|
|
399
440
|
},
|
|
400
441
|
});
|
|
401
442
|
if (hit) {
|
|
@@ -543,7 +584,7 @@ function findEnclosingFunction(pathNode) {
|
|
|
543
584
|
return null;
|
|
544
585
|
}
|
|
545
586
|
|
|
546
|
-
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false) {
|
|
587
|
+
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false, isTypeScript = false) {
|
|
547
588
|
const arrayName = mapCallPath.node.callee.object?.name;
|
|
548
589
|
if (!arrayName) return false;
|
|
549
590
|
let bound = false;
|
|
@@ -568,9 +609,13 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, merg
|
|
|
568
609
|
const body = fnPath.node.body.body;
|
|
569
610
|
const already = body.some((stmt) => recast.print(stmt).code.includes(`const ${arrayName} =`));
|
|
570
611
|
if (!already) {
|
|
612
|
+
const localId = b.identifier(arrayName);
|
|
613
|
+
if (isTypeScript) {
|
|
614
|
+
localId.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
615
|
+
}
|
|
571
616
|
const localDecl = b.variableDeclaration('const', [
|
|
572
617
|
b.variableDeclarator(
|
|
573
|
-
|
|
618
|
+
localId,
|
|
574
619
|
mergeDefaultRefs
|
|
575
620
|
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
576
621
|
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
@@ -588,6 +633,9 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, merg
|
|
|
588
633
|
return false;
|
|
589
634
|
}
|
|
590
635
|
|
|
636
|
+
if (isTypeScript && node.id?.type === 'Identifier' && !node.id.typeAnnotation) {
|
|
637
|
+
node.id.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
638
|
+
}
|
|
591
639
|
node.init = siteDataListBinding(listPath.split('.'), node.init);
|
|
592
640
|
bound = true;
|
|
593
641
|
return false;
|
|
@@ -656,15 +704,41 @@ function resolveSiteDataRuntimeSpecifier(profile) {
|
|
|
656
704
|
return '@deneb-ui/ui';
|
|
657
705
|
}
|
|
658
706
|
|
|
707
|
+
function functionReferencesSiteData(fn) {
|
|
708
|
+
if (!fn || !fn.body) return false;
|
|
709
|
+
let found = false;
|
|
710
|
+
recast.types.visit(fn.body, {
|
|
711
|
+
visitIdentifier(pathNode) {
|
|
712
|
+
if (pathNode.node.name === 'siteData') {
|
|
713
|
+
found = true;
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
this.traverse(pathNode);
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
return found;
|
|
720
|
+
}
|
|
721
|
+
|
|
659
722
|
function injectSiteDataHook(ast) {
|
|
660
|
-
const program = ast.program || ast;
|
|
661
723
|
let injected = false;
|
|
724
|
+
let anyFunctionReferencedSiteData = false;
|
|
725
|
+
let alreadyHasUseSiteData = false;
|
|
726
|
+
|
|
727
|
+
recast.types.visit(ast, {
|
|
728
|
+
visitCallExpression(pathNode) {
|
|
729
|
+
if (pathNode.node.callee && pathNode.node.callee.name === 'useSiteData') {
|
|
730
|
+
alreadyHasUseSiteData = true;
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
this.traverse(pathNode);
|
|
734
|
+
},
|
|
735
|
+
});
|
|
662
736
|
|
|
663
737
|
function injectIntoFunction(fn) {
|
|
664
738
|
if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
|
|
665
739
|
const body = fn.body.body;
|
|
666
740
|
const already = body.some((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
667
|
-
if (already) return
|
|
741
|
+
if (already) return false;
|
|
668
742
|
const hook = b.variableDeclaration('const', [
|
|
669
743
|
b.variableDeclarator(
|
|
670
744
|
b.identifier('siteData'),
|
|
@@ -672,40 +746,70 @@ function injectSiteDataHook(ast) {
|
|
|
672
746
|
),
|
|
673
747
|
]);
|
|
674
748
|
body.unshift(hook);
|
|
749
|
+
alreadyHasUseSiteData = true;
|
|
675
750
|
return true;
|
|
676
751
|
}
|
|
677
752
|
|
|
753
|
+
// Pass 1: Inject useSiteData() into EVERY component function that references siteData
|
|
678
754
|
recast.types.visit(ast, {
|
|
679
|
-
|
|
680
|
-
if (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
injected = injectIntoFunction(decl);
|
|
755
|
+
visitFunctionDeclaration(pathNode) {
|
|
756
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
757
|
+
anyFunctionReferencedSiteData = true;
|
|
758
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
684
759
|
}
|
|
685
760
|
this.traverse(pathNode);
|
|
686
761
|
},
|
|
687
|
-
|
|
688
|
-
if (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
injected = injectIntoFunction(pathNode.node);
|
|
692
|
-
return false;
|
|
762
|
+
visitFunctionExpression(pathNode) {
|
|
763
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
764
|
+
anyFunctionReferencedSiteData = true;
|
|
765
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
693
766
|
}
|
|
694
767
|
this.traverse(pathNode);
|
|
695
768
|
},
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
701
|
-
injected = injectIntoFunction(init);
|
|
702
|
-
return false;
|
|
769
|
+
visitArrowFunctionExpression(pathNode) {
|
|
770
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
771
|
+
anyFunctionReferencedSiteData = true;
|
|
772
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
703
773
|
}
|
|
704
774
|
this.traverse(pathNode);
|
|
705
775
|
},
|
|
706
776
|
});
|
|
707
777
|
|
|
708
|
-
|
|
778
|
+
// Pass 2: If no component references siteData yet (e.g. before subsequent AST edits),
|
|
779
|
+
// ensure the primary component receives useSiteData()
|
|
780
|
+
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
781
|
+
recast.types.visit(ast, {
|
|
782
|
+
visitExportDefaultDeclaration(pathNode) {
|
|
783
|
+
if (injected) return false;
|
|
784
|
+
const decl = pathNode.node.declaration;
|
|
785
|
+
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
786
|
+
injected = injectIntoFunction(decl);
|
|
787
|
+
}
|
|
788
|
+
this.traverse(pathNode);
|
|
789
|
+
},
|
|
790
|
+
visitFunctionDeclaration(pathNode) {
|
|
791
|
+
if (injected) return false;
|
|
792
|
+
const name = pathNode.node.id && pathNode.node.id.name;
|
|
793
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
794
|
+
injected = injectIntoFunction(pathNode.node);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
this.traverse(pathNode);
|
|
798
|
+
},
|
|
799
|
+
visitVariableDeclarator(pathNode) {
|
|
800
|
+
if (injected) return false;
|
|
801
|
+
const id = pathNode.node.id;
|
|
802
|
+
const init = pathNode.node.init;
|
|
803
|
+
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
804
|
+
injected = injectIntoFunction(init);
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
this.traverse(pathNode);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
709
813
|
recast.types.visit(ast, {
|
|
710
814
|
visitFunctionDeclaration(pathNode) {
|
|
711
815
|
if (injected) return false;
|
|
@@ -754,6 +858,8 @@ function applyFilePlan(filePlan, profile) {
|
|
|
754
858
|
'style-bind',
|
|
755
859
|
]);
|
|
756
860
|
|
|
861
|
+
const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
|
|
862
|
+
|
|
757
863
|
// Collections run first: they rewrite the array declaration and add an index
|
|
758
864
|
// parameter, and later per-element edits must observe that shape.
|
|
759
865
|
const ordered = [...filePlan.transformations].sort(
|
|
@@ -768,7 +874,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
768
874
|
|
|
769
875
|
if (transform.operation === 'collection-conversion') {
|
|
770
876
|
try {
|
|
771
|
-
if (applyCollectionTransform(ast, transform, isClient)) applied++;
|
|
877
|
+
if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
|
|
772
878
|
else failures.push({ loc: transform.loc, reason: 'collection-not-bindable' });
|
|
773
879
|
} catch (err) {
|
|
774
880
|
failures.push({ loc: transform.loc, reason: err.message });
|
|
@@ -796,9 +902,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
796
902
|
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
797
903
|
if (isClient) {
|
|
798
904
|
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
799
|
-
|
|
800
|
-
injectSiteDataHook(ast);
|
|
801
|
-
}
|
|
905
|
+
injectSiteDataHook(ast);
|
|
802
906
|
ensureImport(ast, runtimeSpecifier, ['useSiteData']);
|
|
803
907
|
} else {
|
|
804
908
|
ensureDefaultImport(ast, siteDataImport, 'siteData');
|
|
@@ -1137,20 +1241,69 @@ function ensureJsonModule(tsconfig) {
|
|
|
1137
1241
|
function sanitizeContradictoryMarkers(ast) {
|
|
1138
1242
|
let cleaned = 0;
|
|
1139
1243
|
recast.types.visit(ast, {
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1244
|
+
visitJSXElement(pathNode) {
|
|
1245
|
+
const opening = pathNode.node.openingElement;
|
|
1246
|
+
if (!opening || !Array.isArray(opening.attributes)) {
|
|
1247
|
+
this.traverse(pathNode);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const attrs = opening.attributes;
|
|
1251
|
+
const hasStatic = attrs.some(
|
|
1252
|
+
(a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
|
|
1253
|
+
);
|
|
1254
|
+
if (!hasStatic) {
|
|
1255
|
+
this.traverse(pathNode);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// 0. Broad container collision: broad content containers cannot carry data-preview-static
|
|
1260
|
+
const tag = getJsxName(pathNode.node);
|
|
1261
|
+
const lower = String(tag || '').toLowerCase();
|
|
1262
|
+
if (BROAD_CONTENT_CONTAINERS.has(tag) || BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
1263
|
+
opening.attributes = attrs.filter(
|
|
1264
|
+
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
1265
|
+
);
|
|
1266
|
+
cleaned++;
|
|
1267
|
+
this.traverse(pathNode);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// 1. Direct collision: cannot share element with editable markers
|
|
1272
|
+
const hasDirectEditable = attrs.some(
|
|
1143
1273
|
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
1144
1274
|
a.name.name === 'data-preview-field-path' ||
|
|
1145
1275
|
a.name.name === 'data-preview-list-path' ||
|
|
1146
1276
|
a.name.name === 'data-preview-item-path'
|
|
1147
1277
|
)
|
|
1148
1278
|
);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (
|
|
1153
|
-
pathNode.node
|
|
1279
|
+
|
|
1280
|
+
// 2. Hierarchical collision: static element cannot enclose editable descendants
|
|
1281
|
+
let hasNestedEditable = false;
|
|
1282
|
+
if (!hasDirectEditable) {
|
|
1283
|
+
recast.types.visit(pathNode.node, {
|
|
1284
|
+
visitJSXElement(inner) {
|
|
1285
|
+
if (inner.node === pathNode.node) {
|
|
1286
|
+
this.traverse(inner);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const innerAttrs = inner.node.openingElement?.attributes || [];
|
|
1290
|
+
if (innerAttrs.some(
|
|
1291
|
+
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
1292
|
+
a.name.name === 'data-preview-field-path' ||
|
|
1293
|
+
a.name.name === 'data-preview-list-path' ||
|
|
1294
|
+
a.name.name === 'data-preview-item-path'
|
|
1295
|
+
)
|
|
1296
|
+
)) {
|
|
1297
|
+
hasNestedEditable = true;
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
this.traverse(inner);
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (hasDirectEditable || hasNestedEditable) {
|
|
1306
|
+
opening.attributes = attrs.filter(
|
|
1154
1307
|
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
1155
1308
|
);
|
|
1156
1309
|
cleaned++;
|
|
@@ -1162,7 +1315,7 @@ function sanitizeContradictoryMarkers(ast) {
|
|
|
1162
1315
|
}
|
|
1163
1316
|
|
|
1164
1317
|
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
1165
|
-
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path')) {
|
|
1318
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path') && !code.includes('overflow-hidden')) {
|
|
1166
1319
|
return { code, updated: false };
|
|
1167
1320
|
}
|
|
1168
1321
|
let ast;
|
|
@@ -1175,8 +1328,9 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
|
1175
1328
|
const healedBroad = healBroadContainerMarkers(ast);
|
|
1176
1329
|
const healedEmpty = healEmptyStateConditionals(ast);
|
|
1177
1330
|
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
1178
|
-
|
|
1179
|
-
|
|
1331
|
+
const healedOverflow = healSectionOverflowHidden(ast);
|
|
1332
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0 && healedOverflow === 0) return { code, updated: false };
|
|
1333
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden + healedOverflow };
|
|
1180
1334
|
}
|
|
1181
1335
|
|
|
1182
1336
|
function healBroadContainerMarkers(ast) {
|
|
@@ -1415,8 +1569,75 @@ function healLegacyProductDetailLinks(ast) {
|
|
|
1415
1569
|
return healed;
|
|
1416
1570
|
}
|
|
1417
1571
|
|
|
1572
|
+
function healSectionOverflowHidden(ast) {
|
|
1573
|
+
let healed = 0;
|
|
1574
|
+
recast.types.visit(ast, {
|
|
1575
|
+
visitJSXOpeningElement(pathNode) {
|
|
1576
|
+
const node = pathNode.node;
|
|
1577
|
+
const tag = getJsxName(node);
|
|
1578
|
+
const lower = String(tag || '').toLowerCase();
|
|
1579
|
+
const isContainer = lower === 'section' || lower === 'div' || lower === 'main' || lower === 'article';
|
|
1580
|
+
if (!isContainer) {
|
|
1581
|
+
this.traverse(pathNode);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const classAttr = (node.attributes || []).find(
|
|
1585
|
+
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
1586
|
+
);
|
|
1587
|
+
if (!classAttr || !classAttr.value) {
|
|
1588
|
+
this.traverse(pathNode);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
let modified = false;
|
|
1592
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
1593
|
+
const val = String(classAttr.value.value || '');
|
|
1594
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
1595
|
+
classAttr.value.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1596
|
+
modified = true;
|
|
1597
|
+
}
|
|
1598
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
1599
|
+
const expr = classAttr.value.expression;
|
|
1600
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
1601
|
+
const val = String(expr.value || '');
|
|
1602
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
1603
|
+
expr.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1604
|
+
modified = true;
|
|
1605
|
+
}
|
|
1606
|
+
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
1607
|
+
for (const quasi of expr.quasis || []) {
|
|
1608
|
+
if (quasi.value && /\boverflow-hidden\b/.test(quasi.value.raw || '')) {
|
|
1609
|
+
quasi.value.raw = (quasi.value.raw || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1610
|
+
if (quasi.value.cooked) {
|
|
1611
|
+
quasi.value.cooked = (quasi.value.cooked || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1612
|
+
}
|
|
1613
|
+
modified = true;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (modified) healed++;
|
|
1619
|
+
this.traverse(pathNode);
|
|
1620
|
+
},
|
|
1621
|
+
});
|
|
1622
|
+
return healed;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
1626
|
+
if (!code.includes('siteData')) return code;
|
|
1627
|
+
try {
|
|
1628
|
+
const ast = parseSource(code, filePath);
|
|
1629
|
+
const injected = injectSiteDataHook(ast);
|
|
1630
|
+
if (injected) {
|
|
1631
|
+
ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
|
|
1632
|
+
return printSource(ast, code);
|
|
1633
|
+
}
|
|
1634
|
+
} catch {}
|
|
1635
|
+
return code;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1418
1638
|
module.exports = {
|
|
1419
1639
|
applyFilePlan,
|
|
1640
|
+
applyCollectionTransform,
|
|
1420
1641
|
instrumentLayoutSource,
|
|
1421
1642
|
instrumentPageKey,
|
|
1422
1643
|
resolveSiteDataSpecifier,
|
|
@@ -1429,6 +1650,9 @@ module.exports = {
|
|
|
1429
1650
|
healBroadContainerMarkers,
|
|
1430
1651
|
healEmptyStateConditionals,
|
|
1431
1652
|
healHiddenPreviewMarkers,
|
|
1653
|
+
healSectionOverflowHidden,
|
|
1432
1654
|
healLegacyProductDetailLinks,
|
|
1655
|
+
healMissingSiteDataHooks,
|
|
1656
|
+
injectSiteDataHook,
|
|
1433
1657
|
};
|
|
1434
1658
|
|
|
@@ -2178,7 +2178,7 @@ function auditSensitiveAttributes(
|
|
|
2178
2178
|
|
|
2179
2179
|
function isHiddenHtmlElement(token: string, tag: string) {
|
|
2180
2180
|
if (
|
|
2181
|
-
|
|
2181
|
+
/(?:^|\s)hidden(?:\s|=|\/?>)/i.test(token) ||
|
|
2182
2182
|
/\baria-hidden\s*=\s*(?:"true"|'true')/i.test(token)
|
|
2183
2183
|
) {
|
|
2184
2184
|
return true;
|