@deneb-ui/cli 2.0.84 → 2.0.86
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 +82 -12
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +1251 -0
- package/src/arc/adapters/accordion.cjs +32 -0
- package/src/arc/adapters/dialog.cjs +26 -0
- package/src/arc/adapters/embla.cjs +29 -0
- package/src/arc/adapters/gallery.cjs +23 -0
- package/src/arc/adapters/picture-source.cjs +71 -0
- package/src/arc/adapters/registry.cjs +156 -0
- package/src/arc/adapters/slick.cjs +26 -0
- package/src/arc/adapters/swiper.cjs +29 -0
- package/src/arc/adapters/tabs.cjs +29 -0
- package/src/arc/adapters/ui-frameworks.cjs +140 -0
- package/src/arc/adapters.cjs +20 -151
- package/src/arc/canonical-paths.cjs +95 -0
- package/src/arc/data-flow.cjs +158 -0
- package/src/arc/diff.cjs +136 -0
- package/src/arc/explain.cjs +113 -0
- package/src/arc/field-paths.cjs +25 -13
- package/src/arc/fs-utils.cjs +3 -0
- package/src/arc/index.cjs +126 -7
- package/src/arc/ir-builder.cjs +581 -0
- package/src/arc/manifest.cjs +18 -0
- package/src/arc/planner.cjs +106 -2
- package/src/arc/printer.cjs +18 -5
- package/src/arc/residual.cjs +9 -3
- package/src/arc/rsc-boundary.cjs +154 -0
- package/src/arc/runtime-validator.cjs +286 -0
- package/src/arc/semantic.cjs +217 -16
- package/src/arc/text-fragment-analyzer.cjs +173 -0
- package/src/arc/transformer.cjs +819 -20
- package/src/arc/version.cjs +1 -1
- package/src/tools/deneb-doctor.cjs +53 -9
package/src/arc/transformer.cjs
CHANGED
|
@@ -27,16 +27,28 @@ const {
|
|
|
27
27
|
} = require('./ast.cjs');
|
|
28
28
|
const { toPosix } = require('./fs-utils.cjs');
|
|
29
29
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(':') : [];
|
|
34
41
|
const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
|
|
35
42
|
let found = null;
|
|
36
43
|
recast.types.visit(ast, {
|
|
37
44
|
visitJSXElement(pathNode) {
|
|
38
45
|
const k = locKey(pathNode.node);
|
|
39
|
-
|
|
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}`))) {
|
|
40
52
|
found = pathNode;
|
|
41
53
|
return false;
|
|
42
54
|
}
|
|
@@ -235,7 +247,18 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
235
247
|
ensurePreviewPath(node, transform.field);
|
|
236
248
|
return;
|
|
237
249
|
}
|
|
238
|
-
if (transform.operation === 'extract-image') {
|
|
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
|
+
}
|
|
239
262
|
replaceAttrValue(node, 'src', siteDataBinding(transform.field.split('.'), transform.fallback, 'image'));
|
|
240
263
|
ensurePreviewPath(node, transform.field);
|
|
241
264
|
return;
|
|
@@ -257,6 +280,59 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
257
280
|
}
|
|
258
281
|
return;
|
|
259
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
|
+
}
|
|
260
336
|
if (transform.operation === 'wrap-text-span') {
|
|
261
337
|
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
262
338
|
return;
|
|
@@ -283,6 +359,33 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
283
359
|
return;
|
|
284
360
|
}
|
|
285
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;
|
|
286
389
|
}
|
|
287
390
|
}
|
|
288
391
|
|
|
@@ -392,6 +495,22 @@ function applyCollectionTransform(ast, transform, isClient = false, isTypeScript
|
|
|
392
495
|
const itemRoot = findElementByLoc(ast, transform.loc);
|
|
393
496
|
if (!itemRoot) return false;
|
|
394
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
|
+
|
|
395
514
|
if (!hasJsxAttribute(itemRoot.node, 'data-preview-item-path')) {
|
|
396
515
|
itemRoot.node.openingElement.attributes.push(
|
|
397
516
|
jsxTemplatePathAttr('data-preview-item-path', listPath, indexName)
|
|
@@ -416,7 +535,9 @@ function applyCollectionTransform(ast, transform, isClient = false, isTypeScript
|
|
|
416
535
|
} else {
|
|
417
536
|
markItemFields(callback, { listPath, binding, indexName });
|
|
418
537
|
}
|
|
419
|
-
|
|
538
|
+
if (!isCustomChild) {
|
|
539
|
+
markComponentRefItemsStatic(callback, binding);
|
|
540
|
+
}
|
|
420
541
|
|
|
421
542
|
const container = findListContainer(mapCall);
|
|
422
543
|
if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
|
|
@@ -615,6 +736,469 @@ function wrapPrimitiveItemFieldInSpan(node, textChild, listPath, indexName) {
|
|
|
615
736
|
node.children = nextChildren;
|
|
616
737
|
}
|
|
617
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
|
+
|
|
618
1202
|
/** The nearest JSX element that wraps the map expression. */
|
|
619
1203
|
function findListContainer(mapCallPath) {
|
|
620
1204
|
let current = mapCallPath.parent;
|
|
@@ -973,7 +1557,8 @@ function applyFilePlan(filePlan, profile) {
|
|
|
973
1557
|
}
|
|
974
1558
|
|
|
975
1559
|
const ast = parseSource(filePlan.originalCode, filePlan.file);
|
|
976
|
-
|
|
1560
|
+
let isClient = hasDirective(ast, 'use client') || hasClientDirective(ast) || /['"]use client['"]/.test(filePlan.originalCode.slice(0, 400));
|
|
1561
|
+
const hasMetadata = hasMetadataExport(ast);
|
|
977
1562
|
let applied = 0;
|
|
978
1563
|
const failures = [];
|
|
979
1564
|
|
|
@@ -982,6 +1567,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
982
1567
|
'form-submit-action',
|
|
983
1568
|
'extract-url',
|
|
984
1569
|
'extract-image',
|
|
1570
|
+
'extract-picture',
|
|
985
1571
|
'extract-alt',
|
|
986
1572
|
'extract-placeholder',
|
|
987
1573
|
'extract-text',
|
|
@@ -989,6 +1575,12 @@ function applyFilePlan(filePlan, profile) {
|
|
|
989
1575
|
'wrap-text-span',
|
|
990
1576
|
'collection-conversion',
|
|
991
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',
|
|
992
1584
|
]);
|
|
993
1585
|
|
|
994
1586
|
const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
|
|
@@ -1005,6 +1597,24 @@ function applyFilePlan(filePlan, profile) {
|
|
|
1005
1597
|
if (transform.decision === 'skip') continue;
|
|
1006
1598
|
if (!supported.has(transform.operation)) continue;
|
|
1007
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
|
+
|
|
1008
1618
|
if (transform.operation === 'collection-conversion') {
|
|
1009
1619
|
try {
|
|
1010
1620
|
if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
|
|
@@ -1015,7 +1625,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
1015
1625
|
continue;
|
|
1016
1626
|
}
|
|
1017
1627
|
|
|
1018
|
-
const pathNode = findElementByLoc(ast, transform.loc);
|
|
1628
|
+
const pathNode = findElementByLoc(ast, transform.loc, transform.componentName || transform.tag);
|
|
1019
1629
|
if (!pathNode) {
|
|
1020
1630
|
failures.push({ loc: transform.loc, reason: 'node-not-found' });
|
|
1021
1631
|
continue;
|
|
@@ -1032,6 +1642,13 @@ function applyFilePlan(filePlan, profile) {
|
|
|
1032
1642
|
return { code: filePlan.originalCode, changed: false, applied, failures };
|
|
1033
1643
|
}
|
|
1034
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
|
+
|
|
1035
1652
|
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
1036
1653
|
if (isClient) {
|
|
1037
1654
|
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
@@ -1624,6 +2241,108 @@ function findFirstLeafChild(node) {
|
|
|
1624
2241
|
return null;
|
|
1625
2242
|
}
|
|
1626
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
|
+
|
|
1627
2346
|
function healEmptyStateConditionals(ast) {
|
|
1628
2347
|
let healed = 0;
|
|
1629
2348
|
|
|
@@ -1650,9 +2369,11 @@ function healEmptyStateConditionals(ast) {
|
|
|
1650
2369
|
visitLogicalExpression(pathNode) {
|
|
1651
2370
|
const expr = pathNode.node;
|
|
1652
2371
|
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2372
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
2373
|
+
pathNode.replace(expr.right);
|
|
2374
|
+
healed++;
|
|
2375
|
+
return false;
|
|
2376
|
+
}
|
|
1656
2377
|
}
|
|
1657
2378
|
this.traverse(pathNode);
|
|
1658
2379
|
},
|
|
@@ -1663,20 +2384,24 @@ function healEmptyStateConditionals(ast) {
|
|
|
1663
2384
|
return;
|
|
1664
2385
|
}
|
|
1665
2386
|
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2387
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
2388
|
+
pathNode.node.expression = expr.right;
|
|
2389
|
+
healed++;
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
1669
2392
|
}
|
|
1670
2393
|
if (
|
|
1671
2394
|
expr.type === 'ConditionalExpression' &&
|
|
1672
2395
|
jsxHasPreviewMarker(expr.consequent) &&
|
|
1673
2396
|
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
1674
2397
|
) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
+
}
|
|
1680
2405
|
}
|
|
1681
2406
|
this.traverse(pathNode);
|
|
1682
2407
|
},
|
|
@@ -1928,6 +2653,77 @@ function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
|
1928
2653
|
return code;
|
|
1929
2654
|
}
|
|
1930
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
|
+
|
|
1931
2727
|
module.exports = {
|
|
1932
2728
|
applyFilePlan,
|
|
1933
2729
|
applyCollectionTransform,
|
|
@@ -1947,6 +2743,9 @@ module.exports = {
|
|
|
1947
2743
|
healDecorativeOverlays,
|
|
1948
2744
|
healLegacyProductDetailLinks,
|
|
1949
2745
|
healMissingSiteDataHooks,
|
|
2746
|
+
healUnguardedModalConditionals,
|
|
2747
|
+
healUnguardedModalConditionalsInSource,
|
|
1950
2748
|
injectSiteDataHook,
|
|
2749
|
+
instrumentChildCardComponent,
|
|
1951
2750
|
};
|
|
1952
2751
|
|