@deneb-ui/cli 2.0.71 → 2.0.73
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 +34 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +82 -0
- package/src/arc/ast.cjs +46 -0
- package/src/arc/field-paths.cjs +4 -0
- package/src/arc/manifest.cjs +39 -3
- package/src/arc/planner.cjs +10 -0
- package/src/arc/residual.cjs +42 -12
- package/src/arc/semantic.cjs +153 -13
- package/src/arc/transformer.cjs +301 -12
- package/src/tools/deneb-template-validator.cjs +1 -1
package/src/arc/semantic.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
3
|
const recast = require('recast');
|
|
4
4
|
const {
|
|
5
5
|
parseSource,
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
hasJsxAttribute,
|
|
10
10
|
collectJsxText,
|
|
11
11
|
findJsxAttribute,
|
|
12
|
+
unwrapExpr,
|
|
12
13
|
} = require('./ast.cjs');
|
|
13
14
|
const {
|
|
14
15
|
activeAdapters,
|
|
@@ -28,11 +29,46 @@ const {
|
|
|
28
29
|
DECORATIVE_TAGS,
|
|
29
30
|
} = require('./adapters.cjs');
|
|
30
31
|
const { shortHash } = require('./fs-utils.cjs');
|
|
32
|
+
const { resolveImportSpecifier } = require('./scanner.cjs');
|
|
31
33
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
32
34
|
|
|
33
35
|
const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
|
|
34
36
|
const ARIA_ONLY_RE = /^(aria-|data-state|data-slot|data-orientation)/;
|
|
35
37
|
const SKIP_ATTR_NAMES = new Set(['className', 'class', 'style', 'key', 'id', 'role', 'type', 'name', 'htmlFor', 'suppressHydrationWarning']);
|
|
38
|
+
const USER_FACING_PROP_NAMES = new Set([
|
|
39
|
+
'title',
|
|
40
|
+
'heading',
|
|
41
|
+
'subheading',
|
|
42
|
+
'subtitle',
|
|
43
|
+
'label',
|
|
44
|
+
'description',
|
|
45
|
+
'caption',
|
|
46
|
+
'badge',
|
|
47
|
+
'buttonText',
|
|
48
|
+
'ctaText',
|
|
49
|
+
'helperText',
|
|
50
|
+
'summary',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const importedDataCache = new Map();
|
|
54
|
+
|
|
55
|
+
function resolveImportedBinding(specifier, identifierName, currentFileAbs, profile) {
|
|
56
|
+
if (!specifier || !profile?.root || !currentFileAbs) return null;
|
|
57
|
+
const resolved = resolveImportSpecifier(profile, currentFileAbs, specifier);
|
|
58
|
+
if (!resolved || !fs.existsSync(resolved)) return null;
|
|
59
|
+
if (importedDataCache.has(resolved)) {
|
|
60
|
+
return importedDataCache.get(resolved).get(identifierName) || null;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const src = fs.readFileSync(resolved, 'utf8');
|
|
64
|
+
const importedAst = parseSource(src, resolved);
|
|
65
|
+
const importedBindings = collectStringBindings(importedAst);
|
|
66
|
+
importedDataCache.set(resolved, importedBindings);
|
|
67
|
+
return importedBindings.get(identifierName) || null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
36
72
|
|
|
37
73
|
function fingerprintCandidate(features) {
|
|
38
74
|
return shortHash(JSON.stringify(features));
|
|
@@ -42,9 +78,10 @@ function normalizeText(value) {
|
|
|
42
78
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
43
79
|
}
|
|
44
80
|
|
|
45
|
-
function isStaticSkipText(text) {
|
|
81
|
+
function isStaticSkipText(text, isLogoContext = false) {
|
|
46
82
|
const value = normalizeText(text);
|
|
47
|
-
if (!value
|
|
83
|
+
if (!value) return true;
|
|
84
|
+
if (value.length < 2 && !isLogoContext) return true;
|
|
48
85
|
if (TECHNICAL_TEXT_RE.test(value)) return true;
|
|
49
86
|
if (/^[{}`\\]/.test(value)) return true;
|
|
50
87
|
if (/^https?:\/\/(localhost|127\.0\.0\.1)/i.test(value)) return true;
|
|
@@ -350,6 +387,7 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
350
387
|
}
|
|
351
388
|
|
|
352
389
|
let usesItemAsComponent = false;
|
|
390
|
+
let usesDirectItem = false;
|
|
353
391
|
const componentProps = new Set();
|
|
354
392
|
recast.types.visit(callback, {
|
|
355
393
|
visitJSXOpeningElement(pathNode) {
|
|
@@ -364,6 +402,11 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
364
402
|
this.traverse(pathNode);
|
|
365
403
|
},
|
|
366
404
|
visitJSXExpressionContainer(pathNode) {
|
|
405
|
+
const rawExpr = pathNode.node.expression;
|
|
406
|
+
const unwrapped = unwrapTypeCasts(rawExpr);
|
|
407
|
+
if (unwrapped && (unwrapped.type === 'Identifier' || unwrapped.type === 'JSXIdentifier') && unwrapped.name === itemParam) {
|
|
408
|
+
usesDirectItem = true;
|
|
409
|
+
}
|
|
367
410
|
const properties = findItemMemberProperties(pathNode.node.expression);
|
|
368
411
|
for (const property of properties) {
|
|
369
412
|
if (componentProps.has(property)) continue;
|
|
@@ -385,7 +428,7 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
385
428
|
},
|
|
386
429
|
});
|
|
387
430
|
|
|
388
|
-
return { usage, usesItemAsComponent };
|
|
431
|
+
return { usage, usesItemAsComponent, usesDirectItem };
|
|
389
432
|
}
|
|
390
433
|
|
|
391
434
|
function classNameOf(node) {
|
|
@@ -485,7 +528,14 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
485
528
|
const mapInfo = inMapCallback(pathNode);
|
|
486
529
|
let apiOwned = false;
|
|
487
530
|
if (mapInfo && mapInfo.objectName) {
|
|
488
|
-
|
|
531
|
+
let binding = bindings.get(mapInfo.objectName);
|
|
532
|
+
if (!binding && imports[mapInfo.objectName]) {
|
|
533
|
+
const currentFileAbs = profile?.root ? path.join(profile.root, relativeFile) : null;
|
|
534
|
+
binding = resolveImportedBinding(imports[mapInfo.objectName], mapInfo.objectName, currentFileAbs, profile);
|
|
535
|
+
if (binding) {
|
|
536
|
+
bindings.set(mapInfo.objectName, binding);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
489
539
|
if (!binding || binding.kind !== 'array') apiOwned = true;
|
|
490
540
|
}
|
|
491
541
|
|
|
@@ -514,11 +564,24 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
514
564
|
const parents = parentNames(pathNode);
|
|
515
565
|
const actionNode = resolveActionWithAdapters(node, adapters) || (ACTION_TAGS.has(name) ? node : null);
|
|
516
566
|
|
|
567
|
+
const isHeaderNav =
|
|
568
|
+
parents.some((p) => /header|nav|navbar/i.test(p)) ||
|
|
569
|
+
/header|nav|navbar/i.test(name) ||
|
|
570
|
+
/header|nav/i.test(relativeFile) ||
|
|
571
|
+
componentMeta?.role === 'navigation';
|
|
572
|
+
const ariaLabel = getJsxAttributeLiteral(node, 'aria-label') || '';
|
|
573
|
+
const isExplicitLogo = /\b(site-logo|brand-logo|nav-logo|header-logo|monogram)\b/i.test(className);
|
|
574
|
+
const isLogoContext =
|
|
575
|
+
isExplicitLogo ||
|
|
576
|
+
(isHeaderNav && /\blogo\b/i.test(className)) ||
|
|
577
|
+
(isHeaderNav && /\b(logo|home)\b/i.test(ariaLabel)) ||
|
|
578
|
+
(isHeaderNav && (href === '/' || href === ''));
|
|
579
|
+
|
|
517
580
|
const baseMeta = {
|
|
518
581
|
loc,
|
|
519
582
|
tag: name,
|
|
520
583
|
file: relativeFile,
|
|
521
|
-
ownerScope,
|
|
584
|
+
ownerScope: (isLogoContext && isHeaderNav) ? 'common' : ownerScope,
|
|
522
585
|
componentName: componentMeta?.name,
|
|
523
586
|
role: componentMeta?.role,
|
|
524
587
|
className,
|
|
@@ -529,6 +592,43 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
529
592
|
fromBinding: textInfo.fromBinding,
|
|
530
593
|
};
|
|
531
594
|
|
|
595
|
+
if (isLogoContext) {
|
|
596
|
+
if ((IMAGE_TAGS.has(name) || recognition?.kind === 'image') && src && !src.startsWith('{')) {
|
|
597
|
+
usedLocs.add(loc);
|
|
598
|
+
candidates.push({
|
|
599
|
+
...baseMeta,
|
|
600
|
+
kind: 'image',
|
|
601
|
+
operation: 'extract-image',
|
|
602
|
+
value: src,
|
|
603
|
+
extra: { alt: alt || 'Logo', isBrandLogo: true },
|
|
604
|
+
confidence: 0.98,
|
|
605
|
+
reason: 'brand-logo-image',
|
|
606
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'logo-image' }),
|
|
607
|
+
});
|
|
608
|
+
this.traverse(pathNode);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (name === 'Link' || name === 'a' || name === 'span') {
|
|
613
|
+
const logoText = textInfo.text || collectJsxText(node);
|
|
614
|
+
if (logoText && !isStaticSkipText(logoText, true)) {
|
|
615
|
+
usedLocs.add(loc);
|
|
616
|
+
candidates.push({
|
|
617
|
+
...baseMeta,
|
|
618
|
+
kind: 'text',
|
|
619
|
+
operation: 'extract-text',
|
|
620
|
+
value: logoText,
|
|
621
|
+
extra: { tag: name, isBrandLogo: true },
|
|
622
|
+
confidence: 0.98,
|
|
623
|
+
reason: 'brand-logo-text',
|
|
624
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'logo-text', text: logoText }),
|
|
625
|
+
});
|
|
626
|
+
this.traverse(pathNode);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
532
632
|
if ((IMAGE_TAGS.has(name) || recognition?.kind === 'image') && src && !src.startsWith('{')) {
|
|
533
633
|
usedLocs.add(loc);
|
|
534
634
|
candidates.push({
|
|
@@ -569,6 +669,43 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
569
669
|
});
|
|
570
670
|
}
|
|
571
671
|
|
|
672
|
+
const isCustomComponent = Boolean(name && ((name[0] >= 'A' && name[0] <= 'Z') || name.includes('.')));
|
|
673
|
+
if (isCustomComponent && !apiOwned && node.openingElement && Array.isArray(node.openingElement.attributes)) {
|
|
674
|
+
for (const attr of node.openingElement.attributes) {
|
|
675
|
+
if (attr.type === 'JSXAttribute' && attr.name && USER_FACING_PROP_NAMES.has(attr.name.name)) {
|
|
676
|
+
const propName = attr.name.name;
|
|
677
|
+
let propValue = '';
|
|
678
|
+
if (attr.value) {
|
|
679
|
+
if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
|
|
680
|
+
propValue = String(attr.value.value || '');
|
|
681
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
682
|
+
const expr = attr.value.expression;
|
|
683
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
684
|
+
propValue = String(expr.value || '');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (propValue && !isStaticSkipText(propValue)) {
|
|
689
|
+
const propLoc = `${loc}:${propName}`;
|
|
690
|
+
if (!usedLocs.has(propLoc)) {
|
|
691
|
+
usedLocs.add(propLoc);
|
|
692
|
+
candidates.push({
|
|
693
|
+
...baseMeta,
|
|
694
|
+
loc: propLoc,
|
|
695
|
+
kind: 'text',
|
|
696
|
+
operation: 'extract-prop',
|
|
697
|
+
value: propValue,
|
|
698
|
+
extra: { propName, tag: name },
|
|
699
|
+
confidence: 0.88,
|
|
700
|
+
reason: `component-prop-${propName}`,
|
|
701
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'prop', propName }),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
572
709
|
const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
|
|
573
710
|
|
|
574
711
|
const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
|
|
@@ -736,13 +873,15 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
736
873
|
bindings.get(mapInfo.objectName)?.kind === 'array'
|
|
737
874
|
) {
|
|
738
875
|
const arr = bindings.get(mapInfo.objectName);
|
|
739
|
-
const { usage: itemUsage, usesItemAsComponent } = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
|
|
876
|
+
const { usage: itemUsage, usesItemAsComponent, usesDirectItem } = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
|
|
740
877
|
const objectItems = arr.items.every((item) => item.type === 'object');
|
|
878
|
+
const stringItems = arr.items.every((item) => item.type === 'string');
|
|
741
879
|
const boundProperties = [...itemUsage.keys()];
|
|
742
880
|
const convertible =
|
|
743
|
-
objectItems &&
|
|
744
|
-
|
|
745
|
-
|
|
881
|
+
(objectItems &&
|
|
882
|
+
boundProperties.length > 0 &&
|
|
883
|
+
arr.items.some((item) => boundProperties.some((key) => key in item.value))) ||
|
|
884
|
+
(stringItems && usesDirectItem);
|
|
746
885
|
|
|
747
886
|
candidates.push({
|
|
748
887
|
...baseMeta,
|
|
@@ -751,10 +890,11 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
751
890
|
value: arr.items,
|
|
752
891
|
extra: {
|
|
753
892
|
staticCollection: true,
|
|
893
|
+
isPrimitiveArray: stringItems && usesDirectItem,
|
|
754
894
|
binding: mapInfo.objectName,
|
|
755
895
|
itemParam: mapInfo.itemParam,
|
|
756
896
|
indexParam: mapInfo.indexParam,
|
|
757
|
-
itemFields: [...itemUsage.entries()].map(([key, role]) => ({ key, role })),
|
|
897
|
+
itemFields: stringItems ? [] : [...itemUsage.entries()].map(([key, role]) => ({ key, role })),
|
|
758
898
|
objectItems,
|
|
759
899
|
hasComponentRef: usesItemAsComponent,
|
|
760
900
|
},
|
|
@@ -766,7 +906,7 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
766
906
|
reason: usesItemAsComponent
|
|
767
907
|
? 'collection-holds-component-ref'
|
|
768
908
|
: convertible
|
|
769
|
-
? 'static-array-map'
|
|
909
|
+
? (stringItems ? 'static-primitive-array-map' : 'static-array-map')
|
|
770
910
|
: 'collection-shape-partial',
|
|
771
911
|
fingerprint: fingerprintCandidate({ tag: name, kind: 'collection', size: arr.items.length }),
|
|
772
912
|
});
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
jsxPreviewAttr,
|
|
20
20
|
jsxTemplatePathAttr,
|
|
21
21
|
wrapTextInEditableSpan,
|
|
22
|
+
unwrapExpr,
|
|
23
|
+
extractItemMemberName,
|
|
22
24
|
jsxStyleAttrs,
|
|
23
25
|
ensureStyleAttrs,
|
|
24
26
|
b,
|
|
@@ -27,10 +29,14 @@ const { toPosix } = require('./fs-utils.cjs');
|
|
|
27
29
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
28
30
|
|
|
29
31
|
function findElementByLoc(ast, loc) {
|
|
32
|
+
if (!loc) return null;
|
|
33
|
+
const parts = loc.split(':');
|
|
34
|
+
const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
|
|
30
35
|
let found = null;
|
|
31
36
|
recast.types.visit(ast, {
|
|
32
37
|
visitJSXElement(pathNode) {
|
|
33
|
-
|
|
38
|
+
const k = locKey(pathNode.node);
|
|
39
|
+
if (k === targetLoc || k === loc) {
|
|
34
40
|
found = pathNode;
|
|
35
41
|
return false;
|
|
36
42
|
}
|
|
@@ -243,6 +249,14 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
243
249
|
ensurePreviewPath(node, transform.field);
|
|
244
250
|
return;
|
|
245
251
|
}
|
|
252
|
+
if (transform.operation === 'extract-prop') {
|
|
253
|
+
const propName = transform.propName || 'title';
|
|
254
|
+
replaceAttrValue(node, propName, siteDataBinding(transform.field.split('.'), transform.fallback, transform.fieldType || 'text'));
|
|
255
|
+
if (!hasJsxAttribute(node, 'data-preview-field-path') && !hasJsxAttribute(node, 'data-preview-list-path')) {
|
|
256
|
+
ensurePreviewPath(node, transform.field);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
246
260
|
if (transform.operation === 'wrap-text-span') {
|
|
247
261
|
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
248
262
|
return;
|
|
@@ -397,7 +411,11 @@ function applyCollectionTransform(ast, transform, isClient = false, isTypeScript
|
|
|
397
411
|
);
|
|
398
412
|
}
|
|
399
413
|
|
|
400
|
-
|
|
414
|
+
if (transform.isPrimitiveArray) {
|
|
415
|
+
markPrimitiveItemFields(callback, { listPath, binding, indexName });
|
|
416
|
+
} else {
|
|
417
|
+
markItemFields(callback, { listPath, binding, indexName });
|
|
418
|
+
}
|
|
401
419
|
markComponentRefItemsStatic(callback, binding);
|
|
402
420
|
|
|
403
421
|
const container = findListContainer(mapCall);
|
|
@@ -535,9 +553,66 @@ function markComponentRefItemsStatic(callback, binding) {
|
|
|
535
553
|
}
|
|
536
554
|
|
|
537
555
|
function itemMemberName(expr, binding) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
556
|
+
return extractItemMemberName(expr, binding);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isDirectItemExpr(expr, binding) {
|
|
560
|
+
if (!expr) return false;
|
|
561
|
+
const unwrapped = unwrapExpr(expr);
|
|
562
|
+
return Boolean(
|
|
563
|
+
unwrapped &&
|
|
564
|
+
(unwrapped.type === 'Identifier' || unwrapped.type === 'JSXIdentifier') &&
|
|
565
|
+
unwrapped.name === binding
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function markPrimitiveItemFields(callback, { listPath, binding, indexName }) {
|
|
570
|
+
recast.types.visit(callback, {
|
|
571
|
+
visitJSXElement(pathNode) {
|
|
572
|
+
const node = pathNode.node;
|
|
573
|
+
const textChild = (node.children || []).find(
|
|
574
|
+
(child) => child.type === 'JSXExpressionContainer' && isDirectItemExpr(child.expression, binding)
|
|
575
|
+
);
|
|
576
|
+
if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
577
|
+
const tagName = getJsxName(node);
|
|
578
|
+
if (
|
|
579
|
+
node.children.length === 1 &&
|
|
580
|
+
(tagName === 'span' || tagName === 'p' || tagName === 'li') &&
|
|
581
|
+
!hasJsxAttribute(node, 'data-preview-item-path')
|
|
582
|
+
) {
|
|
583
|
+
node.openingElement.attributes.push(
|
|
584
|
+
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, '')
|
|
585
|
+
);
|
|
586
|
+
} else {
|
|
587
|
+
wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.traverse(pathNode);
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName) {
|
|
596
|
+
const nextChildren = [];
|
|
597
|
+
for (const child of node.children || []) {
|
|
598
|
+
if (child === textChild) {
|
|
599
|
+
nextChildren.push(
|
|
600
|
+
b.jsxElement(
|
|
601
|
+
b.jsxOpeningElement(
|
|
602
|
+
b.jsxIdentifier('span'),
|
|
603
|
+
[jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, '')],
|
|
604
|
+
false
|
|
605
|
+
),
|
|
606
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
607
|
+
[child],
|
|
608
|
+
false
|
|
609
|
+
)
|
|
610
|
+
);
|
|
611
|
+
} else {
|
|
612
|
+
nextChildren.push(child);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
node.children = nextChildren;
|
|
541
616
|
}
|
|
542
617
|
|
|
543
618
|
/** The nearest JSX element that wraps the map expression. */
|
|
@@ -642,6 +717,61 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, merg
|
|
|
642
717
|
},
|
|
643
718
|
});
|
|
644
719
|
|
|
720
|
+
if (!bound) {
|
|
721
|
+
recast.types.visit(ast, {
|
|
722
|
+
visitImportDeclaration(pathNode) {
|
|
723
|
+
if (bound) return false;
|
|
724
|
+
const node = pathNode.node;
|
|
725
|
+
const spec = (node.specifiers || []).find(
|
|
726
|
+
(s) => (s.local?.name || s.imported?.name) === arrayName
|
|
727
|
+
);
|
|
728
|
+
if (!spec) {
|
|
729
|
+
this.traverse(pathNode);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const defaultName = 'DEFAULT_' + arrayName;
|
|
734
|
+
if (spec.type === 'ImportSpecifier') {
|
|
735
|
+
spec.local = b.identifier(defaultName);
|
|
736
|
+
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
737
|
+
spec.local = b.identifier(defaultName);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const fnPath = findEnclosingFunction(mapCallPath);
|
|
741
|
+
if (fnPath && fnPath.node.body?.type === 'BlockStatement') {
|
|
742
|
+
const body = fnPath.node.body.body;
|
|
743
|
+
const already = body.some(
|
|
744
|
+
(stmt) =>
|
|
745
|
+
recast.print(stmt).code.includes(`const ${arrayName} =`) ||
|
|
746
|
+
recast.print(stmt).code.includes(`const ${arrayName}:`)
|
|
747
|
+
);
|
|
748
|
+
if (!already) {
|
|
749
|
+
const localId = b.identifier(arrayName);
|
|
750
|
+
if (isTypeScript) {
|
|
751
|
+
localId.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
752
|
+
}
|
|
753
|
+
const localDecl = b.variableDeclaration('const', [
|
|
754
|
+
b.variableDeclarator(
|
|
755
|
+
localId,
|
|
756
|
+
mergeDefaultRefs
|
|
757
|
+
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
758
|
+
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
759
|
+
),
|
|
760
|
+
]);
|
|
761
|
+
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
762
|
+
if (hookIdx >= 0) {
|
|
763
|
+
body.splice(hookIdx + 1, 0, localDecl);
|
|
764
|
+
} else {
|
|
765
|
+
body.unshift(localDecl);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
bound = true;
|
|
770
|
+
return false;
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
645
775
|
return bound;
|
|
646
776
|
}
|
|
647
777
|
|
|
@@ -823,13 +953,15 @@ function injectSiteDataHook(ast) {
|
|
|
823
953
|
}
|
|
824
954
|
|
|
825
955
|
function resolveSiteDataSpecifier(profile, fromRelativeFile) {
|
|
826
|
-
const aliases = profile.aliasMap || {};
|
|
956
|
+
const aliases = (profile && profile.aliasMap) || {};
|
|
827
957
|
const hasAt = Object.keys(aliases).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
828
|
-
const
|
|
829
|
-
|
|
958
|
+
const hasSrc = Boolean(profile && profile.hasSrc);
|
|
959
|
+
const siteDataRel = hasSrc ? 'src/data/site-data.json' : 'data/site-data.json';
|
|
960
|
+
if (hasAt && hasSrc) return '@/data/site-data.json';
|
|
830
961
|
|
|
831
|
-
const
|
|
832
|
-
const
|
|
962
|
+
const root = profile && profile.root ? profile.root : process.cwd();
|
|
963
|
+
const fromAbs = path.join(root, fromRelativeFile || 'page.tsx');
|
|
964
|
+
const toAbs = path.join(root, siteDataRel);
|
|
833
965
|
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
834
966
|
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
835
967
|
return relSpec;
|
|
@@ -853,6 +985,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
853
985
|
'extract-alt',
|
|
854
986
|
'extract-placeholder',
|
|
855
987
|
'extract-text',
|
|
988
|
+
'extract-prop',
|
|
856
989
|
'wrap-text-span',
|
|
857
990
|
'collection-conversion',
|
|
858
991
|
'style-bind',
|
|
@@ -915,6 +1048,8 @@ function applyFilePlan(filePlan, profile) {
|
|
|
915
1048
|
healEmptyStateConditionals(ast);
|
|
916
1049
|
healHiddenPreviewMarkers(ast);
|
|
917
1050
|
healLegacyProductDetailLinks(ast);
|
|
1051
|
+
healSectionOverflowHidden(ast);
|
|
1052
|
+
healDecorativeOverlays(ast);
|
|
918
1053
|
|
|
919
1054
|
// Page keys are stamped in a separate route-driven pass so App Router and
|
|
920
1055
|
// Pages Router projects are handled by the same logic.
|
|
@@ -1152,11 +1287,98 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
|
|
|
1152
1287
|
|
|
1153
1288
|
ensureProviderInitialData(ast, jsonIdent);
|
|
1154
1289
|
injectPlatformAdditionalPages(ast, providerImport);
|
|
1290
|
+
injectDualModeTheme(ast, jsonIdent, providerImport);
|
|
1155
1291
|
sanitizeDuplicateBindings(ast);
|
|
1156
1292
|
const next = printSource(ast, code);
|
|
1157
1293
|
return { code: next, updated: next !== code };
|
|
1158
1294
|
}
|
|
1159
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* Injects <ThemeStyles /> and <ThemeToggle /> into the layout JSX tree.
|
|
1298
|
+
* ThemeStyles applies light/dark variables and auto-contrast rules.
|
|
1299
|
+
* ThemeToggle provides an out-of-the-box floating theme switcher.
|
|
1300
|
+
*/
|
|
1301
|
+
function injectDualModeTheme(ast, jsonIdent, providerImport = '@deneb-ui/ui') {
|
|
1302
|
+
if (!ast) return;
|
|
1303
|
+
let hasThemeStyles = false;
|
|
1304
|
+
let hasThemeToggle = false;
|
|
1305
|
+
recast.types.visit(ast, {
|
|
1306
|
+
visitJSXIdentifier(pathNode) {
|
|
1307
|
+
if (pathNode.node.name === 'ThemeStyles') hasThemeStyles = true;
|
|
1308
|
+
if (pathNode.node.name === 'ThemeToggle') hasThemeToggle = true;
|
|
1309
|
+
this.traverse(pathNode);
|
|
1310
|
+
},
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const importsToAdd = [];
|
|
1314
|
+
if (!hasThemeStyles) importsToAdd.push('ThemeStyles');
|
|
1315
|
+
if (!hasThemeToggle) importsToAdd.push('ThemeToggle');
|
|
1316
|
+
if (importsToAdd.length === 0) return;
|
|
1317
|
+
|
|
1318
|
+
if (!hasThemeStyles) {
|
|
1319
|
+
let stylesInjected = false;
|
|
1320
|
+
const snippet = parseSource(
|
|
1321
|
+
`<ThemeStyles theme={${jsonIdent}?.template?.structure?.theme || ${jsonIdent}?.theme} enableDualMode />`,
|
|
1322
|
+
'snippet.tsx'
|
|
1323
|
+
);
|
|
1324
|
+
const stylesEl = snippet.program.body[0].expression;
|
|
1325
|
+
|
|
1326
|
+
// Try <head> first
|
|
1327
|
+
recast.types.visit(ast, {
|
|
1328
|
+
visitJSXElement(pathNode) {
|
|
1329
|
+
if (stylesInjected) return false;
|
|
1330
|
+
const name = getJsxName(pathNode.node);
|
|
1331
|
+
if (name === 'head' || name === 'Head') {
|
|
1332
|
+
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1333
|
+
stylesInjected = true;
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
this.traverse(pathNode);
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// If no <head>, inject inside SiteDataProvider or <body>
|
|
1341
|
+
if (!stylesInjected) {
|
|
1342
|
+
recast.types.visit(ast, {
|
|
1343
|
+
visitJSXElement(pathNode) {
|
|
1344
|
+
if (stylesInjected) return false;
|
|
1345
|
+
const name = getJsxName(pathNode.node);
|
|
1346
|
+
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1347
|
+
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1348
|
+
stylesInjected = true;
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
this.traverse(pathNode);
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (!hasThemeToggle) {
|
|
1358
|
+
let toggleInjected = false;
|
|
1359
|
+
const snippet = parseSource(
|
|
1360
|
+
'<ThemeToggle showLabel className="fixed bottom-6 left-6 z-40" />',
|
|
1361
|
+
'snippet.tsx'
|
|
1362
|
+
);
|
|
1363
|
+
const toggleEl = snippet.program.body[0].expression;
|
|
1364
|
+
|
|
1365
|
+
recast.types.visit(ast, {
|
|
1366
|
+
visitJSXElement(pathNode) {
|
|
1367
|
+
if (toggleInjected) return false;
|
|
1368
|
+
const name = getJsxName(pathNode.node);
|
|
1369
|
+
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1370
|
+
pathNode.node.children = [...(pathNode.node.children || []), b.jsxText('\n '), toggleEl];
|
|
1371
|
+
toggleInjected = true;
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
this.traverse(pathNode);
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
ensureImport(ast, providerImport || '@deneb-ui/ui', importsToAdd);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1160
1382
|
/**
|
|
1161
1383
|
* Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
|
|
1162
1384
|
* This component handles all additionalPages visual-editing markers correctly
|
|
@@ -1329,8 +1551,9 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
|
1329
1551
|
const healedEmpty = healEmptyStateConditionals(ast);
|
|
1330
1552
|
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
1331
1553
|
const healedOverflow = healSectionOverflowHidden(ast);
|
|
1332
|
-
|
|
1333
|
-
|
|
1554
|
+
const healedOverlay = healDecorativeOverlays(ast);
|
|
1555
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0 && healedOverflow === 0 && healedOverlay === 0) return { code, updated: false };
|
|
1556
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden + healedOverflow + healedOverlay };
|
|
1334
1557
|
}
|
|
1335
1558
|
|
|
1336
1559
|
function healBroadContainerMarkers(ast) {
|
|
@@ -1622,6 +1845,71 @@ function healSectionOverflowHidden(ast) {
|
|
|
1622
1845
|
return healed;
|
|
1623
1846
|
}
|
|
1624
1847
|
|
|
1848
|
+
function healDecorativeOverlays(ast) {
|
|
1849
|
+
let healed = 0;
|
|
1850
|
+
recast.types.visit(ast, {
|
|
1851
|
+
visitJSXElement(pathNode) {
|
|
1852
|
+
const node = pathNode.node;
|
|
1853
|
+
const tag = getJsxName(node);
|
|
1854
|
+
const lower = String(tag || '').toLowerCase();
|
|
1855
|
+
if (lower !== 'div' && lower !== 'span') {
|
|
1856
|
+
this.traverse(pathNode);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const hasRealChildren = (node.children || []).some(
|
|
1860
|
+
(c) => c.type === 'JSXElement' || (c.type === 'JSXText' && c.value.trim().length > 0)
|
|
1861
|
+
);
|
|
1862
|
+
if (hasRealChildren) {
|
|
1863
|
+
this.traverse(pathNode);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const classAttr = (node.openingElement.attributes || []).find(
|
|
1867
|
+
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
1868
|
+
);
|
|
1869
|
+
if (!classAttr || !classAttr.value) {
|
|
1870
|
+
this.traverse(pathNode);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
let modified = false;
|
|
1874
|
+
const overlayPattern = /\b(?:bg-gradient-|bg-black\/|bg-white\/|bg-slate-\d+\/|backdrop-blur)/;
|
|
1875
|
+
const isAbsolute = /\babsolute\b/;
|
|
1876
|
+
const hasPointerEvents = /\bpointer-events-(?:none|auto)\b/;
|
|
1877
|
+
|
|
1878
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
1879
|
+
const val = String(classAttr.value.value || '');
|
|
1880
|
+
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
1881
|
+
classAttr.value.value = `${val} pointer-events-none`;
|
|
1882
|
+
modified = true;
|
|
1883
|
+
}
|
|
1884
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
1885
|
+
const expr = classAttr.value.expression;
|
|
1886
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
1887
|
+
const val = String(expr.value || '');
|
|
1888
|
+
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
1889
|
+
expr.value = `${val} pointer-events-none`;
|
|
1890
|
+
modified = true;
|
|
1891
|
+
}
|
|
1892
|
+
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
1893
|
+
const fullText = (expr.quasis || []).map((q) => q.value?.raw || '').join(' ');
|
|
1894
|
+
if (isAbsolute.test(fullText) && overlayPattern.test(fullText) && !hasPointerEvents.test(fullText)) {
|
|
1895
|
+
const lastQuasi = expr.quasis[expr.quasis.length - 1];
|
|
1896
|
+
if (lastQuasi && lastQuasi.value) {
|
|
1897
|
+
lastQuasi.value.raw = `${lastQuasi.value.raw} pointer-events-none`;
|
|
1898
|
+
if (lastQuasi.value.cooked) {
|
|
1899
|
+
lastQuasi.value.cooked = `${lastQuasi.value.cooked} pointer-events-none`;
|
|
1900
|
+
}
|
|
1901
|
+
modified = true;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (modified) healed++;
|
|
1907
|
+
this.traverse(pathNode);
|
|
1908
|
+
},
|
|
1909
|
+
});
|
|
1910
|
+
return healed;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1625
1913
|
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
1626
1914
|
if (!code.includes('siteData')) return code;
|
|
1627
1915
|
try {
|
|
@@ -1651,6 +1939,7 @@ module.exports = {
|
|
|
1651
1939
|
healEmptyStateConditionals,
|
|
1652
1940
|
healHiddenPreviewMarkers,
|
|
1653
1941
|
healSectionOverflowHidden,
|
|
1942
|
+
healDecorativeOverlays,
|
|
1654
1943
|
healLegacyProductDetailLinks,
|
|
1655
1944
|
healMissingSiteDataHooks,
|
|
1656
1945
|
injectSiteDataHook,
|