@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.
- package/bin/index.js +12 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +96 -11
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +127 -28
- package/src/arc/learning.cjs +19 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +2 -1
- package/src/arc/printer.cjs +9 -0
- package/src/arc/residual.cjs +400 -0
- package/src/arc/scanner.cjs +46 -2
- package/src/arc/semantic.cjs +8 -4
- package/src/arc/transformer.cjs +321 -28
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
|
@@ -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
|
+
};
|
package/src/arc/scanner.cjs
CHANGED
|
@@ -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
|
};
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -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
|
|
132
|
-
|
|
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
|
-
|
|
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));
|