@deneb-ui/cli 2.0.70 → 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.
@@ -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
- indexName = callback.params.some((p) => p.name === 'index') ? 'denebIndex' : 'index';
339
- callback.params.push(b.identifier(indexName));
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
- inner.traverse(inner);
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
- b.identifier(arrayName),
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 true;
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
- visitExportDefaultDeclaration(pathNode) {
680
- if (injected) return false;
681
- const decl = pathNode.node.declaration;
682
- if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
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
- visitFunctionDeclaration(pathNode) {
688
- if (injected) return false;
689
- const name = pathNode.node.id && pathNode.node.id.name;
690
- if (name && /^[A-Z]/.test(name)) {
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
- visitVariableDeclarator(pathNode) {
697
- if (injected) return false;
698
- const id = pathNode.node.id;
699
- const init = pathNode.node.init;
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
- if (!injected) {
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
- if (!fileAlreadyUsesSiteDataHook(ast)) {
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
- visitJSXOpeningElement(pathNode) {
1141
- const attrs = pathNode.node.attributes || [];
1142
- const hasEditable = attrs.some(
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
- const hasStatic = attrs.some(
1150
- (a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
1151
- );
1152
- if (hasEditable && hasStatic) {
1153
- pathNode.node.attributes = attrs.filter(
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
- if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0) return { code, updated: false };
1179
- return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden };
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
- /\bhidden(?:\s|=|\/?>)/i.test(token) ||
2181
+ /(?:^|\s)hidden(?:\s|=|\/?>)/i.test(token) ||
2182
2182
  /\baria-hidden\s*=\s*(?:"true"|'true')/i.test(token)
2183
2183
  ) {
2184
2184
  return true;
@@ -9,6 +9,7 @@
9
9
  "__fivoraIntake.referenceWebsiteUrl",
10
10
  "additionalPages[*].id",
11
11
  "additionalPages[*].route",
12
+ "additionalPages[*].label",
12
13
  "common.currency",
13
14
  "common.whatsappNumber",
14
15
  "common.whatsapp",