@deneb-ui/cli 2.0.112 → 2.0.114
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 +46 -1
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +112 -5
- package/src/arc/adapters.cjs +33 -0
- package/src/arc/auto-editability-fixer.cjs +903 -0
- package/src/arc/component-registry.cjs +12 -0
- package/src/arc/field-paths.cjs +4 -1
- package/src/arc/fivora-schema-authority.cjs +9 -1
- package/src/arc/index.cjs +24 -0
- package/src/arc/planner.cjs +13 -0
- package/src/arc/residual.cjs +4 -2
- package/src/arc/semantic.cjs +200 -1
- package/src/arc/transforms/element-transform.cjs +35 -0
- package/src/arc/transforms/index.cjs +1 -0
- package/src/arc/universal-icon-patcher.cjs +55 -0
- package/src/sites/universal-page-selection.ts +24 -2
- package/src/tools/deneb-template-validator.cjs +15 -15
- package/src/tools/template-converter.cjs +76 -24
- package/src/tools/template-preview-focus-bridge.cjs +2 -2
|
@@ -17,6 +17,8 @@ const KNOWN_EDITABLE_COMPONENTS = new Set([
|
|
|
17
17
|
'EditableQuote',
|
|
18
18
|
'EditableButton',
|
|
19
19
|
'EditableImage',
|
|
20
|
+
'EditableIcon',
|
|
21
|
+
'DynamicIcon',
|
|
20
22
|
'EditableMap',
|
|
21
23
|
'EditableList',
|
|
22
24
|
'EditableBox',
|
|
@@ -26,6 +28,10 @@ const KNOWN_EDITABLE_COMPONENTS = new Set([
|
|
|
26
28
|
|
|
27
29
|
// ─── Product & Commerce ────────────────────────────────────────
|
|
28
30
|
'EditableProductCard',
|
|
31
|
+
'EditableUniversalCard',
|
|
32
|
+
'EditableVehicleCard',
|
|
33
|
+
'EditableListingCard',
|
|
34
|
+
'EditableEntityCard',
|
|
29
35
|
'EditableProductGrid',
|
|
30
36
|
'EditableProductDetail',
|
|
31
37
|
'EditableCartDrawer',
|
|
@@ -78,12 +84,18 @@ const ALIAS_MAP = {
|
|
|
78
84
|
'Badge': 'EditableBadge',
|
|
79
85
|
'Quote': 'EditableQuote',
|
|
80
86
|
'Image': 'EditableImage',
|
|
87
|
+
'Icon': 'EditableIcon',
|
|
88
|
+
'DynamicIcon': 'EditableIcon',
|
|
81
89
|
'Map': 'EditableMap',
|
|
82
90
|
'Grid': 'EditableGrid',
|
|
83
91
|
'Section': 'EditableSection',
|
|
84
92
|
'Box': 'EditableBox',
|
|
85
93
|
'List': 'EditableList',
|
|
86
94
|
'ProductCard': 'EditableProductCard',
|
|
95
|
+
'UniversalCard': 'EditableUniversalCard',
|
|
96
|
+
'ListingCard': 'EditableUniversalCard',
|
|
97
|
+
'EntityCard': 'EditableUniversalCard',
|
|
98
|
+
'VehicleCard': 'EditableVehicleCard',
|
|
87
99
|
'ProductGrid': 'EditableProductGrid',
|
|
88
100
|
'ProductDetail': 'EditableProductDetail',
|
|
89
101
|
'CustomerReviews': 'EditableCustomerReviews',
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -91,7 +91,10 @@ function inferFieldName(kind, tag, text, extra = {}) {
|
|
|
91
91
|
if (kind === 'image') return 'logoUrl';
|
|
92
92
|
return 'logoText';
|
|
93
93
|
}
|
|
94
|
-
if (kind === 'image')
|
|
94
|
+
if (kind === 'image') {
|
|
95
|
+
if (extra.fieldHint) return extra.fieldHint;
|
|
96
|
+
return extra.alt ? toCamel([extra.alt, 'image']) || 'image' : 'image';
|
|
97
|
+
}
|
|
95
98
|
if (kind === 'alt') return extra.imageField ? extra.imageField.replace(/Image$/, 'ImageAlt').replace(/image$/, 'imageAlt') : 'imageAlt';
|
|
96
99
|
if (kind === 'placeholder') return toCamel([text || '', 'placeholder']) || 'placeholder';
|
|
97
100
|
|
|
@@ -36,7 +36,7 @@ const TYPE_COERCION_MAP = {
|
|
|
36
36
|
float: 'number',
|
|
37
37
|
bool: 'boolean',
|
|
38
38
|
img: 'image',
|
|
39
|
-
icon: '
|
|
39
|
+
icon: 'text',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -49,6 +49,11 @@ function normalizeFieldType(rawType, fieldKey = '') {
|
|
|
49
49
|
if (!rawType || typeof rawType !== 'string') return 'text';
|
|
50
50
|
const lower = rawType.toLowerCase().trim();
|
|
51
51
|
|
|
52
|
+
// Explicit icon or emoji fields always coerce to text
|
|
53
|
+
if (lower === 'icon' || lower === 'emoji') {
|
|
54
|
+
return 'text';
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
if (PLATFORM_ALLOWED_TYPES.has(lower)) {
|
|
53
58
|
return lower;
|
|
54
59
|
}
|
|
@@ -59,6 +64,9 @@ function normalizeFieldType(rawType, fieldKey = '') {
|
|
|
59
64
|
|
|
60
65
|
// Heuristic from fieldKey
|
|
61
66
|
const keyLower = fieldKey.toLowerCase();
|
|
67
|
+
if (keyLower.endsWith('icon') || keyLower.startsWith('icon') || keyLower.includes('emoji')) {
|
|
68
|
+
return 'text';
|
|
69
|
+
}
|
|
62
70
|
if (keyLower.endsWith('url') || keyLower.endsWith('link') || keyLower.endsWith('href')) {
|
|
63
71
|
return 'url';
|
|
64
72
|
}
|
package/src/arc/index.cjs
CHANGED
|
@@ -69,6 +69,7 @@ const {
|
|
|
69
69
|
testMutationResilience,
|
|
70
70
|
runFuzzHarness,
|
|
71
71
|
} = require('./fuzz-engine.cjs');
|
|
72
|
+
const { runAutoEditabilityFix } = require('./auto-editability-fixer.cjs');
|
|
72
73
|
const {
|
|
73
74
|
analyzeBlockedProject,
|
|
74
75
|
formatBlockedExplanationTerminal,
|
|
@@ -1306,6 +1307,27 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
|
|
|
1306
1307
|
}
|
|
1307
1308
|
}
|
|
1308
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* Standalone entry point for `deneb fix-editability`.
|
|
1312
|
+
* Scans the project for missing field paths and auto-patches everything.
|
|
1313
|
+
*
|
|
1314
|
+
* @param {string} projectDir
|
|
1315
|
+
* @param {{ dryRun?: boolean, verbose?: boolean }} options
|
|
1316
|
+
*/
|
|
1317
|
+
function runFixEditability(projectDir, options) {
|
|
1318
|
+
const verbose = !!(options && options.verbose);
|
|
1319
|
+
const dryRun = !!(options && options.dryRun);
|
|
1320
|
+
if (verbose) console.log('[deneb] Running auto-editability fix on ' + projectDir);
|
|
1321
|
+
const report = runAutoEditabilityFix(projectDir, { dryRun, verbose: true });
|
|
1322
|
+
if (report.fixed.length === 0) {
|
|
1323
|
+
console.log('[deneb] ✅ All fields already present — nothing to fix.');
|
|
1324
|
+
} else {
|
|
1325
|
+
console.log('[deneb] ✅ Fixed ' + report.fixed.length + ' file(s):');
|
|
1326
|
+
for (const f of report.fixed) console.log(' • ' + f);
|
|
1327
|
+
}
|
|
1328
|
+
return report;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1309
1331
|
module.exports = {
|
|
1310
1332
|
runDenebArc,
|
|
1311
1333
|
runTransactionalPipeline,
|
|
@@ -1332,4 +1354,6 @@ module.exports = {
|
|
|
1332
1354
|
printBlockedExplanation,
|
|
1333
1355
|
ENGINE_ID,
|
|
1334
1356
|
ARC_VERSION,
|
|
1357
|
+
runAutoEditabilityFix,
|
|
1358
|
+
runFixEditability,
|
|
1335
1359
|
};
|
package/src/arc/planner.cjs
CHANGED
|
@@ -163,6 +163,7 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
163
163
|
used: usedPaths,
|
|
164
164
|
});
|
|
165
165
|
transform.fieldType = 'image';
|
|
166
|
+
transform.dynamicSrc = Boolean(extra.dynamicSrc);
|
|
166
167
|
} else if (candidate.operation === 'extract-alt') {
|
|
167
168
|
transform.field = buildFieldPath({
|
|
168
169
|
scope,
|
|
@@ -233,6 +234,18 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
233
234
|
transform.targetComponent = extra.componentName;
|
|
234
235
|
transform.targetFile = extra.componentFile;
|
|
235
236
|
transform.previewPath = prefix;
|
|
237
|
+
} else if (candidate.operation === 'feature-icon') {
|
|
238
|
+
const iconBase = extra.iconName || 'icon';
|
|
239
|
+
const fieldName = inferFieldName(iconBase + 'Icon', candidate.tag, candidate.value, extra);
|
|
240
|
+
transform.field = buildFieldPath({
|
|
241
|
+
scope,
|
|
242
|
+
section,
|
|
243
|
+
field: fieldName,
|
|
244
|
+
used: usedPaths,
|
|
245
|
+
});
|
|
246
|
+
transform.fieldType = 'text';
|
|
247
|
+
transform.control = 'icon-picker';
|
|
248
|
+
transform.fallback = candidate.value || iconBase;
|
|
236
249
|
} else if (candidate.operation === 'extract-tailwind-bg') {
|
|
237
250
|
transform.field = buildFieldPath({
|
|
238
251
|
scope,
|
package/src/arc/residual.cjs
CHANGED
|
@@ -31,6 +31,8 @@ const {
|
|
|
31
31
|
IMAGE_TAGS,
|
|
32
32
|
DECORATIVE_TAGS,
|
|
33
33
|
isIconComponent,
|
|
34
|
+
isUtilityIcon,
|
|
35
|
+
isFeatureIcon,
|
|
34
36
|
} = require('./adapters.cjs');
|
|
35
37
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
36
38
|
const { inferSection, inferFieldName, buildFieldPath, classifyFieldType } = require('./field-paths.cjs');
|
|
@@ -118,7 +120,7 @@ function isDecorativeCopy(text, tag, className) {
|
|
|
118
120
|
function classifyResidual(node, text, tag) {
|
|
119
121
|
const className = attrLiteral(node, 'className') || attrLiteral(node, 'class');
|
|
120
122
|
const ariaHidden = attrLiteral(node, 'aria-hidden') === 'true' || hasJsxAttribute(node, 'hidden') || /(^|\s)hidden(\s|$)/.test(className);
|
|
121
|
-
if (ariaHidden || DECORATIVE_TAGS.has(tag) ||
|
|
123
|
+
if (ariaHidden || DECORATIVE_TAGS.has(tag) || isUtilityIcon(tag)) {
|
|
122
124
|
return { bind: false, reason: 'decorative-icon' };
|
|
123
125
|
}
|
|
124
126
|
if (isDecorativeCopy(text, tag, className)) {
|
|
@@ -224,7 +226,7 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
|
|
|
224
226
|
return;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
if (DECORATIVE_TAGS.has(tag) ||
|
|
229
|
+
if (DECORATIVE_TAGS.has(tag) || isUtilityIcon(tag)) {
|
|
228
230
|
if (ensureStaticOnLeaf(node, 'decorative-icon')) applied++;
|
|
229
231
|
this.traverse(pathNode);
|
|
230
232
|
return;
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -10,12 +10,16 @@ const {
|
|
|
10
10
|
collectJsxText,
|
|
11
11
|
findJsxAttribute,
|
|
12
12
|
unwrapExpr,
|
|
13
|
+
extractItemMemberName,
|
|
13
14
|
} = require('./ast.cjs');
|
|
14
15
|
const {
|
|
15
16
|
activeAdapters,
|
|
16
17
|
recognizeWithAdapters,
|
|
17
18
|
resolveActionWithAdapters,
|
|
18
19
|
isIconComponent,
|
|
20
|
+
isUtilityIcon,
|
|
21
|
+
isFeatureIcon,
|
|
22
|
+
extractIconBaseName,
|
|
19
23
|
classifyHref,
|
|
20
24
|
classifyActionIntent,
|
|
21
25
|
isLikelyCtaClass,
|
|
@@ -498,6 +502,141 @@ function classNameOf(node) {
|
|
|
498
502
|
return getJsxAttributeLiteral(node, 'className') || getJsxAttributeLiteral(node, 'class') || '';
|
|
499
503
|
}
|
|
500
504
|
|
|
505
|
+
function collectPatternNames(param, out) {
|
|
506
|
+
if (!param) return;
|
|
507
|
+
if (param.type === 'Identifier') out.push(param.name);
|
|
508
|
+
else if (param.type === 'AssignmentPattern') collectPatternNames(param.left, out);
|
|
509
|
+
else if (param.type === 'RestElement') collectPatternNames(param.argument, out);
|
|
510
|
+
else if (param.type === 'ObjectPattern') {
|
|
511
|
+
for (const prop of param.properties || []) {
|
|
512
|
+
if (prop.type === 'RestElement') collectPatternNames(prop.argument, out);
|
|
513
|
+
else collectPatternNames(prop.value || prop.argument, out);
|
|
514
|
+
}
|
|
515
|
+
} else if (param.type === 'ArrayPattern') {
|
|
516
|
+
for (const el of param.elements || []) collectPatternNames(el, out);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function enclosingComponentParams(pathNode) {
|
|
521
|
+
let current = pathNode.parent;
|
|
522
|
+
while (current) {
|
|
523
|
+
const node = current.node || current.value;
|
|
524
|
+
if (
|
|
525
|
+
node &&
|
|
526
|
+
(node.type === 'FunctionDeclaration' ||
|
|
527
|
+
node.type === 'FunctionExpression' ||
|
|
528
|
+
node.type === 'ArrowFunctionExpression')
|
|
529
|
+
) {
|
|
530
|
+
const parent = current.parent?.node || current.parent?.value;
|
|
531
|
+
const isMapCallback =
|
|
532
|
+
parent?.type === 'CallExpression' &&
|
|
533
|
+
parent.callee?.type === 'MemberExpression' &&
|
|
534
|
+
parent.callee.property?.name === 'map';
|
|
535
|
+
if (!isMapCallback) {
|
|
536
|
+
const names = [];
|
|
537
|
+
for (const param of node.params || []) collectPatternNames(param, names);
|
|
538
|
+
return { fn: node, names };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
current = current.parentPath || current.parent;
|
|
542
|
+
}
|
|
543
|
+
return { fn: null, names: [] };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function identifiersIn(expr, out) {
|
|
547
|
+
const node = unwrapExpr(expr);
|
|
548
|
+
if (!node) return;
|
|
549
|
+
if (node.type === 'Identifier') {
|
|
550
|
+
out.add(node.name);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
|
|
554
|
+
identifiersIn(node.object, out);
|
|
555
|
+
if (node.computed) identifiersIn(node.property, out);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (node.type === 'ChainExpression') {
|
|
559
|
+
identifiersIn(node.expression, out);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (node.type === 'LogicalExpression' || node.type === 'BinaryExpression') {
|
|
563
|
+
identifiersIn(node.left, out);
|
|
564
|
+
identifiersIn(node.right, out);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (node.type === 'ConditionalExpression') {
|
|
568
|
+
identifiersIn(node.test, out);
|
|
569
|
+
identifiersIn(node.consequent, out);
|
|
570
|
+
identifiersIn(node.alternate, out);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function namesTiedToParams(fn, paramNames) {
|
|
575
|
+
const blocked = new Set(paramNames);
|
|
576
|
+
if (!fn?.body) return blocked;
|
|
577
|
+
let grew = true;
|
|
578
|
+
while (grew) {
|
|
579
|
+
grew = false;
|
|
580
|
+
recast.types.visit(fn.body, {
|
|
581
|
+
visitVariableDeclarator(pathNode) {
|
|
582
|
+
const node = pathNode.node;
|
|
583
|
+
if (node.id?.type === 'Identifier' && node.init && !blocked.has(node.id.name)) {
|
|
584
|
+
const used = new Set();
|
|
585
|
+
identifiersIn(node.init, used);
|
|
586
|
+
for (const name of used) {
|
|
587
|
+
if (blocked.has(name)) {
|
|
588
|
+
blocked.add(node.id.name);
|
|
589
|
+
grew = true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
this.traverse(pathNode);
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return blocked;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function imageExpression(node) {
|
|
601
|
+
if (!node) return null;
|
|
602
|
+
const attr = findJsxAttribute(node, 'src');
|
|
603
|
+
if (!attr?.value || attr.value.type !== 'JSXExpressionContainer') return null;
|
|
604
|
+
const expr = unwrapExpr(attr.value.expression);
|
|
605
|
+
if (!expr || expr.type === 'JSXEmptyExpression') return null;
|
|
606
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal' || expr.type === 'TemplateLiteral') return null;
|
|
607
|
+
if (expr.type === 'Identifier' && expr.name === 'siteData') return null;
|
|
608
|
+
if (
|
|
609
|
+
expr.type === 'Identifier' ||
|
|
610
|
+
expr.type === 'MemberExpression' ||
|
|
611
|
+
expr.type === 'OptionalMemberExpression' ||
|
|
612
|
+
expr.type === 'ChainExpression' ||
|
|
613
|
+
expr.type === 'LogicalExpression' ||
|
|
614
|
+
expr.type === 'ConditionalExpression'
|
|
615
|
+
) {
|
|
616
|
+
return expr;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function imageFieldHint(expr) {
|
|
622
|
+
const node = unwrapExpr(expr);
|
|
623
|
+
if (!node) return 'photoImage';
|
|
624
|
+
if (node.type === 'Identifier') {
|
|
625
|
+
return /image|photo|picture|banner|logo/i.test(node.name) ? node.name : `${node.name}Image`;
|
|
626
|
+
}
|
|
627
|
+
if ((node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') && !node.computed) {
|
|
628
|
+
const prop = node.property?.name || '';
|
|
629
|
+
const objectName = node.object?.type === 'Identifier' ? node.object.name : '';
|
|
630
|
+
const base = /^(src|url|href)$/i.test(prop) ? objectName || prop : prop || objectName;
|
|
631
|
+
if (!base) return 'photoImage';
|
|
632
|
+
return /image|photo|picture|banner|logo/i.test(base) ? base : `${base}Image`;
|
|
633
|
+
}
|
|
634
|
+
if (node.type === 'ChainExpression') return imageFieldHint(node.expression);
|
|
635
|
+
if (node.type === 'LogicalExpression') return imageFieldHint(node.left) || imageFieldHint(node.right);
|
|
636
|
+
if (node.type === 'ConditionalExpression') return imageFieldHint(node.consequent) || imageFieldHint(node.alternate);
|
|
637
|
+
return 'photoImage';
|
|
638
|
+
}
|
|
639
|
+
|
|
501
640
|
function confidenceFor(kind, extras = {}) {
|
|
502
641
|
if (extras.already) return 1;
|
|
503
642
|
if (extras.dynamic) return 0.15;
|
|
@@ -641,7 +780,7 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
641
780
|
if (!binding || binding.kind !== 'array') apiOwned = true;
|
|
642
781
|
}
|
|
643
782
|
|
|
644
|
-
if (DECORATIVE_TAGS.has(name) ||
|
|
783
|
+
if (DECORATIVE_TAGS.has(name) || isUtilityIcon(name)) {
|
|
645
784
|
candidates.push({
|
|
646
785
|
loc,
|
|
647
786
|
tag: name,
|
|
@@ -657,6 +796,29 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
657
796
|
return;
|
|
658
797
|
}
|
|
659
798
|
|
|
799
|
+
if (isFeatureIcon(name, importSource)) {
|
|
800
|
+
const iconBase = extractIconBaseName(name);
|
|
801
|
+
candidates.push({
|
|
802
|
+
loc,
|
|
803
|
+
tag: name,
|
|
804
|
+
kind: 'feature-icon',
|
|
805
|
+
operation: 'feature-icon',
|
|
806
|
+
confidence: 0.95,
|
|
807
|
+
skip: false,
|
|
808
|
+
value: iconBase,
|
|
809
|
+
file: relativeFile,
|
|
810
|
+
ownerScope,
|
|
811
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'icon', importSource }),
|
|
812
|
+
extra: {
|
|
813
|
+
iconName: iconBase,
|
|
814
|
+
originalComponent: name,
|
|
815
|
+
importSource,
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
this.traverse(pathNode);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
660
822
|
const parents = parentNames(pathNode);
|
|
661
823
|
|
|
662
824
|
if (name === 'source' && (parents[0] === 'picture' || recognition?.kind === 'responsive-image-source')) {
|
|
@@ -818,6 +980,43 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
818
980
|
return;
|
|
819
981
|
}
|
|
820
982
|
|
|
983
|
+
const dynamicImageNode = name === 'picture'
|
|
984
|
+
? (node.children || []).find((c) => c && c.type === 'JSXElement' && (getJsxName(c) === 'img' || getJsxName(c) === 'Image'))
|
|
985
|
+
: node;
|
|
986
|
+
const dynamicExpr = !src ? imageExpression(dynamicImageNode) : null;
|
|
987
|
+
if ((IMAGE_TAGS.has(name) || recognition?.kind === 'image') && dynamicExpr && !hasJsxAttribute(dynamicImageNode, 'data-preview-field-path')) {
|
|
988
|
+
const itemBound = mapInfo?.itemParam && extractItemMemberName(dynamicExpr, mapInfo.itemParam);
|
|
989
|
+
const componentParams = enclosingComponentParams(pathNode);
|
|
990
|
+
const blocked = namesTiedToParams(componentParams.fn, componentParams.names);
|
|
991
|
+
const usedNames = new Set();
|
|
992
|
+
identifiersIn(dynamicExpr, usedNames);
|
|
993
|
+
const fromProps = [...usedNames].some((usedName) => blocked.has(usedName));
|
|
994
|
+
// A photo that lives on one page (the home hero, for example) gets its
|
|
995
|
+
// own image field even when the src is a prop. A shared image widget
|
|
996
|
+
// must not, or every call site would show the same photo.
|
|
997
|
+
const pageOwned = Boolean(ownerScope) && ownerScope !== 'common';
|
|
998
|
+
if (!itemBound && (!fromProps || pageOwned)) {
|
|
999
|
+
usedLocs.add(loc);
|
|
1000
|
+
candidates.push({
|
|
1001
|
+
...baseMeta,
|
|
1002
|
+
kind: 'image',
|
|
1003
|
+
operation: 'extract-image',
|
|
1004
|
+
value: '',
|
|
1005
|
+
extra: {
|
|
1006
|
+
alt,
|
|
1007
|
+
dynamicSrc: true,
|
|
1008
|
+
fieldHint: isLogoContext ? undefined : imageFieldHint(dynamicExpr),
|
|
1009
|
+
isBrandLogo: Boolean(isLogoContext),
|
|
1010
|
+
},
|
|
1011
|
+
confidence: 0.9,
|
|
1012
|
+
reason: 'dynamic-image-source',
|
|
1013
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'image', dynamic: true, hint: imageFieldHint(dynamicExpr) }),
|
|
1014
|
+
});
|
|
1015
|
+
this.traverse(pathNode);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
821
1020
|
if (placeholder && !isStaticSkipText(placeholder)) {
|
|
822
1021
|
usedLocs.add(loc);
|
|
823
1022
|
candidates.push({
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
findJsxAttribute,
|
|
6
6
|
hasJsxAttribute,
|
|
7
7
|
siteDataBinding,
|
|
8
|
+
optionalMember,
|
|
8
9
|
ensureStyleAttrs,
|
|
9
10
|
b,
|
|
10
11
|
} = require('../ast.cjs');
|
|
@@ -20,6 +21,22 @@ const { splitActionChildren } = require('./primitives/action.cjs');
|
|
|
20
21
|
const { bindButtonWithIcon, bindHighlightedHeading } = require('./components/compound-content.cjs');
|
|
21
22
|
const { extractTailwindBg } = require('./assets/tailwind-bg.cjs');
|
|
22
23
|
|
|
24
|
+
function bindDynamicImageSource(node, field) {
|
|
25
|
+
const tagName = getJsxName(node);
|
|
26
|
+
const target = tagName === 'picture'
|
|
27
|
+
? (node.children || []).find((child) => child && child.type === 'JSXElement' && (getJsxName(child) === 'img' || getJsxName(child) === 'Image'))
|
|
28
|
+
: node;
|
|
29
|
+
if (!target) return;
|
|
30
|
+
const srcAttr = findJsxAttribute(target, 'src');
|
|
31
|
+
if (srcAttr?.value?.type === 'JSXExpressionContainer' && srcAttr.value.expression) {
|
|
32
|
+
const original = srcAttr.value.expression;
|
|
33
|
+
const edited = optionalMember(['siteData', 'content', ...String(field).split('.')]);
|
|
34
|
+
srcAttr.value = b.jsxExpressionContainer(b.logicalExpression('||', edited, original));
|
|
35
|
+
}
|
|
36
|
+
ensurePreviewPath(target, field);
|
|
37
|
+
if (target !== node) ensurePreviewPath(node, field);
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
function applyTransformToElement(pathNode, transform) {
|
|
24
41
|
const node = pathNode.node;
|
|
25
42
|
if (transform.operation === 'form-submit-action') {
|
|
@@ -96,6 +113,10 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
96
113
|
return;
|
|
97
114
|
}
|
|
98
115
|
if (transform.operation === 'extract-image' || transform.operation === 'extract-picture') {
|
|
116
|
+
if (transform.dynamicSrc) {
|
|
117
|
+
bindDynamicImageSource(node, transform.field);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
99
120
|
const tagName = getJsxName(node);
|
|
100
121
|
if (tagName === 'picture') {
|
|
101
122
|
const children = node.children || [];
|
|
@@ -185,6 +206,20 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
185
206
|
extractTailwindBg(node, transform);
|
|
186
207
|
return;
|
|
187
208
|
}
|
|
209
|
+
if (transform.operation === 'feature-icon') {
|
|
210
|
+
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
211
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
212
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
replaceAttrValue(node, 'data-preview-field-path', b.stringLiteral(transform.field));
|
|
216
|
+
if (!hasJsxAttribute(node, 'data-preview-control')) {
|
|
217
|
+
node.openingElement.attributes.push(
|
|
218
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-control'), b.stringLiteral('icon-picker'))
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
module.exports = {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Helper to ensure nested object path exists
|
|
7
|
+
function setDeep(obj, pathArr, value) {
|
|
8
|
+
let cur = obj;
|
|
9
|
+
for (let i = 0; i < pathArr.length - 1; i++) {
|
|
10
|
+
const k = pathArr[i];
|
|
11
|
+
if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {};
|
|
12
|
+
cur = cur[k];
|
|
13
|
+
}
|
|
14
|
+
if (cur[pathArr[pathArr.length - 1]] === undefined) {
|
|
15
|
+
cur[pathArr[pathArr.length - 1]] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function patchUniversalIcons(projectDir, dryRun = false, log = () => {}) {
|
|
20
|
+
if (!projectDir || typeof projectDir !== 'string' || !fs.existsSync(projectDir)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const siteDataPath = path.join(projectDir, 'data', 'site-data.json');
|
|
25
|
+
if (!fs.existsSync(siteDataPath)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let siteData;
|
|
30
|
+
try {
|
|
31
|
+
siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!siteData.content) siteData.content = {};
|
|
37
|
+
if (!siteData.content.common) siteData.content.common = {};
|
|
38
|
+
|
|
39
|
+
setDeep(siteData.content, ['common', 'whatsapp', 'copyIcon'], 'copy');
|
|
40
|
+
setDeep(siteData.content, ['common', 'whatsapp', 'icon'], 'message-circle');
|
|
41
|
+
setDeep(siteData.content, ['common', 'search', 'searchIcon'], 'search');
|
|
42
|
+
|
|
43
|
+
if (!dryRun) {
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(siteDataPath, JSON.stringify(siteData, null, 2), 'utf8');
|
|
46
|
+
log('[universal-icons] Registered common icon keys in site-data.json');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log('[universal-icons] Warning: failed to save site-data.json: ' + err.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
patchUniversalIcons,
|
|
55
|
+
};
|
|
@@ -519,10 +519,32 @@ export function findLinksToUnselectedPages(input: {
|
|
|
519
519
|
return findings;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
/** Detect links that contain the configured base path but omit its leading
|
|
523
|
+
* slash. Browsers resolve these relative to the current page and duplicate the
|
|
524
|
+
* generated-site prefix after navigation. */
|
|
525
|
+
export function findMalformedBasePathLinks(input: {
|
|
526
|
+
html: string;
|
|
527
|
+
basePath: string;
|
|
528
|
+
}) {
|
|
529
|
+
const prefix = input.basePath.replace(/^\/+|\/+$/g, '');
|
|
530
|
+
if (!prefix) return [];
|
|
531
|
+
const findings = new Set<string>();
|
|
532
|
+
for (const match of input.html.matchAll(
|
|
533
|
+
/<[a-z][^>]*\b(?:href|formaction|data-href|data-route|data-url)\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>/gi,
|
|
534
|
+
)) {
|
|
535
|
+
const value = (match[1] ?? match[2] ?? '').trim();
|
|
536
|
+
const path = value.split(/[?#]/, 1)[0].replace(/^\.\//, '');
|
|
537
|
+
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
|
538
|
+
findings.add(value);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return [...findings];
|
|
542
|
+
}
|
|
543
|
+
|
|
522
544
|
/**
|
|
523
545
|
* Next's client router applies next.config `basePath` automatically. Wrapping
|
|
524
|
-
* a router destination in a helper that also prepends the base path
|
|
525
|
-
* the generated-site prefix at runtime.
|
|
546
|
+
* a router destination in a package helper that also prepends the base path
|
|
547
|
+
* duplicates the generated-site prefix at runtime.
|
|
526
548
|
*/
|
|
527
549
|
export function findDoubleBasePathNextRouterCalls(source: string) {
|
|
528
550
|
const findings: Array<{
|