@deneb-ui/cli 2.0.88 → 2.0.90
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 +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/common/template-visual-edit-contract.ts +4 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const {
|
|
5
|
+
parseSource,
|
|
6
|
+
printSource,
|
|
7
|
+
getJsxName,
|
|
8
|
+
findJsxAttribute,
|
|
9
|
+
hasJsxAttribute,
|
|
10
|
+
b,
|
|
11
|
+
} = require('../../ast.cjs');
|
|
12
|
+
const {
|
|
13
|
+
sanitizeContradictoryMarkers,
|
|
14
|
+
healBroadContainerMarkers,
|
|
15
|
+
healHiddenPreviewMarkers,
|
|
16
|
+
healSectionOverflowHidden,
|
|
17
|
+
healDecorativeOverlays,
|
|
18
|
+
} = require('./sanitizer.cjs');
|
|
19
|
+
|
|
20
|
+
function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
|
|
21
|
+
if (!leftExpr || !rightExpr) return true;
|
|
22
|
+
|
|
23
|
+
// 1. Collect all identifiers used on the left-hand side
|
|
24
|
+
const leftIdentifiers = new Set();
|
|
25
|
+
recast.types.visit(leftExpr, {
|
|
26
|
+
visitIdentifier(idPath) {
|
|
27
|
+
leftIdentifiers.add(idPath.node.name);
|
|
28
|
+
return false;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (leftIdentifiers.size === 0) return true;
|
|
33
|
+
|
|
34
|
+
// 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
|
|
35
|
+
const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
|
|
36
|
+
for (const id of leftIdentifiers) {
|
|
37
|
+
if (stateKeywordRegex.test(id)) {
|
|
38
|
+
return true; // NEVER strip modal/state/selection guards
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Null / undefined / equality / comparison checks
|
|
43
|
+
let hasComparisonOrNullCheck = false;
|
|
44
|
+
recast.types.visit(leftExpr, {
|
|
45
|
+
visitBinaryExpression(bPath) {
|
|
46
|
+
const op = bPath.node.operator;
|
|
47
|
+
if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
|
|
48
|
+
hasComparisonOrNullCheck = true;
|
|
49
|
+
}
|
|
50
|
+
this.traverse(bPath);
|
|
51
|
+
},
|
|
52
|
+
visitUnaryExpression(uPath) {
|
|
53
|
+
if (uPath.node.operator === '!') {
|
|
54
|
+
hasComparisonOrNullCheck = true;
|
|
55
|
+
}
|
|
56
|
+
this.traverse(uPath);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (hasComparisonOrNullCheck) return true;
|
|
60
|
+
|
|
61
|
+
// 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
|
|
62
|
+
let rightReferencesLeftId = false;
|
|
63
|
+
recast.types.visit(rightExpr, {
|
|
64
|
+
visitIdentifier(idPath) {
|
|
65
|
+
if (leftIdentifiers.has(idPath.node.name)) {
|
|
66
|
+
rightReferencesLeftId = true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
this.traverse(idPath);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (rightReferencesLeftId) return true;
|
|
73
|
+
|
|
74
|
+
// 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
|
|
75
|
+
let curr = pathNode;
|
|
76
|
+
while (curr && curr.parent) {
|
|
77
|
+
const parentNode = curr.parent.node;
|
|
78
|
+
if (parentNode && parentNode.type === 'JSXElement') {
|
|
79
|
+
const parentTag = getJsxName(parentNode);
|
|
80
|
+
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(parentTag)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
curr = curr.parent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let isModalOrOverlay = false;
|
|
88
|
+
recast.types.visit(rightExpr, {
|
|
89
|
+
visitJSXElement(elPath) {
|
|
90
|
+
const node = elPath.node;
|
|
91
|
+
const tag = getJsxName(node);
|
|
92
|
+
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(tag)) {
|
|
93
|
+
isModalOrOverlay = true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
|
|
97
|
+
if (classAttr && classAttr.value) {
|
|
98
|
+
const val = classAttr.value.value || '';
|
|
99
|
+
if (typeof val === 'string' && (/fixed\s+inset-0/i.test(val) || /z-(?:50|\[50\]|40)/i.test(val) || /backdrop-blur/i.test(val))) {
|
|
100
|
+
isModalOrOverlay = true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const roleAttr = findJsxAttribute(node, 'role');
|
|
105
|
+
if (roleAttr && roleAttr.value && /^(?:dialog|alertdialog)$/i.test(String(roleAttr.value.value || ''))) {
|
|
106
|
+
isModalOrOverlay = true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
this.traverse(elPath);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
if (isModalOrOverlay) return true;
|
|
113
|
+
|
|
114
|
+
// 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
|
|
115
|
+
const leftCode = recast.print(leftExpr).code;
|
|
116
|
+
const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
|
|
117
|
+
if (!isDirectContentCheck) return true;
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isNullish(node) {
|
|
123
|
+
if (!node) return true;
|
|
124
|
+
if (node.type === 'NullLiteral') return true;
|
|
125
|
+
if (node.type === 'Literal' && (node.value === null || node.value === false || node.value === undefined)) return true;
|
|
126
|
+
if (node.type === 'BooleanLiteral' && node.value === false) return true;
|
|
127
|
+
if (node.type === 'Identifier' && (node.name === 'undefined' || node.name === 'null')) return true;
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function healEmptyStateConditionals(ast) {
|
|
132
|
+
let healed = 0;
|
|
133
|
+
|
|
134
|
+
function jsxHasPreviewMarker(node) {
|
|
135
|
+
if (!node) return false;
|
|
136
|
+
if (node.type === 'JSXElement') {
|
|
137
|
+
if (
|
|
138
|
+
hasJsxAttribute(node, 'data-preview-field-path') ||
|
|
139
|
+
hasJsxAttribute(node, 'data-preview-list-path') ||
|
|
140
|
+
hasJsxAttribute(node, 'data-preview-item-path')
|
|
141
|
+
) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
145
|
+
}
|
|
146
|
+
if (node.type === 'JSXFragment') {
|
|
147
|
+
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
148
|
+
}
|
|
149
|
+
if (node.type === 'ParenthesizedExpression') return jsxHasPreviewMarker(node.expression);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
recast.types.visit(ast, {
|
|
154
|
+
visitLogicalExpression(pathNode) {
|
|
155
|
+
const expr = pathNode.node;
|
|
156
|
+
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
157
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
158
|
+
pathNode.replace(expr.right);
|
|
159
|
+
healed++;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.traverse(pathNode);
|
|
164
|
+
},
|
|
165
|
+
visitJSXExpressionContainer(pathNode) {
|
|
166
|
+
const expr = pathNode.node.expression;
|
|
167
|
+
if (!expr) {
|
|
168
|
+
this.traverse(pathNode);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
172
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
173
|
+
pathNode.node.expression = expr.right;
|
|
174
|
+
healed++;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
expr.type === 'ConditionalExpression' &&
|
|
180
|
+
jsxHasPreviewMarker(expr.consequent) &&
|
|
181
|
+
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
182
|
+
) {
|
|
183
|
+
if (!isUnsafeToStripCondition(expr.test, expr.consequent, pathNode)) {
|
|
184
|
+
pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
|
|
185
|
+
? expr.consequent
|
|
186
|
+
: expr.alternate;
|
|
187
|
+
healed++;
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.traverse(pathNode);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
return healed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function healUnguardedModalConditionals(ast) {
|
|
198
|
+
let healed = 0;
|
|
199
|
+
const stateVariables = [];
|
|
200
|
+
recast.types.visit(ast, {
|
|
201
|
+
visitVariableDeclarator(pathNode) {
|
|
202
|
+
const node = pathNode.node;
|
|
203
|
+
if (node.id && node.id.type === 'ArrayPattern') {
|
|
204
|
+
const first = node.id.elements[0];
|
|
205
|
+
if (first && first.type === 'Identifier') {
|
|
206
|
+
stateVariables.push(first.name);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.traverse(pathNode);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (stateVariables.length === 0) return 0;
|
|
214
|
+
|
|
215
|
+
recast.types.visit(ast, {
|
|
216
|
+
visitJSXElement(pathNode) {
|
|
217
|
+
const tag = getJsxName(pathNode.node);
|
|
218
|
+
if (tag === 'AnimatePresence') {
|
|
219
|
+
for (const child of pathNode.node.children || []) {
|
|
220
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
221
|
+
let inner = child.expression;
|
|
222
|
+
if (inner && inner.type === 'ParenthesizedExpression') {
|
|
223
|
+
inner = inner.expression;
|
|
224
|
+
}
|
|
225
|
+
if (inner && (inner.type === 'JSXElement' || inner.type === 'JSXFragment')) {
|
|
226
|
+
const innerIds = new Set();
|
|
227
|
+
recast.types.visit(inner, {
|
|
228
|
+
visitIdentifier(idPath) {
|
|
229
|
+
innerIds.add(idPath.node.name);
|
|
230
|
+
return false;
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const matchingVar = stateVariables.find((sv) => innerIds.has(sv));
|
|
235
|
+
if (matchingVar) {
|
|
236
|
+
let guardExpr;
|
|
237
|
+
if (/index/i.test(matchingVar)) {
|
|
238
|
+
guardExpr = b.binaryExpression('!==', b.identifier(matchingVar), b.nullLiteral());
|
|
239
|
+
} else {
|
|
240
|
+
guardExpr = b.identifier(matchingVar);
|
|
241
|
+
}
|
|
242
|
+
child.expression = b.logicalExpression('&&', guardExpr, inner);
|
|
243
|
+
healed++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.traverse(pathNode);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return healed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function healUnguardedModalConditionalsInSource(code, filePath = 'file.tsx') {
|
|
257
|
+
if (!code.includes('AnimatePresence') && !code.includes('fixed inset-0')) return { code, updated: false };
|
|
258
|
+
try {
|
|
259
|
+
const ast = parseSource(code, filePath);
|
|
260
|
+
const healed = healUnguardedModalConditionals(ast);
|
|
261
|
+
if (healed > 0) {
|
|
262
|
+
return { code: printSource(ast, code), updated: true, count: healed };
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
return { code, updated: false };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
269
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path') && !code.includes('overflow-hidden') && !code.includes('hidden')) {
|
|
270
|
+
return { code, updated: false };
|
|
271
|
+
}
|
|
272
|
+
let ast;
|
|
273
|
+
try {
|
|
274
|
+
ast = parseSource(code, relativeFile);
|
|
275
|
+
} catch {
|
|
276
|
+
return { code, updated: false };
|
|
277
|
+
}
|
|
278
|
+
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
279
|
+
const healedBroad = healBroadContainerMarkers(ast);
|
|
280
|
+
const healedEmpty = healEmptyStateConditionals(ast);
|
|
281
|
+
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
282
|
+
const healedOverflow = healSectionOverflowHidden(ast);
|
|
283
|
+
const healedOverlay = healDecorativeOverlays(ast);
|
|
284
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0 && healedOverflow === 0 && healedOverlay === 0) return { code, updated: false };
|
|
285
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden + healedOverflow + healedOverlay };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
isUnsafeToStripCondition,
|
|
290
|
+
isNullish,
|
|
291
|
+
healEmptyStateConditionals,
|
|
292
|
+
healUnguardedModalConditionals,
|
|
293
|
+
healUnguardedModalConditionalsInSource,
|
|
294
|
+
sanitizeContradictoryMarkersInSource,
|
|
295
|
+
};
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const {
|
|
5
|
+
parseSource,
|
|
6
|
+
printSource,
|
|
7
|
+
getJsxName,
|
|
8
|
+
findJsxAttribute,
|
|
9
|
+
hasJsxAttribute,
|
|
10
|
+
ensureImport,
|
|
11
|
+
b,
|
|
12
|
+
} = require('../../ast.cjs');
|
|
13
|
+
const { BROAD_CONTENT_CONTAINERS } = require('../../fivora-contract.cjs');
|
|
14
|
+
const { injectSiteDataHook } = require('../runtime/provider.cjs');
|
|
15
|
+
|
|
16
|
+
function sanitizeContradictoryMarkers(ast) {
|
|
17
|
+
let cleaned = 0;
|
|
18
|
+
recast.types.visit(ast, {
|
|
19
|
+
visitJSXElement(pathNode) {
|
|
20
|
+
const opening = pathNode.node.openingElement;
|
|
21
|
+
if (!opening || !Array.isArray(opening.attributes)) {
|
|
22
|
+
this.traverse(pathNode);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const attrs = opening.attributes;
|
|
26
|
+
const hasStatic = attrs.some(
|
|
27
|
+
(a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
|
|
28
|
+
);
|
|
29
|
+
if (!hasStatic) {
|
|
30
|
+
this.traverse(pathNode);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tag = getJsxName(pathNode.node);
|
|
35
|
+
const lower = String(tag || '').toLowerCase();
|
|
36
|
+
if (BROAD_CONTENT_CONTAINERS.has(tag) || BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
37
|
+
opening.attributes = attrs.filter(
|
|
38
|
+
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
39
|
+
);
|
|
40
|
+
cleaned++;
|
|
41
|
+
this.traverse(pathNode);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const hasDirectEditable = attrs.some(
|
|
46
|
+
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
47
|
+
a.name.name === 'data-preview-field-path' ||
|
|
48
|
+
a.name.name === 'data-preview-list-path' ||
|
|
49
|
+
a.name.name === 'data-preview-item-path'
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let hasNestedEditable = false;
|
|
54
|
+
if (!hasDirectEditable) {
|
|
55
|
+
recast.types.visit(pathNode.node, {
|
|
56
|
+
visitJSXElement(inner) {
|
|
57
|
+
if (inner.node === pathNode.node) {
|
|
58
|
+
this.traverse(inner);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const innerAttrs = inner.node.openingElement?.attributes || [];
|
|
62
|
+
if (innerAttrs.some(
|
|
63
|
+
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
64
|
+
a.name.name === 'data-preview-field-path' ||
|
|
65
|
+
a.name.name === 'data-preview-list-path' ||
|
|
66
|
+
a.name.name === 'data-preview-item-path'
|
|
67
|
+
)
|
|
68
|
+
)) {
|
|
69
|
+
hasNestedEditable = true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
this.traverse(inner);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (hasDirectEditable || hasNestedEditable) {
|
|
78
|
+
opening.attributes = attrs.filter(
|
|
79
|
+
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
80
|
+
);
|
|
81
|
+
cleaned++;
|
|
82
|
+
}
|
|
83
|
+
this.traverse(pathNode);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return cleaned;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function healBroadContainerMarkers(ast) {
|
|
90
|
+
let healed = 0;
|
|
91
|
+
recast.types.visit(ast, {
|
|
92
|
+
visitJSXElement(pathNode) {
|
|
93
|
+
const node = pathNode.node;
|
|
94
|
+
const tag = getJsxName(node);
|
|
95
|
+
const lower = String(tag || '').toLowerCase();
|
|
96
|
+
if (!BROAD_CONTENT_CONTAINERS.has(tag) && !BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
97
|
+
this.traverse(pathNode);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const fieldAttr = findJsxAttribute(node, 'data-preview-field-path');
|
|
101
|
+
if (!fieldAttr) {
|
|
102
|
+
this.traverse(pathNode);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fieldValue = fieldAttr.value;
|
|
107
|
+
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
108
|
+
(attr) =>
|
|
109
|
+
!(
|
|
110
|
+
attr.type === 'JSXAttribute' &&
|
|
111
|
+
attr.name &&
|
|
112
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
113
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
114
|
+
attr.name.name === 'data-preview-style-type')
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const leaf = findFirstLeafChild(node);
|
|
119
|
+
if (leaf && !hasJsxAttribute(leaf, 'data-preview-field-path')) {
|
|
120
|
+
leaf.openingElement.attributes.push(
|
|
121
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)
|
|
122
|
+
);
|
|
123
|
+
healed++;
|
|
124
|
+
} else {
|
|
125
|
+
const span = b.jsxElement(
|
|
126
|
+
b.jsxOpeningElement(
|
|
127
|
+
b.jsxIdentifier('span'),
|
|
128
|
+
[b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)],
|
|
129
|
+
false
|
|
130
|
+
),
|
|
131
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
132
|
+
node.children || [],
|
|
133
|
+
false
|
|
134
|
+
);
|
|
135
|
+
node.children = [span];
|
|
136
|
+
healed++;
|
|
137
|
+
}
|
|
138
|
+
this.traverse(pathNode);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return healed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findFirstLeafChild(node) {
|
|
145
|
+
const leafTags = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'img', 'button', 'a', 'strong', 'em', 'small']);
|
|
146
|
+
for (const child of node.children || []) {
|
|
147
|
+
if (child && child.type === 'JSXElement') {
|
|
148
|
+
const name = getJsxName(child);
|
|
149
|
+
if (leafTags.has(name) || leafTags.has(String(name).toLowerCase())) return child;
|
|
150
|
+
const nested = findFirstLeafChild(child);
|
|
151
|
+
if (nested) return nested;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function healHiddenPreviewMarkers(ast) {
|
|
158
|
+
let healed = 0;
|
|
159
|
+
recast.types.visit(ast, {
|
|
160
|
+
visitJSXOpeningElement(pathNode) {
|
|
161
|
+
const attrs = pathNode.node.attributes || [];
|
|
162
|
+
const hasEditable = attrs.some(
|
|
163
|
+
(attr) =>
|
|
164
|
+
attr.type === 'JSXAttribute' &&
|
|
165
|
+
attr.name &&
|
|
166
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
167
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
168
|
+
attr.name.name === 'data-preview-item-path')
|
|
169
|
+
);
|
|
170
|
+
if (!hasEditable) {
|
|
171
|
+
this.traverse(pathNode);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const classAttr = attrs.find((attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'className' || attr.name.name === 'class'));
|
|
175
|
+
let classText = '';
|
|
176
|
+
if (classAttr && classAttr.value) {
|
|
177
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') classText = String(classAttr.value.value || '');
|
|
178
|
+
else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression) {
|
|
179
|
+
const expr = classAttr.value.expression;
|
|
180
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal') classText = String(expr.value || '');
|
|
181
|
+
if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const isHidden =
|
|
185
|
+
attrs.some((attr) => attr.type === 'JSXAttribute' && attr.name && (
|
|
186
|
+
attr.name.name === 'hidden' ||
|
|
187
|
+
(attr.name.name === 'aria-hidden' && /true/.test(recast.print(attr).code))
|
|
188
|
+
)) ||
|
|
189
|
+
/(?<![\w-])hidden(?![a-zA-Z0-9_-])/.test(classText) ||
|
|
190
|
+
attrs.some((attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'style' && /(?:display\s*:\s*['"]none['"]|visibility\s*:\s*['"]hidden['"])/.test(recast.print(attr).code));
|
|
191
|
+
|
|
192
|
+
if (isHidden) {
|
|
193
|
+
pathNode.node.attributes = attrs.filter(
|
|
194
|
+
(attr) =>
|
|
195
|
+
!(
|
|
196
|
+
attr.type === 'JSXAttribute' &&
|
|
197
|
+
attr.name &&
|
|
198
|
+
(attr.name.name === 'data-preview-field-path' ||
|
|
199
|
+
attr.name.name === 'data-preview-list-path' ||
|
|
200
|
+
attr.name.name === 'data-preview-item-path' ||
|
|
201
|
+
attr.name.name === 'data-preview-style-target' ||
|
|
202
|
+
attr.name.name === 'data-preview-style-type')
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
healed++;
|
|
206
|
+
}
|
|
207
|
+
this.traverse(pathNode);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return healed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function healLegacyProductDetailLinks(ast) {
|
|
214
|
+
let healed = 0;
|
|
215
|
+
let needsImport = false;
|
|
216
|
+
|
|
217
|
+
recast.types.visit(ast, {
|
|
218
|
+
visitJSXAttribute(pathNode) {
|
|
219
|
+
const attr = pathNode.node;
|
|
220
|
+
if (attr.name && attr.name.name === 'href' && attr.value) {
|
|
221
|
+
if (attr.value.type === 'JSXExpressionContainer' && attr.value.expression) {
|
|
222
|
+
const expr = attr.value.expression;
|
|
223
|
+
if (expr.type === 'TemplateLiteral') {
|
|
224
|
+
const quasis = expr.quasis || [];
|
|
225
|
+
if (quasis.length >= 1 && typeof quasis[0].value.raw === 'string' && quasis[0].value.raw.startsWith('/products/')) {
|
|
226
|
+
const arg = expr.expressions && expr.expressions[0];
|
|
227
|
+
if (arg) {
|
|
228
|
+
attr.value.expression = b.callExpression(
|
|
229
|
+
b.identifier('platformProductDetailHref'),
|
|
230
|
+
[arg]
|
|
231
|
+
);
|
|
232
|
+
healed++;
|
|
233
|
+
needsImport = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else if (
|
|
237
|
+
expr.type === 'BinaryExpression' &&
|
|
238
|
+
expr.operator === '+' &&
|
|
239
|
+
expr.left &&
|
|
240
|
+
(expr.left.value === '/products/' || expr.left.value === '/products')
|
|
241
|
+
) {
|
|
242
|
+
attr.value.expression = b.callExpression(
|
|
243
|
+
b.identifier('platformProductDetailHref'),
|
|
244
|
+
[expr.right]
|
|
245
|
+
);
|
|
246
|
+
healed++;
|
|
247
|
+
needsImport = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.traverse(pathNode);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (needsImport) {
|
|
256
|
+
ensureImport(ast, '@deneb-ui/ui', ['platformProductDetailHref']);
|
|
257
|
+
}
|
|
258
|
+
return healed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function healSectionOverflowHidden(ast) {
|
|
262
|
+
let healed = 0;
|
|
263
|
+
recast.types.visit(ast, {
|
|
264
|
+
visitJSXOpeningElement(pathNode) {
|
|
265
|
+
const node = pathNode.node;
|
|
266
|
+
const tag = getJsxName(node);
|
|
267
|
+
const lower = String(tag || '').toLowerCase();
|
|
268
|
+
const isContainer = lower === 'section' || lower === 'div' || lower === 'main' || lower === 'article';
|
|
269
|
+
if (!isContainer) {
|
|
270
|
+
this.traverse(pathNode);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const classAttr = (node.attributes || []).find(
|
|
274
|
+
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
275
|
+
);
|
|
276
|
+
if (!classAttr || !classAttr.value) {
|
|
277
|
+
this.traverse(pathNode);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let modified = false;
|
|
281
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
282
|
+
const val = String(classAttr.value.value || '');
|
|
283
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
284
|
+
classAttr.value.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
285
|
+
modified = true;
|
|
286
|
+
}
|
|
287
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
288
|
+
const expr = classAttr.value.expression;
|
|
289
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
290
|
+
const val = String(expr.value || '');
|
|
291
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
292
|
+
expr.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
293
|
+
modified = true;
|
|
294
|
+
}
|
|
295
|
+
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
296
|
+
for (const quasi of expr.quasis || []) {
|
|
297
|
+
if (quasi.value && /\boverflow-hidden\b/.test(quasi.value.raw || '')) {
|
|
298
|
+
quasi.value.raw = (quasi.value.raw || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
299
|
+
if (quasi.value.cooked) {
|
|
300
|
+
quasi.value.cooked = (quasi.value.cooked || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
301
|
+
}
|
|
302
|
+
modified = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (modified) healed++;
|
|
308
|
+
this.traverse(pathNode);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
return healed;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function healDecorativeOverlays(ast) {
|
|
315
|
+
let healed = 0;
|
|
316
|
+
recast.types.visit(ast, {
|
|
317
|
+
visitJSXElement(pathNode) {
|
|
318
|
+
const node = pathNode.node;
|
|
319
|
+
const tag = getJsxName(node);
|
|
320
|
+
const lower = String(tag || '').toLowerCase();
|
|
321
|
+
if (lower !== 'div' && lower !== 'span') {
|
|
322
|
+
this.traverse(pathNode);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const hasRealChildren = (node.children || []).some(
|
|
326
|
+
(c) => c.type === 'JSXElement' || (c.type === 'JSXText' && c.value.trim().length > 0)
|
|
327
|
+
);
|
|
328
|
+
if (hasRealChildren) {
|
|
329
|
+
this.traverse(pathNode);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const classAttr = (node.openingElement.attributes || []).find(
|
|
333
|
+
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
334
|
+
);
|
|
335
|
+
if (!classAttr || !classAttr.value) {
|
|
336
|
+
this.traverse(pathNode);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let modified = false;
|
|
340
|
+
const overlayPattern = /\b(?:bg-gradient-|bg-black\/|bg-white\/|bg-slate-\d+\/|backdrop-blur)/;
|
|
341
|
+
const isAbsolute = /\babsolute\b/;
|
|
342
|
+
const hasPointerEvents = /\bpointer-events-(?:none|auto)\b/;
|
|
343
|
+
|
|
344
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
345
|
+
const val = String(classAttr.value.value || '');
|
|
346
|
+
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
347
|
+
classAttr.value.value = `${val} pointer-events-none`;
|
|
348
|
+
modified = true;
|
|
349
|
+
}
|
|
350
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
351
|
+
const expr = classAttr.value.expression;
|
|
352
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
353
|
+
const val = String(expr.value || '');
|
|
354
|
+
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
355
|
+
expr.value = `${val} pointer-events-none`;
|
|
356
|
+
modified = true;
|
|
357
|
+
}
|
|
358
|
+
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
359
|
+
const fullText = (expr.quasis || []).map((q) => q.value?.raw || '').join(' ');
|
|
360
|
+
if (isAbsolute.test(fullText) && overlayPattern.test(fullText) && !hasPointerEvents.test(fullText)) {
|
|
361
|
+
const lastQuasi = expr.quasis[expr.quasis.length - 1];
|
|
362
|
+
if (lastQuasi && lastQuasi.value) {
|
|
363
|
+
lastQuasi.value.raw = `${lastQuasi.value.raw} pointer-events-none`;
|
|
364
|
+
if (lastQuasi.value.cooked) {
|
|
365
|
+
lastQuasi.value.cooked = `${lastQuasi.value.cooked} pointer-events-none`;
|
|
366
|
+
}
|
|
367
|
+
modified = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (modified) healed++;
|
|
373
|
+
this.traverse(pathNode);
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
return healed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
380
|
+
if (!code.includes('siteData')) return code;
|
|
381
|
+
try {
|
|
382
|
+
const ast = parseSource(code, filePath);
|
|
383
|
+
const injected = injectSiteDataHook(ast);
|
|
384
|
+
if (injected) {
|
|
385
|
+
ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
|
|
386
|
+
return printSource(ast, code);
|
|
387
|
+
}
|
|
388
|
+
} catch {}
|
|
389
|
+
return code;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
sanitizeContradictoryMarkers,
|
|
394
|
+
healBroadContainerMarkers,
|
|
395
|
+
findFirstLeafChild,
|
|
396
|
+
healHiddenPreviewMarkers,
|
|
397
|
+
healLegacyProductDetailLinks,
|
|
398
|
+
healSectionOverflowHidden,
|
|
399
|
+
healDecorativeOverlays,
|
|
400
|
+
healMissingSiteDataHooks,
|
|
401
|
+
};
|