@deneb-ui/cli 2.0.50 → 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 +219 -10
- package/src/arc/adapters.cjs +19 -0
- package/src/arc/field-paths.cjs +4 -0
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +129 -30
- package/src/arc/learning.cjs +20 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +10 -6
- 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 +47 -8
- package/src/arc/style-candidates.cjs +2 -1
- package/src/arc/transformer.cjs +335 -20
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
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
|
@@ -23,6 +23,8 @@ const {
|
|
|
23
23
|
TEXT_TAGS,
|
|
24
24
|
ACTION_TAGS,
|
|
25
25
|
IMAGE_TAGS,
|
|
26
|
+
FORM_INPUT_TAGS,
|
|
27
|
+
FORM_CONTAINER_TAGS,
|
|
26
28
|
DECORATIVE_TAGS,
|
|
27
29
|
} = require('./adapters.cjs');
|
|
28
30
|
const { shortHash } = require('./fs-utils.cjs');
|
|
@@ -126,8 +128,9 @@ function objectLiteralToPlain(node) {
|
|
|
126
128
|
out[key] = val.quasis?.map((q) => q.value?.cooked || q.value?.raw || '').join('') || '';
|
|
127
129
|
} else if (val.type === 'Identifier') {
|
|
128
130
|
if (/^[A-Z]/.test(val.name)) {
|
|
129
|
-
// Component references (e.g. icon: Truck
|
|
130
|
-
|
|
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;
|
|
131
134
|
}
|
|
132
135
|
out[key] = val.name;
|
|
133
136
|
} else if (val.type === 'UnaryExpression' && val.argument) {
|
|
@@ -347,12 +350,15 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
let usesItemAsComponent = false;
|
|
353
|
+
const componentProps = new Set();
|
|
350
354
|
recast.types.visit(callback, {
|
|
351
355
|
visitJSXOpeningElement(pathNode) {
|
|
352
356
|
const name = pathNode.node.name;
|
|
353
357
|
if (name?.type === 'JSXMemberExpression') {
|
|
354
|
-
|
|
358
|
+
const objectName = name.object && name.object.name;
|
|
359
|
+
if ((name.object?.type === 'Identifier' || name.object?.type === 'JSXIdentifier') && objectName === itemParam) {
|
|
355
360
|
usesItemAsComponent = true;
|
|
361
|
+
if (name.property?.name) componentProps.add(name.property.name);
|
|
356
362
|
}
|
|
357
363
|
}
|
|
358
364
|
this.traverse(pathNode);
|
|
@@ -360,6 +366,7 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
360
366
|
visitJSXExpressionContainer(pathNode) {
|
|
361
367
|
const properties = findItemMemberProperties(pathNode.node.expression);
|
|
362
368
|
for (const property of properties) {
|
|
369
|
+
if (componentProps.has(property)) continue;
|
|
363
370
|
const parent = pathNode.parent?.node || pathNode.parent?.value;
|
|
364
371
|
if (parent?.type === 'JSXAttribute') {
|
|
365
372
|
const attribute = parent.name?.name;
|
|
@@ -392,7 +399,7 @@ function confidenceFor(kind, extras = {}) {
|
|
|
392
399
|
if (extras.icon) return 0.1;
|
|
393
400
|
if (kind === 'url' && extras.action === 'whatsapp') return 0.96;
|
|
394
401
|
if (kind === 'url' && extras.social) return 0.93;
|
|
395
|
-
if (kind === 'split-action-contract') return 0.94;
|
|
402
|
+
if (kind === 'split-action-contract' || kind === 'form-submit-action') return 0.94;
|
|
396
403
|
if (kind === 'text' && HEADING_TAGS.has(extras.tag)) return 0.95;
|
|
397
404
|
if (kind === 'text' && extras.tag === 'p') return 0.9;
|
|
398
405
|
if (kind === 'image') return 0.88;
|
|
@@ -562,14 +569,47 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
562
569
|
});
|
|
563
570
|
}
|
|
564
571
|
|
|
572
|
+
const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
|
|
573
|
+
|
|
565
574
|
const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
|
|
566
575
|
const innerText = textInfo.dynamic ? '' : textInfo.text;
|
|
567
576
|
const typeAttr = getJsxAttributeLiteral(node, 'type');
|
|
568
577
|
const isSubmit = typeAttr === 'submit';
|
|
569
578
|
|
|
570
|
-
// Check for action intent via classifyActionIntent (covers WhatsApp, Call/Phone, Directions, Location, Shop, Email)
|
|
571
|
-
const actionIntent = !
|
|
572
|
-
|
|
579
|
+
// Check for action intent via classifyActionIntent (covers Form Submit, WhatsApp, Call/Phone, Directions, Location, Shop, Email)
|
|
580
|
+
const actionIntent = !apiOwned ? classifyActionIntent(innerText, actionableHref) : null;
|
|
581
|
+
|
|
582
|
+
const isFormSubmit =
|
|
583
|
+
!apiOwned &&
|
|
584
|
+
(ACTION_TAGS.has(name) || isLikelyCtaClass(className)) &&
|
|
585
|
+
(isSubmit ||
|
|
586
|
+
(insideForm && (actionIntent?.action === 'form-submit' || (innerText && !actionableHref))) ||
|
|
587
|
+
actionIntent?.action === 'form-submit');
|
|
588
|
+
|
|
589
|
+
if (isFormSubmit && innerText && !textInfo.dynamic) {
|
|
590
|
+
usedLocs.add(loc);
|
|
591
|
+
const resolvedHref = actionableHref || (actionIntent ? actionIntent.defaultUrl : 'https://wa.me/1234567890');
|
|
592
|
+
candidates.push({
|
|
593
|
+
...baseMeta,
|
|
594
|
+
kind: 'form-submit-action',
|
|
595
|
+
operation: 'form-submit-action',
|
|
596
|
+
value: resolvedHref,
|
|
597
|
+
label: innerText,
|
|
598
|
+
extra: {
|
|
599
|
+
action: 'form-submit',
|
|
600
|
+
external: true,
|
|
601
|
+
insideForm,
|
|
602
|
+
isSubmit,
|
|
603
|
+
},
|
|
604
|
+
confidence: confidenceFor('form-submit-action'),
|
|
605
|
+
reason: 'form-submit-whatsapp-dispatch',
|
|
606
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'form-submit-action', action: 'form-submit' }),
|
|
607
|
+
});
|
|
608
|
+
this.traverse(pathNode);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const isAction = !isSubmit && (Boolean(actionableHref) || Boolean(actionIntent)) && (ACTION_TAGS.has(name) || isLikelyCtaClass(className));
|
|
573
613
|
|
|
574
614
|
if (isAction && (actionableHref || actionIntent)) {
|
|
575
615
|
const action = actionIntent ? actionIntent.action : classifyHref(actionableHref);
|
|
@@ -684,7 +724,6 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
684
724
|
const objectItems = arr.items.every((item) => item.type === 'object');
|
|
685
725
|
const boundProperties = [...itemUsage.keys()];
|
|
686
726
|
const convertible =
|
|
687
|
-
!usesItemAsComponent &&
|
|
688
727
|
objectItems &&
|
|
689
728
|
boundProperties.length > 0 &&
|
|
690
729
|
arr.items.some((item) => boundProperties.some((key) => key in item.value));
|
|
@@ -5,6 +5,7 @@ const BUTTON_TAGS = new Set(['button', 'Button', 'CTAButton']);
|
|
|
5
5
|
|
|
6
6
|
function inferStyleKind(transform) {
|
|
7
7
|
if (transform.operation === 'collection-conversion') return 'grid';
|
|
8
|
+
if (transform.operation === 'form-submit-action') return 'button';
|
|
8
9
|
if (transform.operation === 'split-action-contract') return 'text';
|
|
9
10
|
if (BUTTON_TAGS.has(transform.tag) && transform.operation === 'extract-text') return 'button';
|
|
10
11
|
if (TEXT_OPS.has(transform.operation)) return 'text';
|
|
@@ -14,7 +15,7 @@ function inferStyleKind(transform) {
|
|
|
14
15
|
function stylePathFor(transform, kind) {
|
|
15
16
|
if (kind === 'grid' && transform.listField) return `${transform.listField}.grid`;
|
|
16
17
|
if (kind === 'card' && transform.listField) return `${transform.listField}[*].card`;
|
|
17
|
-
if (transform.operation === 'split-action-contract') return transform.labelField;
|
|
18
|
+
if (transform.operation === 'split-action-contract' || transform.operation === 'form-submit-action') return transform.labelField;
|
|
18
19
|
return transform.field || transform.labelField || null;
|
|
19
20
|
}
|
|
20
21
|
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -152,6 +152,32 @@ function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
|
|
|
152
152
|
|
|
153
153
|
function applyTransformToElement(pathNode, transform) {
|
|
154
154
|
const node = pathNode.node;
|
|
155
|
+
if (transform.operation === 'form-submit-action') {
|
|
156
|
+
const tagName = getJsxName(node);
|
|
157
|
+
if (!hasJsxAttribute(node, 'type') && tagName === 'button') {
|
|
158
|
+
node.openingElement.attributes.push(
|
|
159
|
+
b.jsxAttribute(b.jsxIdentifier('type'), b.stringLiteral('submit'))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (transform.labelField) {
|
|
163
|
+
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
164
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
165
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
169
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
170
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
174
|
+
} else if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
175
|
+
node.openingElement.attributes.push(
|
|
176
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-button'))
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
155
181
|
if (transform.operation === 'split-action-contract') {
|
|
156
182
|
const tagName = getJsxName(node);
|
|
157
183
|
if (tagName === 'button') {
|
|
@@ -185,18 +211,17 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
214
|
+
// Fivora Strict Action/Label Contract:
|
|
215
|
+
// Bind the URL action marker directly to the outer <a> element (never hidden)
|
|
216
|
+
replaceAttrValue(node, 'data-preview-field-path', b.stringLiteral(transform.urlField));
|
|
217
|
+
// Remove data-preview-static if present to prevent static/field collisions
|
|
218
|
+
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
194
219
|
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
195
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-
|
|
220
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
196
221
|
);
|
|
197
222
|
}
|
|
223
|
+
// Bind the label to the inner text child
|
|
198
224
|
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
199
|
-
wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
|
|
200
225
|
return;
|
|
201
226
|
}
|
|
202
227
|
if (transform.operation === 'extract-url') {
|
|
@@ -234,11 +259,29 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
234
259
|
return;
|
|
235
260
|
}
|
|
236
261
|
if (transform.operation === 'style-bind') {
|
|
237
|
-
if (transform.styleKind === 'grid'
|
|
262
|
+
if (transform.styleKind === 'grid') {
|
|
263
|
+
const container = findAncestorJsxElement(pathNode) || node;
|
|
264
|
+
ensureStyleAttrs(container, transform.stylePath, 'grid');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (transform.styleKind === 'card') {
|
|
268
|
+
ensureStyleAttrs(node, transform.stylePath, 'card');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
238
271
|
ensureStyleAttrs(node, transform.stylePath, transform.styleKind || 'text');
|
|
239
272
|
}
|
|
240
273
|
}
|
|
241
274
|
|
|
275
|
+
function findAncestorJsxElement(pathNode) {
|
|
276
|
+
let current = pathNode.parent;
|
|
277
|
+
while (current) {
|
|
278
|
+
const candidate = current.node || current.value;
|
|
279
|
+
if (candidate?.type === 'JSXElement') return candidate;
|
|
280
|
+
current = current.parentPath || current.parent;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
242
285
|
function inferButtonKind(tag) {
|
|
243
286
|
if (tag === 'button' || tag === 'Button') return 'button';
|
|
244
287
|
return 'text';
|
|
@@ -314,6 +357,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
314
357
|
}
|
|
315
358
|
|
|
316
359
|
markItemFields(callback, { listPath, binding, indexName });
|
|
360
|
+
markComponentRefItemsStatic(callback, binding);
|
|
317
361
|
|
|
318
362
|
const container = findListContainer(mapCall);
|
|
319
363
|
if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
|
|
@@ -330,7 +374,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
330
374
|
);
|
|
331
375
|
}
|
|
332
376
|
|
|
333
|
-
return bindArrayDeclaration(ast, mapCall, listPath, isClient);
|
|
377
|
+
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef));
|
|
334
378
|
}
|
|
335
379
|
|
|
336
380
|
function findMapCall(ast, loc) {
|
|
@@ -389,9 +433,14 @@ function markItemFields(callback, { listPath, binding, indexName }) {
|
|
|
389
433
|
);
|
|
390
434
|
if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
391
435
|
const property = itemMemberName(textChild.expression, binding);
|
|
392
|
-
node
|
|
393
|
-
|
|
394
|
-
|
|
436
|
+
const tagName = getJsxName(node);
|
|
437
|
+
if (BROAD_CONTENT_CONTAINERS.has(tagName) || BROAD_CONTENT_CONTAINERS.has(String(tagName).toLowerCase())) {
|
|
438
|
+
wrapItemFieldInSpan(node, textChild, listPath, indexName, property);
|
|
439
|
+
} else {
|
|
440
|
+
node.openingElement.attributes.push(
|
|
441
|
+
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
395
444
|
}
|
|
396
445
|
|
|
397
446
|
this.traverse(pathNode);
|
|
@@ -399,6 +448,51 @@ function markItemFields(callback, { listPath, binding, indexName }) {
|
|
|
399
448
|
});
|
|
400
449
|
}
|
|
401
450
|
|
|
451
|
+
function wrapItemFieldInSpan(node, textChild, listPath, indexName, property) {
|
|
452
|
+
const nextChildren = [];
|
|
453
|
+
for (const child of node.children || []) {
|
|
454
|
+
if (child === textChild) {
|
|
455
|
+
nextChildren.push(
|
|
456
|
+
b.jsxElement(
|
|
457
|
+
b.jsxOpeningElement(
|
|
458
|
+
b.jsxIdentifier('span'),
|
|
459
|
+
[jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)],
|
|
460
|
+
false
|
|
461
|
+
),
|
|
462
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
463
|
+
[child],
|
|
464
|
+
false
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
nextChildren.push(child);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
node.children = nextChildren;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function markComponentRefItemsStatic(callback, binding) {
|
|
475
|
+
recast.types.visit(callback, {
|
|
476
|
+
visitJSXElement(pathNode) {
|
|
477
|
+
const name = pathNode.node.openingElement && pathNode.node.openingElement.name;
|
|
478
|
+
if (
|
|
479
|
+
name &&
|
|
480
|
+
name.type === 'JSXMemberExpression' &&
|
|
481
|
+
name.object &&
|
|
482
|
+
(name.object.type === 'Identifier' || name.object.type === 'JSXIdentifier') &&
|
|
483
|
+
name.object.name === binding
|
|
484
|
+
) {
|
|
485
|
+
if (!hasJsxAttribute(pathNode.node, 'data-preview-static') && !hasJsxAttribute(pathNode.node, 'data-preview-field-path')) {
|
|
486
|
+
pathNode.node.openingElement.attributes.push(
|
|
487
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('component-ref'))
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this.traverse(pathNode);
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
402
496
|
function itemMemberName(expr, binding) {
|
|
403
497
|
if (!expr || expr.type !== 'MemberExpression' || expr.computed) return null;
|
|
404
498
|
if (expr.object?.type !== 'Identifier' || expr.object.name !== binding) return null;
|
|
@@ -449,7 +543,7 @@ function findEnclosingFunction(pathNode) {
|
|
|
449
543
|
return null;
|
|
450
544
|
}
|
|
451
545
|
|
|
452
|
-
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
546
|
+
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false) {
|
|
453
547
|
const arrayName = mapCallPath.node.callee.object?.name;
|
|
454
548
|
if (!arrayName) return false;
|
|
455
549
|
let bound = false;
|
|
@@ -477,7 +571,9 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
|
477
571
|
const localDecl = b.variableDeclaration('const', [
|
|
478
572
|
b.variableDeclarator(
|
|
479
573
|
b.identifier(arrayName),
|
|
480
|
-
|
|
574
|
+
mergeDefaultRefs
|
|
575
|
+
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
576
|
+
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
481
577
|
),
|
|
482
578
|
]);
|
|
483
579
|
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
@@ -501,6 +597,28 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
|
501
597
|
return bound;
|
|
502
598
|
}
|
|
503
599
|
|
|
600
|
+
function mergeDefaultItemRefsBinding(pathParts, defaultName) {
|
|
601
|
+
const listExpr = siteDataListBinding(pathParts, b.identifier(defaultName));
|
|
602
|
+
return b.callExpression(
|
|
603
|
+
b.memberExpression(listExpr, b.identifier('map'), false),
|
|
604
|
+
[
|
|
605
|
+
b.arrowFunctionExpression(
|
|
606
|
+
[b.identifier('item'), b.identifier('index')],
|
|
607
|
+
b.objectExpression([
|
|
608
|
+
b.spreadElement(
|
|
609
|
+
b.logicalExpression(
|
|
610
|
+
'??',
|
|
611
|
+
b.memberExpression(b.identifier(defaultName), b.identifier('index'), true),
|
|
612
|
+
b.objectExpression([])
|
|
613
|
+
)
|
|
614
|
+
),
|
|
615
|
+
b.spreadElement(b.identifier('item')),
|
|
616
|
+
])
|
|
617
|
+
),
|
|
618
|
+
]
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
504
622
|
function fileAlreadyUsesSiteDataHook(ast) {
|
|
505
623
|
let found = false;
|
|
506
624
|
recast.types.visit(ast, {
|
|
@@ -625,6 +743,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
625
743
|
|
|
626
744
|
const supported = new Set([
|
|
627
745
|
'split-action-contract',
|
|
746
|
+
'form-submit-action',
|
|
628
747
|
'extract-url',
|
|
629
748
|
'extract-image',
|
|
630
749
|
'extract-alt',
|
|
@@ -688,6 +807,9 @@ function applyFilePlan(filePlan, profile) {
|
|
|
688
807
|
|
|
689
808
|
// Sanitize any conflicting data-preview-static on elements with editable markers
|
|
690
809
|
sanitizeContradictoryMarkers(ast);
|
|
810
|
+
healBroadContainerMarkers(ast);
|
|
811
|
+
healEmptyStateConditionals(ast);
|
|
812
|
+
healHiddenPreviewMarkers(ast);
|
|
691
813
|
|
|
692
814
|
// Page keys are stamped in a separate route-driven pass so App Router and
|
|
693
815
|
// Pages Router projects are handled by the same logic.
|
|
@@ -972,8 +1094,7 @@ function sanitizeContradictoryMarkers(ast) {
|
|
|
972
1094
|
}
|
|
973
1095
|
|
|
974
1096
|
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
975
|
-
if (!code.includes('data-preview-static')
|
|
976
|
-
if (!code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path')) {
|
|
1097
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path')) {
|
|
977
1098
|
return { code, updated: false };
|
|
978
1099
|
}
|
|
979
1100
|
let ast;
|
|
@@ -982,9 +1103,200 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
|
982
1103
|
} catch {
|
|
983
1104
|
return { code, updated: false };
|
|
984
1105
|
}
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
1106
|
+
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
1107
|
+
const healedBroad = healBroadContainerMarkers(ast);
|
|
1108
|
+
const healedEmpty = healEmptyStateConditionals(ast);
|
|
1109
|
+
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
1110
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0) return { code, updated: false };
|
|
1111
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function healBroadContainerMarkers(ast) {
|
|
1115
|
+
let healed = 0;
|
|
1116
|
+
recast.types.visit(ast, {
|
|
1117
|
+
visitJSXElement(pathNode) {
|
|
1118
|
+
const node = pathNode.node;
|
|
1119
|
+
const tag = getJsxName(node);
|
|
1120
|
+
const lower = String(tag || '').toLowerCase();
|
|
1121
|
+
if (!BROAD_CONTENT_CONTAINERS.has(tag) && !BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
1122
|
+
this.traverse(pathNode);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const fieldAttr = findJsxAttribute(node, 'data-preview-field-path');
|
|
1126
|
+
if (!fieldAttr) {
|
|
1127
|
+
this.traverse(pathNode);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const fieldValue = fieldAttr.value;
|
|
1132
|
+
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
1133
|
+
(attr) =>
|
|
1134
|
+
!(
|
|
1135
|
+
attr.type === 'JSXAttribute' &&
|
|
1136
|
+
attr.name &&
|
|
1137
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1138
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
1139
|
+
attr.name.name === 'data-preview-style-type')
|
|
1140
|
+
)
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
const leaf = findFirstLeafChild(node);
|
|
1144
|
+
if (leaf && !hasJsxAttribute(leaf, 'data-preview-field-path')) {
|
|
1145
|
+
leaf.openingElement.attributes.push(
|
|
1146
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)
|
|
1147
|
+
);
|
|
1148
|
+
healed++;
|
|
1149
|
+
} else {
|
|
1150
|
+
const span = b.jsxElement(
|
|
1151
|
+
b.jsxOpeningElement(
|
|
1152
|
+
b.jsxIdentifier('span'),
|
|
1153
|
+
[b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)],
|
|
1154
|
+
false
|
|
1155
|
+
),
|
|
1156
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
1157
|
+
node.children || [],
|
|
1158
|
+
false
|
|
1159
|
+
);
|
|
1160
|
+
node.children = [span];
|
|
1161
|
+
healed++;
|
|
1162
|
+
}
|
|
1163
|
+
this.traverse(pathNode);
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
return healed;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function findFirstLeafChild(node) {
|
|
1170
|
+
const leafTags = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'img', 'button', 'a', 'strong', 'em', 'small']);
|
|
1171
|
+
for (const child of node.children || []) {
|
|
1172
|
+
if (child && child.type === 'JSXElement') {
|
|
1173
|
+
const name = getJsxName(child);
|
|
1174
|
+
if (leafTags.has(name) || leafTags.has(String(name).toLowerCase())) return child;
|
|
1175
|
+
const nested = findFirstLeafChild(child);
|
|
1176
|
+
if (nested) return nested;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function healEmptyStateConditionals(ast) {
|
|
1183
|
+
let healed = 0;
|
|
1184
|
+
|
|
1185
|
+
function jsxHasPreviewMarker(node) {
|
|
1186
|
+
if (!node) return false;
|
|
1187
|
+
if (node.type === 'JSXElement') {
|
|
1188
|
+
if (
|
|
1189
|
+
hasJsxAttribute(node, 'data-preview-field-path') ||
|
|
1190
|
+
hasJsxAttribute(node, 'data-preview-list-path') ||
|
|
1191
|
+
hasJsxAttribute(node, 'data-preview-item-path')
|
|
1192
|
+
) {
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
1196
|
+
}
|
|
1197
|
+
if (node.type === 'JSXFragment') {
|
|
1198
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
1199
|
+
}
|
|
1200
|
+
if (node.type === 'ParenthesizedExpression') return jsxHasPreviewMarker(node.expression);
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
recast.types.visit(ast, {
|
|
1205
|
+
visitLogicalExpression(pathNode) {
|
|
1206
|
+
const expr = pathNode.node;
|
|
1207
|
+
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1208
|
+
pathNode.replace(expr.right);
|
|
1209
|
+
healed++;
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
this.traverse(pathNode);
|
|
1213
|
+
},
|
|
1214
|
+
visitJSXExpressionContainer(pathNode) {
|
|
1215
|
+
const expr = pathNode.node.expression;
|
|
1216
|
+
if (!expr) {
|
|
1217
|
+
this.traverse(pathNode);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1221
|
+
pathNode.node.expression = expr.right;
|
|
1222
|
+
healed++;
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
if (
|
|
1226
|
+
expr.type === 'ConditionalExpression' &&
|
|
1227
|
+
jsxHasPreviewMarker(expr.consequent) &&
|
|
1228
|
+
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
1229
|
+
) {
|
|
1230
|
+
pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
|
|
1231
|
+
? expr.consequent
|
|
1232
|
+
: expr.alternate;
|
|
1233
|
+
healed++;
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
this.traverse(pathNode);
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
return healed;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function isNullish(node) {
|
|
1243
|
+
if (!node) return true;
|
|
1244
|
+
if (node.type === 'NullLiteral') return true;
|
|
1245
|
+
if (node.type === 'Literal' && (node.value === null || node.value === false || node.value === undefined)) return true;
|
|
1246
|
+
if (node.type === 'BooleanLiteral' && node.value === false) return true;
|
|
1247
|
+
if (node.type === 'Identifier' && (node.name === 'undefined' || node.name === 'null')) return true;
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function healHiddenPreviewMarkers(ast) {
|
|
1252
|
+
let healed = 0;
|
|
1253
|
+
recast.types.visit(ast, {
|
|
1254
|
+
visitJSXOpeningElement(pathNode) {
|
|
1255
|
+
const attrs = pathNode.node.attributes || [];
|
|
1256
|
+
const hasEditable = attrs.some(
|
|
1257
|
+
(attr) =>
|
|
1258
|
+
attr.type === 'JSXAttribute' &&
|
|
1259
|
+
attr.name &&
|
|
1260
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1261
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
1262
|
+
attr.name.name === 'data-preview-item-path')
|
|
1263
|
+
);
|
|
1264
|
+
if (!hasEditable) {
|
|
1265
|
+
this.traverse(pathNode);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const classAttr = attrs.find((attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'className' || attr.name.name === 'class'));
|
|
1269
|
+
let classText = '';
|
|
1270
|
+
if (classAttr && classAttr.value) {
|
|
1271
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') classText = String(classAttr.value.value || '');
|
|
1272
|
+
else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression) {
|
|
1273
|
+
const expr = classAttr.value.expression;
|
|
1274
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal') classText = String(expr.value || '');
|
|
1275
|
+
if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const hiddenAttr = attrs.some(
|
|
1279
|
+
(attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'hidden' || (attr.name.name === 'aria-hidden' && recast.print(attr).code.includes('true')))
|
|
1280
|
+
);
|
|
1281
|
+
if (hiddenAttr || /(^|\s)hidden(\s|$)/.test(classText)) {
|
|
1282
|
+
pathNode.node.attributes = attrs.filter(
|
|
1283
|
+
(attr) =>
|
|
1284
|
+
!(
|
|
1285
|
+
attr.type === 'JSXAttribute' &&
|
|
1286
|
+
attr.name &&
|
|
1287
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
1288
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
1289
|
+
attr.name.name === 'data-preview-item-path' ||
|
|
1290
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
1291
|
+
attr.name.name === 'data-preview-style-type')
|
|
1292
|
+
)
|
|
1293
|
+
);
|
|
1294
|
+
healed++;
|
|
1295
|
+
}
|
|
1296
|
+
this.traverse(pathNode);
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
return healed;
|
|
988
1300
|
}
|
|
989
1301
|
|
|
990
1302
|
module.exports = {
|
|
@@ -998,4 +1310,7 @@ module.exports = {
|
|
|
998
1310
|
inferPageKey,
|
|
999
1311
|
sanitizeContradictoryMarkers,
|
|
1000
1312
|
sanitizeContradictoryMarkersInSource,
|
|
1313
|
+
healBroadContainerMarkers,
|
|
1314
|
+
healEmptyStateConditionals,
|
|
1315
|
+
healHiddenPreviewMarkers,
|
|
1001
1316
|
};
|