@deneb-ui/cli 2.0.84 → 2.0.86

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.
@@ -27,16 +27,28 @@ const {
27
27
  } = require('./ast.cjs');
28
28
  const { toPosix } = require('./fs-utils.cjs');
29
29
  const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
30
-
31
- function findElementByLoc(ast, loc) {
32
- if (!loc) return null;
33
- const parts = loc.split(':');
30
+ const {
31
+ hasMetadataExport,
32
+ hasClientDirective,
33
+ needsClientDirective,
34
+ canSafelyInjectClientDirective,
35
+ ensureClientDirective,
36
+ } = require('./rsc-boundary.cjs');
37
+
38
+ function findElementByLoc(ast, loc, tagName = null) {
39
+ if (!loc && !tagName) return null;
40
+ const parts = loc ? loc.split(':') : [];
34
41
  const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
35
42
  let found = null;
36
43
  recast.types.visit(ast, {
37
44
  visitJSXElement(pathNode) {
38
45
  const k = locKey(pathNode.node);
39
- if (k === targetLoc || k === loc) {
46
+ const name = getJsxName(pathNode.node);
47
+ if (loc && (k === targetLoc || k === loc || (parts.length >= 2 && k && k.startsWith(`${parts[0]}:${parts[1]}`)))) {
48
+ found = pathNode;
49
+ return false;
50
+ }
51
+ if (!loc && tagName && (name === tagName || name.endsWith(`.${tagName}`))) {
40
52
  found = pathNode;
41
53
  return false;
42
54
  }
@@ -235,7 +247,18 @@ function applyTransformToElement(pathNode, transform) {
235
247
  ensurePreviewPath(node, transform.field);
236
248
  return;
237
249
  }
238
- if (transform.operation === 'extract-image') {
250
+ if (transform.operation === 'extract-image' || transform.operation === 'extract-picture') {
251
+ const tagName = getJsxName(node);
252
+ if (tagName === 'picture') {
253
+ const children = node.children || [];
254
+ const imgChild = children.find((c) => c && c.type === 'JSXElement' && (getJsxName(c) === 'img' || getJsxName(c) === 'Image'));
255
+ if (imgChild) {
256
+ replaceAttrValue(imgChild, 'src', siteDataBinding(transform.field.split('.'), transform.fallback, 'image'));
257
+ ensurePreviewPath(imgChild, transform.field);
258
+ }
259
+ ensurePreviewPath(node, transform.field);
260
+ return;
261
+ }
239
262
  replaceAttrValue(node, 'src', siteDataBinding(transform.field.split('.'), transform.fallback, 'image'));
240
263
  ensurePreviewPath(node, transform.field);
241
264
  return;
@@ -257,6 +280,59 @@ function applyTransformToElement(pathNode, transform) {
257
280
  }
258
281
  return;
259
282
  }
283
+ if (transform.operation === 'bind-button-with-icon') {
284
+ ensurePreviewPath(node, transform.field);
285
+ ensureStyleAttrs(node, transform.field, 'button');
286
+ for (let i = 0; i < (node.children || []).length; i++) {
287
+ const child = node.children[i];
288
+ if (child.type === 'JSXText' && child.value.trim().length > 0) {
289
+ node.children[i] = b.jsxExpressionContainer(
290
+ siteDataBinding(transform.field.split('.'), child.value.trim(), 'text')
291
+ );
292
+ }
293
+ }
294
+ return;
295
+ }
296
+ if (transform.operation === 'bind-highlighted-heading') {
297
+ ensurePreviewPath(node, transform.field);
298
+ ensureStyleAttrs(node, transform.field, 'text');
299
+ for (let i = 0; i < (node.children || []).length; i++) {
300
+ const child = node.children[i];
301
+ if (child.type === 'JSXText' && child.value.trim().length > 0) {
302
+ node.children[i] = b.jsxExpressionContainer(
303
+ siteDataBinding(transform.field.split('.'), child.value.trim(), 'text')
304
+ );
305
+ } else if (child.type === 'JSXElement') {
306
+ const highlightField = `${transform.field}Highlight`;
307
+ ensurePreviewPath(child, highlightField);
308
+ for (let j = 0; j < (child.children || []).length; j++) {
309
+ if (child.children[j].type === 'JSXText' && child.children[j].value.trim().length > 0) {
310
+ child.children[j] = b.jsxExpressionContainer(
311
+ siteDataBinding(highlightField.split('.'), child.children[j].value.trim(), 'text')
312
+ );
313
+ }
314
+ }
315
+ }
316
+ }
317
+ return;
318
+ }
319
+ if (transform.operation === 'prop-flow-callsite') {
320
+ if (transform.propTransforms) {
321
+ for (const [propName, propInfo] of Object.entries(transform.propTransforms)) {
322
+ replaceAttrValue(
323
+ node,
324
+ propName,
325
+ siteDataBinding(propInfo.field.split('.'), propInfo.fallback, propInfo.type || 'text')
326
+ );
327
+ }
328
+ }
329
+ if (transform.previewPath && !hasJsxAttribute(node, 'previewPath')) {
330
+ node.openingElement.attributes.push(
331
+ b.jsxAttribute(b.jsxIdentifier('previewPath'), b.stringLiteral(transform.previewPath))
332
+ );
333
+ }
334
+ return;
335
+ }
260
336
  if (transform.operation === 'wrap-text-span') {
261
337
  wrapLiteralTextChildren(node, transform.field, transform.fallback);
262
338
  return;
@@ -283,6 +359,33 @@ function applyTransformToElement(pathNode, transform) {
283
359
  return;
284
360
  }
285
361
  ensureStyleAttrs(node, transform.stylePath, transform.styleKind || 'text');
362
+ return;
363
+ }
364
+ if (transform.operation === 'extract-tailwind-bg') {
365
+ const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
366
+ if (classAttr && classAttr.value) {
367
+ if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
368
+ const cleaned = String(classAttr.value.value).replace(/\bbg-\[url\([^)]+\)\]\s*/g, '').trim();
369
+ classAttr.value = b.stringLiteral(cleaned);
370
+ }
371
+ }
372
+ const urlParts = transform.field.split('.');
373
+ const styleExpr = b.objectExpression([
374
+ b.property(
375
+ 'init',
376
+ b.identifier('backgroundImage'),
377
+ b.templateLiteral(
378
+ [
379
+ b.templateElement({ raw: 'url(', cooked: 'url(' }, false),
380
+ b.templateElement({ raw: ')', cooked: ')' }, true),
381
+ ],
382
+ [siteDataBinding(urlParts, transform.fallback || transform.bgUrl || '', 'image')]
383
+ )
384
+ ),
385
+ ]);
386
+ replaceAttrValue(node, 'style', styleExpr);
387
+ ensurePreviewPath(node, transform.field);
388
+ return;
286
389
  }
287
390
  }
288
391
 
@@ -392,6 +495,22 @@ function applyCollectionTransform(ast, transform, isClient = false, isTypeScript
392
495
  const itemRoot = findElementByLoc(ast, transform.loc);
393
496
  if (!itemRoot) return false;
394
497
 
498
+ const rootTagName = getJsxName(itemRoot.node);
499
+ const isCustomChild = /^[A-Z]/.test(rootTagName);
500
+
501
+ if (isCustomChild) {
502
+ if (!hasJsxAttribute(itemRoot.node, 'previewItemPath')) {
503
+ itemRoot.node.openingElement.attributes.push(
504
+ jsxTemplatePathAttr('previewItemPath', listPath, indexName)
505
+ );
506
+ }
507
+ if (!hasJsxAttribute(itemRoot.node, 'index')) {
508
+ itemRoot.node.openingElement.attributes.push(
509
+ b.jsxAttribute(b.jsxIdentifier('index'), b.jsxExpressionContainer(b.identifier(indexName)))
510
+ );
511
+ }
512
+ }
513
+
395
514
  if (!hasJsxAttribute(itemRoot.node, 'data-preview-item-path')) {
396
515
  itemRoot.node.openingElement.attributes.push(
397
516
  jsxTemplatePathAttr('data-preview-item-path', listPath, indexName)
@@ -416,7 +535,9 @@ function applyCollectionTransform(ast, transform, isClient = false, isTypeScript
416
535
  } else {
417
536
  markItemFields(callback, { listPath, binding, indexName });
418
537
  }
419
- markComponentRefItemsStatic(callback, binding);
538
+ if (!isCustomChild) {
539
+ markComponentRefItemsStatic(callback, binding);
540
+ }
420
541
 
421
542
  const container = findListContainer(mapCall);
422
543
  if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
@@ -615,6 +736,469 @@ function wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName) {
615
736
  node.children = nextChildren;
616
737
  }
617
738
 
739
+ function instrumentChildCardComponent(ast, transform, isTypeScript = false) {
740
+ const componentName = transform.componentName;
741
+ const listPath = transform.listField || transform.listPath || '';
742
+ if (!componentName) return false;
743
+
744
+ let instrumented = false;
745
+
746
+ function updateTsInterface(rootAst, interfaceName) {
747
+ recast.types.visit(rootAst, {
748
+ visitTSInterfaceDeclaration(pathNode) {
749
+ if (pathNode.node.id?.name === interfaceName) {
750
+ const body = pathNode.node.body?.body || [];
751
+ if (!body.some((prop) => prop.key?.name === 'previewItemPath')) {
752
+ const sig1 = b.tsPropertySignature(b.identifier('previewItemPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
753
+ sig1.optional = true;
754
+ body.push(sig1);
755
+ instrumented = true;
756
+ }
757
+ if (!body.some((prop) => prop.key?.name === 'index')) {
758
+ const sig2 = b.tsPropertySignature(b.identifier('index'), b.tsTypeAnnotation(b.tsNumberKeyword()));
759
+ sig2.optional = true;
760
+ body.push(sig2);
761
+ instrumented = true;
762
+ }
763
+ }
764
+ this.traverse(pathNode);
765
+ },
766
+ visitTSTypeAliasDeclaration(pathNode) {
767
+ if (pathNode.node.id?.name === interfaceName && pathNode.node.typeAnnotation?.type === 'TSTypeLiteral') {
768
+ const members = pathNode.node.typeAnnotation.members || [];
769
+ if (!members.some((m) => m.key?.name === 'previewItemPath')) {
770
+ const sig1 = b.tsPropertySignature(b.identifier('previewItemPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
771
+ sig1.optional = true;
772
+ members.push(sig1);
773
+ instrumented = true;
774
+ }
775
+ if (!members.some((m) => m.key?.name === 'index')) {
776
+ const sig2 = b.tsPropertySignature(b.identifier('index'), b.tsTypeAnnotation(b.tsNumberKeyword()));
777
+ sig2.optional = true;
778
+ members.push(sig2);
779
+ instrumented = true;
780
+ }
781
+ }
782
+ this.traverse(pathNode);
783
+ },
784
+ });
785
+ }
786
+
787
+ function instrumentFunctionProps(fnNode, isTs) {
788
+ let changed = false;
789
+ let params = fnNode.params || [];
790
+ if (!params.length) {
791
+ fnNode.params = [
792
+ b.objectPattern([
793
+ b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath')),
794
+ b.property('init', b.identifier('index'), b.identifier('index')),
795
+ ]),
796
+ ];
797
+ changed = true;
798
+ } else {
799
+ const firstParam = params[0];
800
+ if (firstParam.type === 'ObjectPattern') {
801
+ const hasPreviewPath = firstParam.properties.some(
802
+ (p) => p.key?.name === 'previewItemPath' || p.argument?.name === 'previewItemPath'
803
+ );
804
+ if (!hasPreviewPath) {
805
+ const prop = b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath'));
806
+ prop.shorthand = true;
807
+ firstParam.properties.push(prop);
808
+ changed = true;
809
+ }
810
+ const hasIndex = firstParam.properties.some(
811
+ (p) => p.key?.name === 'index' || p.argument?.name === 'index'
812
+ );
813
+ if (!hasIndex) {
814
+ const prop = b.property('init', b.identifier('index'), b.identifier('index'));
815
+ prop.shorthand = true;
816
+ firstParam.properties.push(prop);
817
+ changed = true;
818
+ }
819
+
820
+ if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
821
+ const typeAnn = firstParam.typeAnnotation.typeAnnotation;
822
+ if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
823
+ updateTsInterface(ast, typeAnn.typeName.name);
824
+ }
825
+ }
826
+ } else if (firstParam.type === 'Identifier') {
827
+ const paramName = firstParam.name;
828
+ if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
829
+ const typeAnn = firstParam.typeAnnotation.typeAnnotation;
830
+ if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
831
+ updateTsInterface(ast, typeAnn.typeName.name);
832
+ }
833
+ }
834
+ recast.types.visit(fnNode.body || fnNode, {
835
+ visitVariableDeclarator(vPath) {
836
+ if (vPath.node.id?.type === 'ObjectPattern' && vPath.node.init?.name === paramName) {
837
+ const propsList = vPath.node.id.properties;
838
+ if (!propsList.some((p) => p.key?.name === 'previewItemPath')) {
839
+ const prop = b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath'));
840
+ prop.shorthand = true;
841
+ propsList.push(prop);
842
+ changed = true;
843
+ }
844
+ if (!propsList.some((p) => p.key?.name === 'index')) {
845
+ const prop = b.property('init', b.identifier('index'), b.identifier('index'));
846
+ prop.shorthand = true;
847
+ propsList.push(prop);
848
+ changed = true;
849
+ }
850
+ }
851
+ this.traverse(vPath);
852
+ },
853
+ });
854
+ }
855
+ }
856
+ return changed;
857
+ }
858
+
859
+ recast.types.visit(ast, {
860
+ visitFunctionDeclaration(pathNode) {
861
+ if (pathNode.node.id?.name === componentName) {
862
+ if (instrumentFunctionProps(pathNode.node, isTypeScript)) instrumented = true;
863
+ }
864
+ this.traverse(pathNode);
865
+ },
866
+ visitVariableDeclarator(pathNode) {
867
+ if (pathNode.node.id?.name === componentName) {
868
+ const init = pathNode.node.init;
869
+ if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
870
+ if (instrumentFunctionProps(init, isTypeScript)) instrumented = true;
871
+ }
872
+ }
873
+ this.traverse(pathNode);
874
+ },
875
+ visitExportDefaultDeclaration(pathNode) {
876
+ const decl = pathNode.node.declaration;
877
+ if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
878
+ const fnName = decl.id?.name;
879
+ if (!fnName || fnName === componentName) {
880
+ if (instrumentFunctionProps(decl, isTypeScript)) instrumented = true;
881
+ }
882
+ }
883
+ this.traverse(pathNode);
884
+ },
885
+ });
886
+
887
+ let rootElement = null;
888
+ recast.types.visit(ast, {
889
+ visitReturnStatement(pathNode) {
890
+ if (!rootElement && pathNode.node.argument) {
891
+ let arg = pathNode.node.argument;
892
+ if (arg.type === 'ParenthesizedExpression') arg = arg.expression;
893
+ if (arg.type === 'JSXElement') {
894
+ rootElement = arg;
895
+ }
896
+ }
897
+ this.traverse(pathNode);
898
+ },
899
+ });
900
+
901
+ if (rootElement) {
902
+ if (!hasJsxAttribute(rootElement, 'data-preview-item-path')) {
903
+ if (listPath) {
904
+ rootElement.openingElement.attributes.push(
905
+ jsxTemplatePathAttr('data-preview-item-path', listPath, 'index')
906
+ );
907
+ } else {
908
+ rootElement.openingElement.attributes.push(
909
+ b.jsxAttribute(
910
+ b.jsxIdentifier('data-preview-item-path'),
911
+ b.jsxExpressionContainer(b.identifier('previewItemPath'))
912
+ )
913
+ );
914
+ }
915
+ instrumented = true;
916
+ }
917
+ }
918
+
919
+ recast.types.visit(ast, {
920
+ visitJSXElement(pathNode) {
921
+ const node = pathNode.node;
922
+ const tagName = getJsxName(node);
923
+
924
+ const srcAttr = findJsxAttribute(node, 'src');
925
+ if (srcAttr && (tagName === 'img' || tagName === 'Image') && !hasJsxAttribute(node, 'data-preview-field-path')) {
926
+ const rawCode = recast.print(srcAttr.value).code;
927
+ if (/image|photo|avatar|thumb|img|src/i.test(rawCode)) {
928
+ const imgField = (transform.itemFields || []).find((f) => f.type === 'image' || /image|photo/i.test(f.key))?.key || 'image';
929
+ if (listPath) {
930
+ node.openingElement.attributes.push(
931
+ jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${imgField}`)
932
+ );
933
+ } else {
934
+ node.openingElement.attributes.push(
935
+ b.jsxAttribute(
936
+ b.jsxIdentifier('data-preview-field-path'),
937
+ b.jsxExpressionContainer(
938
+ b.templateLiteral(
939
+ [
940
+ b.templateElement({ raw: '', cooked: '' }, false),
941
+ b.templateElement({ raw: `.${imgField}`, cooked: `.${imgField}` }, true),
942
+ ],
943
+ [b.identifier('previewItemPath')]
944
+ )
945
+ )
946
+ )
947
+ );
948
+ }
949
+ instrumented = true;
950
+ }
951
+ }
952
+
953
+ for (const child of node.children || []) {
954
+ if (child.type === 'JSXExpressionContainer') {
955
+ const exprCode = recast.print(child.expression).code;
956
+ if (!hasJsxAttribute(node, 'data-preview-field-path')) {
957
+ if (/name|title|heading/i.test(exprCode)) {
958
+ const nameField = (transform.itemFields || []).find((f) => /name|title|heading/i.test(f.key))?.key || 'name';
959
+ if (listPath) {
960
+ node.openingElement.attributes.push(
961
+ jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${nameField}`)
962
+ );
963
+ } else {
964
+ node.openingElement.attributes.push(
965
+ b.jsxAttribute(
966
+ b.jsxIdentifier('data-preview-field-path'),
967
+ b.jsxExpressionContainer(
968
+ b.templateLiteral(
969
+ [
970
+ b.templateElement({ raw: '', cooked: '' }, false),
971
+ b.templateElement({ raw: `.${nameField}`, cooked: `.${nameField}` }, true),
972
+ ],
973
+ [b.identifier('previewItemPath')]
974
+ )
975
+ )
976
+ )
977
+ );
978
+ }
979
+ instrumented = true;
980
+ break;
981
+ } else if (/price|cost|amount/i.test(exprCode)) {
982
+ const priceField = (transform.itemFields || []).find((f) => /price|cost/i.test(f.key))?.key || 'price';
983
+ if (listPath) {
984
+ node.openingElement.attributes.push(
985
+ jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${priceField}`)
986
+ );
987
+ } else {
988
+ node.openingElement.attributes.push(
989
+ b.jsxAttribute(
990
+ b.jsxIdentifier('data-preview-field-path'),
991
+ b.jsxExpressionContainer(
992
+ b.templateLiteral(
993
+ [
994
+ b.templateElement({ raw: '', cooked: '' }, false),
995
+ b.templateElement({ raw: `.${priceField}`, cooked: `.${priceField}` }, true),
996
+ ],
997
+ [b.identifier('previewItemPath')]
998
+ )
999
+ )
1000
+ )
1001
+ );
1002
+ }
1003
+ instrumented = true;
1004
+ break;
1005
+ } else if (/description|desc|subtitle/i.test(exprCode)) {
1006
+ const descField = (transform.itemFields || []).find((f) => /desc/i.test(f.key))?.key || 'description';
1007
+ if (listPath) {
1008
+ node.openingElement.attributes.push(
1009
+ jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${descField}`)
1010
+ );
1011
+ } else {
1012
+ node.openingElement.attributes.push(
1013
+ b.jsxAttribute(
1014
+ b.jsxIdentifier('data-preview-field-path'),
1015
+ b.jsxExpressionContainer(
1016
+ b.templateLiteral(
1017
+ [
1018
+ b.templateElement({ raw: '', cooked: '' }, false),
1019
+ b.templateElement({ raw: `.${descField}`, cooked: `.${descField}` }, true),
1020
+ ],
1021
+ [b.identifier('previewItemPath')]
1022
+ )
1023
+ )
1024
+ )
1025
+ );
1026
+ }
1027
+ instrumented = true;
1028
+ break;
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ this.traverse(pathNode);
1034
+ },
1035
+ });
1036
+
1037
+ return instrumented;
1038
+ }
1039
+
1040
+ function instrumentReusableComponent(ast, transform, isTypeScript = false) {
1041
+ const componentName = transform.componentName;
1042
+ const propTransforms = transform.propTransforms || {};
1043
+ const propNames = Object.keys(propTransforms);
1044
+ if (!propNames.length) return false;
1045
+
1046
+ let instrumented = false;
1047
+
1048
+ function updateTsInterface(rootAst, interfaceName) {
1049
+ recast.types.visit(rootAst, {
1050
+ visitTSInterfaceDeclaration(pathNode) {
1051
+ if (pathNode.node.id?.name === interfaceName) {
1052
+ const body = pathNode.node.body?.body || [];
1053
+ if (!body.some((m) => m.key?.name === 'previewPath')) {
1054
+ const sig = b.tsPropertySignature(b.identifier('previewPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
1055
+ sig.optional = true;
1056
+ body.push(sig);
1057
+ instrumented = true;
1058
+ }
1059
+ }
1060
+ this.traverse(pathNode);
1061
+ },
1062
+ visitTSTypeAliasDeclaration(pathNode) {
1063
+ if (pathNode.node.id?.name === interfaceName && pathNode.node.typeAnnotation?.type === 'TSTypeLiteral') {
1064
+ const members = pathNode.node.typeAnnotation.members || [];
1065
+ if (!members.some((m) => m.key?.name === 'previewPath')) {
1066
+ const sig = b.tsPropertySignature(b.identifier('previewPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
1067
+ sig.optional = true;
1068
+ members.push(sig);
1069
+ instrumented = true;
1070
+ }
1071
+ }
1072
+ this.traverse(pathNode);
1073
+ },
1074
+ });
1075
+ }
1076
+
1077
+ function instrumentFunctionProps(fnNode, isTs) {
1078
+ let changed = false;
1079
+ let params = fnNode.params || [];
1080
+ if (!params.length) {
1081
+ fnNode.params = [
1082
+ b.objectPattern([
1083
+ b.property('init', b.identifier('previewPath'), b.identifier('previewPath')),
1084
+ ]),
1085
+ ];
1086
+ changed = true;
1087
+ } else {
1088
+ const firstParam = params[0];
1089
+ if (firstParam.type === 'ObjectPattern') {
1090
+ const hasPreviewPath = firstParam.properties.some(
1091
+ (p) => p.key?.name === 'previewPath' || p.argument?.name === 'previewPath'
1092
+ );
1093
+ if (!hasPreviewPath) {
1094
+ const prop = b.property('init', b.identifier('previewPath'), b.identifier('previewPath'));
1095
+ prop.shorthand = true;
1096
+ firstParam.properties.push(prop);
1097
+ changed = true;
1098
+ }
1099
+ if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
1100
+ const typeAnn = firstParam.typeAnnotation.typeAnnotation;
1101
+ if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
1102
+ updateTsInterface(ast, typeAnn.typeName.name);
1103
+ }
1104
+ }
1105
+ } else if (firstParam.type === 'Identifier') {
1106
+ const paramName = firstParam.name;
1107
+ if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
1108
+ const typeAnn = firstParam.typeAnnotation.typeAnnotation;
1109
+ if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
1110
+ updateTsInterface(ast, typeAnn.typeName.name);
1111
+ }
1112
+ }
1113
+ recast.types.visit(fnNode.body || fnNode, {
1114
+ visitVariableDeclarator(vPath) {
1115
+ if (vPath.node.id?.type === 'ObjectPattern' && vPath.node.init?.name === paramName) {
1116
+ const propsList = vPath.node.id.properties;
1117
+ if (!propsList.some((p) => p.key?.name === 'previewPath')) {
1118
+ const prop = b.property('init', b.identifier('previewPath'), b.identifier('previewPath'));
1119
+ prop.shorthand = true;
1120
+ propsList.push(prop);
1121
+ changed = true;
1122
+ }
1123
+ }
1124
+ this.traverse(vPath);
1125
+ },
1126
+ });
1127
+ }
1128
+ }
1129
+ return changed;
1130
+ }
1131
+
1132
+ recast.types.visit(ast, {
1133
+ visitFunctionDeclaration(pathNode) {
1134
+ if (!componentName || pathNode.node.id?.name === componentName) {
1135
+ if (instrumentFunctionProps(pathNode.node, isTypeScript)) instrumented = true;
1136
+ }
1137
+ this.traverse(pathNode);
1138
+ },
1139
+ visitVariableDeclarator(pathNode) {
1140
+ if (pathNode.node.id?.name === componentName) {
1141
+ const init = pathNode.node.init;
1142
+ if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
1143
+ if (instrumentFunctionProps(init, isTypeScript)) instrumented = true;
1144
+ }
1145
+ }
1146
+ this.traverse(pathNode);
1147
+ },
1148
+ visitExportDefaultDeclaration(pathNode) {
1149
+ const decl = pathNode.node.declaration;
1150
+ if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
1151
+ const fnName = decl.id?.name;
1152
+ if (!fnName || fnName === componentName) {
1153
+ if (instrumentFunctionProps(decl, isTypeScript)) instrumented = true;
1154
+ }
1155
+ }
1156
+ this.traverse(pathNode);
1157
+ },
1158
+ });
1159
+
1160
+ recast.types.visit(ast, {
1161
+ visitJSXElement(pathNode) {
1162
+ const node = pathNode.node;
1163
+ for (const child of node.children || []) {
1164
+ if (child.type === 'JSXExpressionContainer') {
1165
+ const expr = child.expression;
1166
+ let propMatch = null;
1167
+ if (expr && expr.type === 'Identifier' && propNames.includes(expr.name)) {
1168
+ propMatch = expr.name;
1169
+ } else if (expr && expr.type === 'LogicalExpression' && expr.left?.name && propNames.includes(expr.left.name)) {
1170
+ propMatch = expr.left.name;
1171
+ }
1172
+ if (propMatch && !hasJsxAttribute(node, 'data-preview-field-path')) {
1173
+ const previewAttr = b.jsxAttribute(
1174
+ b.jsxIdentifier('data-preview-field-path'),
1175
+ b.jsxExpressionContainer(
1176
+ b.conditionalExpression(
1177
+ b.identifier('previewPath'),
1178
+ b.templateLiteral(
1179
+ [
1180
+ b.templateElement({ raw: '', cooked: '' }, false),
1181
+ b.templateElement({ raw: `.${propMatch}`, cooked: `.${propMatch}` }, true),
1182
+ ],
1183
+ [b.identifier('previewPath')]
1184
+ ),
1185
+ b.identifier('undefined')
1186
+ )
1187
+ )
1188
+ );
1189
+ node.openingElement.attributes.push(previewAttr);
1190
+ instrumented = true;
1191
+ break;
1192
+ }
1193
+ }
1194
+ }
1195
+ this.traverse(pathNode);
1196
+ },
1197
+ });
1198
+
1199
+ return instrumented;
1200
+ }
1201
+
618
1202
  /** The nearest JSX element that wraps the map expression. */
619
1203
  function findListContainer(mapCallPath) {
620
1204
  let current = mapCallPath.parent;
@@ -973,7 +1557,8 @@ function applyFilePlan(filePlan, profile) {
973
1557
  }
974
1558
 
975
1559
  const ast = parseSource(filePlan.originalCode, filePlan.file);
976
- const isClient = hasDirective(ast, 'use client') || /['"]use client['"]/.test(filePlan.originalCode.slice(0, 400));
1560
+ let isClient = hasDirective(ast, 'use client') || hasClientDirective(ast) || /['"]use client['"]/.test(filePlan.originalCode.slice(0, 400));
1561
+ const hasMetadata = hasMetadataExport(ast);
977
1562
  let applied = 0;
978
1563
  const failures = [];
979
1564
 
@@ -982,6 +1567,7 @@ function applyFilePlan(filePlan, profile) {
982
1567
  'form-submit-action',
983
1568
  'extract-url',
984
1569
  'extract-image',
1570
+ 'extract-picture',
985
1571
  'extract-alt',
986
1572
  'extract-placeholder',
987
1573
  'extract-text',
@@ -989,6 +1575,12 @@ function applyFilePlan(filePlan, profile) {
989
1575
  'wrap-text-span',
990
1576
  'collection-conversion',
991
1577
  'style-bind',
1578
+ 'extract-tailwind-bg',
1579
+ 'instrument-child-card',
1580
+ 'bind-button-with-icon',
1581
+ 'bind-highlighted-heading',
1582
+ 'prop-flow-callsite',
1583
+ 'instrument-reusable-component',
992
1584
  ]);
993
1585
 
994
1586
  const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
@@ -1005,6 +1597,24 @@ function applyFilePlan(filePlan, profile) {
1005
1597
  if (transform.decision === 'skip') continue;
1006
1598
  if (!supported.has(transform.operation)) continue;
1007
1599
 
1600
+ if (transform.operation === 'instrument-child-card') {
1601
+ try {
1602
+ if (instrumentChildCardComponent(ast, transform, isTypeScript)) applied++;
1603
+ } catch (err) {
1604
+ failures.push({ loc: transform.loc, reason: err.message });
1605
+ }
1606
+ continue;
1607
+ }
1608
+
1609
+ if (transform.operation === 'instrument-reusable-component') {
1610
+ try {
1611
+ if (instrumentReusableComponent(ast, transform, isTypeScript)) applied++;
1612
+ } catch (err) {
1613
+ failures.push({ loc: transform.loc, reason: err.message });
1614
+ }
1615
+ continue;
1616
+ }
1617
+
1008
1618
  if (transform.operation === 'collection-conversion') {
1009
1619
  try {
1010
1620
  if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
@@ -1015,7 +1625,7 @@ function applyFilePlan(filePlan, profile) {
1015
1625
  continue;
1016
1626
  }
1017
1627
 
1018
- const pathNode = findElementByLoc(ast, transform.loc);
1628
+ const pathNode = findElementByLoc(ast, transform.loc, transform.componentName || transform.tag);
1019
1629
  if (!pathNode) {
1020
1630
  failures.push({ loc: transform.loc, reason: 'node-not-found' });
1021
1631
  continue;
@@ -1032,6 +1642,13 @@ function applyFilePlan(filePlan, profile) {
1032
1642
  return { code: filePlan.originalCode, changed: false, applied, failures };
1033
1643
  }
1034
1644
 
1645
+ if (!isClient && profile?.router === 'next-app' && !hasMetadata && canSafelyInjectClientDirective(ast, filePlan.file, profile)) {
1646
+ if (needsClientDirective(ast, filePlan.originalCode)) {
1647
+ ensureClientDirective(ast);
1648
+ isClient = true;
1649
+ }
1650
+ }
1651
+
1035
1652
  const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
1036
1653
  if (isClient) {
1037
1654
  const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
@@ -1624,6 +2241,108 @@ function findFirstLeafChild(node) {
1624
2241
  return null;
1625
2242
  }
1626
2243
 
2244
+ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
2245
+ if (!leftExpr || !rightExpr) return true;
2246
+
2247
+ // 1. Collect all identifiers used on the left-hand side
2248
+ const leftIdentifiers = new Set();
2249
+ recast.types.visit(leftExpr, {
2250
+ visitIdentifier(idPath) {
2251
+ leftIdentifiers.add(idPath.node.name);
2252
+ return false;
2253
+ },
2254
+ });
2255
+
2256
+ if (leftIdentifiers.size === 0) return true;
2257
+
2258
+ // 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
2259
+ const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
2260
+ for (const id of leftIdentifiers) {
2261
+ if (stateKeywordRegex.test(id)) {
2262
+ return true; // NEVER strip modal/state/selection guards
2263
+ }
2264
+ }
2265
+
2266
+ // 3. Null / undefined / equality / comparison checks
2267
+ let hasComparisonOrNullCheck = false;
2268
+ recast.types.visit(leftExpr, {
2269
+ visitBinaryExpression(bPath) {
2270
+ const op = bPath.node.operator;
2271
+ if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
2272
+ hasComparisonOrNullCheck = true;
2273
+ }
2274
+ this.traverse(bPath);
2275
+ },
2276
+ visitUnaryExpression(uPath) {
2277
+ if (uPath.node.operator === '!') {
2278
+ hasComparisonOrNullCheck = true;
2279
+ }
2280
+ this.traverse(uPath);
2281
+ },
2282
+ });
2283
+ if (hasComparisonOrNullCheck) return true;
2284
+
2285
+ // 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
2286
+ let rightReferencesLeftId = false;
2287
+ recast.types.visit(rightExpr, {
2288
+ visitIdentifier(idPath) {
2289
+ if (leftIdentifiers.has(idPath.node.name)) {
2290
+ rightReferencesLeftId = true;
2291
+ return false;
2292
+ }
2293
+ this.traverse(idPath);
2294
+ },
2295
+ });
2296
+ if (rightReferencesLeftId) return true;
2297
+
2298
+ // 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
2299
+ let curr = pathNode;
2300
+ while (curr && curr.parent) {
2301
+ const parentNode = curr.parent.node;
2302
+ if (parentNode && parentNode.type === 'JSXElement') {
2303
+ const parentTag = getJsxName(parentNode);
2304
+ if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(parentTag)) {
2305
+ return true;
2306
+ }
2307
+ }
2308
+ curr = curr.parent;
2309
+ }
2310
+
2311
+ let isModalOrOverlay = false;
2312
+ recast.types.visit(rightExpr, {
2313
+ visitJSXElement(elPath) {
2314
+ const node = elPath.node;
2315
+ const tag = getJsxName(node);
2316
+ if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(tag)) {
2317
+ isModalOrOverlay = true;
2318
+ return false;
2319
+ }
2320
+ const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
2321
+ if (classAttr && classAttr.value) {
2322
+ const val = classAttr.value.value || '';
2323
+ if (typeof val === 'string' && (/fixed\s+inset-0/i.test(val) || /z-(?:50|\[50\]|40)/i.test(val) || /backdrop-blur/i.test(val))) {
2324
+ isModalOrOverlay = true;
2325
+ return false;
2326
+ }
2327
+ }
2328
+ const roleAttr = findJsxAttribute(node, 'role');
2329
+ if (roleAttr && roleAttr.value && /^(?:dialog|alertdialog)$/i.test(String(roleAttr.value.value || ''))) {
2330
+ isModalOrOverlay = true;
2331
+ return false;
2332
+ }
2333
+ this.traverse(elPath);
2334
+ },
2335
+ });
2336
+ if (isModalOrOverlay) return true;
2337
+
2338
+ // 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
2339
+ const leftCode = recast.print(leftExpr).code;
2340
+ const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
2341
+ if (!isDirectContentCheck) return true;
2342
+
2343
+ return false;
2344
+ }
2345
+
1627
2346
  function healEmptyStateConditionals(ast) {
1628
2347
  let healed = 0;
1629
2348
 
@@ -1650,9 +2369,11 @@ function healEmptyStateConditionals(ast) {
1650
2369
  visitLogicalExpression(pathNode) {
1651
2370
  const expr = pathNode.node;
1652
2371
  if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1653
- pathNode.replace(expr.right);
1654
- healed++;
1655
- return false;
2372
+ if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
2373
+ pathNode.replace(expr.right);
2374
+ healed++;
2375
+ return false;
2376
+ }
1656
2377
  }
1657
2378
  this.traverse(pathNode);
1658
2379
  },
@@ -1663,20 +2384,24 @@ function healEmptyStateConditionals(ast) {
1663
2384
  return;
1664
2385
  }
1665
2386
  if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1666
- pathNode.node.expression = expr.right;
1667
- healed++;
1668
- return false;
2387
+ if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
2388
+ pathNode.node.expression = expr.right;
2389
+ healed++;
2390
+ return false;
2391
+ }
1669
2392
  }
1670
2393
  if (
1671
2394
  expr.type === 'ConditionalExpression' &&
1672
2395
  jsxHasPreviewMarker(expr.consequent) &&
1673
2396
  (isNullish(expr.alternate) || isNullish(expr.consequent))
1674
2397
  ) {
1675
- pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
1676
- ? expr.consequent
1677
- : expr.alternate;
1678
- healed++;
1679
- return false;
2398
+ if (!isUnsafeToStripCondition(expr.test, expr.consequent, pathNode)) {
2399
+ pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
2400
+ ? expr.consequent
2401
+ : expr.alternate;
2402
+ healed++;
2403
+ return false;
2404
+ }
1680
2405
  }
1681
2406
  this.traverse(pathNode);
1682
2407
  },
@@ -1928,6 +2653,77 @@ function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
1928
2653
  return code;
1929
2654
  }
1930
2655
 
2656
+ function healUnguardedModalConditionals(ast) {
2657
+ let healed = 0;
2658
+ const stateVariables = [];
2659
+ recast.types.visit(ast, {
2660
+ visitVariableDeclarator(pathNode) {
2661
+ const node = pathNode.node;
2662
+ if (node.id && node.id.type === 'ArrayPattern') {
2663
+ const first = node.id.elements[0];
2664
+ if (first && first.type === 'Identifier') {
2665
+ stateVariables.push(first.name);
2666
+ }
2667
+ }
2668
+ this.traverse(pathNode);
2669
+ },
2670
+ });
2671
+
2672
+ if (stateVariables.length === 0) return 0;
2673
+
2674
+ recast.types.visit(ast, {
2675
+ visitJSXElement(pathNode) {
2676
+ const tag = getJsxName(pathNode.node);
2677
+ if (tag === 'AnimatePresence') {
2678
+ for (const child of pathNode.node.children || []) {
2679
+ if (child.type === 'JSXExpressionContainer') {
2680
+ let inner = child.expression;
2681
+ if (inner && inner.type === 'ParenthesizedExpression') {
2682
+ inner = inner.expression;
2683
+ }
2684
+ if (inner && (inner.type === 'JSXElement' || inner.type === 'JSXFragment')) {
2685
+ const innerIds = new Set();
2686
+ recast.types.visit(inner, {
2687
+ visitIdentifier(idPath) {
2688
+ innerIds.add(idPath.node.name);
2689
+ return false;
2690
+ },
2691
+ });
2692
+
2693
+ const matchingVar = stateVariables.find((sv) => innerIds.has(sv));
2694
+ if (matchingVar) {
2695
+ let guardExpr;
2696
+ if (/index/i.test(matchingVar)) {
2697
+ guardExpr = b.binaryExpression('!==', b.identifier(matchingVar), b.nullLiteral());
2698
+ } else {
2699
+ guardExpr = b.identifier(matchingVar);
2700
+ }
2701
+ child.expression = b.logicalExpression('&&', guardExpr, inner);
2702
+ healed++;
2703
+ }
2704
+ }
2705
+ }
2706
+ }
2707
+ }
2708
+ this.traverse(pathNode);
2709
+ },
2710
+ });
2711
+
2712
+ return healed;
2713
+ }
2714
+
2715
+ function healUnguardedModalConditionalsInSource(code, filePath = 'file.tsx') {
2716
+ if (!code.includes('AnimatePresence') && !code.includes('fixed inset-0')) return { code, updated: false };
2717
+ try {
2718
+ const ast = parseSource(code, filePath);
2719
+ const healed = healUnguardedModalConditionals(ast);
2720
+ if (healed > 0) {
2721
+ return { code: printSource(ast, code), updated: true, count: healed };
2722
+ }
2723
+ } catch {}
2724
+ return { code, updated: false };
2725
+ }
2726
+
1931
2727
  module.exports = {
1932
2728
  applyFilePlan,
1933
2729
  applyCollectionTransform,
@@ -1947,6 +2743,9 @@ module.exports = {
1947
2743
  healDecorativeOverlays,
1948
2744
  healLegacyProductDetailLinks,
1949
2745
  healMissingSiteDataHooks,
2746
+ healUnguardedModalConditionals,
2747
+ healUnguardedModalConditionalsInSource,
1950
2748
  injectSiteDataHook,
2749
+ instrumentChildCardComponent,
1951
2750
  };
1952
2751