@deneb-ui/cli 2.0.51 → 2.0.52

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.
@@ -0,0 +1,400 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pass B of the ARC planner: residual bind-or-static closure.
5
+ *
6
+ * Confidence cannot rescue dynamic leftovers (c_sem = 0.15). After Pass A,
7
+ * every remaining visible literal is either bound to a field or marked
8
+ * data-preview-static with a reason. This pass ignores the 0.60 skip floor.
9
+ */
10
+
11
+ const recast = require('recast');
12
+ const {
13
+ parseSource,
14
+ printSource,
15
+ getJsxName,
16
+ hasJsxAttribute,
17
+ findJsxAttribute,
18
+ collectJsxText,
19
+ wrapTextInEditableSpan,
20
+ jsxPreviewAttr,
21
+ jsxStaticAttr,
22
+ ensureStyleAttrs,
23
+ siteDataBinding,
24
+ b,
25
+ } = require('./ast.cjs');
26
+ const {
27
+ SKIP_TAGS,
28
+ HEADING_TAGS,
29
+ TEXT_TAGS,
30
+ ACTION_TAGS,
31
+ IMAGE_TAGS,
32
+ DECORATIVE_TAGS,
33
+ isIconComponent,
34
+ } = require('./adapters.cjs');
35
+ const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
36
+ const { inferSection, inferFieldName, buildFieldPath, classifyFieldType } = require('./field-paths.cjs');
37
+ const { isStaticSkipText } = require('./semantic.cjs');
38
+
39
+ const LEAF_TEXT_TAGS = new Set([
40
+ ...HEADING_TAGS,
41
+ ...TEXT_TAGS,
42
+ 'em', 'strong', 'small', 'b', 'i', 'u', 'code', 'time', 'cite', 'mark',
43
+ 'dt', 'dd', 'th', 'td', 'legend', 'caption',
44
+ ]);
45
+
46
+ const CHROME_TEXT_RE = /^(×|x|✕|✖|\+|−|-|•|·|…|\.|…|\||\/|©|®|™|\d+)$/i;
47
+ const CART_CHROME_RE = /^(qty|quantity|subtotal|total|cart|checkout|remove|close|menu)$/i;
48
+
49
+ function attrLiteral(node, name) {
50
+ const attr = findJsxAttribute(node, name);
51
+ if (!attr || !attr.value) return '';
52
+ if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') return String(attr.value.value || '');
53
+ if (attr.value.type === 'JSXExpressionContainer') {
54
+ const expr = attr.value.expression;
55
+ if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) return String(expr.value || '');
56
+ }
57
+ return '';
58
+ }
59
+
60
+ function hasEditableMarker(node) {
61
+ return (
62
+ hasJsxAttribute(node, 'data-preview-field-path') ||
63
+ hasJsxAttribute(node, 'data-preview-list-path') ||
64
+ hasJsxAttribute(node, 'data-preview-item-path') ||
65
+ hasJsxAttribute(node, 'data-preview-static')
66
+ );
67
+ }
68
+
69
+ function ancestorHasStatic(pathNode) {
70
+ let current = pathNode.parent;
71
+ while (current) {
72
+ const node = current.node || current.value;
73
+ if (node && node.type === 'JSXElement' && hasJsxAttribute(node, 'data-preview-static')) return true;
74
+ current = current.parentPath || current.parent;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ function inMapCallback(pathNode) {
80
+ let current = pathNode.parent;
81
+ while (current) {
82
+ const node = current.node || current.value;
83
+ if (
84
+ node &&
85
+ node.type === 'CallExpression' &&
86
+ node.callee &&
87
+ node.callee.type === 'MemberExpression' &&
88
+ node.callee.property &&
89
+ node.callee.property.name === 'map'
90
+ ) {
91
+ return true;
92
+ }
93
+ current = current.parentPath || current.parent;
94
+ }
95
+ return false;
96
+ }
97
+
98
+ function isMeaningfulVisibleText(text) {
99
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
100
+ if (!value || value.length <= 2) return false;
101
+ if (!/\p{L}/u.test(value)) return false;
102
+ if (isStaticSkipText(value)) return false;
103
+ if (CHROME_TEXT_RE.test(value)) return false;
104
+ return true;
105
+ }
106
+
107
+ function isDecorativeCopy(text, tag, className) {
108
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
109
+ if (!value) return true;
110
+ if (CHROME_TEXT_RE.test(value)) return true;
111
+ if (CART_CHROME_RE.test(value) && /cart|drawer|qty|quantity/i.test(className || '')) return true;
112
+ if (DECORATIVE_TAGS.has(tag) || tag === 'svg') return true;
113
+ if (/\bsr-only\b|\bhidden\b/.test(className || '')) return true;
114
+ return false;
115
+ }
116
+
117
+ function classifyResidual(node, text, tag) {
118
+ const className = attrLiteral(node, 'className') || attrLiteral(node, 'class');
119
+ const ariaHidden = attrLiteral(node, 'aria-hidden') === 'true' || hasJsxAttribute(node, 'hidden') || /(^|\s)hidden(\s|$)/.test(className);
120
+ if (ariaHidden || DECORATIVE_TAGS.has(tag) || isIconComponent(tag, '')) {
121
+ return { bind: false, reason: 'decorative-icon' };
122
+ }
123
+ if (isDecorativeCopy(text, tag, className)) {
124
+ return { bind: false, reason: 'chrome' };
125
+ }
126
+ if (IMAGE_TAGS.has(tag) && attrLiteral(node, 'src')) {
127
+ return { bind: true, kind: 'image', value: attrLiteral(node, 'src') };
128
+ }
129
+ if (isMeaningfulVisibleText(text)) {
130
+ return { bind: true, kind: 'text', value: text };
131
+ }
132
+ if (text && text.trim()) {
133
+ return { bind: false, reason: 'decorative-copy' };
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function ensureStaticOnLeaf(node, reason) {
139
+ const tag = getJsxName(node);
140
+ if (BROAD_CONTENT_CONTAINERS.has(tag.toLowerCase()) || BROAD_CONTENT_CONTAINERS.has(tag)) {
141
+ return false;
142
+ }
143
+ if (hasJsxAttribute(node, 'data-preview-static') || hasEditableMarker(node)) return false;
144
+ node.openingElement.attributes.push(jsxStaticAttr(reason || 'decorative'));
145
+ return true;
146
+ }
147
+
148
+ function wrapFirstLiteralAsStatic(node, reason) {
149
+ const nextChildren = [];
150
+ let wrapped = false;
151
+ for (const child of node.children || []) {
152
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
153
+ const leading = child.value.match(/^\s*/)?.[0] || '';
154
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
155
+ const text = child.value.trim();
156
+ if (leading) nextChildren.push(b.jsxText(leading));
157
+ nextChildren.push(
158
+ b.jsxElement(
159
+ b.jsxOpeningElement(b.jsxIdentifier('span'), [jsxStaticAttr(reason || 'decorative')], false),
160
+ b.jsxClosingElement(b.jsxIdentifier('span')),
161
+ [b.jsxText(text)],
162
+ false
163
+ )
164
+ );
165
+ if (trailing) nextChildren.push(b.jsxText(trailing));
166
+ wrapped = true;
167
+ continue;
168
+ }
169
+ nextChildren.push(child);
170
+ }
171
+ if (wrapped) node.children = nextChildren;
172
+ return wrapped;
173
+ }
174
+
175
+ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, role }) {
176
+ if (!code || !code.includes('<')) {
177
+ return { code, changed: false, fields: [], applied: 0 };
178
+ }
179
+
180
+ let ast;
181
+ try {
182
+ ast = parseSource(code, file);
183
+ } catch {
184
+ return { code, changed: false, fields: [], applied: 0 };
185
+ }
186
+
187
+ const fields = [];
188
+ let applied = 0;
189
+ const used = usedPaths instanceof Set ? usedPaths : new Set(usedPaths || []);
190
+
191
+ recast.types.visit(ast, {
192
+ visitJSXElement(pathNode) {
193
+ const node = pathNode.node;
194
+ const tag = getJsxName(node);
195
+ const lower = tag.toLowerCase();
196
+
197
+ if (!tag || SKIP_TAGS.has(tag) || tag === 'React.Fragment' || tag === 'Fragment') {
198
+ this.traverse(pathNode);
199
+ return;
200
+ }
201
+
202
+ if (hasEditableMarker(node) || ancestorHasStatic(pathNode)) {
203
+ this.traverse(pathNode);
204
+ return;
205
+ }
206
+
207
+ if (DECORATIVE_TAGS.has(tag) || isIconComponent(tag, '')) {
208
+ if (ensureStaticOnLeaf(node, 'decorative-icon')) applied++;
209
+ this.traverse(pathNode);
210
+ return;
211
+ }
212
+
213
+ const text = collectJsxText(node);
214
+ const src = attrLiteral(node, 'src');
215
+ const placeholder = attrLiteral(node, 'placeholder');
216
+ const alt = attrLiteral(node, 'alt');
217
+
218
+ const decision = classifyResidual(node, text, tag);
219
+ const insideMap = inMapCallback(pathNode);
220
+
221
+ if (insideMap && decision && decision.bind && ACTION_TAGS.has(tag)) {
222
+ // Repeated chrome inside a list stays static so we never couple parallel arrays.
223
+ if (BROAD_CONTENT_CONTAINERS.has(lower)) {
224
+ if (wrapFirstLiteralAsStatic(node, 'collection-chrome')) applied++;
225
+ } else if (ensureStaticOnLeaf(node, 'collection-chrome')) {
226
+ applied++;
227
+ }
228
+ this.traverse(pathNode);
229
+ return;
230
+ }
231
+
232
+ if (IMAGE_TAGS.has(tag) && src && !src.startsWith('{') && !hasJsxAttribute(node, 'data-preview-field-path')) {
233
+ const section = inferSection({
234
+ componentName,
235
+ fileName: file,
236
+ className: attrLiteral(node, 'className'),
237
+ tag,
238
+ role,
239
+ });
240
+ const field = buildFieldPath({
241
+ scope: ownerScope || 'home',
242
+ section,
243
+ field: inferFieldName('image', tag, src, {}),
244
+ used,
245
+ });
246
+ node.openingElement.attributes.push(jsxPreviewAttr(field));
247
+ const srcAttr = findJsxAttribute(node, 'src');
248
+ if (srcAttr) {
249
+ srcAttr.value = b.jsxExpressionContainer(siteDataBinding(field.split('.'), src, 'image'));
250
+ }
251
+ fields.push({ path: field, type: 'image', value: src });
252
+ applied++;
253
+ this.traverse(pathNode);
254
+ return;
255
+ }
256
+
257
+ if (placeholder && !hasJsxAttribute(node, 'data-preview-field-path')) {
258
+ const section = inferSection({ componentName, fileName: file, tag, role });
259
+ const field = buildFieldPath({
260
+ scope: ownerScope || 'home',
261
+ section,
262
+ field: inferFieldName('placeholder', tag, placeholder, {}),
263
+ used,
264
+ });
265
+ node.openingElement.attributes.push(jsxPreviewAttr(field));
266
+ const ph = findJsxAttribute(node, 'placeholder');
267
+ if (ph) {
268
+ ph.value = b.jsxExpressionContainer(siteDataBinding(field.split('.'), placeholder, 'text'));
269
+ }
270
+ fields.push({ path: field, type: 'text', value: placeholder });
271
+ applied++;
272
+ }
273
+
274
+ if (alt && isMeaningfulVisibleText(alt) && !hasJsxAttribute(node, 'data-preview-field-path')) {
275
+ const section = inferSection({ componentName, fileName: file, tag, role });
276
+ const field = buildFieldPath({
277
+ scope: ownerScope || 'home',
278
+ section,
279
+ field: inferFieldName('alt', tag, alt, {}),
280
+ used,
281
+ });
282
+ const altAttr = findJsxAttribute(node, 'alt');
283
+ if (altAttr) {
284
+ altAttr.value = b.jsxExpressionContainer(siteDataBinding(field.split('.'), alt, 'text'));
285
+ }
286
+ fields.push({ path: field, type: 'text', value: alt });
287
+ applied++;
288
+ }
289
+
290
+ if (decision && decision.bind && decision.kind === 'text' && isMeaningfulVisibleText(text)) {
291
+ const section = inferSection({
292
+ componentName,
293
+ fileName: file,
294
+ className: attrLiteral(node, 'className'),
295
+ tag,
296
+ role,
297
+ });
298
+ const field = buildFieldPath({
299
+ scope: ownerScope || 'home',
300
+ section,
301
+ field: inferFieldName('text', tag, text, { tag }),
302
+ used,
303
+ });
304
+ const fieldType = classifyFieldType('text', text);
305
+
306
+ if (BROAD_CONTENT_CONTAINERS.has(lower) || BROAD_CONTENT_CONTAINERS.has(tag)) {
307
+ const nextChildren = [];
308
+ let wrapped = false;
309
+ for (const child of node.children || []) {
310
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
311
+ const leading = child.value.match(/^\s*/)?.[0] || '';
312
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
313
+ if (leading) nextChildren.push(b.jsxText(leading));
314
+ nextChildren.push(wrapTextInEditableSpan(field, text, fieldType));
315
+ if (trailing) nextChildren.push(b.jsxText(trailing));
316
+ wrapped = true;
317
+ continue;
318
+ }
319
+ nextChildren.push(child);
320
+ }
321
+ if (wrapped) {
322
+ node.children = nextChildren;
323
+ fields.push({ path: field, type: fieldType, value: text });
324
+ applied++;
325
+ }
326
+ } else if (LEAF_TEXT_TAGS.has(tag) || HEADING_TAGS.has(tag) || tag === 'button' || tag === 'Button') {
327
+ if (ACTION_TAGS.has(tag) && (hasJsxAttribute(node, 'href') || tag === 'a' || tag === 'Link')) {
328
+ const nextChildren = [];
329
+ let wrapped = false;
330
+ for (const child of node.children || []) {
331
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
332
+ const leading = child.value.match(/^\s*/)?.[0] || '';
333
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
334
+ if (leading) nextChildren.push(b.jsxText(leading));
335
+ nextChildren.push(wrapTextInEditableSpan(field, text, fieldType));
336
+ if (trailing) nextChildren.push(b.jsxText(trailing));
337
+ wrapped = true;
338
+ continue;
339
+ }
340
+ nextChildren.push(child);
341
+ }
342
+ if (wrapped) {
343
+ node.children = nextChildren;
344
+ fields.push({ path: field, type: fieldType, value: text });
345
+ applied++;
346
+ }
347
+ } else {
348
+ node.openingElement.attributes.push(jsxPreviewAttr(field));
349
+ ensureStyleAttrs(node, field, tag === 'button' || tag === 'Button' ? 'button' : 'text');
350
+ const nextChildren = [];
351
+ let replaced = false;
352
+ for (const child of node.children || []) {
353
+ if (!replaced && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
354
+ nextChildren.push(b.jsxExpressionContainer(siteDataBinding(field.split('.'), text, fieldType)));
355
+ replaced = true;
356
+ } else {
357
+ nextChildren.push(child);
358
+ }
359
+ }
360
+ if (replaced) node.children = nextChildren;
361
+ fields.push({ path: field, type: fieldType, value: text });
362
+ applied++;
363
+ }
364
+ } else if (!BROAD_CONTENT_CONTAINERS.has(lower)) {
365
+ if (ensureStaticOnLeaf(node, 'non-leaf-copy')) applied++;
366
+ } else if (wrapFirstLiteralAsStatic(node, 'container-copy')) {
367
+ applied++;
368
+ }
369
+ this.traverse(pathNode);
370
+ return;
371
+ }
372
+
373
+ if (decision && !decision.bind && text) {
374
+ if (BROAD_CONTENT_CONTAINERS.has(lower)) {
375
+ if (wrapFirstLiteralAsStatic(node, decision.reason || 'decorative')) applied++;
376
+ } else if (ensureStaticOnLeaf(node, decision.reason || 'decorative')) {
377
+ applied++;
378
+ }
379
+ }
380
+
381
+ this.traverse(pathNode);
382
+ },
383
+ });
384
+
385
+ if (applied === 0) {
386
+ return { code, changed: false, fields: [], applied: 0 };
387
+ }
388
+
389
+ return {
390
+ code: printSource(ast, code),
391
+ changed: true,
392
+ fields,
393
+ applied,
394
+ };
395
+ }
396
+
397
+ module.exports = {
398
+ applyResidualPass,
399
+ isMeaningfulVisibleText,
400
+ };
@@ -277,8 +277,50 @@ function scanPagesRouterRoutes(pagesDir, projectDir) {
277
277
  * ever declares routes it actually found.
278
278
  */
279
279
  function keepExportableRoutes(routes) {
280
- const exportable = routes.filter((route) => !route.dynamic);
281
- return exportable.length ? exportable : routes;
280
+ const exportable = routes.filter((route) => !route.dynamic && (route.file || route.inferred));
281
+ return exportable.length ? exportable : routes.filter((route) => route.inferred);
282
+ }
283
+
284
+ function resolvePageFileOnDisk(projectDir, page) {
285
+ if (!page) return null;
286
+ if (page.file && fs.existsSync(path.join(projectDir, page.file))) return page.file.replace(/\\/g, '/');
287
+ const route = String(page.route || '').replace(/^\//, '').replace(/\/+$/, '');
288
+ const id = page.id === 'home' ? '' : page.id;
289
+ const segments = route || id || '';
290
+ const candidates = [];
291
+ if (!segments || page.id === 'home' || page.route === '/') {
292
+ candidates.push(
293
+ 'src/app/page.tsx',
294
+ 'src/app/page.jsx',
295
+ 'src/app/page.js',
296
+ 'app/page.tsx',
297
+ 'app/page.jsx',
298
+ 'app/page.js',
299
+ 'pages/index.tsx',
300
+ 'pages/index.jsx',
301
+ 'pages/index.js',
302
+ 'src/pages/index.tsx',
303
+ 'src/pages/index.jsx',
304
+ 'src/pages/index.js',
305
+ );
306
+ }
307
+ if (segments) {
308
+ for (const base of ['src/app', 'app']) {
309
+ for (const name of ['page.tsx', 'page.jsx', 'page.js']) {
310
+ candidates.push(`${base}/${segments}/${name}`);
311
+ }
312
+ }
313
+ for (const base of ['pages', 'src/pages']) {
314
+ for (const ext of ['tsx', 'jsx', 'js']) {
315
+ candidates.push(`${base}/${segments}.${ext}`);
316
+ candidates.push(`${base}/${segments}/index.${ext}`);
317
+ }
318
+ }
319
+ }
320
+ for (const relative of candidates) {
321
+ if (fs.existsSync(path.join(projectDir, relative))) return relative.replace(/\\/g, '/');
322
+ }
323
+ return null;
282
324
  }
283
325
 
284
326
  function detectLanguage(sourceFiles) {
@@ -610,4 +652,6 @@ module.exports = {
610
652
  resolveImportSpecifier,
611
653
  extractImportSpecifiers,
612
654
  parseTsconfig,
655
+ keepExportableRoutes,
656
+ resolvePageFileOnDisk,
613
657
  };
@@ -128,8 +128,9 @@ function objectLiteralToPlain(node) {
128
128
  out[key] = val.quasis?.map((q) => q.value?.cooked || q.value?.raw || '').join('') || '';
129
129
  } else if (val.type === 'Identifier') {
130
130
  if (/^[A-Z]/.test(val.name)) {
131
- // Component references (e.g. icon: Truck, Icon: ShieldCheck) are not merchant content
132
- return null;
131
+ // Component references (e.g. icon: Truck) are not merchant content.
132
+ // Skip this key only so primitive siblings (title, body) stay convertible.
133
+ continue;
133
134
  }
134
135
  out[key] = val.name;
135
136
  } else if (val.type === 'UnaryExpression' && val.argument) {
@@ -349,12 +350,15 @@ function collectItemFieldUsage(callback, itemParam) {
349
350
  }
350
351
 
351
352
  let usesItemAsComponent = false;
353
+ const componentProps = new Set();
352
354
  recast.types.visit(callback, {
353
355
  visitJSXOpeningElement(pathNode) {
354
356
  const name = pathNode.node.name;
355
357
  if (name?.type === 'JSXMemberExpression') {
356
- if (name.object?.type === 'Identifier' && name.object.name === itemParam) {
358
+ const objectName = name.object && name.object.name;
359
+ if ((name.object?.type === 'Identifier' || name.object?.type === 'JSXIdentifier') && objectName === itemParam) {
357
360
  usesItemAsComponent = true;
361
+ if (name.property?.name) componentProps.add(name.property.name);
358
362
  }
359
363
  }
360
364
  this.traverse(pathNode);
@@ -362,6 +366,7 @@ function collectItemFieldUsage(callback, itemParam) {
362
366
  visitJSXExpressionContainer(pathNode) {
363
367
  const properties = findItemMemberProperties(pathNode.node.expression);
364
368
  for (const property of properties) {
369
+ if (componentProps.has(property)) continue;
365
370
  const parent = pathNode.parent?.node || pathNode.parent?.value;
366
371
  if (parent?.type === 'JSXAttribute') {
367
372
  const attribute = parent.name?.name;
@@ -719,7 +724,6 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
719
724
  const objectItems = arr.items.every((item) => item.type === 'object');
720
725
  const boundProperties = [...itemUsage.keys()];
721
726
  const convertible =
722
- !usesItemAsComponent &&
723
727
  objectItems &&
724
728
  boundProperties.length > 0 &&
725
729
  arr.items.some((item) => boundProperties.some((key) => key in item.value));