@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
package/src/arc/transformer.cjs
CHANGED
|
@@ -1,2751 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const recast = require('recast');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const {
|
|
7
|
-
parseSource,
|
|
8
|
-
printSource,
|
|
9
|
-
locKey,
|
|
10
|
-
getJsxName,
|
|
11
|
-
findJsxAttribute,
|
|
12
|
-
hasJsxAttribute,
|
|
13
|
-
hasDirective,
|
|
14
|
-
ensureImport,
|
|
15
|
-
ensureDefaultImport,
|
|
16
|
-
sanitizeDuplicateBindings,
|
|
17
|
-
siteDataBinding,
|
|
18
|
-
siteDataListBinding,
|
|
19
|
-
jsxPreviewAttr,
|
|
20
|
-
jsxTemplatePathAttr,
|
|
21
|
-
wrapTextInEditableSpan,
|
|
22
|
-
unwrapExpr,
|
|
23
|
-
extractItemMemberName,
|
|
24
|
-
jsxStyleAttrs,
|
|
25
|
-
ensureStyleAttrs,
|
|
26
|
-
b,
|
|
27
|
-
} = require('./ast.cjs');
|
|
28
|
-
const { toPosix } = require('./fs-utils.cjs');
|
|
29
|
-
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
30
|
-
const {
|
|
31
|
-
hasMetadataExport,
|
|
32
|
-
hasClientDirective,
|
|
33
|
-
needsClientDirective,
|
|
34
|
-
canSafelyInjectClientDirective,
|
|
35
|
-
ensureClientDirective,
|
|
36
|
-
} = require('./rsc-boundary.cjs');
|
|
37
|
-
|
|
38
|
-
function findElementByLoc(ast, loc, tagName = null) {
|
|
39
|
-
if (!loc && !tagName) return null;
|
|
40
|
-
const parts = loc ? loc.split(':') : [];
|
|
41
|
-
const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
|
|
42
|
-
let found = null;
|
|
43
|
-
recast.types.visit(ast, {
|
|
44
|
-
visitJSXElement(pathNode) {
|
|
45
|
-
const k = locKey(pathNode.node);
|
|
46
|
-
const name = getJsxName(pathNode.node);
|
|
47
|
-
if (loc && (k === targetLoc || k === loc || (parts.length >= 2 && k && k.startsWith(`${parts[0]}:${parts[1]}`)))) {
|
|
48
|
-
found = pathNode;
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
if (!loc && tagName && (name === tagName || name.endsWith(`.${tagName}`))) {
|
|
52
|
-
found = pathNode;
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
this.traverse(pathNode);
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
return found;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function replaceAttrValue(node, attrName, expression) {
|
|
62
|
-
const attr = findJsxAttribute(node, attrName);
|
|
63
|
-
if (!attr) {
|
|
64
|
-
node.openingElement.attributes.push(
|
|
65
|
-
b.jsxAttribute(b.jsxIdentifier(attrName), b.jsxExpressionContainer(expression))
|
|
66
|
-
);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
attr.value = b.jsxExpressionContainer(expression);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function stripStaticAttribute(node) {
|
|
73
|
-
if (node && node.openingElement && Array.isArray(node.openingElement.attributes)) {
|
|
74
|
-
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
75
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function ensurePreviewPath(node, fieldPath) {
|
|
81
|
-
stripStaticAttribute(node);
|
|
82
|
-
if (hasJsxAttribute(node, 'data-preview-field-path')) return;
|
|
83
|
-
node.openingElement.attributes.push(jsxPreviewAttr(fieldPath));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function replaceTextChildren(node, fieldPath, fallback, fieldType) {
|
|
87
|
-
const nextChildren = [];
|
|
88
|
-
let replaced = false;
|
|
89
|
-
for (const child of node.children || []) {
|
|
90
|
-
if (!child) continue;
|
|
91
|
-
if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
|
|
92
|
-
nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
|
|
93
|
-
replaced = true;
|
|
94
|
-
} else if (
|
|
95
|
-
child.type === 'JSXExpressionContainer' &&
|
|
96
|
-
child.expression &&
|
|
97
|
-
(child.expression.type === 'StringLiteral' ||
|
|
98
|
-
child.expression.type === 'Literal' ||
|
|
99
|
-
child.expression.type === 'Identifier')
|
|
100
|
-
) {
|
|
101
|
-
nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
|
|
102
|
-
replaced = true;
|
|
103
|
-
} else {
|
|
104
|
-
nextChildren.push(child);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!replaced) {
|
|
108
|
-
nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
|
|
109
|
-
}
|
|
110
|
-
node.children = nextChildren;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function splitActionChildren(node, labelField, labelFallback) {
|
|
114
|
-
const nextChildren = [];
|
|
115
|
-
let wrapped = false;
|
|
116
|
-
for (const child of node.children || []) {
|
|
117
|
-
if (!child) continue;
|
|
118
|
-
if (child.type === 'JSXElement' && getJsxName(child) === 'span' && !hasJsxAttribute(child, 'data-preview-field-path')) {
|
|
119
|
-
ensurePreviewPath(child, labelField);
|
|
120
|
-
ensureStyleAttrs(child, labelField, 'text');
|
|
121
|
-
replaceTextChildren(child, labelField, labelFallback, 'text');
|
|
122
|
-
nextChildren.push(child);
|
|
123
|
-
wrapped = true;
|
|
124
|
-
} else if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
|
|
125
|
-
const leading = child.value.match(/^\s*/)?.[0] || '';
|
|
126
|
-
const trailing = child.value.match(/\s*$/)?.[0] || '';
|
|
127
|
-
if (leading) nextChildren.push(b.jsxText(leading));
|
|
128
|
-
nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
|
|
129
|
-
if (trailing && trailing !== leading) nextChildren.push(b.jsxText(trailing));
|
|
130
|
-
wrapped = true;
|
|
131
|
-
} else if (
|
|
132
|
-
child.type === 'JSXExpressionContainer' &&
|
|
133
|
-
child.expression &&
|
|
134
|
-
(child.expression.type === 'StringLiteral' || child.expression.type === 'Literal')
|
|
135
|
-
) {
|
|
136
|
-
nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
|
|
137
|
-
wrapped = true;
|
|
138
|
-
} else {
|
|
139
|
-
nextChildren.push(child);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (!wrapped) {
|
|
143
|
-
nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
|
|
144
|
-
}
|
|
145
|
-
node.children = nextChildren;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
|
|
149
|
-
const urlParts = urlField.split('.');
|
|
150
|
-
const hiddenUrl = b.jsxElement(
|
|
151
|
-
b.jsxOpeningElement(
|
|
152
|
-
b.jsxIdentifier('span'),
|
|
153
|
-
[
|
|
154
|
-
b.jsxAttribute(b.jsxIdentifier('hidden')),
|
|
155
|
-
b.jsxAttribute(b.jsxIdentifier('aria-hidden'), b.stringLiteral('true')),
|
|
156
|
-
jsxPreviewAttr(urlField),
|
|
157
|
-
],
|
|
158
|
-
false
|
|
159
|
-
),
|
|
160
|
-
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
161
|
-
[b.jsxExpressionContainer(siteDataBinding(urlParts, fallback, 'url'))],
|
|
162
|
-
false
|
|
163
|
-
);
|
|
164
|
-
if (pathNode.parent && Array.isArray(pathNode.parent.node?.children)) {
|
|
165
|
-
pathNode.insertAfter(hiddenUrl);
|
|
166
|
-
} else if (pathNode.node && Array.isArray(pathNode.node.children)) {
|
|
167
|
-
pathNode.node.children = [hiddenUrl, ...(pathNode.node.children || [])];
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function applyTransformToElement(pathNode, transform) {
|
|
172
|
-
const node = pathNode.node;
|
|
173
|
-
if (transform.operation === 'form-submit-action') {
|
|
174
|
-
const tagName = getJsxName(node);
|
|
175
|
-
if (!hasJsxAttribute(node, 'type') && tagName === 'button') {
|
|
176
|
-
node.openingElement.attributes.push(
|
|
177
|
-
b.jsxAttribute(b.jsxIdentifier('type'), b.stringLiteral('submit'))
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (transform.labelField) {
|
|
181
|
-
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
182
|
-
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
183
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
187
|
-
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
188
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
192
|
-
} else if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
193
|
-
node.openingElement.attributes.push(
|
|
194
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-button'))
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (transform.operation === 'split-action-contract') {
|
|
200
|
-
const tagName = getJsxName(node);
|
|
201
|
-
if (tagName === 'button') {
|
|
202
|
-
node.openingElement.name = b.jsxIdentifier('a');
|
|
203
|
-
if (node.closingElement) {
|
|
204
|
-
node.closingElement.name = b.jsxIdentifier('a');
|
|
205
|
-
}
|
|
206
|
-
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
207
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'type')
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const urlParts = transform.urlField.split('.');
|
|
212
|
-
replaceAttrValue(node, 'href', siteDataBinding(urlParts, transform.fallback, 'url'));
|
|
213
|
-
|
|
214
|
-
const isExternal =
|
|
215
|
-
transform.extra?.external ||
|
|
216
|
-
['whatsapp', 'directions', 'location'].includes(transform.extra?.action) ||
|
|
217
|
-
/^https?:\/\//i.test(String(transform.fallback || ''));
|
|
218
|
-
|
|
219
|
-
if (isExternal && (tagName === 'button' || tagName === 'a')) {
|
|
220
|
-
if (!hasJsxAttribute(node, 'target')) {
|
|
221
|
-
node.openingElement.attributes.push(
|
|
222
|
-
b.jsxAttribute(b.jsxIdentifier('target'), b.stringLiteral('_blank'))
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
if (!hasJsxAttribute(node, 'rel')) {
|
|
226
|
-
node.openingElement.attributes.push(
|
|
227
|
-
b.jsxAttribute(b.jsxIdentifier('rel'), b.stringLiteral('noopener noreferrer'))
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Fivora Strict Action/Label Contract:
|
|
233
|
-
// Bind the URL action marker directly to the outer <a> element (never hidden)
|
|
234
|
-
replaceAttrValue(node, 'data-preview-field-path', b.stringLiteral(transform.urlField));
|
|
235
|
-
// Remove data-preview-static if present to prevent static/field collisions
|
|
236
|
-
if (hasJsxAttribute(node, 'data-preview-static')) {
|
|
237
|
-
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
238
|
-
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
// Bind the label to the inner text child
|
|
242
|
-
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (transform.operation === 'extract-url') {
|
|
246
|
-
replaceAttrValue(node, 'href', siteDataBinding(transform.field.split('.'), transform.fallback, 'url'));
|
|
247
|
-
ensurePreviewPath(node, transform.field);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (transform.operation === 'extract-image' || transform.operation === 'extract-picture') {
|
|
251
|
-
const tagName = getJsxName(node);
|
|
252
|
-
if (tagName === 'picture') {
|
|
253
|
-
const children = node.children || [];
|
|
254
|
-
const imgChild = children.find((c) => c && c.type === 'JSXElement' && (getJsxName(c) === 'img' || getJsxName(c) === 'Image'));
|
|
255
|
-
if (imgChild) {
|
|
256
|
-
replaceAttrValue(imgChild, 'src', siteDataBinding(transform.field.split('.'), transform.fallback, 'image'));
|
|
257
|
-
ensurePreviewPath(imgChild, transform.field);
|
|
258
|
-
}
|
|
259
|
-
ensurePreviewPath(node, transform.field);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
replaceAttrValue(node, 'src', siteDataBinding(transform.field.split('.'), transform.fallback, 'image'));
|
|
263
|
-
ensurePreviewPath(node, transform.field);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (transform.operation === 'extract-alt') {
|
|
267
|
-
replaceAttrValue(node, 'alt', siteDataBinding(transform.field.split('.'), transform.fallback, 'text'));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (transform.operation === 'extract-placeholder') {
|
|
271
|
-
replaceAttrValue(node, 'placeholder', siteDataBinding(transform.field.split('.'), transform.fallback, 'text'));
|
|
272
|
-
ensurePreviewPath(node, transform.field);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (transform.operation === 'extract-prop') {
|
|
276
|
-
const propName = transform.propName || 'title';
|
|
277
|
-
replaceAttrValue(node, propName, siteDataBinding(transform.field.split('.'), transform.fallback, transform.fieldType || 'text'));
|
|
278
|
-
if (!hasJsxAttribute(node, 'data-preview-field-path') && !hasJsxAttribute(node, 'data-preview-list-path')) {
|
|
279
|
-
ensurePreviewPath(node, transform.field);
|
|
280
|
-
}
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (transform.operation === 'bind-button-with-icon') {
|
|
284
|
-
ensurePreviewPath(node, transform.field);
|
|
285
|
-
ensureStyleAttrs(node, transform.field, 'button');
|
|
286
|
-
for (let i = 0; i < (node.children || []).length; i++) {
|
|
287
|
-
const child = node.children[i];
|
|
288
|
-
if (child.type === 'JSXText' && child.value.trim().length > 0) {
|
|
289
|
-
node.children[i] = b.jsxExpressionContainer(
|
|
290
|
-
siteDataBinding(transform.field.split('.'), child.value.trim(), 'text')
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (transform.operation === 'bind-highlighted-heading') {
|
|
297
|
-
ensurePreviewPath(node, transform.field);
|
|
298
|
-
ensureStyleAttrs(node, transform.field, 'text');
|
|
299
|
-
for (let i = 0; i < (node.children || []).length; i++) {
|
|
300
|
-
const child = node.children[i];
|
|
301
|
-
if (child.type === 'JSXText' && child.value.trim().length > 0) {
|
|
302
|
-
node.children[i] = b.jsxExpressionContainer(
|
|
303
|
-
siteDataBinding(transform.field.split('.'), child.value.trim(), 'text')
|
|
304
|
-
);
|
|
305
|
-
} else if (child.type === 'JSXElement') {
|
|
306
|
-
const highlightField = `${transform.field}Highlight`;
|
|
307
|
-
ensurePreviewPath(child, highlightField);
|
|
308
|
-
for (let j = 0; j < (child.children || []).length; j++) {
|
|
309
|
-
if (child.children[j].type === 'JSXText' && child.children[j].value.trim().length > 0) {
|
|
310
|
-
child.children[j] = b.jsxExpressionContainer(
|
|
311
|
-
siteDataBinding(highlightField.split('.'), child.children[j].value.trim(), 'text')
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (transform.operation === 'prop-flow-callsite') {
|
|
320
|
-
if (transform.propTransforms) {
|
|
321
|
-
for (const [propName, propInfo] of Object.entries(transform.propTransforms)) {
|
|
322
|
-
replaceAttrValue(
|
|
323
|
-
node,
|
|
324
|
-
propName,
|
|
325
|
-
siteDataBinding(propInfo.field.split('.'), propInfo.fallback, propInfo.type || 'text')
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (transform.previewPath && !hasJsxAttribute(node, 'previewPath')) {
|
|
330
|
-
node.openingElement.attributes.push(
|
|
331
|
-
b.jsxAttribute(b.jsxIdentifier('previewPath'), b.stringLiteral(transform.previewPath))
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (transform.operation === 'wrap-text-span') {
|
|
337
|
-
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (transform.operation === 'extract-text') {
|
|
341
|
-
const tagName = getJsxName(node);
|
|
342
|
-
if (BROAD_CONTENT_CONTAINERS.has(tagName)) {
|
|
343
|
-
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
ensurePreviewPath(node, transform.field);
|
|
347
|
-
ensureStyleAttrs(node, transform.field, inferButtonKind(transform.tag));
|
|
348
|
-
replaceTextChildren(node, transform.field, transform.fallback, transform.fieldType || 'text');
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (transform.operation === 'style-bind') {
|
|
352
|
-
if (transform.styleKind === 'grid') {
|
|
353
|
-
const container = findAncestorJsxElement(pathNode) || node;
|
|
354
|
-
ensureStyleAttrs(container, transform.stylePath, 'grid');
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (transform.styleKind === 'card') {
|
|
358
|
-
ensureStyleAttrs(node, transform.stylePath, 'card');
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
ensureStyleAttrs(node, transform.stylePath, transform.styleKind || 'text');
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (transform.operation === 'extract-tailwind-bg') {
|
|
365
|
-
const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
|
|
366
|
-
if (classAttr && classAttr.value) {
|
|
367
|
-
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
368
|
-
const cleaned = String(classAttr.value.value).replace(/\bbg-\[url\([^)]+\)\]\s*/g, '').trim();
|
|
369
|
-
classAttr.value = b.stringLiteral(cleaned);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const urlParts = transform.field.split('.');
|
|
373
|
-
const styleExpr = b.objectExpression([
|
|
374
|
-
b.property(
|
|
375
|
-
'init',
|
|
376
|
-
b.identifier('backgroundImage'),
|
|
377
|
-
b.templateLiteral(
|
|
378
|
-
[
|
|
379
|
-
b.templateElement({ raw: 'url(', cooked: 'url(' }, false),
|
|
380
|
-
b.templateElement({ raw: ')', cooked: ')' }, true),
|
|
381
|
-
],
|
|
382
|
-
[siteDataBinding(urlParts, transform.fallback || transform.bgUrl || '', 'image')]
|
|
383
|
-
)
|
|
384
|
-
),
|
|
385
|
-
]);
|
|
386
|
-
replaceAttrValue(node, 'style', styleExpr);
|
|
387
|
-
ensurePreviewPath(node, transform.field);
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function findAncestorJsxElement(pathNode) {
|
|
393
|
-
let current = pathNode.parent;
|
|
394
|
-
while (current) {
|
|
395
|
-
const candidate = current.node || current.value;
|
|
396
|
-
if (candidate?.type === 'JSXElement') return candidate;
|
|
397
|
-
current = current.parentPath || current.parent;
|
|
398
|
-
}
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function inferButtonKind(tag) {
|
|
403
|
-
if (tag === 'button' || tag === 'Button') return 'button';
|
|
404
|
-
return 'text';
|
|
405
|
-
}
|
|
406
|
-
|
|
407
3
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*/
|
|
411
|
-
function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
412
|
-
const nextChildren = [];
|
|
413
|
-
let wrapped = false;
|
|
414
|
-
|
|
415
|
-
for (const child of node.children || []) {
|
|
416
|
-
if (!child) continue;
|
|
417
|
-
if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
|
|
418
|
-
const leading = child.value.match(/^\s*/)?.[0] || '';
|
|
419
|
-
const trailing = child.value.match(/\s*$/)?.[0] || '';
|
|
420
|
-
if (leading) nextChildren.push(b.jsxText(leading));
|
|
421
|
-
nextChildren.push(wrapTextInEditableSpan(fieldPath, fallback, 'text'));
|
|
422
|
-
if (trailing) nextChildren.push(b.jsxText(trailing));
|
|
423
|
-
wrapped = true;
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) continue;
|
|
427
|
-
nextChildren.push(child);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (wrapped) node.children = nextChildren;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function buildCompositeKeyAttribute(binding, indexName) {
|
|
434
|
-
const fields = ['id', 'slug', 'title', 'name'];
|
|
435
|
-
let expr = b.memberExpression(b.identifier(binding), b.identifier(fields[0]), false);
|
|
436
|
-
for (let i = 1; i < fields.length; i++) {
|
|
437
|
-
expr = b.logicalExpression(
|
|
438
|
-
'||',
|
|
439
|
-
expr,
|
|
440
|
-
b.memberExpression(b.identifier(binding), b.identifier(fields[i]), false)
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
expr = b.logicalExpression('||', expr, b.identifier(indexName));
|
|
444
|
-
return b.jsxAttribute(b.jsxIdentifier('key'), b.jsxExpressionContainer(expr));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Converts a literal-array `.map()` render into a Fivora list contract.
|
|
4
|
+
* Deneb ARC — Adaptive Refactoring Compiler
|
|
5
|
+
* Modular Transforms Facade
|
|
449
6
|
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
7
|
+
* This module delegates all transformation passes to the modular `transforms/` pipeline:
|
|
8
|
+
* - `transforms/transform-context.cjs`: AST traversal, node lookup, and attribute helpers
|
|
9
|
+
* - `transforms/primitives/`: Primitive text, image, link, and button transforms
|
|
10
|
+
* - `transforms/components/`: Reusable component, child card, and compound content transforms
|
|
11
|
+
* - `transforms/collections/`: `.map()` list and item path threading
|
|
12
|
+
* - `transforms/assets/`: Tailwind background images and media transforms
|
|
13
|
+
* - `transforms/routing/`: Route page key inference and layout provider wrapping
|
|
14
|
+
* - `transforms/runtime/`: Provider hooks and site data context rewrite
|
|
15
|
+
* - `transforms/healing/`: Marker sanitization and empty-state/modal conditionals
|
|
16
|
+
* - `transforms/element-transform.cjs`: Element-level AST mutation dispatcher
|
|
17
|
+
* - `transforms/index.cjs`: File-level plan execution orchestrator
|
|
454
18
|
*/
|
|
455
|
-
function applyCollectionTransform(ast, transform, isClient = false, isTypeScript = false) {
|
|
456
|
-
const listPath = transform.listField;
|
|
457
|
-
const binding = transform.itemParam;
|
|
458
|
-
if (!listPath || !binding) return false;
|
|
459
|
-
|
|
460
|
-
const mapCall = findMapCall(ast, transform.loc);
|
|
461
|
-
if (!mapCall) return false;
|
|
462
|
-
|
|
463
|
-
const callback = (mapCall.node.arguments || [])[0];
|
|
464
|
-
if (!callback) return false;
|
|
465
|
-
|
|
466
|
-
// The contract needs a concrete index for each item marker.
|
|
467
|
-
let indexName = transform.indexParam;
|
|
468
|
-
if (!indexName) {
|
|
469
|
-
if (callback.params && callback.params.length >= 2 && callback.params[1]?.type === 'Identifier') {
|
|
470
|
-
indexName = callback.params[1].name;
|
|
471
|
-
} else {
|
|
472
|
-
indexName = callback.params.some((p) => p?.name === 'index') ? 'denebIndex' : 'index';
|
|
473
|
-
const indexId = b.identifier(indexName);
|
|
474
|
-
if (isTypeScript) {
|
|
475
|
-
indexId.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
476
|
-
}
|
|
477
|
-
callback.params.push(indexId);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// In TypeScript files, prevent TS7006 implicit any on callback parameters
|
|
482
|
-
if (isTypeScript && callback.params && callback.params.length > 0) {
|
|
483
|
-
const itemParam = callback.params[0];
|
|
484
|
-
if (itemParam && !itemParam.typeAnnotation) {
|
|
485
|
-
itemParam.typeAnnotation = b.tsTypeAnnotation(b.tsAnyKeyword());
|
|
486
|
-
}
|
|
487
|
-
if (callback.params.length >= 2) {
|
|
488
|
-
const idxParam = callback.params[1];
|
|
489
|
-
if (idxParam && !idxParam.typeAnnotation) {
|
|
490
|
-
idxParam.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const itemRoot = findElementByLoc(ast, transform.loc);
|
|
496
|
-
if (!itemRoot) return false;
|
|
497
|
-
|
|
498
|
-
const rootTagName = getJsxName(itemRoot.node);
|
|
499
|
-
const isCustomChild = /^[A-Z]/.test(rootTagName);
|
|
500
|
-
|
|
501
|
-
if (isCustomChild) {
|
|
502
|
-
if (!hasJsxAttribute(itemRoot.node, 'previewItemPath')) {
|
|
503
|
-
itemRoot.node.openingElement.attributes.push(
|
|
504
|
-
jsxTemplatePathAttr('previewItemPath', listPath, indexName)
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
if (!hasJsxAttribute(itemRoot.node, 'index')) {
|
|
508
|
-
itemRoot.node.openingElement.attributes.push(
|
|
509
|
-
b.jsxAttribute(b.jsxIdentifier('index'), b.jsxExpressionContainer(b.identifier(indexName)))
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (!hasJsxAttribute(itemRoot.node, 'data-preview-item-path')) {
|
|
515
|
-
itemRoot.node.openingElement.attributes.push(
|
|
516
|
-
jsxTemplatePathAttr('data-preview-item-path', listPath, indexName)
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
if (!hasJsxAttribute(itemRoot.node, 'data-preview-style-target')) {
|
|
520
|
-
itemRoot.node.openingElement.attributes.push(
|
|
521
|
-
jsxTemplatePathAttr('data-preview-style-target', listPath, indexName, '.card')
|
|
522
|
-
);
|
|
523
|
-
itemRoot.node.openingElement.attributes.push(
|
|
524
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-style-type'), b.stringLiteral('card'))
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
if (!hasJsxAttribute(itemRoot.node, 'key')) {
|
|
528
|
-
itemRoot.node.openingElement.attributes.push(
|
|
529
|
-
buildCompositeKeyAttribute(binding, indexName)
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (transform.isPrimitiveArray) {
|
|
534
|
-
markPrimitiveItemFields(callback, { listPath, binding, indexName });
|
|
535
|
-
} else {
|
|
536
|
-
markItemFields(callback, { listPath, binding, indexName });
|
|
537
|
-
}
|
|
538
|
-
if (!isCustomChild) {
|
|
539
|
-
markComponentRefItemsStatic(callback, binding);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const container = findListContainer(mapCall);
|
|
543
|
-
if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
|
|
544
|
-
container.openingElement.attributes.push(
|
|
545
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-list-path'), b.stringLiteral(listPath))
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
if (container && !hasJsxAttribute(container, 'data-preview-style-target')) {
|
|
549
|
-
container.openingElement.attributes.push(
|
|
550
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-style-target'), b.stringLiteral(`${listPath}.grid`))
|
|
551
|
-
);
|
|
552
|
-
container.openingElement.attributes.push(
|
|
553
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-style-type'), b.stringLiteral('grid'))
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef), isTypeScript);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function findMapCall(ast, loc) {
|
|
561
|
-
let found = null;
|
|
562
|
-
recast.types.visit(ast, {
|
|
563
|
-
visitCallExpression(pathNode) {
|
|
564
|
-
const callee = pathNode.node.callee;
|
|
565
|
-
if (
|
|
566
|
-
!found &&
|
|
567
|
-
callee?.type === 'MemberExpression' &&
|
|
568
|
-
!callee.computed &&
|
|
569
|
-
callee.property?.name === 'map'
|
|
570
|
-
) {
|
|
571
|
-
let hit = false;
|
|
572
|
-
recast.types.visit(pathNode.node, {
|
|
573
|
-
visitJSXElement(inner) {
|
|
574
|
-
if (locKey(inner.node) === loc) {
|
|
575
|
-
hit = true;
|
|
576
|
-
return false;
|
|
577
|
-
}
|
|
578
|
-
this.traverse(inner);
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
if (hit) {
|
|
582
|
-
found = pathNode;
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
this.traverse(pathNode);
|
|
587
|
-
},
|
|
588
|
-
});
|
|
589
|
-
return found;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/** Attaches field markers to the elements that render item properties. */
|
|
593
|
-
function markItemFields(callback, { listPath, binding, indexName }) {
|
|
594
|
-
recast.types.visit(callback, {
|
|
595
|
-
visitJSXElement(pathNode) {
|
|
596
|
-
const node = pathNode.node;
|
|
597
|
-
|
|
598
|
-
for (const attr of node.openingElement.attributes || []) {
|
|
599
|
-
if (attr.type !== 'JSXAttribute' || !attr.value) continue;
|
|
600
|
-
const attrName = attr.name?.name;
|
|
601
|
-
if (attrName !== 'src' && attrName !== 'href') continue;
|
|
602
|
-
const property = itemMemberName(attr.value.expression, binding);
|
|
603
|
-
if (!property) continue;
|
|
604
|
-
if (!hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
605
|
-
node.openingElement.attributes.push(
|
|
606
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const textChild = (node.children || []).find(
|
|
612
|
-
(child) => child.type === 'JSXExpressionContainer' && itemMemberName(child.expression, binding)
|
|
613
|
-
);
|
|
614
|
-
if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
615
|
-
const property = itemMemberName(textChild.expression, binding);
|
|
616
|
-
const tagName = getJsxName(node);
|
|
617
|
-
if (BROAD_CONTENT_CONTAINERS.has(tagName) || BROAD_CONTENT_CONTAINERS.has(String(tagName).toLowerCase())) {
|
|
618
|
-
wrapItemFieldInSpan(node, textChild, listPath, indexName, property);
|
|
619
|
-
} else {
|
|
620
|
-
node.openingElement.attributes.push(
|
|
621
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
this.traverse(pathNode);
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function wrapItemFieldInSpan(node, textChild, listPath, indexName, property) {
|
|
632
|
-
const nextChildren = [];
|
|
633
|
-
for (const child of node.children || []) {
|
|
634
|
-
if (child === textChild) {
|
|
635
|
-
nextChildren.push(
|
|
636
|
-
b.jsxElement(
|
|
637
|
-
b.jsxOpeningElement(
|
|
638
|
-
b.jsxIdentifier('span'),
|
|
639
|
-
[jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)],
|
|
640
|
-
false
|
|
641
|
-
),
|
|
642
|
-
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
643
|
-
[child],
|
|
644
|
-
false
|
|
645
|
-
)
|
|
646
|
-
);
|
|
647
|
-
} else {
|
|
648
|
-
nextChildren.push(child);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
node.children = nextChildren;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function markComponentRefItemsStatic(callback, binding) {
|
|
655
|
-
recast.types.visit(callback, {
|
|
656
|
-
visitJSXElement(pathNode) {
|
|
657
|
-
const name = pathNode.node.openingElement && pathNode.node.openingElement.name;
|
|
658
|
-
if (
|
|
659
|
-
name &&
|
|
660
|
-
name.type === 'JSXMemberExpression' &&
|
|
661
|
-
name.object &&
|
|
662
|
-
(name.object.type === 'Identifier' || name.object.type === 'JSXIdentifier') &&
|
|
663
|
-
name.object.name === binding
|
|
664
|
-
) {
|
|
665
|
-
if (!hasJsxAttribute(pathNode.node, 'data-preview-static') && !hasJsxAttribute(pathNode.node, 'data-preview-field-path')) {
|
|
666
|
-
pathNode.node.openingElement.attributes.push(
|
|
667
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('component-ref'))
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
this.traverse(pathNode);
|
|
672
|
-
},
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function itemMemberName(expr, binding) {
|
|
677
|
-
return extractItemMemberName(expr, binding);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function isDirectItemExpr(expr, binding) {
|
|
681
|
-
if (!expr) return false;
|
|
682
|
-
const unwrapped = unwrapExpr(expr);
|
|
683
|
-
return Boolean(
|
|
684
|
-
unwrapped &&
|
|
685
|
-
(unwrapped.type === 'Identifier' || unwrapped.type === 'JSXIdentifier') &&
|
|
686
|
-
unwrapped.name === binding
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function markPrimitiveItemFields(callback, { listPath, binding, indexName }) {
|
|
691
|
-
recast.types.visit(callback, {
|
|
692
|
-
visitJSXElement(pathNode) {
|
|
693
|
-
const node = pathNode.node;
|
|
694
|
-
const textChild = (node.children || []).find(
|
|
695
|
-
(child) => child.type === 'JSXExpressionContainer' && isDirectItemExpr(child.expression, binding)
|
|
696
|
-
);
|
|
697
|
-
if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
698
|
-
const tagName = getJsxName(node);
|
|
699
|
-
if (
|
|
700
|
-
node.children.length === 1 &&
|
|
701
|
-
(tagName === 'span' || tagName === 'p' || tagName === 'li') &&
|
|
702
|
-
!hasJsxAttribute(node, 'data-preview-item-path')
|
|
703
|
-
) {
|
|
704
|
-
node.openingElement.attributes.push(
|
|
705
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, '')
|
|
706
|
-
);
|
|
707
|
-
} else {
|
|
708
|
-
wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
this.traverse(pathNode);
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName) {
|
|
717
|
-
const nextChildren = [];
|
|
718
|
-
for (const child of node.children || []) {
|
|
719
|
-
if (child === textChild) {
|
|
720
|
-
nextChildren.push(
|
|
721
|
-
b.jsxElement(
|
|
722
|
-
b.jsxOpeningElement(
|
|
723
|
-
b.jsxIdentifier('span'),
|
|
724
|
-
[jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, '')],
|
|
725
|
-
false
|
|
726
|
-
),
|
|
727
|
-
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
728
|
-
[child],
|
|
729
|
-
false
|
|
730
|
-
)
|
|
731
|
-
);
|
|
732
|
-
} else {
|
|
733
|
-
nextChildren.push(child);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
node.children = nextChildren;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function instrumentChildCardComponent(ast, transform, isTypeScript = false) {
|
|
740
|
-
const componentName = transform.componentName;
|
|
741
|
-
const listPath = transform.listField || transform.listPath || '';
|
|
742
|
-
if (!componentName) return false;
|
|
743
|
-
|
|
744
|
-
let instrumented = false;
|
|
745
|
-
|
|
746
|
-
function updateTsInterface(rootAst, interfaceName) {
|
|
747
|
-
recast.types.visit(rootAst, {
|
|
748
|
-
visitTSInterfaceDeclaration(pathNode) {
|
|
749
|
-
if (pathNode.node.id?.name === interfaceName) {
|
|
750
|
-
const body = pathNode.node.body?.body || [];
|
|
751
|
-
if (!body.some((prop) => prop.key?.name === 'previewItemPath')) {
|
|
752
|
-
const sig1 = b.tsPropertySignature(b.identifier('previewItemPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
|
|
753
|
-
sig1.optional = true;
|
|
754
|
-
body.push(sig1);
|
|
755
|
-
instrumented = true;
|
|
756
|
-
}
|
|
757
|
-
if (!body.some((prop) => prop.key?.name === 'index')) {
|
|
758
|
-
const sig2 = b.tsPropertySignature(b.identifier('index'), b.tsTypeAnnotation(b.tsNumberKeyword()));
|
|
759
|
-
sig2.optional = true;
|
|
760
|
-
body.push(sig2);
|
|
761
|
-
instrumented = true;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
this.traverse(pathNode);
|
|
765
|
-
},
|
|
766
|
-
visitTSTypeAliasDeclaration(pathNode) {
|
|
767
|
-
if (pathNode.node.id?.name === interfaceName && pathNode.node.typeAnnotation?.type === 'TSTypeLiteral') {
|
|
768
|
-
const members = pathNode.node.typeAnnotation.members || [];
|
|
769
|
-
if (!members.some((m) => m.key?.name === 'previewItemPath')) {
|
|
770
|
-
const sig1 = b.tsPropertySignature(b.identifier('previewItemPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
|
|
771
|
-
sig1.optional = true;
|
|
772
|
-
members.push(sig1);
|
|
773
|
-
instrumented = true;
|
|
774
|
-
}
|
|
775
|
-
if (!members.some((m) => m.key?.name === 'index')) {
|
|
776
|
-
const sig2 = b.tsPropertySignature(b.identifier('index'), b.tsTypeAnnotation(b.tsNumberKeyword()));
|
|
777
|
-
sig2.optional = true;
|
|
778
|
-
members.push(sig2);
|
|
779
|
-
instrumented = true;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
this.traverse(pathNode);
|
|
783
|
-
},
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function instrumentFunctionProps(fnNode, isTs) {
|
|
788
|
-
let changed = false;
|
|
789
|
-
let params = fnNode.params || [];
|
|
790
|
-
if (!params.length) {
|
|
791
|
-
fnNode.params = [
|
|
792
|
-
b.objectPattern([
|
|
793
|
-
b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath')),
|
|
794
|
-
b.property('init', b.identifier('index'), b.identifier('index')),
|
|
795
|
-
]),
|
|
796
|
-
];
|
|
797
|
-
changed = true;
|
|
798
|
-
} else {
|
|
799
|
-
const firstParam = params[0];
|
|
800
|
-
if (firstParam.type === 'ObjectPattern') {
|
|
801
|
-
const hasPreviewPath = firstParam.properties.some(
|
|
802
|
-
(p) => p.key?.name === 'previewItemPath' || p.argument?.name === 'previewItemPath'
|
|
803
|
-
);
|
|
804
|
-
if (!hasPreviewPath) {
|
|
805
|
-
const prop = b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath'));
|
|
806
|
-
prop.shorthand = true;
|
|
807
|
-
firstParam.properties.push(prop);
|
|
808
|
-
changed = true;
|
|
809
|
-
}
|
|
810
|
-
const hasIndex = firstParam.properties.some(
|
|
811
|
-
(p) => p.key?.name === 'index' || p.argument?.name === 'index'
|
|
812
|
-
);
|
|
813
|
-
if (!hasIndex) {
|
|
814
|
-
const prop = b.property('init', b.identifier('index'), b.identifier('index'));
|
|
815
|
-
prop.shorthand = true;
|
|
816
|
-
firstParam.properties.push(prop);
|
|
817
|
-
changed = true;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
|
|
821
|
-
const typeAnn = firstParam.typeAnnotation.typeAnnotation;
|
|
822
|
-
if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
|
|
823
|
-
updateTsInterface(ast, typeAnn.typeName.name);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
} else if (firstParam.type === 'Identifier') {
|
|
827
|
-
const paramName = firstParam.name;
|
|
828
|
-
if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
|
|
829
|
-
const typeAnn = firstParam.typeAnnotation.typeAnnotation;
|
|
830
|
-
if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
|
|
831
|
-
updateTsInterface(ast, typeAnn.typeName.name);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
recast.types.visit(fnNode.body || fnNode, {
|
|
835
|
-
visitVariableDeclarator(vPath) {
|
|
836
|
-
if (vPath.node.id?.type === 'ObjectPattern' && vPath.node.init?.name === paramName) {
|
|
837
|
-
const propsList = vPath.node.id.properties;
|
|
838
|
-
if (!propsList.some((p) => p.key?.name === 'previewItemPath')) {
|
|
839
|
-
const prop = b.property('init', b.identifier('previewItemPath'), b.identifier('previewItemPath'));
|
|
840
|
-
prop.shorthand = true;
|
|
841
|
-
propsList.push(prop);
|
|
842
|
-
changed = true;
|
|
843
|
-
}
|
|
844
|
-
if (!propsList.some((p) => p.key?.name === 'index')) {
|
|
845
|
-
const prop = b.property('init', b.identifier('index'), b.identifier('index'));
|
|
846
|
-
prop.shorthand = true;
|
|
847
|
-
propsList.push(prop);
|
|
848
|
-
changed = true;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
this.traverse(vPath);
|
|
852
|
-
},
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
return changed;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
recast.types.visit(ast, {
|
|
860
|
-
visitFunctionDeclaration(pathNode) {
|
|
861
|
-
if (pathNode.node.id?.name === componentName) {
|
|
862
|
-
if (instrumentFunctionProps(pathNode.node, isTypeScript)) instrumented = true;
|
|
863
|
-
}
|
|
864
|
-
this.traverse(pathNode);
|
|
865
|
-
},
|
|
866
|
-
visitVariableDeclarator(pathNode) {
|
|
867
|
-
if (pathNode.node.id?.name === componentName) {
|
|
868
|
-
const init = pathNode.node.init;
|
|
869
|
-
if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
870
|
-
if (instrumentFunctionProps(init, isTypeScript)) instrumented = true;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
this.traverse(pathNode);
|
|
874
|
-
},
|
|
875
|
-
visitExportDefaultDeclaration(pathNode) {
|
|
876
|
-
const decl = pathNode.node.declaration;
|
|
877
|
-
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
878
|
-
const fnName = decl.id?.name;
|
|
879
|
-
if (!fnName || fnName === componentName) {
|
|
880
|
-
if (instrumentFunctionProps(decl, isTypeScript)) instrumented = true;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
this.traverse(pathNode);
|
|
884
|
-
},
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
let rootElement = null;
|
|
888
|
-
recast.types.visit(ast, {
|
|
889
|
-
visitReturnStatement(pathNode) {
|
|
890
|
-
if (!rootElement && pathNode.node.argument) {
|
|
891
|
-
let arg = pathNode.node.argument;
|
|
892
|
-
if (arg.type === 'ParenthesizedExpression') arg = arg.expression;
|
|
893
|
-
if (arg.type === 'JSXElement') {
|
|
894
|
-
rootElement = arg;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
this.traverse(pathNode);
|
|
898
|
-
},
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
if (rootElement) {
|
|
902
|
-
if (!hasJsxAttribute(rootElement, 'data-preview-item-path')) {
|
|
903
|
-
if (listPath) {
|
|
904
|
-
rootElement.openingElement.attributes.push(
|
|
905
|
-
jsxTemplatePathAttr('data-preview-item-path', listPath, 'index')
|
|
906
|
-
);
|
|
907
|
-
} else {
|
|
908
|
-
rootElement.openingElement.attributes.push(
|
|
909
|
-
b.jsxAttribute(
|
|
910
|
-
b.jsxIdentifier('data-preview-item-path'),
|
|
911
|
-
b.jsxExpressionContainer(b.identifier('previewItemPath'))
|
|
912
|
-
)
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
instrumented = true;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
recast.types.visit(ast, {
|
|
920
|
-
visitJSXElement(pathNode) {
|
|
921
|
-
const node = pathNode.node;
|
|
922
|
-
const tagName = getJsxName(node);
|
|
923
|
-
|
|
924
|
-
const srcAttr = findJsxAttribute(node, 'src');
|
|
925
|
-
if (srcAttr && (tagName === 'img' || tagName === 'Image') && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
926
|
-
const rawCode = recast.print(srcAttr.value).code;
|
|
927
|
-
if (/image|photo|avatar|thumb|img|src/i.test(rawCode)) {
|
|
928
|
-
const imgField = (transform.itemFields || []).find((f) => f.type === 'image' || /image|photo/i.test(f.key))?.key || 'image';
|
|
929
|
-
if (listPath) {
|
|
930
|
-
node.openingElement.attributes.push(
|
|
931
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${imgField}`)
|
|
932
|
-
);
|
|
933
|
-
} else {
|
|
934
|
-
node.openingElement.attributes.push(
|
|
935
|
-
b.jsxAttribute(
|
|
936
|
-
b.jsxIdentifier('data-preview-field-path'),
|
|
937
|
-
b.jsxExpressionContainer(
|
|
938
|
-
b.templateLiteral(
|
|
939
|
-
[
|
|
940
|
-
b.templateElement({ raw: '', cooked: '' }, false),
|
|
941
|
-
b.templateElement({ raw: `.${imgField}`, cooked: `.${imgField}` }, true),
|
|
942
|
-
],
|
|
943
|
-
[b.identifier('previewItemPath')]
|
|
944
|
-
)
|
|
945
|
-
)
|
|
946
|
-
)
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
instrumented = true;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
for (const child of node.children || []) {
|
|
954
|
-
if (child.type === 'JSXExpressionContainer') {
|
|
955
|
-
const exprCode = recast.print(child.expression).code;
|
|
956
|
-
if (!hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
957
|
-
if (/name|title|heading/i.test(exprCode)) {
|
|
958
|
-
const nameField = (transform.itemFields || []).find((f) => /name|title|heading/i.test(f.key))?.key || 'name';
|
|
959
|
-
if (listPath) {
|
|
960
|
-
node.openingElement.attributes.push(
|
|
961
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${nameField}`)
|
|
962
|
-
);
|
|
963
|
-
} else {
|
|
964
|
-
node.openingElement.attributes.push(
|
|
965
|
-
b.jsxAttribute(
|
|
966
|
-
b.jsxIdentifier('data-preview-field-path'),
|
|
967
|
-
b.jsxExpressionContainer(
|
|
968
|
-
b.templateLiteral(
|
|
969
|
-
[
|
|
970
|
-
b.templateElement({ raw: '', cooked: '' }, false),
|
|
971
|
-
b.templateElement({ raw: `.${nameField}`, cooked: `.${nameField}` }, true),
|
|
972
|
-
],
|
|
973
|
-
[b.identifier('previewItemPath')]
|
|
974
|
-
)
|
|
975
|
-
)
|
|
976
|
-
)
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
instrumented = true;
|
|
980
|
-
break;
|
|
981
|
-
} else if (/price|cost|amount/i.test(exprCode)) {
|
|
982
|
-
const priceField = (transform.itemFields || []).find((f) => /price|cost/i.test(f.key))?.key || 'price';
|
|
983
|
-
if (listPath) {
|
|
984
|
-
node.openingElement.attributes.push(
|
|
985
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${priceField}`)
|
|
986
|
-
);
|
|
987
|
-
} else {
|
|
988
|
-
node.openingElement.attributes.push(
|
|
989
|
-
b.jsxAttribute(
|
|
990
|
-
b.jsxIdentifier('data-preview-field-path'),
|
|
991
|
-
b.jsxExpressionContainer(
|
|
992
|
-
b.templateLiteral(
|
|
993
|
-
[
|
|
994
|
-
b.templateElement({ raw: '', cooked: '' }, false),
|
|
995
|
-
b.templateElement({ raw: `.${priceField}`, cooked: `.${priceField}` }, true),
|
|
996
|
-
],
|
|
997
|
-
[b.identifier('previewItemPath')]
|
|
998
|
-
)
|
|
999
|
-
)
|
|
1000
|
-
)
|
|
1001
|
-
);
|
|
1002
|
-
}
|
|
1003
|
-
instrumented = true;
|
|
1004
|
-
break;
|
|
1005
|
-
} else if (/description|desc|subtitle/i.test(exprCode)) {
|
|
1006
|
-
const descField = (transform.itemFields || []).find((f) => /desc/i.test(f.key))?.key || 'description';
|
|
1007
|
-
if (listPath) {
|
|
1008
|
-
node.openingElement.attributes.push(
|
|
1009
|
-
jsxTemplatePathAttr('data-preview-field-path', listPath, 'index', `.${descField}`)
|
|
1010
|
-
);
|
|
1011
|
-
} else {
|
|
1012
|
-
node.openingElement.attributes.push(
|
|
1013
|
-
b.jsxAttribute(
|
|
1014
|
-
b.jsxIdentifier('data-preview-field-path'),
|
|
1015
|
-
b.jsxExpressionContainer(
|
|
1016
|
-
b.templateLiteral(
|
|
1017
|
-
[
|
|
1018
|
-
b.templateElement({ raw: '', cooked: '' }, false),
|
|
1019
|
-
b.templateElement({ raw: `.${descField}`, cooked: `.${descField}` }, true),
|
|
1020
|
-
],
|
|
1021
|
-
[b.identifier('previewItemPath')]
|
|
1022
|
-
)
|
|
1023
|
-
)
|
|
1024
|
-
)
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
instrumented = true;
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
this.traverse(pathNode);
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
return instrumented;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function instrumentReusableComponent(ast, transform, isTypeScript = false) {
|
|
1041
|
-
const componentName = transform.componentName;
|
|
1042
|
-
const propTransforms = transform.propTransforms || {};
|
|
1043
|
-
const propNames = Object.keys(propTransforms);
|
|
1044
|
-
if (!propNames.length) return false;
|
|
1045
|
-
|
|
1046
|
-
let instrumented = false;
|
|
1047
|
-
|
|
1048
|
-
function updateTsInterface(rootAst, interfaceName) {
|
|
1049
|
-
recast.types.visit(rootAst, {
|
|
1050
|
-
visitTSInterfaceDeclaration(pathNode) {
|
|
1051
|
-
if (pathNode.node.id?.name === interfaceName) {
|
|
1052
|
-
const body = pathNode.node.body?.body || [];
|
|
1053
|
-
if (!body.some((m) => m.key?.name === 'previewPath')) {
|
|
1054
|
-
const sig = b.tsPropertySignature(b.identifier('previewPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
|
|
1055
|
-
sig.optional = true;
|
|
1056
|
-
body.push(sig);
|
|
1057
|
-
instrumented = true;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
this.traverse(pathNode);
|
|
1061
|
-
},
|
|
1062
|
-
visitTSTypeAliasDeclaration(pathNode) {
|
|
1063
|
-
if (pathNode.node.id?.name === interfaceName && pathNode.node.typeAnnotation?.type === 'TSTypeLiteral') {
|
|
1064
|
-
const members = pathNode.node.typeAnnotation.members || [];
|
|
1065
|
-
if (!members.some((m) => m.key?.name === 'previewPath')) {
|
|
1066
|
-
const sig = b.tsPropertySignature(b.identifier('previewPath'), b.tsTypeAnnotation(b.tsStringKeyword()));
|
|
1067
|
-
sig.optional = true;
|
|
1068
|
-
members.push(sig);
|
|
1069
|
-
instrumented = true;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
this.traverse(pathNode);
|
|
1073
|
-
},
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function instrumentFunctionProps(fnNode, isTs) {
|
|
1078
|
-
let changed = false;
|
|
1079
|
-
let params = fnNode.params || [];
|
|
1080
|
-
if (!params.length) {
|
|
1081
|
-
fnNode.params = [
|
|
1082
|
-
b.objectPattern([
|
|
1083
|
-
b.property('init', b.identifier('previewPath'), b.identifier('previewPath')),
|
|
1084
|
-
]),
|
|
1085
|
-
];
|
|
1086
|
-
changed = true;
|
|
1087
|
-
} else {
|
|
1088
|
-
const firstParam = params[0];
|
|
1089
|
-
if (firstParam.type === 'ObjectPattern') {
|
|
1090
|
-
const hasPreviewPath = firstParam.properties.some(
|
|
1091
|
-
(p) => p.key?.name === 'previewPath' || p.argument?.name === 'previewPath'
|
|
1092
|
-
);
|
|
1093
|
-
if (!hasPreviewPath) {
|
|
1094
|
-
const prop = b.property('init', b.identifier('previewPath'), b.identifier('previewPath'));
|
|
1095
|
-
prop.shorthand = true;
|
|
1096
|
-
firstParam.properties.push(prop);
|
|
1097
|
-
changed = true;
|
|
1098
|
-
}
|
|
1099
|
-
if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
|
|
1100
|
-
const typeAnn = firstParam.typeAnnotation.typeAnnotation;
|
|
1101
|
-
if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
|
|
1102
|
-
updateTsInterface(ast, typeAnn.typeName.name);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
} else if (firstParam.type === 'Identifier') {
|
|
1106
|
-
const paramName = firstParam.name;
|
|
1107
|
-
if (isTs && firstParam.typeAnnotation?.typeAnnotation) {
|
|
1108
|
-
const typeAnn = firstParam.typeAnnotation.typeAnnotation;
|
|
1109
|
-
if (typeAnn.type === 'TSTypeReference' && typeAnn.typeName?.name) {
|
|
1110
|
-
updateTsInterface(ast, typeAnn.typeName.name);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
recast.types.visit(fnNode.body || fnNode, {
|
|
1114
|
-
visitVariableDeclarator(vPath) {
|
|
1115
|
-
if (vPath.node.id?.type === 'ObjectPattern' && vPath.node.init?.name === paramName) {
|
|
1116
|
-
const propsList = vPath.node.id.properties;
|
|
1117
|
-
if (!propsList.some((p) => p.key?.name === 'previewPath')) {
|
|
1118
|
-
const prop = b.property('init', b.identifier('previewPath'), b.identifier('previewPath'));
|
|
1119
|
-
prop.shorthand = true;
|
|
1120
|
-
propsList.push(prop);
|
|
1121
|
-
changed = true;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
this.traverse(vPath);
|
|
1125
|
-
},
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
return changed;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
recast.types.visit(ast, {
|
|
1133
|
-
visitFunctionDeclaration(pathNode) {
|
|
1134
|
-
if (!componentName || pathNode.node.id?.name === componentName) {
|
|
1135
|
-
if (instrumentFunctionProps(pathNode.node, isTypeScript)) instrumented = true;
|
|
1136
|
-
}
|
|
1137
|
-
this.traverse(pathNode);
|
|
1138
|
-
},
|
|
1139
|
-
visitVariableDeclarator(pathNode) {
|
|
1140
|
-
if (pathNode.node.id?.name === componentName) {
|
|
1141
|
-
const init = pathNode.node.init;
|
|
1142
|
-
if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
1143
|
-
if (instrumentFunctionProps(init, isTypeScript)) instrumented = true;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
this.traverse(pathNode);
|
|
1147
|
-
},
|
|
1148
|
-
visitExportDefaultDeclaration(pathNode) {
|
|
1149
|
-
const decl = pathNode.node.declaration;
|
|
1150
|
-
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
1151
|
-
const fnName = decl.id?.name;
|
|
1152
|
-
if (!fnName || fnName === componentName) {
|
|
1153
|
-
if (instrumentFunctionProps(decl, isTypeScript)) instrumented = true;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
this.traverse(pathNode);
|
|
1157
|
-
},
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
recast.types.visit(ast, {
|
|
1161
|
-
visitJSXElement(pathNode) {
|
|
1162
|
-
const node = pathNode.node;
|
|
1163
|
-
for (const child of node.children || []) {
|
|
1164
|
-
if (child.type === 'JSXExpressionContainer') {
|
|
1165
|
-
const expr = child.expression;
|
|
1166
|
-
let propMatch = null;
|
|
1167
|
-
if (expr && expr.type === 'Identifier' && propNames.includes(expr.name)) {
|
|
1168
|
-
propMatch = expr.name;
|
|
1169
|
-
} else if (expr && expr.type === 'LogicalExpression' && expr.left?.name && propNames.includes(expr.left.name)) {
|
|
1170
|
-
propMatch = expr.left.name;
|
|
1171
|
-
}
|
|
1172
|
-
if (propMatch && !hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
1173
|
-
const previewAttr = b.jsxAttribute(
|
|
1174
|
-
b.jsxIdentifier('data-preview-field-path'),
|
|
1175
|
-
b.jsxExpressionContainer(
|
|
1176
|
-
b.conditionalExpression(
|
|
1177
|
-
b.identifier('previewPath'),
|
|
1178
|
-
b.templateLiteral(
|
|
1179
|
-
[
|
|
1180
|
-
b.templateElement({ raw: '', cooked: '' }, false),
|
|
1181
|
-
b.templateElement({ raw: `.${propMatch}`, cooked: `.${propMatch}` }, true),
|
|
1182
|
-
],
|
|
1183
|
-
[b.identifier('previewPath')]
|
|
1184
|
-
),
|
|
1185
|
-
b.identifier('undefined')
|
|
1186
|
-
)
|
|
1187
|
-
)
|
|
1188
|
-
);
|
|
1189
|
-
node.openingElement.attributes.push(previewAttr);
|
|
1190
|
-
instrumented = true;
|
|
1191
|
-
break;
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
this.traverse(pathNode);
|
|
1196
|
-
},
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
return instrumented;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
/** The nearest JSX element that wraps the map expression. */
|
|
1203
|
-
function findListContainer(mapCallPath) {
|
|
1204
|
-
let current = mapCallPath.parent;
|
|
1205
|
-
while (current) {
|
|
1206
|
-
const node = current.node || current.value;
|
|
1207
|
-
if (node?.type === 'JSXElement') return node;
|
|
1208
|
-
current = current.parentPath || current.parent;
|
|
1209
|
-
}
|
|
1210
|
-
return null;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function isModuleLevel(pathNode) {
|
|
1214
|
-
let current = pathNode.parentPath || pathNode.parent;
|
|
1215
|
-
while (current) {
|
|
1216
|
-
const type = current.node?.type;
|
|
1217
|
-
if (
|
|
1218
|
-
type === 'FunctionDeclaration' ||
|
|
1219
|
-
type === 'FunctionExpression' ||
|
|
1220
|
-
type === 'ArrowFunctionExpression'
|
|
1221
|
-
) {
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
if (type === 'Program') return true;
|
|
1225
|
-
current = current.parentPath || current.parent;
|
|
1226
|
-
}
|
|
1227
|
-
return true;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function findEnclosingFunction(pathNode) {
|
|
1231
|
-
let current = pathNode.parentPath || pathNode.parent;
|
|
1232
|
-
while (current) {
|
|
1233
|
-
const type = current.node?.type;
|
|
1234
|
-
if (
|
|
1235
|
-
type === 'FunctionDeclaration' ||
|
|
1236
|
-
type === 'FunctionExpression' ||
|
|
1237
|
-
type === 'ArrowFunctionExpression'
|
|
1238
|
-
) {
|
|
1239
|
-
return current;
|
|
1240
|
-
}
|
|
1241
|
-
current = current.parentPath || current.parent;
|
|
1242
|
-
}
|
|
1243
|
-
return null;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false, isTypeScript = false) {
|
|
1247
|
-
const arrayName = mapCallPath.node.callee.object?.name;
|
|
1248
|
-
if (!arrayName) return false;
|
|
1249
|
-
let bound = false;
|
|
1250
|
-
|
|
1251
|
-
recast.types.visit(ast, {
|
|
1252
|
-
visitVariableDeclarator(pathNode) {
|
|
1253
|
-
const node = pathNode.node;
|
|
1254
|
-
if (bound || node.id?.type !== 'Identifier' || node.id.name !== arrayName) {
|
|
1255
|
-
this.traverse(pathNode);
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
if (node.init?.type !== 'ArrayExpression') {
|
|
1259
|
-
this.traverse(pathNode);
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
if (isClient && isModuleLevel(pathNode)) {
|
|
1264
|
-
const defaultName = 'DEFAULT_' + arrayName;
|
|
1265
|
-
node.id.name = defaultName;
|
|
1266
|
-
const fnPath = findEnclosingFunction(mapCallPath);
|
|
1267
|
-
if (fnPath && fnPath.node.body?.type === 'BlockStatement') {
|
|
1268
|
-
const body = fnPath.node.body.body;
|
|
1269
|
-
const already = body.some((stmt) => recast.print(stmt).code.includes(`const ${arrayName} =`));
|
|
1270
|
-
if (!already) {
|
|
1271
|
-
const localId = b.identifier(arrayName);
|
|
1272
|
-
if (isTypeScript) {
|
|
1273
|
-
localId.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
1274
|
-
}
|
|
1275
|
-
const localDecl = b.variableDeclaration('const', [
|
|
1276
|
-
b.variableDeclarator(
|
|
1277
|
-
localId,
|
|
1278
|
-
mergeDefaultRefs
|
|
1279
|
-
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
1280
|
-
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
1281
|
-
),
|
|
1282
|
-
]);
|
|
1283
|
-
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
1284
|
-
if (hookIdx >= 0) {
|
|
1285
|
-
body.splice(hookIdx + 1, 0, localDecl);
|
|
1286
|
-
} else {
|
|
1287
|
-
body.unshift(localDecl);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
bound = true;
|
|
1292
|
-
return false;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
if (isTypeScript && node.id?.type === 'Identifier' && !node.id.typeAnnotation) {
|
|
1296
|
-
node.id.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
1297
|
-
}
|
|
1298
|
-
node.init = siteDataListBinding(listPath.split('.'), node.init);
|
|
1299
|
-
bound = true;
|
|
1300
|
-
return false;
|
|
1301
|
-
},
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
if (!bound) {
|
|
1305
|
-
recast.types.visit(ast, {
|
|
1306
|
-
visitImportDeclaration(pathNode) {
|
|
1307
|
-
if (bound) return false;
|
|
1308
|
-
const node = pathNode.node;
|
|
1309
|
-
const spec = (node.specifiers || []).find(
|
|
1310
|
-
(s) => (s.local?.name || s.imported?.name) === arrayName
|
|
1311
|
-
);
|
|
1312
|
-
if (!spec) {
|
|
1313
|
-
this.traverse(pathNode);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const defaultName = 'DEFAULT_' + arrayName;
|
|
1318
|
-
if (spec.type === 'ImportSpecifier') {
|
|
1319
|
-
spec.local = b.identifier(defaultName);
|
|
1320
|
-
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
1321
|
-
spec.local = b.identifier(defaultName);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
const fnPath = findEnclosingFunction(mapCallPath);
|
|
1325
|
-
if (fnPath && fnPath.node.body?.type === 'BlockStatement') {
|
|
1326
|
-
const body = fnPath.node.body.body;
|
|
1327
|
-
const already = body.some(
|
|
1328
|
-
(stmt) =>
|
|
1329
|
-
recast.print(stmt).code.includes(`const ${arrayName} =`) ||
|
|
1330
|
-
recast.print(stmt).code.includes(`const ${arrayName}:`)
|
|
1331
|
-
);
|
|
1332
|
-
if (!already) {
|
|
1333
|
-
const localId = b.identifier(arrayName);
|
|
1334
|
-
if (isTypeScript) {
|
|
1335
|
-
localId.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
1336
|
-
}
|
|
1337
|
-
const localDecl = b.variableDeclaration('const', [
|
|
1338
|
-
b.variableDeclarator(
|
|
1339
|
-
localId,
|
|
1340
|
-
mergeDefaultRefs
|
|
1341
|
-
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
1342
|
-
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
1343
|
-
),
|
|
1344
|
-
]);
|
|
1345
|
-
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
1346
|
-
if (hookIdx >= 0) {
|
|
1347
|
-
body.splice(hookIdx + 1, 0, localDecl);
|
|
1348
|
-
} else {
|
|
1349
|
-
body.unshift(localDecl);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
bound = true;
|
|
1354
|
-
return false;
|
|
1355
|
-
},
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
return bound;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
function mergeDefaultItemRefsBinding(pathParts, defaultName) {
|
|
1363
|
-
const listExpr = siteDataListBinding(pathParts, b.identifier(defaultName));
|
|
1364
|
-
return b.callExpression(
|
|
1365
|
-
b.memberExpression(listExpr, b.identifier('map'), false),
|
|
1366
|
-
[
|
|
1367
|
-
b.arrowFunctionExpression(
|
|
1368
|
-
[b.identifier('item'), b.identifier('index')],
|
|
1369
|
-
b.objectExpression([
|
|
1370
|
-
b.spreadElement(
|
|
1371
|
-
b.logicalExpression(
|
|
1372
|
-
'??',
|
|
1373
|
-
b.memberExpression(b.identifier(defaultName), b.identifier('index'), true),
|
|
1374
|
-
b.objectExpression([])
|
|
1375
|
-
)
|
|
1376
|
-
),
|
|
1377
|
-
b.spreadElement(b.identifier('item')),
|
|
1378
|
-
])
|
|
1379
|
-
),
|
|
1380
|
-
]
|
|
1381
|
-
);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
function fileAlreadyUsesSiteDataHook(ast) {
|
|
1385
|
-
let found = false;
|
|
1386
|
-
recast.types.visit(ast, {
|
|
1387
|
-
visitCallExpression(pathNode) {
|
|
1388
|
-
const callee = pathNode.node.callee;
|
|
1389
|
-
if (callee && callee.type === 'Identifier' && callee.name === 'useSiteData') {
|
|
1390
|
-
found = true;
|
|
1391
|
-
return false;
|
|
1392
|
-
}
|
|
1393
|
-
this.traverse(pathNode);
|
|
1394
|
-
},
|
|
1395
|
-
});
|
|
1396
|
-
return found;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
function resolveSiteDataRuntimeSpecifier(profile) {
|
|
1400
|
-
const root = profile && profile.root;
|
|
1401
|
-
if (!root) return '@deneb-ui/ui';
|
|
1402
|
-
const candidates = [
|
|
1403
|
-
['src/lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
1404
|
-
['src/lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
1405
|
-
['lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
1406
|
-
['lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
1407
|
-
];
|
|
1408
|
-
const hasAt = Object.keys(profile.aliasMap || {}).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
1409
|
-
for (const [relative, alias] of candidates) {
|
|
1410
|
-
if (!fs.existsSync(path.join(root, relative))) continue;
|
|
1411
|
-
if (hasAt) return alias;
|
|
1412
|
-
const fromAbs = path.join(root, 'src/components/placeholder.tsx');
|
|
1413
|
-
const toAbs = path.join(root, relative.replace(/\.tsx?$/, ''));
|
|
1414
|
-
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
1415
|
-
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
1416
|
-
return relSpec;
|
|
1417
|
-
}
|
|
1418
|
-
return '@deneb-ui/ui';
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
function functionReferencesSiteData(fn) {
|
|
1422
|
-
if (!fn || !fn.body) return false;
|
|
1423
|
-
let found = false;
|
|
1424
|
-
recast.types.visit(fn.body, {
|
|
1425
|
-
visitIdentifier(pathNode) {
|
|
1426
|
-
if (pathNode.node.name === 'siteData') {
|
|
1427
|
-
found = true;
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
this.traverse(pathNode);
|
|
1431
|
-
},
|
|
1432
|
-
});
|
|
1433
|
-
return found;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function injectSiteDataHook(ast) {
|
|
1437
|
-
let injected = false;
|
|
1438
|
-
let anyFunctionReferencedSiteData = false;
|
|
1439
|
-
let alreadyHasUseSiteData = false;
|
|
1440
|
-
|
|
1441
|
-
recast.types.visit(ast, {
|
|
1442
|
-
visitCallExpression(pathNode) {
|
|
1443
|
-
if (pathNode.node.callee && pathNode.node.callee.name === 'useSiteData') {
|
|
1444
|
-
alreadyHasUseSiteData = true;
|
|
1445
|
-
return false;
|
|
1446
|
-
}
|
|
1447
|
-
this.traverse(pathNode);
|
|
1448
|
-
},
|
|
1449
|
-
});
|
|
1450
|
-
|
|
1451
|
-
function injectIntoFunction(fn) {
|
|
1452
|
-
if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
|
|
1453
|
-
const body = fn.body.body;
|
|
1454
|
-
const already = body.some((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
1455
|
-
if (already) return false;
|
|
1456
|
-
const hook = b.variableDeclaration('const', [
|
|
1457
|
-
b.variableDeclarator(
|
|
1458
|
-
b.identifier('siteData'),
|
|
1459
|
-
b.callExpression(b.identifier('useSiteData'), [])
|
|
1460
|
-
),
|
|
1461
|
-
]);
|
|
1462
|
-
body.unshift(hook);
|
|
1463
|
-
alreadyHasUseSiteData = true;
|
|
1464
|
-
return true;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// Pass 1: Inject useSiteData() into EVERY component function that references siteData
|
|
1468
|
-
recast.types.visit(ast, {
|
|
1469
|
-
visitFunctionDeclaration(pathNode) {
|
|
1470
|
-
if (functionReferencesSiteData(pathNode.node)) {
|
|
1471
|
-
anyFunctionReferencedSiteData = true;
|
|
1472
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
1473
|
-
}
|
|
1474
|
-
this.traverse(pathNode);
|
|
1475
|
-
},
|
|
1476
|
-
visitFunctionExpression(pathNode) {
|
|
1477
|
-
if (functionReferencesSiteData(pathNode.node)) {
|
|
1478
|
-
anyFunctionReferencedSiteData = true;
|
|
1479
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
1480
|
-
}
|
|
1481
|
-
this.traverse(pathNode);
|
|
1482
|
-
},
|
|
1483
|
-
visitArrowFunctionExpression(pathNode) {
|
|
1484
|
-
if (functionReferencesSiteData(pathNode.node)) {
|
|
1485
|
-
anyFunctionReferencedSiteData = true;
|
|
1486
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
1487
|
-
}
|
|
1488
|
-
this.traverse(pathNode);
|
|
1489
|
-
},
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// Pass 2: If no component references siteData yet (e.g. before subsequent AST edits),
|
|
1493
|
-
// ensure the primary component receives useSiteData()
|
|
1494
|
-
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
1495
|
-
recast.types.visit(ast, {
|
|
1496
|
-
visitExportDefaultDeclaration(pathNode) {
|
|
1497
|
-
if (injected) return false;
|
|
1498
|
-
const decl = pathNode.node.declaration;
|
|
1499
|
-
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
1500
|
-
injected = injectIntoFunction(decl);
|
|
1501
|
-
}
|
|
1502
|
-
this.traverse(pathNode);
|
|
1503
|
-
},
|
|
1504
|
-
visitFunctionDeclaration(pathNode) {
|
|
1505
|
-
if (injected) return false;
|
|
1506
|
-
const name = pathNode.node.id && pathNode.node.id.name;
|
|
1507
|
-
if (name && /^[A-Z]/.test(name)) {
|
|
1508
|
-
injected = injectIntoFunction(pathNode.node);
|
|
1509
|
-
return false;
|
|
1510
|
-
}
|
|
1511
|
-
this.traverse(pathNode);
|
|
1512
|
-
},
|
|
1513
|
-
visitVariableDeclarator(pathNode) {
|
|
1514
|
-
if (injected) return false;
|
|
1515
|
-
const id = pathNode.node.id;
|
|
1516
|
-
const init = pathNode.node.init;
|
|
1517
|
-
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
1518
|
-
injected = injectIntoFunction(init);
|
|
1519
|
-
return false;
|
|
1520
|
-
}
|
|
1521
|
-
this.traverse(pathNode);
|
|
1522
|
-
},
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
1527
|
-
recast.types.visit(ast, {
|
|
1528
|
-
visitFunctionDeclaration(pathNode) {
|
|
1529
|
-
if (injected) return false;
|
|
1530
|
-
injected = injectIntoFunction(pathNode.node);
|
|
1531
|
-
return false;
|
|
1532
|
-
},
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
return injected;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
function resolveSiteDataSpecifier(profile, fromRelativeFile) {
|
|
1540
|
-
const aliases = (profile && profile.aliasMap) || {};
|
|
1541
|
-
const hasAt = Object.keys(aliases).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
1542
|
-
const hasSrc = Boolean(profile && profile.hasSrc);
|
|
1543
|
-
const siteDataRel = hasSrc ? 'src/data/site-data.json' : 'data/site-data.json';
|
|
1544
|
-
if (hasAt && hasSrc) return '@/data/site-data.json';
|
|
1545
|
-
|
|
1546
|
-
const root = profile && profile.root ? profile.root : process.cwd();
|
|
1547
|
-
const fromAbs = path.join(root, fromRelativeFile || 'page.tsx');
|
|
1548
|
-
const toAbs = path.join(root, siteDataRel);
|
|
1549
|
-
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
1550
|
-
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
1551
|
-
return relSpec;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function applyFilePlan(filePlan, profile) {
|
|
1555
|
-
if (!filePlan.originalCode || !filePlan.transformations.length) {
|
|
1556
|
-
return { code: filePlan.originalCode, changed: false };
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const ast = parseSource(filePlan.originalCode, filePlan.file);
|
|
1560
|
-
let isClient = hasDirective(ast, 'use client') || hasClientDirective(ast) || /['"]use client['"]/.test(filePlan.originalCode.slice(0, 400));
|
|
1561
|
-
const hasMetadata = hasMetadataExport(ast);
|
|
1562
|
-
let applied = 0;
|
|
1563
|
-
const failures = [];
|
|
1564
|
-
|
|
1565
|
-
const supported = new Set([
|
|
1566
|
-
'split-action-contract',
|
|
1567
|
-
'form-submit-action',
|
|
1568
|
-
'extract-url',
|
|
1569
|
-
'extract-image',
|
|
1570
|
-
'extract-picture',
|
|
1571
|
-
'extract-alt',
|
|
1572
|
-
'extract-placeholder',
|
|
1573
|
-
'extract-text',
|
|
1574
|
-
'extract-prop',
|
|
1575
|
-
'wrap-text-span',
|
|
1576
|
-
'collection-conversion',
|
|
1577
|
-
'style-bind',
|
|
1578
|
-
'extract-tailwind-bg',
|
|
1579
|
-
'instrument-child-card',
|
|
1580
|
-
'bind-button-with-icon',
|
|
1581
|
-
'bind-highlighted-heading',
|
|
1582
|
-
'prop-flow-callsite',
|
|
1583
|
-
'instrument-reusable-component',
|
|
1584
|
-
]);
|
|
1585
|
-
|
|
1586
|
-
const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
|
|
1587
|
-
|
|
1588
|
-
// Collections run first: they rewrite the array declaration and add an index
|
|
1589
|
-
// parameter, and later per-element edits must observe that shape.
|
|
1590
|
-
const ordered = [...filePlan.transformations].sort(
|
|
1591
|
-
(left, right) =>
|
|
1592
|
-
Number(right.operation === 'collection-conversion') -
|
|
1593
|
-
Number(left.operation === 'collection-conversion')
|
|
1594
|
-
);
|
|
1595
|
-
|
|
1596
|
-
for (const transform of ordered) {
|
|
1597
|
-
if (transform.decision === 'skip') continue;
|
|
1598
|
-
if (!supported.has(transform.operation)) continue;
|
|
1599
|
-
|
|
1600
|
-
if (transform.operation === 'instrument-child-card') {
|
|
1601
|
-
try {
|
|
1602
|
-
if (instrumentChildCardComponent(ast, transform, isTypeScript)) applied++;
|
|
1603
|
-
} catch (err) {
|
|
1604
|
-
failures.push({ loc: transform.loc, reason: err.message });
|
|
1605
|
-
}
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
if (transform.operation === 'instrument-reusable-component') {
|
|
1610
|
-
try {
|
|
1611
|
-
if (instrumentReusableComponent(ast, transform, isTypeScript)) applied++;
|
|
1612
|
-
} catch (err) {
|
|
1613
|
-
failures.push({ loc: transform.loc, reason: err.message });
|
|
1614
|
-
}
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
if (transform.operation === 'collection-conversion') {
|
|
1619
|
-
try {
|
|
1620
|
-
if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
|
|
1621
|
-
else failures.push({ loc: transform.loc, reason: 'collection-not-bindable' });
|
|
1622
|
-
} catch (err) {
|
|
1623
|
-
failures.push({ loc: transform.loc, reason: err.message });
|
|
1624
|
-
}
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const pathNode = findElementByLoc(ast, transform.loc, transform.componentName || transform.tag);
|
|
1629
|
-
if (!pathNode) {
|
|
1630
|
-
failures.push({ loc: transform.loc, reason: 'node-not-found' });
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
try {
|
|
1634
|
-
applyTransformToElement(pathNode, transform);
|
|
1635
|
-
applied++;
|
|
1636
|
-
} catch (err) {
|
|
1637
|
-
failures.push({ loc: transform.loc, reason: err.message });
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
if (applied === 0) {
|
|
1642
|
-
return { code: filePlan.originalCode, changed: false, applied, failures };
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
if (!isClient && profile?.router === 'next-app' && !hasMetadata && canSafelyInjectClientDirective(ast, filePlan.file, profile)) {
|
|
1646
|
-
if (needsClientDirective(ast, filePlan.originalCode)) {
|
|
1647
|
-
ensureClientDirective(ast);
|
|
1648
|
-
isClient = true;
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
1653
|
-
if (isClient) {
|
|
1654
|
-
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
1655
|
-
injectSiteDataHook(ast);
|
|
1656
|
-
ensureImport(ast, runtimeSpecifier, ['useSiteData']);
|
|
1657
|
-
} else {
|
|
1658
|
-
ensureDefaultImport(ast, siteDataImport, 'siteData');
|
|
1659
|
-
}
|
|
1660
|
-
sanitizeDuplicateBindings(ast);
|
|
1661
|
-
|
|
1662
|
-
// Sanitize any conflicting data-preview-static on elements with editable markers
|
|
1663
|
-
sanitizeContradictoryMarkers(ast);
|
|
1664
|
-
healBroadContainerMarkers(ast);
|
|
1665
|
-
healEmptyStateConditionals(ast);
|
|
1666
|
-
healHiddenPreviewMarkers(ast);
|
|
1667
|
-
healLegacyProductDetailLinks(ast);
|
|
1668
|
-
healSectionOverflowHidden(ast);
|
|
1669
|
-
healDecorativeOverlays(ast);
|
|
1670
|
-
|
|
1671
|
-
// Page keys are stamped in a separate route-driven pass so App Router and
|
|
1672
|
-
// Pages Router projects are handled by the same logic.
|
|
1673
|
-
const code = printSource(ast, filePlan.originalCode);
|
|
1674
|
-
return { code, changed: true, applied, failures, usedClientHook: isClient };
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
/**
|
|
1678
|
-
* Stamps a route's page component with data-preview-page-key. Fivora requires
|
|
1679
|
-
* exactly one per exported route, so the key comes from the scanned route id
|
|
1680
|
-
* rather than being guessed from the filename — Pages Router uses
|
|
1681
|
-
* `pages/contact.jsx`, App Router uses `app/contact/page.tsx`.
|
|
1682
|
-
*
|
|
1683
|
-
* `<main>` is preferred, but any project shape is supported by falling back to
|
|
1684
|
-
* the outermost element the page component returns.
|
|
1685
|
-
*/
|
|
1686
|
-
function instrumentPageKey(code, relativeFile, pageKey) {
|
|
1687
|
-
if (code.includes('data-preview-page-key')) return { code, updated: false };
|
|
1688
|
-
const key = pageKey || inferPageKey(relativeFile);
|
|
1689
|
-
|
|
1690
|
-
let ast;
|
|
1691
|
-
try {
|
|
1692
|
-
ast = parseSource(code, relativeFile);
|
|
1693
|
-
} catch {
|
|
1694
|
-
return { code, updated: false };
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
const attribute = () =>
|
|
1698
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key));
|
|
1699
|
-
|
|
1700
|
-
let target = null;
|
|
1701
|
-
recast.types.visit(ast, {
|
|
1702
|
-
visitJSXOpeningElement(pathNode) {
|
|
1703
|
-
const name = pathNode.node.name && pathNode.node.name.name;
|
|
1704
|
-
if (!target && name === 'main') {
|
|
1705
|
-
target = pathNode.node;
|
|
1706
|
-
return false;
|
|
1707
|
-
}
|
|
1708
|
-
this.traverse(pathNode);
|
|
1709
|
-
},
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
if (!target) {
|
|
1713
|
-
// No <main>: use the first host element of the returned tree so the marker
|
|
1714
|
-
// still lands inside this route's exported HTML.
|
|
1715
|
-
recast.types.visit(ast, {
|
|
1716
|
-
visitJSXElement(pathNode) {
|
|
1717
|
-
if (target) return false;
|
|
1718
|
-
const name = getJsxName(pathNode.node);
|
|
1719
|
-
if (typeof name === 'string' && /^[a-z]/.test(name)) {
|
|
1720
|
-
target = pathNode.node.openingElement;
|
|
1721
|
-
return false;
|
|
1722
|
-
}
|
|
1723
|
-
this.traverse(pathNode);
|
|
1724
|
-
},
|
|
1725
|
-
});
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
if (!target) return { code, updated: false };
|
|
1729
|
-
target.attributes = target.attributes || [];
|
|
1730
|
-
target.attributes.unshift(attribute());
|
|
1731
|
-
return { code: printSource(ast, code), updated: true };
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
function inferPageKey(relativeFile) {
|
|
1735
|
-
const posix = toPosix(relativeFile);
|
|
1736
|
-
if (/(^|\/)page\.(tsx|jsx|js)$/.test(posix)) {
|
|
1737
|
-
const parts = posix.split('/');
|
|
1738
|
-
const idx = parts.lastIndexOf('app');
|
|
1739
|
-
const segs = parts.slice(idx + 1, -1).filter((s) => !(s.startsWith('(') && s.endsWith(')')));
|
|
1740
|
-
if (!segs.length) return 'home';
|
|
1741
|
-
return segs.join('_').replace(/[^a-z0-9_-]/gi, '_').toLowerCase();
|
|
1742
|
-
}
|
|
1743
|
-
return 'home';
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
function ensureHtmlBodyHydration(ast) {
|
|
1747
|
-
recast.types.visit(ast, {
|
|
1748
|
-
visitJSXOpeningElement(pathNode) {
|
|
1749
|
-
const name = pathNode.node.name;
|
|
1750
|
-
const tag = name && name.type === 'JSXIdentifier' ? name.name : '';
|
|
1751
|
-
if (tag === 'html' || tag === 'body') {
|
|
1752
|
-
const has = (pathNode.node.attributes || []).some(
|
|
1753
|
-
(attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'suppressHydrationWarning',
|
|
1754
|
-
);
|
|
1755
|
-
if (!has) {
|
|
1756
|
-
pathNode.node.attributes.push(b.jsxAttribute(b.jsxIdentifier('suppressHydrationWarning')));
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
this.traverse(pathNode);
|
|
1760
|
-
},
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
function findSiteDataJsonLocalName(ast) {
|
|
1765
|
-
const program = ast.program || ast;
|
|
1766
|
-
for (const node of program.body || []) {
|
|
1767
|
-
if (node.type !== 'ImportDeclaration') continue;
|
|
1768
|
-
const src = node.source && node.source.value;
|
|
1769
|
-
if (typeof src !== 'string' || !/site-data\.json/.test(src)) continue;
|
|
1770
|
-
const spec = (node.specifiers || []).find((s) => s.type === 'ImportDefaultSpecifier');
|
|
1771
|
-
if (spec && spec.local) return spec.local.name;
|
|
1772
|
-
}
|
|
1773
|
-
return null;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
function ensureProviderInitialData(ast, ident) {
|
|
1777
|
-
let added = false;
|
|
1778
|
-
recast.types.visit(ast, {
|
|
1779
|
-
visitJSXOpeningElement(pathNode) {
|
|
1780
|
-
const name = pathNode.node.name;
|
|
1781
|
-
const tag = name && name.type === 'JSXIdentifier' ? name.name : '';
|
|
1782
|
-
if (tag === 'SiteDataProvider' || tag === 'DenebDataProvider' || tag === 'DenebSiteDataProvider') {
|
|
1783
|
-
const has = hasJsxAttribute(pathNode.node, 'initialSiteData');
|
|
1784
|
-
if (!has) {
|
|
1785
|
-
pathNode.node.attributes = pathNode.node.attributes || [];
|
|
1786
|
-
pathNode.node.attributes.push(
|
|
1787
|
-
b.jsxAttribute(
|
|
1788
|
-
b.jsxIdentifier('initialSiteData'),
|
|
1789
|
-
b.jsxExpressionContainer(b.identifier(ident))
|
|
1790
|
-
)
|
|
1791
|
-
);
|
|
1792
|
-
added = true;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
this.traverse(pathNode);
|
|
1796
|
-
},
|
|
1797
|
-
});
|
|
1798
|
-
return added;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
const CANONICAL_SITE_DATA_CONTEXT = `'use client';
|
|
1802
|
-
|
|
1803
|
-
export {
|
|
1804
|
-
SiteDataProvider,
|
|
1805
|
-
useSiteData,
|
|
1806
|
-
contentText,
|
|
1807
|
-
contentObject,
|
|
1808
|
-
contentList,
|
|
1809
|
-
PREVIEW_DATA_MESSAGE,
|
|
1810
|
-
LEGACY_PREVIEW_DATA_MESSAGE,
|
|
1811
|
-
PREVIEW_READY_MESSAGE,
|
|
1812
|
-
LEGACY_PREVIEW_READY_MESSAGE,
|
|
1813
|
-
PREVIEW_FOCUS_MESSAGE,
|
|
1814
|
-
LEGACY_PREVIEW_FOCUS_MESSAGE,
|
|
1815
|
-
PREVIEW_FIELD_ATTRIBUTE,
|
|
1816
|
-
} from '@deneb-ui/ui';
|
|
1817
|
-
|
|
1818
|
-
export type { SiteData, SiteDataProviderProps } from '@deneb-ui/ui';
|
|
1819
|
-
`;
|
|
1820
|
-
|
|
1821
|
-
function rewriteRecursiveSiteDataContext(code) {
|
|
1822
|
-
const recursive =
|
|
1823
|
-
/export\s+function\s+SiteDataProvider\b/.test(code) &&
|
|
1824
|
-
/<(?:Base)?SiteDataProvider\b/.test(code);
|
|
1825
|
-
if (!recursive) return { code, updated: false };
|
|
1826
|
-
return { code: CANONICAL_SITE_DATA_CONTEXT, updated: true };
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-ui/ui') {
|
|
1830
|
-
const ast = parseSource(code, 'layout.tsx');
|
|
1831
|
-
ensureHtmlBodyHydration(ast);
|
|
1832
|
-
|
|
1833
|
-
let jsonIdent = findSiteDataJsonLocalName(ast);
|
|
1834
|
-
if (!jsonIdent) {
|
|
1835
|
-
jsonIdent = 'initialSiteData';
|
|
1836
|
-
ensureDefaultImport(ast, siteDataImport, jsonIdent);
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
const hasProvider = /SiteDataProvider|DenebDataProvider|<Providers\b/.test(code);
|
|
1840
|
-
let wrapped = hasProvider;
|
|
1841
|
-
|
|
1842
|
-
if (!hasProvider) {
|
|
1843
|
-
ensureImport(ast, providerImport, ['SiteDataProvider']);
|
|
1844
|
-
recast.types.visit(ast, {
|
|
1845
|
-
visitJSXExpressionContainer(pathNode) {
|
|
1846
|
-
if (wrapped) return false;
|
|
1847
|
-
const expr = pathNode.node.expression;
|
|
1848
|
-
if (expr && expr.type === 'Identifier' && expr.name === 'children') {
|
|
1849
|
-
pathNode.replace(
|
|
1850
|
-
b.jsxElement(
|
|
1851
|
-
b.jsxOpeningElement(
|
|
1852
|
-
b.jsxIdentifier('SiteDataProvider'),
|
|
1853
|
-
[
|
|
1854
|
-
b.jsxAttribute(
|
|
1855
|
-
b.jsxIdentifier('initialSiteData'),
|
|
1856
|
-
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
1857
|
-
),
|
|
1858
|
-
],
|
|
1859
|
-
false
|
|
1860
|
-
),
|
|
1861
|
-
b.jsxClosingElement(b.jsxIdentifier('SiteDataProvider')),
|
|
1862
|
-
[pathNode.node],
|
|
1863
|
-
false
|
|
1864
|
-
)
|
|
1865
|
-
);
|
|
1866
|
-
wrapped = true;
|
|
1867
|
-
return false;
|
|
1868
|
-
}
|
|
1869
|
-
this.traverse(pathNode);
|
|
1870
|
-
},
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
if (!wrapped) {
|
|
1874
|
-
recast.types.visit(ast, {
|
|
1875
|
-
visitJSXElement(pathNode) {
|
|
1876
|
-
if (wrapped) return false;
|
|
1877
|
-
const name = getJsxName(pathNode.node);
|
|
1878
|
-
if (name === 'Component') {
|
|
1879
|
-
pathNode.replace(
|
|
1880
|
-
b.jsxElement(
|
|
1881
|
-
b.jsxOpeningElement(
|
|
1882
|
-
b.jsxIdentifier('SiteDataProvider'),
|
|
1883
|
-
[
|
|
1884
|
-
b.jsxAttribute(
|
|
1885
|
-
b.jsxIdentifier('initialSiteData'),
|
|
1886
|
-
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
1887
|
-
),
|
|
1888
|
-
],
|
|
1889
|
-
false
|
|
1890
|
-
),
|
|
1891
|
-
b.jsxClosingElement(b.jsxIdentifier('SiteDataProvider')),
|
|
1892
|
-
[pathNode.node],
|
|
1893
|
-
false
|
|
1894
|
-
)
|
|
1895
|
-
);
|
|
1896
|
-
wrapped = true;
|
|
1897
|
-
return false;
|
|
1898
|
-
}
|
|
1899
|
-
this.traverse(pathNode);
|
|
1900
|
-
},
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
ensureProviderInitialData(ast, jsonIdent);
|
|
1906
|
-
injectPlatformAdditionalPages(ast, providerImport);
|
|
1907
|
-
injectDualModeTheme(ast, jsonIdent, providerImport);
|
|
1908
|
-
sanitizeDuplicateBindings(ast);
|
|
1909
|
-
const next = printSource(ast, code);
|
|
1910
|
-
return { code: next, updated: next !== code };
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
/**
|
|
1914
|
-
* Injects <ThemeStyles /> and <ThemeToggle /> into the layout JSX tree.
|
|
1915
|
-
* ThemeStyles applies light/dark variables and auto-contrast rules.
|
|
1916
|
-
* ThemeToggle provides an out-of-the-box floating theme switcher.
|
|
1917
|
-
*/
|
|
1918
|
-
function injectDualModeTheme(ast, jsonIdent, providerImport = '@deneb-ui/ui') {
|
|
1919
|
-
if (!ast) return;
|
|
1920
|
-
let hasThemeStyles = false;
|
|
1921
|
-
let hasThemeToggle = false;
|
|
1922
|
-
recast.types.visit(ast, {
|
|
1923
|
-
visitJSXIdentifier(pathNode) {
|
|
1924
|
-
if (pathNode.node.name === 'ThemeStyles') hasThemeStyles = true;
|
|
1925
|
-
if (pathNode.node.name === 'ThemeToggle') hasThemeToggle = true;
|
|
1926
|
-
this.traverse(pathNode);
|
|
1927
|
-
},
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
const importsToAdd = [];
|
|
1931
|
-
if (!hasThemeStyles) importsToAdd.push('ThemeStyles');
|
|
1932
|
-
if (!hasThemeToggle) importsToAdd.push('ThemeToggle');
|
|
1933
|
-
if (importsToAdd.length === 0) return;
|
|
1934
|
-
|
|
1935
|
-
if (!hasThemeStyles) {
|
|
1936
|
-
let stylesInjected = false;
|
|
1937
|
-
const snippet = parseSource(
|
|
1938
|
-
`<ThemeStyles theme={${jsonIdent}?.template?.structure?.theme || ${jsonIdent}?.theme} enableDualMode />`,
|
|
1939
|
-
'snippet.tsx'
|
|
1940
|
-
);
|
|
1941
|
-
const stylesEl = snippet.program.body[0].expression;
|
|
1942
|
-
|
|
1943
|
-
// Try <head> first
|
|
1944
|
-
recast.types.visit(ast, {
|
|
1945
|
-
visitJSXElement(pathNode) {
|
|
1946
|
-
if (stylesInjected) return false;
|
|
1947
|
-
const name = getJsxName(pathNode.node);
|
|
1948
|
-
if (name === 'head' || name === 'Head') {
|
|
1949
|
-
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1950
|
-
stylesInjected = true;
|
|
1951
|
-
return false;
|
|
1952
|
-
}
|
|
1953
|
-
this.traverse(pathNode);
|
|
1954
|
-
},
|
|
1955
|
-
});
|
|
1956
|
-
|
|
1957
|
-
// If no <head>, inject inside SiteDataProvider or <body>
|
|
1958
|
-
if (!stylesInjected) {
|
|
1959
|
-
recast.types.visit(ast, {
|
|
1960
|
-
visitJSXElement(pathNode) {
|
|
1961
|
-
if (stylesInjected) return false;
|
|
1962
|
-
const name = getJsxName(pathNode.node);
|
|
1963
|
-
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1964
|
-
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1965
|
-
stylesInjected = true;
|
|
1966
|
-
return false;
|
|
1967
|
-
}
|
|
1968
|
-
this.traverse(pathNode);
|
|
1969
|
-
},
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
if (!hasThemeToggle) {
|
|
1975
|
-
let toggleInjected = false;
|
|
1976
|
-
const snippet = parseSource(
|
|
1977
|
-
'<ThemeToggle showLabel className="fixed bottom-6 left-6 z-40" />',
|
|
1978
|
-
'snippet.tsx'
|
|
1979
|
-
);
|
|
1980
|
-
const toggleEl = snippet.program.body[0].expression;
|
|
1981
|
-
|
|
1982
|
-
recast.types.visit(ast, {
|
|
1983
|
-
visitJSXElement(pathNode) {
|
|
1984
|
-
if (toggleInjected) return false;
|
|
1985
|
-
const name = getJsxName(pathNode.node);
|
|
1986
|
-
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1987
|
-
pathNode.node.children = [...(pathNode.node.children || []), b.jsxText('\n '), toggleEl];
|
|
1988
|
-
toggleInjected = true;
|
|
1989
|
-
return false;
|
|
1990
|
-
}
|
|
1991
|
-
this.traverse(pathNode);
|
|
1992
|
-
},
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
ensureImport(ast, providerImport || '@deneb-ui/ui', importsToAdd);
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
/**
|
|
2000
|
-
* Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
|
|
2001
|
-
* This component handles all additionalPages visual-editing markers correctly
|
|
2002
|
-
* so template developers never need to know the complex nesting rules.
|
|
2003
|
-
*
|
|
2004
|
-
* The injected pattern looks like:
|
|
2005
|
-
* <main>{children}</main>
|
|
2006
|
-
* <PlatformAdditionalPages /> ← injected (inside the SiteDataProvider/body)
|
|
2007
|
-
*
|
|
2008
|
-
* If PlatformAdditionalPages is already in the source, it's a no-op.
|
|
2009
|
-
*/
|
|
2010
|
-
function injectPlatformAdditionalPages(ast, providerImport) {
|
|
2011
|
-
if (!ast) return;
|
|
2012
|
-
// Already injected - skip
|
|
2013
|
-
let alreadyPresent = false;
|
|
2014
|
-
recast.types.visit(ast, {
|
|
2015
|
-
visitJSXIdentifier(pathNode) {
|
|
2016
|
-
if (pathNode.node.name === 'PlatformAdditionalPages') {
|
|
2017
|
-
alreadyPresent = true;
|
|
2018
|
-
return false;
|
|
2019
|
-
}
|
|
2020
|
-
this.traverse(pathNode);
|
|
2021
|
-
},
|
|
2022
|
-
});
|
|
2023
|
-
if (alreadyPresent) return;
|
|
2024
|
-
|
|
2025
|
-
// Inject after the first <main> element in any JSX tree
|
|
2026
|
-
let injected = false;
|
|
2027
|
-
recast.types.visit(ast, {
|
|
2028
|
-
visitJSXElement(pathNode) {
|
|
2029
|
-
if (injected) return false;
|
|
2030
|
-
const name = getJsxName(pathNode.node);
|
|
2031
|
-
if (name === 'main') {
|
|
2032
|
-
const parent = pathNode.parent;
|
|
2033
|
-
if (!parent || !Array.isArray(parent.node.children)) {
|
|
2034
|
-
this.traverse(pathNode);
|
|
2035
|
-
return;
|
|
2036
|
-
}
|
|
2037
|
-
const siblings = parent.node.children;
|
|
2038
|
-
const idx = siblings.indexOf(pathNode.node);
|
|
2039
|
-
if (idx === -1) {
|
|
2040
|
-
this.traverse(pathNode);
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
// Build <PlatformAdditionalPages />
|
|
2044
|
-
const platformElement = b.jsxElement(
|
|
2045
|
-
b.jsxOpeningElement(
|
|
2046
|
-
b.jsxIdentifier('PlatformAdditionalPages'),
|
|
2047
|
-
[],
|
|
2048
|
-
true // self-closing
|
|
2049
|
-
),
|
|
2050
|
-
null,
|
|
2051
|
-
[],
|
|
2052
|
-
true
|
|
2053
|
-
);
|
|
2054
|
-
const newline = b.jsxText('\n ');
|
|
2055
|
-
siblings.splice(idx + 1, 0, newline, platformElement);
|
|
2056
|
-
ensureImport(ast, providerImport || '@deneb-ui/ui', ['PlatformAdditionalPages']);
|
|
2057
|
-
injected = true;
|
|
2058
|
-
return false;
|
|
2059
|
-
}
|
|
2060
|
-
this.traverse(pathNode);
|
|
2061
|
-
},
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
function ensureJsonModule(tsconfig) {
|
|
2066
|
-
if (!tsconfig || !tsconfig.compilerOptions) return { config: tsconfig, changed: false };
|
|
2067
|
-
if (tsconfig.compilerOptions.resolveJsonModule) return { config: tsconfig, changed: false };
|
|
2068
|
-
return {
|
|
2069
|
-
config: {
|
|
2070
|
-
...tsconfig,
|
|
2071
|
-
compilerOptions: {
|
|
2072
|
-
...tsconfig.compilerOptions,
|
|
2073
|
-
resolveJsonModule: true,
|
|
2074
|
-
},
|
|
2075
|
-
},
|
|
2076
|
-
changed: true,
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
function sanitizeContradictoryMarkers(ast) {
|
|
2081
|
-
let cleaned = 0;
|
|
2082
|
-
recast.types.visit(ast, {
|
|
2083
|
-
visitJSXElement(pathNode) {
|
|
2084
|
-
const opening = pathNode.node.openingElement;
|
|
2085
|
-
if (!opening || !Array.isArray(opening.attributes)) {
|
|
2086
|
-
this.traverse(pathNode);
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
const attrs = opening.attributes;
|
|
2090
|
-
const hasStatic = attrs.some(
|
|
2091
|
-
(a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
|
|
2092
|
-
);
|
|
2093
|
-
if (!hasStatic) {
|
|
2094
|
-
this.traverse(pathNode);
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
// 0. Broad container collision: broad content containers cannot carry data-preview-static
|
|
2099
|
-
const tag = getJsxName(pathNode.node);
|
|
2100
|
-
const lower = String(tag || '').toLowerCase();
|
|
2101
|
-
if (BROAD_CONTENT_CONTAINERS.has(tag) || BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
2102
|
-
opening.attributes = attrs.filter(
|
|
2103
|
-
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
2104
|
-
);
|
|
2105
|
-
cleaned++;
|
|
2106
|
-
this.traverse(pathNode);
|
|
2107
|
-
return;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// 1. Direct collision: cannot share element with editable markers
|
|
2111
|
-
const hasDirectEditable = attrs.some(
|
|
2112
|
-
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
2113
|
-
a.name.name === 'data-preview-field-path' ||
|
|
2114
|
-
a.name.name === 'data-preview-list-path' ||
|
|
2115
|
-
a.name.name === 'data-preview-item-path'
|
|
2116
|
-
)
|
|
2117
|
-
);
|
|
2118
|
-
|
|
2119
|
-
// 2. Hierarchical collision: static element cannot enclose editable descendants
|
|
2120
|
-
let hasNestedEditable = false;
|
|
2121
|
-
if (!hasDirectEditable) {
|
|
2122
|
-
recast.types.visit(pathNode.node, {
|
|
2123
|
-
visitJSXElement(inner) {
|
|
2124
|
-
if (inner.node === pathNode.node) {
|
|
2125
|
-
this.traverse(inner);
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
const innerAttrs = inner.node.openingElement?.attributes || [];
|
|
2129
|
-
if (innerAttrs.some(
|
|
2130
|
-
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
2131
|
-
a.name.name === 'data-preview-field-path' ||
|
|
2132
|
-
a.name.name === 'data-preview-list-path' ||
|
|
2133
|
-
a.name.name === 'data-preview-item-path'
|
|
2134
|
-
)
|
|
2135
|
-
)) {
|
|
2136
|
-
hasNestedEditable = true;
|
|
2137
|
-
return false;
|
|
2138
|
-
}
|
|
2139
|
-
this.traverse(inner);
|
|
2140
|
-
},
|
|
2141
|
-
});
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
if (hasDirectEditable || hasNestedEditable) {
|
|
2145
|
-
opening.attributes = attrs.filter(
|
|
2146
|
-
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
2147
|
-
);
|
|
2148
|
-
cleaned++;
|
|
2149
|
-
}
|
|
2150
|
-
this.traverse(pathNode);
|
|
2151
|
-
},
|
|
2152
|
-
});
|
|
2153
|
-
return cleaned;
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
2157
|
-
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')) {
|
|
2158
|
-
return { code, updated: false };
|
|
2159
|
-
}
|
|
2160
|
-
let ast;
|
|
2161
|
-
try {
|
|
2162
|
-
ast = parseSource(code, relativeFile);
|
|
2163
|
-
} catch {
|
|
2164
|
-
return { code, updated: false };
|
|
2165
|
-
}
|
|
2166
|
-
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
2167
|
-
const healedBroad = healBroadContainerMarkers(ast);
|
|
2168
|
-
const healedEmpty = healEmptyStateConditionals(ast);
|
|
2169
|
-
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
2170
|
-
const healedOverflow = healSectionOverflowHidden(ast);
|
|
2171
|
-
const healedOverlay = healDecorativeOverlays(ast);
|
|
2172
|
-
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0 && healedOverflow === 0 && healedOverlay === 0) return { code, updated: false };
|
|
2173
|
-
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden + healedOverflow + healedOverlay };
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
function healBroadContainerMarkers(ast) {
|
|
2177
|
-
let healed = 0;
|
|
2178
|
-
recast.types.visit(ast, {
|
|
2179
|
-
visitJSXElement(pathNode) {
|
|
2180
|
-
const node = pathNode.node;
|
|
2181
|
-
const tag = getJsxName(node);
|
|
2182
|
-
const lower = String(tag || '').toLowerCase();
|
|
2183
|
-
if (!BROAD_CONTENT_CONTAINERS.has(tag) && !BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
2184
|
-
this.traverse(pathNode);
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
const fieldAttr = findJsxAttribute(node, 'data-preview-field-path');
|
|
2188
|
-
if (!fieldAttr) {
|
|
2189
|
-
this.traverse(pathNode);
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
const fieldValue = fieldAttr.value;
|
|
2194
|
-
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
2195
|
-
(attr) =>
|
|
2196
|
-
!(
|
|
2197
|
-
attr.type === 'JSXAttribute' &&
|
|
2198
|
-
attr.name &&
|
|
2199
|
-
(attr.name.name === 'data-preview-field-path' ||
|
|
2200
|
-
attr.name.name === 'data-preview-style-target' ||
|
|
2201
|
-
attr.name.name === 'data-preview-style-type')
|
|
2202
|
-
)
|
|
2203
|
-
);
|
|
2204
|
-
|
|
2205
|
-
const leaf = findFirstLeafChild(node);
|
|
2206
|
-
if (leaf && !hasJsxAttribute(leaf, 'data-preview-field-path')) {
|
|
2207
|
-
leaf.openingElement.attributes.push(
|
|
2208
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)
|
|
2209
|
-
);
|
|
2210
|
-
healed++;
|
|
2211
|
-
} else {
|
|
2212
|
-
const span = b.jsxElement(
|
|
2213
|
-
b.jsxOpeningElement(
|
|
2214
|
-
b.jsxIdentifier('span'),
|
|
2215
|
-
[b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)],
|
|
2216
|
-
false
|
|
2217
|
-
),
|
|
2218
|
-
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
2219
|
-
node.children || [],
|
|
2220
|
-
false
|
|
2221
|
-
);
|
|
2222
|
-
node.children = [span];
|
|
2223
|
-
healed++;
|
|
2224
|
-
}
|
|
2225
|
-
this.traverse(pathNode);
|
|
2226
|
-
},
|
|
2227
|
-
});
|
|
2228
|
-
return healed;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
function findFirstLeafChild(node) {
|
|
2232
|
-
const leafTags = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'img', 'button', 'a', 'strong', 'em', 'small']);
|
|
2233
|
-
for (const child of node.children || []) {
|
|
2234
|
-
if (child && child.type === 'JSXElement') {
|
|
2235
|
-
const name = getJsxName(child);
|
|
2236
|
-
if (leafTags.has(name) || leafTags.has(String(name).toLowerCase())) return child;
|
|
2237
|
-
const nested = findFirstLeafChild(child);
|
|
2238
|
-
if (nested) return nested;
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
return null;
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
|
|
2245
|
-
if (!leftExpr || !rightExpr) return true;
|
|
2246
|
-
|
|
2247
|
-
// 1. Collect all identifiers used on the left-hand side
|
|
2248
|
-
const leftIdentifiers = new Set();
|
|
2249
|
-
recast.types.visit(leftExpr, {
|
|
2250
|
-
visitIdentifier(idPath) {
|
|
2251
|
-
leftIdentifiers.add(idPath.node.name);
|
|
2252
|
-
return false;
|
|
2253
|
-
},
|
|
2254
|
-
});
|
|
2255
|
-
|
|
2256
|
-
if (leftIdentifiers.size === 0) return true;
|
|
2257
|
-
|
|
2258
|
-
// 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
|
|
2259
|
-
const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
|
|
2260
|
-
for (const id of leftIdentifiers) {
|
|
2261
|
-
if (stateKeywordRegex.test(id)) {
|
|
2262
|
-
return true; // NEVER strip modal/state/selection guards
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
// 3. Null / undefined / equality / comparison checks
|
|
2267
|
-
let hasComparisonOrNullCheck = false;
|
|
2268
|
-
recast.types.visit(leftExpr, {
|
|
2269
|
-
visitBinaryExpression(bPath) {
|
|
2270
|
-
const op = bPath.node.operator;
|
|
2271
|
-
if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
|
|
2272
|
-
hasComparisonOrNullCheck = true;
|
|
2273
|
-
}
|
|
2274
|
-
this.traverse(bPath);
|
|
2275
|
-
},
|
|
2276
|
-
visitUnaryExpression(uPath) {
|
|
2277
|
-
if (uPath.node.operator === '!') {
|
|
2278
|
-
hasComparisonOrNullCheck = true;
|
|
2279
|
-
}
|
|
2280
|
-
this.traverse(uPath);
|
|
2281
|
-
},
|
|
2282
|
-
});
|
|
2283
|
-
if (hasComparisonOrNullCheck) return true;
|
|
2284
|
-
|
|
2285
|
-
// 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
|
|
2286
|
-
let rightReferencesLeftId = false;
|
|
2287
|
-
recast.types.visit(rightExpr, {
|
|
2288
|
-
visitIdentifier(idPath) {
|
|
2289
|
-
if (leftIdentifiers.has(idPath.node.name)) {
|
|
2290
|
-
rightReferencesLeftId = true;
|
|
2291
|
-
return false;
|
|
2292
|
-
}
|
|
2293
|
-
this.traverse(idPath);
|
|
2294
|
-
},
|
|
2295
|
-
});
|
|
2296
|
-
if (rightReferencesLeftId) return true;
|
|
2297
|
-
|
|
2298
|
-
// 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
|
|
2299
|
-
let curr = pathNode;
|
|
2300
|
-
while (curr && curr.parent) {
|
|
2301
|
-
const parentNode = curr.parent.node;
|
|
2302
|
-
if (parentNode && parentNode.type === 'JSXElement') {
|
|
2303
|
-
const parentTag = getJsxName(parentNode);
|
|
2304
|
-
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(parentTag)) {
|
|
2305
|
-
return true;
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
curr = curr.parent;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
let isModalOrOverlay = false;
|
|
2312
|
-
recast.types.visit(rightExpr, {
|
|
2313
|
-
visitJSXElement(elPath) {
|
|
2314
|
-
const node = elPath.node;
|
|
2315
|
-
const tag = getJsxName(node);
|
|
2316
|
-
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(tag)) {
|
|
2317
|
-
isModalOrOverlay = true;
|
|
2318
|
-
return false;
|
|
2319
|
-
}
|
|
2320
|
-
const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
|
|
2321
|
-
if (classAttr && classAttr.value) {
|
|
2322
|
-
const val = classAttr.value.value || '';
|
|
2323
|
-
if (typeof val === 'string' && (/fixed\s+inset-0/i.test(val) || /z-(?:50|\[50\]|40)/i.test(val) || /backdrop-blur/i.test(val))) {
|
|
2324
|
-
isModalOrOverlay = true;
|
|
2325
|
-
return false;
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
const roleAttr = findJsxAttribute(node, 'role');
|
|
2329
|
-
if (roleAttr && roleAttr.value && /^(?:dialog|alertdialog)$/i.test(String(roleAttr.value.value || ''))) {
|
|
2330
|
-
isModalOrOverlay = true;
|
|
2331
|
-
return false;
|
|
2332
|
-
}
|
|
2333
|
-
this.traverse(elPath);
|
|
2334
|
-
},
|
|
2335
|
-
});
|
|
2336
|
-
if (isModalOrOverlay) return true;
|
|
2337
|
-
|
|
2338
|
-
// 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
|
|
2339
|
-
const leftCode = recast.print(leftExpr).code;
|
|
2340
|
-
const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
|
|
2341
|
-
if (!isDirectContentCheck) return true;
|
|
2342
|
-
|
|
2343
|
-
return false;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
function healEmptyStateConditionals(ast) {
|
|
2347
|
-
let healed = 0;
|
|
2348
|
-
|
|
2349
|
-
function jsxHasPreviewMarker(node) {
|
|
2350
|
-
if (!node) return false;
|
|
2351
|
-
if (node.type === 'JSXElement') {
|
|
2352
|
-
if (
|
|
2353
|
-
hasJsxAttribute(node, 'data-preview-field-path') ||
|
|
2354
|
-
hasJsxAttribute(node, 'data-preview-list-path') ||
|
|
2355
|
-
hasJsxAttribute(node, 'data-preview-item-path')
|
|
2356
|
-
) {
|
|
2357
|
-
return true;
|
|
2358
|
-
}
|
|
2359
|
-
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
2360
|
-
}
|
|
2361
|
-
if (node.type === 'JSXFragment') {
|
|
2362
|
-
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
2363
|
-
}
|
|
2364
|
-
if (node.type === 'ParenthesizedExpression') return jsxHasPreviewMarker(node.expression);
|
|
2365
|
-
return false;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
recast.types.visit(ast, {
|
|
2369
|
-
visitLogicalExpression(pathNode) {
|
|
2370
|
-
const expr = pathNode.node;
|
|
2371
|
-
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
2372
|
-
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
2373
|
-
pathNode.replace(expr.right);
|
|
2374
|
-
healed++;
|
|
2375
|
-
return false;
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
this.traverse(pathNode);
|
|
2379
|
-
},
|
|
2380
|
-
visitJSXExpressionContainer(pathNode) {
|
|
2381
|
-
const expr = pathNode.node.expression;
|
|
2382
|
-
if (!expr) {
|
|
2383
|
-
this.traverse(pathNode);
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
2387
|
-
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
2388
|
-
pathNode.node.expression = expr.right;
|
|
2389
|
-
healed++;
|
|
2390
|
-
return false;
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
if (
|
|
2394
|
-
expr.type === 'ConditionalExpression' &&
|
|
2395
|
-
jsxHasPreviewMarker(expr.consequent) &&
|
|
2396
|
-
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
2397
|
-
) {
|
|
2398
|
-
if (!isUnsafeToStripCondition(expr.test, expr.consequent, pathNode)) {
|
|
2399
|
-
pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
|
|
2400
|
-
? expr.consequent
|
|
2401
|
-
: expr.alternate;
|
|
2402
|
-
healed++;
|
|
2403
|
-
return false;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
this.traverse(pathNode);
|
|
2407
|
-
},
|
|
2408
|
-
});
|
|
2409
|
-
return healed;
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
function isNullish(node) {
|
|
2413
|
-
if (!node) return true;
|
|
2414
|
-
if (node.type === 'NullLiteral') return true;
|
|
2415
|
-
if (node.type === 'Literal' && (node.value === null || node.value === false || node.value === undefined)) return true;
|
|
2416
|
-
if (node.type === 'BooleanLiteral' && node.value === false) return true;
|
|
2417
|
-
if (node.type === 'Identifier' && (node.name === 'undefined' || node.name === 'null')) return true;
|
|
2418
|
-
return false;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
function healHiddenPreviewMarkers(ast) {
|
|
2422
|
-
let healed = 0;
|
|
2423
|
-
recast.types.visit(ast, {
|
|
2424
|
-
visitJSXOpeningElement(pathNode) {
|
|
2425
|
-
const attrs = pathNode.node.attributes || [];
|
|
2426
|
-
const hasEditable = attrs.some(
|
|
2427
|
-
(attr) =>
|
|
2428
|
-
attr.type === 'JSXAttribute' &&
|
|
2429
|
-
attr.name &&
|
|
2430
|
-
(attr.name.name === 'data-preview-field-path' ||
|
|
2431
|
-
attr.name.name === 'data-preview-list-path' ||
|
|
2432
|
-
attr.name.name === 'data-preview-item-path')
|
|
2433
|
-
);
|
|
2434
|
-
if (!hasEditable) {
|
|
2435
|
-
this.traverse(pathNode);
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
const classAttr = attrs.find((attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'className' || attr.name.name === 'class'));
|
|
2439
|
-
let classText = '';
|
|
2440
|
-
if (classAttr && classAttr.value) {
|
|
2441
|
-
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') classText = String(classAttr.value.value || '');
|
|
2442
|
-
else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression) {
|
|
2443
|
-
const expr = classAttr.value.expression;
|
|
2444
|
-
if (expr.type === 'StringLiteral' || expr.type === 'Literal') classText = String(expr.value || '');
|
|
2445
|
-
if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
const isHidden =
|
|
2449
|
-
attrs.some((attr) => attr.type === 'JSXAttribute' && attr.name && (
|
|
2450
|
-
attr.name.name === 'hidden' ||
|
|
2451
|
-
(attr.name.name === 'aria-hidden' && /true/.test(recast.print(attr).code))
|
|
2452
|
-
)) ||
|
|
2453
|
-
/(?<![\w-])hidden(?![a-zA-Z0-9_-])/.test(classText) ||
|
|
2454
|
-
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));
|
|
2455
|
-
|
|
2456
|
-
if (isHidden) {
|
|
2457
|
-
pathNode.node.attributes = attrs.filter(
|
|
2458
|
-
(attr) =>
|
|
2459
|
-
!(
|
|
2460
|
-
attr.type === 'JSXAttribute' &&
|
|
2461
|
-
attr.name &&
|
|
2462
|
-
(attr.name.name === 'data-preview-field-path' ||
|
|
2463
|
-
attr.name.name === 'data-preview-list-path' ||
|
|
2464
|
-
attr.name.name === 'data-preview-item-path' ||
|
|
2465
|
-
attr.name.name === 'data-preview-style-target' ||
|
|
2466
|
-
attr.name.name === 'data-preview-style-type')
|
|
2467
|
-
)
|
|
2468
|
-
);
|
|
2469
|
-
healed++;
|
|
2470
|
-
}
|
|
2471
|
-
this.traverse(pathNode);
|
|
2472
|
-
},
|
|
2473
|
-
});
|
|
2474
|
-
return healed;
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
function healLegacyProductDetailLinks(ast) {
|
|
2478
|
-
let healed = 0;
|
|
2479
|
-
let needsImport = false;
|
|
2480
|
-
|
|
2481
|
-
recast.types.visit(ast, {
|
|
2482
|
-
visitJSXAttribute(pathNode) {
|
|
2483
|
-
const attr = pathNode.node;
|
|
2484
|
-
if (attr.name && attr.name.name === 'href' && attr.value) {
|
|
2485
|
-
if (attr.value.type === 'JSXExpressionContainer' && attr.value.expression) {
|
|
2486
|
-
const expr = attr.value.expression;
|
|
2487
|
-
if (expr.type === 'TemplateLiteral') {
|
|
2488
|
-
const quasis = expr.quasis || [];
|
|
2489
|
-
if (quasis.length >= 1 && typeof quasis[0].value.raw === 'string' && quasis[0].value.raw.startsWith('/products/')) {
|
|
2490
|
-
const arg = expr.expressions && expr.expressions[0];
|
|
2491
|
-
if (arg) {
|
|
2492
|
-
attr.value.expression = b.callExpression(
|
|
2493
|
-
b.identifier('platformProductDetailHref'),
|
|
2494
|
-
[arg]
|
|
2495
|
-
);
|
|
2496
|
-
healed++;
|
|
2497
|
-
needsImport = true;
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
} else if (
|
|
2501
|
-
expr.type === 'BinaryExpression' &&
|
|
2502
|
-
expr.operator === '+' &&
|
|
2503
|
-
expr.left &&
|
|
2504
|
-
(expr.left.value === '/products/' || expr.left.value === '/products')
|
|
2505
|
-
) {
|
|
2506
|
-
attr.value.expression = b.callExpression(
|
|
2507
|
-
b.identifier('platformProductDetailHref'),
|
|
2508
|
-
[expr.right]
|
|
2509
|
-
);
|
|
2510
|
-
healed++;
|
|
2511
|
-
needsImport = true;
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
this.traverse(pathNode);
|
|
2516
|
-
},
|
|
2517
|
-
});
|
|
2518
|
-
|
|
2519
|
-
if (needsImport) {
|
|
2520
|
-
ensureImport(ast, '@deneb-ui/ui', ['platformProductDetailHref']);
|
|
2521
|
-
}
|
|
2522
|
-
return healed;
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
function healSectionOverflowHidden(ast) {
|
|
2526
|
-
let healed = 0;
|
|
2527
|
-
recast.types.visit(ast, {
|
|
2528
|
-
visitJSXOpeningElement(pathNode) {
|
|
2529
|
-
const node = pathNode.node;
|
|
2530
|
-
const tag = getJsxName(node);
|
|
2531
|
-
const lower = String(tag || '').toLowerCase();
|
|
2532
|
-
const isContainer = lower === 'section' || lower === 'div' || lower === 'main' || lower === 'article';
|
|
2533
|
-
if (!isContainer) {
|
|
2534
|
-
this.traverse(pathNode);
|
|
2535
|
-
return;
|
|
2536
|
-
}
|
|
2537
|
-
const classAttr = (node.attributes || []).find(
|
|
2538
|
-
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
2539
|
-
);
|
|
2540
|
-
if (!classAttr || !classAttr.value) {
|
|
2541
|
-
this.traverse(pathNode);
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
let modified = false;
|
|
2545
|
-
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
2546
|
-
const val = String(classAttr.value.value || '');
|
|
2547
|
-
if (/\boverflow-hidden\b/.test(val)) {
|
|
2548
|
-
classAttr.value.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
2549
|
-
modified = true;
|
|
2550
|
-
}
|
|
2551
|
-
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
2552
|
-
const expr = classAttr.value.expression;
|
|
2553
|
-
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
2554
|
-
const val = String(expr.value || '');
|
|
2555
|
-
if (/\boverflow-hidden\b/.test(val)) {
|
|
2556
|
-
expr.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
2557
|
-
modified = true;
|
|
2558
|
-
}
|
|
2559
|
-
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
2560
|
-
for (const quasi of expr.quasis || []) {
|
|
2561
|
-
if (quasi.value && /\boverflow-hidden\b/.test(quasi.value.raw || '')) {
|
|
2562
|
-
quasi.value.raw = (quasi.value.raw || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
2563
|
-
if (quasi.value.cooked) {
|
|
2564
|
-
quasi.value.cooked = (quasi.value.cooked || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
2565
|
-
}
|
|
2566
|
-
modified = true;
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
if (modified) healed++;
|
|
2572
|
-
this.traverse(pathNode);
|
|
2573
|
-
},
|
|
2574
|
-
});
|
|
2575
|
-
return healed;
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
function healDecorativeOverlays(ast) {
|
|
2579
|
-
let healed = 0;
|
|
2580
|
-
recast.types.visit(ast, {
|
|
2581
|
-
visitJSXElement(pathNode) {
|
|
2582
|
-
const node = pathNode.node;
|
|
2583
|
-
const tag = getJsxName(node);
|
|
2584
|
-
const lower = String(tag || '').toLowerCase();
|
|
2585
|
-
if (lower !== 'div' && lower !== 'span') {
|
|
2586
|
-
this.traverse(pathNode);
|
|
2587
|
-
return;
|
|
2588
|
-
}
|
|
2589
|
-
const hasRealChildren = (node.children || []).some(
|
|
2590
|
-
(c) => c.type === 'JSXElement' || (c.type === 'JSXText' && c.value.trim().length > 0)
|
|
2591
|
-
);
|
|
2592
|
-
if (hasRealChildren) {
|
|
2593
|
-
this.traverse(pathNode);
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2596
|
-
const classAttr = (node.openingElement.attributes || []).find(
|
|
2597
|
-
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
2598
|
-
);
|
|
2599
|
-
if (!classAttr || !classAttr.value) {
|
|
2600
|
-
this.traverse(pathNode);
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
let modified = false;
|
|
2604
|
-
const overlayPattern = /\b(?:bg-gradient-|bg-black\/|bg-white\/|bg-slate-\d+\/|backdrop-blur)/;
|
|
2605
|
-
const isAbsolute = /\babsolute\b/;
|
|
2606
|
-
const hasPointerEvents = /\bpointer-events-(?:none|auto)\b/;
|
|
2607
|
-
|
|
2608
|
-
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
2609
|
-
const val = String(classAttr.value.value || '');
|
|
2610
|
-
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
2611
|
-
classAttr.value.value = `${val} pointer-events-none`;
|
|
2612
|
-
modified = true;
|
|
2613
|
-
}
|
|
2614
|
-
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
2615
|
-
const expr = classAttr.value.expression;
|
|
2616
|
-
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
2617
|
-
const val = String(expr.value || '');
|
|
2618
|
-
if (isAbsolute.test(val) && overlayPattern.test(val) && !hasPointerEvents.test(val)) {
|
|
2619
|
-
expr.value = `${val} pointer-events-none`;
|
|
2620
|
-
modified = true;
|
|
2621
|
-
}
|
|
2622
|
-
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
2623
|
-
const fullText = (expr.quasis || []).map((q) => q.value?.raw || '').join(' ');
|
|
2624
|
-
if (isAbsolute.test(fullText) && overlayPattern.test(fullText) && !hasPointerEvents.test(fullText)) {
|
|
2625
|
-
const lastQuasi = expr.quasis[expr.quasis.length - 1];
|
|
2626
|
-
if (lastQuasi && lastQuasi.value) {
|
|
2627
|
-
lastQuasi.value.raw = `${lastQuasi.value.raw} pointer-events-none`;
|
|
2628
|
-
if (lastQuasi.value.cooked) {
|
|
2629
|
-
lastQuasi.value.cooked = `${lastQuasi.value.cooked} pointer-events-none`;
|
|
2630
|
-
}
|
|
2631
|
-
modified = true;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
if (modified) healed++;
|
|
2637
|
-
this.traverse(pathNode);
|
|
2638
|
-
},
|
|
2639
|
-
});
|
|
2640
|
-
return healed;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
2644
|
-
if (!code.includes('siteData')) return code;
|
|
2645
|
-
try {
|
|
2646
|
-
const ast = parseSource(code, filePath);
|
|
2647
|
-
const injected = injectSiteDataHook(ast);
|
|
2648
|
-
if (injected) {
|
|
2649
|
-
ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
|
|
2650
|
-
return printSource(ast, code);
|
|
2651
|
-
}
|
|
2652
|
-
} catch {}
|
|
2653
|
-
return code;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
function healUnguardedModalConditionals(ast) {
|
|
2657
|
-
let healed = 0;
|
|
2658
|
-
const stateVariables = [];
|
|
2659
|
-
recast.types.visit(ast, {
|
|
2660
|
-
visitVariableDeclarator(pathNode) {
|
|
2661
|
-
const node = pathNode.node;
|
|
2662
|
-
if (node.id && node.id.type === 'ArrayPattern') {
|
|
2663
|
-
const first = node.id.elements[0];
|
|
2664
|
-
if (first && first.type === 'Identifier') {
|
|
2665
|
-
stateVariables.push(first.name);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
this.traverse(pathNode);
|
|
2669
|
-
},
|
|
2670
|
-
});
|
|
2671
|
-
|
|
2672
|
-
if (stateVariables.length === 0) return 0;
|
|
2673
|
-
|
|
2674
|
-
recast.types.visit(ast, {
|
|
2675
|
-
visitJSXElement(pathNode) {
|
|
2676
|
-
const tag = getJsxName(pathNode.node);
|
|
2677
|
-
if (tag === 'AnimatePresence') {
|
|
2678
|
-
for (const child of pathNode.node.children || []) {
|
|
2679
|
-
if (child.type === 'JSXExpressionContainer') {
|
|
2680
|
-
let inner = child.expression;
|
|
2681
|
-
if (inner && inner.type === 'ParenthesizedExpression') {
|
|
2682
|
-
inner = inner.expression;
|
|
2683
|
-
}
|
|
2684
|
-
if (inner && (inner.type === 'JSXElement' || inner.type === 'JSXFragment')) {
|
|
2685
|
-
const innerIds = new Set();
|
|
2686
|
-
recast.types.visit(inner, {
|
|
2687
|
-
visitIdentifier(idPath) {
|
|
2688
|
-
innerIds.add(idPath.node.name);
|
|
2689
|
-
return false;
|
|
2690
|
-
},
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
|
-
const matchingVar = stateVariables.find((sv) => innerIds.has(sv));
|
|
2694
|
-
if (matchingVar) {
|
|
2695
|
-
let guardExpr;
|
|
2696
|
-
if (/index/i.test(matchingVar)) {
|
|
2697
|
-
guardExpr = b.binaryExpression('!==', b.identifier(matchingVar), b.nullLiteral());
|
|
2698
|
-
} else {
|
|
2699
|
-
guardExpr = b.identifier(matchingVar);
|
|
2700
|
-
}
|
|
2701
|
-
child.expression = b.logicalExpression('&&', guardExpr, inner);
|
|
2702
|
-
healed++;
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
this.traverse(pathNode);
|
|
2709
|
-
},
|
|
2710
|
-
});
|
|
2711
|
-
|
|
2712
|
-
return healed;
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
function healUnguardedModalConditionalsInSource(code, filePath = 'file.tsx') {
|
|
2716
|
-
if (!code.includes('AnimatePresence') && !code.includes('fixed inset-0')) return { code, updated: false };
|
|
2717
|
-
try {
|
|
2718
|
-
const ast = parseSource(code, filePath);
|
|
2719
|
-
const healed = healUnguardedModalConditionals(ast);
|
|
2720
|
-
if (healed > 0) {
|
|
2721
|
-
return { code: printSource(ast, code), updated: true, count: healed };
|
|
2722
|
-
}
|
|
2723
|
-
} catch {}
|
|
2724
|
-
return { code, updated: false };
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
module.exports = {
|
|
2728
|
-
applyFilePlan,
|
|
2729
|
-
applyCollectionTransform,
|
|
2730
|
-
instrumentLayoutSource,
|
|
2731
|
-
instrumentPageKey,
|
|
2732
|
-
resolveSiteDataSpecifier,
|
|
2733
|
-
resolveSiteDataRuntimeSpecifier,
|
|
2734
|
-
rewriteRecursiveSiteDataContext,
|
|
2735
|
-
ensureJsonModule,
|
|
2736
|
-
inferPageKey,
|
|
2737
|
-
sanitizeContradictoryMarkers,
|
|
2738
|
-
sanitizeContradictoryMarkersInSource,
|
|
2739
|
-
healBroadContainerMarkers,
|
|
2740
|
-
healEmptyStateConditionals,
|
|
2741
|
-
healHiddenPreviewMarkers,
|
|
2742
|
-
healSectionOverflowHidden,
|
|
2743
|
-
healDecorativeOverlays,
|
|
2744
|
-
healLegacyProductDetailLinks,
|
|
2745
|
-
healMissingSiteDataHooks,
|
|
2746
|
-
healUnguardedModalConditionals,
|
|
2747
|
-
healUnguardedModalConditionalsInSource,
|
|
2748
|
-
injectSiteDataHook,
|
|
2749
|
-
instrumentChildCardComponent,
|
|
2750
|
-
};
|
|
2751
19
|
|
|
20
|
+
module.exports = require('./transforms/index.cjs');
|