@deneb-ui/cli 2.0.85 → 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.
@@ -30,6 +30,8 @@ const {
30
30
  } = require('./adapters.cjs');
31
31
  const { shortHash } = require('./fs-utils.cjs');
32
32
  const { resolveImportSpecifier } = require('./scanner.cjs');
33
+ const { resolvePublicAssetUrl } = require('./ir-builder.cjs');
34
+ const { analyzeElementFragments } = require('./text-fragment-analyzer.cjs');
33
35
  const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
34
36
 
35
37
  const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
@@ -239,7 +241,34 @@ function isDynamicExpression(expr, bindings) {
239
241
  return true;
240
242
  }
241
243
 
244
+ function getBindingVal(bindings, name) {
245
+ if (!bindings) return undefined;
246
+ if (typeof bindings.get === 'function') return bindings.get(name);
247
+ return bindings[name];
248
+ }
249
+
250
+ function hasDynamicExpressionChild(node, bindings) {
251
+ const children = node.children || [];
252
+ for (const child of children) {
253
+ if (!child) continue;
254
+ if (child.type === 'JSXExpressionContainer') {
255
+ const expr = child.expression;
256
+ if (!expr) continue;
257
+ if (expr.type === 'StringLiteral' || (expr.type === 'Literal' && typeof expr.value === 'string')) continue;
258
+ if (expr.type === 'NumericLiteral' || (expr.type === 'Literal' && typeof expr.value === 'number')) continue;
259
+ if (expr.type === 'TemplateLiteral' && (!expr.expressions || expr.expressions.length === 0)) continue;
260
+ if (expr.type === 'Identifier' && typeof getBindingVal(bindings, expr.name) === 'string') continue;
261
+ return true;
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+
242
267
  function resolveChildText(node, bindings) {
268
+ if (hasDynamicExpressionChild(node, bindings)) {
269
+ return { text: '', dynamic: true, fromBinding: false };
270
+ }
271
+
243
272
  const direct = collectJsxText(node);
244
273
  if (direct) return { text: direct, dynamic: false, fromBinding: false };
245
274
 
@@ -256,9 +285,14 @@ function resolveChildText(node, bindings) {
256
285
  if (!expr) continue;
257
286
  if (expr.type === 'StringLiteral' || (expr.type === 'Literal' && typeof expr.value === 'string')) {
258
287
  texts.push(String(expr.value));
259
- } else if (expr.type === 'Identifier' && typeof bindings.get(expr.name) === 'string') {
260
- texts.push(bindings.get(expr.name));
261
- fromBinding = true;
288
+ } else if (expr.type === 'Identifier') {
289
+ const val = getBindingVal(bindings, expr.name);
290
+ if (typeof val === 'string') {
291
+ texts.push(val);
292
+ fromBinding = true;
293
+ } else {
294
+ return { text: '', dynamic: true, fromBinding: false };
295
+ }
262
296
  } else if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
263
297
  texts.push(expr.quasis.map((q) => q.value.cooked || '').join(''));
264
298
  } else {
@@ -496,7 +530,7 @@ function collectLooseText(code) {
496
530
  return /<(h[1-6]|p|Button|span)[^>]*>\s*[A-Za-z]/.test(code);
497
531
  }
498
532
 
499
- function analyzeFile({ code, relativeFile, profile, graph, ownerScope, componentMeta }) {
533
+ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, componentMeta, ir = profile?.ir || null }) {
500
534
  const adapters = activeAdapters(profile);
501
535
  const fileSkip = skipReasonForFile(relativeFile, code);
502
536
  if (fileSkip) {
@@ -598,13 +632,73 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
598
632
  return;
599
633
  }
600
634
 
635
+ const parents = parentNames(pathNode);
636
+
637
+ if (name === 'source' && (parents[0] === 'picture' || recognition?.kind === 'responsive-image-source')) {
638
+ candidates.push({
639
+ loc,
640
+ tag: name,
641
+ kind: 'decoration',
642
+ confidence: 0.95,
643
+ skip: true,
644
+ reason: 'picture-source-child',
645
+ file: relativeFile,
646
+ ownerScope,
647
+ });
648
+ this.traverse(pathNode);
649
+ return;
650
+ }
651
+
601
652
  const href = getJsxAttributeLiteral(node, 'href');
602
- const src = getJsxAttributeLiteral(node, 'src');
653
+ let src = getJsxAttributeLiteral(node, 'src');
654
+ let importedAssetIdentifier = null;
655
+
656
+ if (name === 'picture') {
657
+ const children = node.children || [];
658
+ const imgChild = children.find((c) => c && c.type === 'JSXElement' && (getJsxName(c) === 'img' || getJsxName(c) === 'Image'));
659
+ if (imgChild) {
660
+ src = getJsxAttributeLiteral(imgChild, 'src');
661
+ if (!src && imgChild.openingElement) {
662
+ const srcAttr = findJsxAttribute(imgChild, 'src');
663
+ if (srcAttr && srcAttr.value?.type === 'JSXExpressionContainer') {
664
+ const expr = srcAttr.value.expression;
665
+ if (expr?.type === 'Identifier') {
666
+ const spec = imports[expr.name];
667
+ if (spec) {
668
+ const currentFileAbs = profile?.root ? path.join(profile.root, relativeFile) : null;
669
+ const publicUrl = resolvePublicAssetUrl(profile, currentFileAbs, spec);
670
+ if (publicUrl) {
671
+ src = publicUrl;
672
+ importedAssetIdentifier = expr.name;
673
+ }
674
+ }
675
+ }
676
+ }
677
+ }
678
+ }
679
+ }
680
+
681
+ if (!src && node.openingElement) {
682
+ const srcAttr = findJsxAttribute(node, 'src');
683
+ if (srcAttr && srcAttr.value?.type === 'JSXExpressionContainer') {
684
+ const expr = srcAttr.value.expression;
685
+ if (expr?.type === 'Identifier') {
686
+ const spec = imports[expr.name];
687
+ if (spec) {
688
+ const currentFileAbs = profile?.root ? path.join(profile.root, relativeFile) : null;
689
+ const publicUrl = resolvePublicAssetUrl(profile, currentFileAbs, spec);
690
+ if (publicUrl) {
691
+ src = publicUrl;
692
+ importedAssetIdentifier = expr.name;
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
603
698
  const alt = getJsxAttributeLiteral(node, 'alt');
604
699
  const placeholder = getJsxAttributeLiteral(node, 'placeholder');
605
700
  const className = classNameOf(node);
606
701
  const textInfo = resolveChildText(node, bindings);
607
- const parents = parentNames(pathNode);
608
702
  const actionNode = resolveActionWithAdapters(node, adapters) || (ACTION_TAGS.has(name) ? node : null);
609
703
 
610
704
  const isHeaderNav =
@@ -679,10 +773,10 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
679
773
  kind: 'image',
680
774
  operation: 'extract-image',
681
775
  value: src,
682
- extra: { alt },
776
+ extra: { alt, importedIdentifier: importedAssetIdentifier },
683
777
  confidence: confidenceFor('image', { dynamic: false }),
684
- reason: 'literal-image-source',
685
- fingerprint: fingerprintCandidate({ tag: name, kind: 'image', hasAlt: Boolean(alt) }),
778
+ reason: importedAssetIdentifier ? 'imported-static-asset-image' : 'literal-image-source',
779
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'image', hasAlt: Boolean(alt), imported: Boolean(importedAssetIdentifier) }),
686
780
  });
687
781
  if (alt && !isStaticSkipText(alt)) {
688
782
  candidates.push({
@@ -712,8 +806,24 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
712
806
  });
713
807
  }
714
808
 
809
+ const bgMatch = className && className.match(/\bbg-\[url\(['"]?([^'"\)]+)['"]?\)\]/);
810
+ if (bgMatch && bgMatch[1] && !usedLocs.has(loc + ':bg')) {
811
+ usedLocs.add(loc + ':bg');
812
+ candidates.push({
813
+ ...baseMeta,
814
+ kind: 'image',
815
+ operation: 'extract-tailwind-bg',
816
+ value: bgMatch[1],
817
+ extra: { rawClass: className, bgUrl: bgMatch[1] },
818
+ confidence: 0.92,
819
+ reason: 'tailwind-arbitrary-background-image',
820
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'bg-image', url: bgMatch[1] }),
821
+ });
822
+ }
823
+
715
824
  const isCustomComponent = Boolean(name && ((name[0] >= 'A' && name[0] <= 'Z') || name.includes('.')));
716
825
  if (isCustomComponent && !apiOwned && node.openingElement && Array.isArray(node.openingElement.attributes)) {
826
+ const passedLiteralProps = {};
717
827
  for (const attr of node.openingElement.attributes) {
718
828
  if (attr.type === 'JSXAttribute' && attr.name && USER_FACING_PROP_NAMES.has(attr.name.name)) {
719
829
  const propName = attr.name.name;
@@ -725,10 +835,13 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
725
835
  const expr = attr.value.expression;
726
836
  if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
727
837
  propValue = String(expr.value || '');
838
+ } else if (expr && (expr.type === 'NumericLiteral' || (expr.type === 'Literal' && typeof expr.value === 'number'))) {
839
+ propValue = expr.value;
728
840
  }
729
841
  }
730
842
  }
731
- if (propValue && !isStaticSkipText(propValue)) {
843
+ if (propValue && !isStaticSkipText(String(propValue))) {
844
+ passedLiteralProps[propName] = propValue;
732
845
  const propLoc = `${loc}:${propName}`;
733
846
  if (!usedLocs.has(propLoc)) {
734
847
  usedLocs.add(propLoc);
@@ -747,6 +860,27 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
747
860
  }
748
861
  }
749
862
  }
863
+ if (Object.keys(passedLiteralProps).length > 0 && !usedLocs.has(loc + ':prop-flow')) {
864
+ usedLocs.add(loc + ':prop-flow');
865
+ const compDef = (profile?.components || []).find((c) => c.name === name) ||
866
+ (ir && typeof ir.getComponent === 'function' && ir.getComponent(name));
867
+ if (compDef) {
868
+ candidates.push({
869
+ ...baseMeta,
870
+ kind: 'prop-flow',
871
+ operation: 'prop-flow-callsite',
872
+ value: passedLiteralProps,
873
+ extra: {
874
+ componentName: name,
875
+ componentFile: compDef.file,
876
+ literalProps: passedLiteralProps,
877
+ },
878
+ confidence: 0.95,
879
+ reason: `reusable-component-prop-flow-${name}`,
880
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'prop-flow', props: Object.keys(passedLiteralProps) }),
881
+ });
882
+ }
883
+ }
750
884
  }
751
885
 
752
886
  const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
@@ -850,6 +984,47 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
850
984
  }
851
985
  }
852
986
 
987
+ const fragAnalysis = analyzeElementFragments(node);
988
+ if (fragAnalysis.isButtonWithIcon && !apiOwned && !textInfo.dynamic && !usedLocs.has(loc)) {
989
+ usedLocs.add(loc);
990
+ candidates.push({
991
+ ...baseMeta,
992
+ kind: 'text',
993
+ operation: 'bind-button-with-icon',
994
+ value: fragAnalysis.combinedText,
995
+ extra: { tag: name, cta: true, hasIcons: true },
996
+ confidence: 0.94,
997
+ reason: 'action-button-with-icon-preserved',
998
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'btn-icon', text: fragAnalysis.combinedText }),
999
+ });
1000
+ this.traverse(pathNode);
1001
+ return;
1002
+ }
1003
+
1004
+ if (fragAnalysis.isHighlightedHeading && !apiOwned && !textInfo.dynamic && !usedLocs.has(loc)) {
1005
+ usedLocs.add(loc);
1006
+ candidates.push({
1007
+ ...baseMeta,
1008
+ kind: 'text',
1009
+ operation: 'bind-highlighted-heading',
1010
+ value: fragAnalysis.combinedText,
1011
+ extra: {
1012
+ tag: name,
1013
+ fragments: fragAnalysis.pureTextFragments.map((f) => ({
1014
+ type: f.type,
1015
+ text: f.text,
1016
+ className: f.className || '',
1017
+ tag: f.tag || 'span',
1018
+ })),
1019
+ },
1020
+ confidence: 0.96,
1021
+ reason: 'semantic-heading-with-highlight-span',
1022
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'heading-highlight', text: fragAnalysis.combinedText }),
1023
+ });
1024
+ this.traverse(pathNode);
1025
+ return;
1026
+ }
1027
+
853
1028
  const headingLike = HEADING_TAGS.has(name) || (recognition && recognition.kind === 'text' && HEADING_TAGS.has(recognition.tag || name));
854
1029
  const textLike = TEXT_TAGS.has(name) || headingLike || name === 'Button' || name === 'button' || (recognition && recognition.kind === 'text');
855
1030
  if (textLike && !isAction) {
@@ -919,12 +1094,33 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
919
1094
  const { usage: itemUsage, usesItemAsComponent, usesDirectItem } = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
920
1095
  const objectItems = arr.items.every((item) => item.type === 'object');
921
1096
  const stringItems = arr.items.every((item) => item.type === 'string');
1097
+ const rootTagName = getJsxName(mapInfo.rootElement);
1098
+ const isChildCustomComponent = /^[A-Z]/.test(rootTagName);
1099
+
1100
+ // If the map returns a custom child component (e.g. <ProductCard product={item} />),
1101
+ // derive item fields from the sample object items if direct JSX member usage was empty
1102
+ if (isChildCustomComponent && objectItems && itemUsage.size === 0 && arr.items[0]?.value) {
1103
+ const sampleObj = arr.items[0].value;
1104
+ for (const key of Object.keys(sampleObj)) {
1105
+ if (/^(id|_id|key|slug)$/i.test(key)) continue;
1106
+ const role = /image|photo|avatar|icon|thumb|img/i.test(key)
1107
+ ? 'image'
1108
+ : /price|cost|amount|count|rating|stars/i.test(key)
1109
+ ? 'number'
1110
+ : /url|link|href/i.test(key)
1111
+ ? 'url'
1112
+ : 'text';
1113
+ itemUsage.set(key, role);
1114
+ }
1115
+ }
1116
+
922
1117
  const boundProperties = [...itemUsage.keys()];
923
1118
  const convertible =
924
1119
  (objectItems &&
925
1120
  boundProperties.length > 0 &&
926
1121
  arr.items.some((item) => boundProperties.some((key) => key in item.value))) ||
927
- (stringItems && usesDirectItem);
1122
+ (stringItems && usesDirectItem) ||
1123
+ (isChildCustomComponent && objectItems && boundProperties.length > 0);
928
1124
 
929
1125
  candidates.push({
930
1126
  ...baseMeta,
@@ -939,18 +1135,21 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
939
1135
  indexParam: mapInfo.indexParam,
940
1136
  itemFields: stringItems ? [] : [...itemUsage.entries()].map(([key, role]) => ({ key, role })),
941
1137
  objectItems,
942
- hasComponentRef: usesItemAsComponent,
1138
+ hasComponentRef: usesItemAsComponent && !isChildCustomComponent,
1139
+ childComponentName: isChildCustomComponent ? rootTagName : null,
943
1140
  },
944
1141
  confidence: convertible
945
1142
  ? confidenceFor('collection', { staticCollection: true })
946
1143
  : usesItemAsComponent
947
1144
  ? 0.2
948
1145
  : 0.82,
949
- reason: usesItemAsComponent
1146
+ reason: usesItemAsComponent && !isChildCustomComponent
950
1147
  ? 'collection-holds-component-ref'
951
- : convertible
952
- ? (stringItems ? 'static-primitive-array-map' : 'static-array-map')
953
- : 'collection-shape-partial',
1148
+ : isChildCustomComponent
1149
+ ? 'collection-with-child-component'
1150
+ : convertible
1151
+ ? (stringItems ? 'static-primitive-array-map' : 'static-array-map')
1152
+ : 'collection-shape-partial',
954
1153
  fingerprint: fingerprintCandidate({ tag: name, kind: 'collection', size: arr.items.length }),
955
1154
  });
956
1155
  }
@@ -995,4 +1194,6 @@ module.exports = {
995
1194
  fingerprintCandidate,
996
1195
  normalizeText,
997
1196
  isStaticSkipText,
1197
+ resolveChildText,
998
1198
  };
1199
+
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const { getJsxName, collectJsxText, findJsxAttribute } = require('./ast.cjs');
4
+ const { isIconComponent, DECORATIVE_TAGS, HEADING_TAGS, ACTION_TAGS } = require('./adapters.cjs');
5
+
6
+ const INLINE_FORMAT_TAGS = new Set([
7
+ 'span', 'strong', 'em', 'b', 'i', 'u', 'code', 'mark', 'small', 'sub', 'sup',
8
+ ]);
9
+
10
+ function getAttributeString(node, attrName) {
11
+ const attr = findJsxAttribute(node, attrName);
12
+ if (!attr || !attr.value) return '';
13
+ if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
14
+ return String(attr.value.value || '');
15
+ }
16
+ if (attr.value.type === 'JSXExpressionContainer') {
17
+ const expr = attr.value.expression;
18
+ if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
19
+ return String(expr.value || '');
20
+ }
21
+ }
22
+ return '';
23
+ }
24
+
25
+ /**
26
+ * Analyzes the children of a JSX element into structured fragments.
27
+ * Identifies:
28
+ * - raw text literals
29
+ * - inline formatted elements (span, strong, em, etc.)
30
+ * - decorative icon components
31
+ * - nested links or buttons
32
+ * - dynamic expression containers
33
+ */
34
+ function analyzeElementFragments(elementNode) {
35
+ if (!elementNode || elementNode.type !== 'JSXElement') {
36
+ return {
37
+ fragments: [],
38
+ hasIcons: false,
39
+ hasInlineFormatting: false,
40
+ hasDynamicExpressions: false,
41
+ isCompoundText: false,
42
+ isButtonWithIcon: false,
43
+ isHighlightedHeading: false,
44
+ pureTextFragments: [],
45
+ combinedText: '',
46
+ };
47
+ }
48
+
49
+ const tag = getJsxName(elementNode);
50
+ const children = elementNode.children || [];
51
+ const fragments = [];
52
+
53
+ for (let i = 0; i < children.length; i++) {
54
+ const child = children[i];
55
+
56
+ if (child.type === 'JSXText') {
57
+ const raw = child.value;
58
+ const trimmed = raw.replace(/\s+/g, ' ').trim();
59
+ if (trimmed.length > 0) {
60
+ fragments.push({
61
+ type: 'raw-text',
62
+ raw,
63
+ text: trimmed,
64
+ nodeIndex: i,
65
+ node: child,
66
+ });
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (child.type === 'JSXElement') {
72
+ const childTag = getJsxName(child);
73
+ const childClassName = getAttributeString(child, 'className') || getAttributeString(child, 'class');
74
+
75
+ if (isIconComponent(childTag, '') || DECORATIVE_TAGS.has(childTag)) {
76
+ fragments.push({
77
+ type: 'icon',
78
+ tag: childTag,
79
+ nodeIndex: i,
80
+ node: child,
81
+ });
82
+ continue;
83
+ }
84
+
85
+ if (INLINE_FORMAT_TAGS.has(childTag.toLowerCase())) {
86
+ const innerText = collectJsxText(child).trim();
87
+ fragments.push({
88
+ type: 'inline-format',
89
+ tag: childTag,
90
+ className: childClassName,
91
+ text: innerText,
92
+ nodeIndex: i,
93
+ node: child,
94
+ });
95
+ continue;
96
+ }
97
+
98
+ if (childTag === 'a' || childTag === 'Link') {
99
+ const href = getAttributeString(child, 'href');
100
+ const linkText = collectJsxText(child).trim();
101
+ fragments.push({
102
+ type: 'link',
103
+ tag: childTag,
104
+ href,
105
+ text: linkText,
106
+ nodeIndex: i,
107
+ node: child,
108
+ });
109
+ continue;
110
+ }
111
+
112
+ // Other nested element
113
+ fragments.push({
114
+ type: 'element',
115
+ tag: childTag,
116
+ className: childClassName,
117
+ text: collectJsxText(child).trim(),
118
+ nodeIndex: i,
119
+ node: child,
120
+ });
121
+ continue;
122
+ }
123
+
124
+ if (child.type === 'JSXExpressionContainer') {
125
+ fragments.push({
126
+ type: 'dynamic-expr',
127
+ nodeIndex: i,
128
+ node: child,
129
+ });
130
+ continue;
131
+ }
132
+ }
133
+
134
+ const hasIcons = fragments.some((f) => f.type === 'icon');
135
+ const hasInlineFormatting = fragments.some((f) => f.type === 'inline-format');
136
+ const hasDynamicExpressions = fragments.some((f) => f.type === 'dynamic-expr');
137
+ const pureTextFragments = fragments.filter((f) => f.type === 'raw-text' || f.type === 'inline-format');
138
+ const combinedText = pureTextFragments.map((f) => f.text).join(' ').replace(/\s+/g, ' ').trim();
139
+
140
+ const isButtonWithIcon =
141
+ ACTION_TAGS.has(tag) &&
142
+ hasIcons &&
143
+ pureTextFragments.length >= 1 &&
144
+ !hasDynamicExpressions;
145
+
146
+ const isHighlightedHeading =
147
+ HEADING_TAGS.has(tag) &&
148
+ hasInlineFormatting &&
149
+ pureTextFragments.length >= 2 &&
150
+ !hasDynamicExpressions;
151
+
152
+ const isCompoundText =
153
+ (hasInlineFormatting || (hasIcons && pureTextFragments.length > 0)) &&
154
+ pureTextFragments.length > 0;
155
+
156
+ return {
157
+ tag,
158
+ fragments,
159
+ hasIcons,
160
+ hasInlineFormatting,
161
+ hasDynamicExpressions,
162
+ isCompoundText,
163
+ isButtonWithIcon,
164
+ isHighlightedHeading,
165
+ pureTextFragments,
166
+ combinedText,
167
+ };
168
+ }
169
+
170
+ module.exports = {
171
+ INLINE_FORMAT_TAGS,
172
+ analyzeElementFragments,
173
+ };