@deneb-ui/cli 2.0.70 → 2.0.72
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 +68 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +242 -2
- package/src/arc/field-paths.cjs +3 -3
- package/src/arc/fivora-contract.cjs +2 -2
- package/src/arc/learning.cjs +9 -1
- package/src/arc/manifest.cjs +58 -8
- package/src/arc/planner.cjs +10 -0
- package/src/arc/residual.cjs +37 -6
- package/src/arc/semantic.cjs +67 -0
- package/src/arc/transformer.cjs +375 -49
- package/src/common/template-visual-edit-contract.ts +1 -1
- package/src/platform/platform-contract.json +1 -0
- package/src/tools/deneb-doctor.cjs +298 -10
- package/src/tools/deneb-template-validator.cjs +2 -2
package/src/arc/transformer.cjs
CHANGED
|
@@ -27,10 +27,14 @@ const { toPosix } = require('./fs-utils.cjs');
|
|
|
27
27
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
28
28
|
|
|
29
29
|
function findElementByLoc(ast, loc) {
|
|
30
|
+
if (!loc) return null;
|
|
31
|
+
const parts = loc.split(':');
|
|
32
|
+
const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
|
|
30
33
|
let found = null;
|
|
31
34
|
recast.types.visit(ast, {
|
|
32
35
|
visitJSXElement(pathNode) {
|
|
33
|
-
|
|
36
|
+
const k = locKey(pathNode.node);
|
|
37
|
+
if (k === targetLoc || k === loc) {
|
|
34
38
|
found = pathNode;
|
|
35
39
|
return false;
|
|
36
40
|
}
|
|
@@ -243,6 +247,14 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
243
247
|
ensurePreviewPath(node, transform.field);
|
|
244
248
|
return;
|
|
245
249
|
}
|
|
250
|
+
if (transform.operation === 'extract-prop') {
|
|
251
|
+
const propName = transform.propName || 'title';
|
|
252
|
+
replaceAttrValue(node, propName, siteDataBinding(transform.field.split('.'), transform.fallback, transform.fieldType || 'text'));
|
|
253
|
+
if (!hasJsxAttribute(node, 'data-preview-field-path') && !hasJsxAttribute(node, 'data-preview-list-path')) {
|
|
254
|
+
ensurePreviewPath(node, transform.field);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
246
258
|
if (transform.operation === 'wrap-text-span') {
|
|
247
259
|
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
248
260
|
return;
|
|
@@ -313,6 +325,20 @@ function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
|
313
325
|
if (wrapped) node.children = nextChildren;
|
|
314
326
|
}
|
|
315
327
|
|
|
328
|
+
function buildCompositeKeyAttribute(binding, indexName) {
|
|
329
|
+
const fields = ['id', 'slug', 'title', 'name'];
|
|
330
|
+
let expr = b.memberExpression(b.identifier(binding), b.identifier(fields[0]), false);
|
|
331
|
+
for (let i = 1; i < fields.length; i++) {
|
|
332
|
+
expr = b.logicalExpression(
|
|
333
|
+
'||',
|
|
334
|
+
expr,
|
|
335
|
+
b.memberExpression(b.identifier(binding), b.identifier(fields[i]), false)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
expr = b.logicalExpression('||', expr, b.identifier(indexName));
|
|
339
|
+
return b.jsxAttribute(b.jsxIdentifier('key'), b.jsxExpressionContainer(expr));
|
|
340
|
+
}
|
|
341
|
+
|
|
316
342
|
/**
|
|
317
343
|
* Converts a literal-array `.map()` render into a Fivora list contract.
|
|
318
344
|
*
|
|
@@ -321,7 +347,7 @@ function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
|
321
347
|
* emitted as JSX template literals (`items[${index}].title`) so added and
|
|
322
348
|
* reordered items stay editable, which is what strict mode requires.
|
|
323
349
|
*/
|
|
324
|
-
function applyCollectionTransform(ast, transform, isClient = false) {
|
|
350
|
+
function applyCollectionTransform(ast, transform, isClient = false, isTypeScript = false) {
|
|
325
351
|
const listPath = transform.listField;
|
|
326
352
|
const binding = transform.itemParam;
|
|
327
353
|
if (!listPath || !binding) return false;
|
|
@@ -335,8 +361,30 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
335
361
|
// The contract needs a concrete index for each item marker.
|
|
336
362
|
let indexName = transform.indexParam;
|
|
337
363
|
if (!indexName) {
|
|
338
|
-
|
|
339
|
-
|
|
364
|
+
if (callback.params && callback.params.length >= 2 && callback.params[1]?.type === 'Identifier') {
|
|
365
|
+
indexName = callback.params[1].name;
|
|
366
|
+
} else {
|
|
367
|
+
indexName = callback.params.some((p) => p?.name === 'index') ? 'denebIndex' : 'index';
|
|
368
|
+
const indexId = b.identifier(indexName);
|
|
369
|
+
if (isTypeScript) {
|
|
370
|
+
indexId.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
371
|
+
}
|
|
372
|
+
callback.params.push(indexId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// In TypeScript files, prevent TS7006 implicit any on callback parameters
|
|
377
|
+
if (isTypeScript && callback.params && callback.params.length > 0) {
|
|
378
|
+
const itemParam = callback.params[0];
|
|
379
|
+
if (itemParam && !itemParam.typeAnnotation) {
|
|
380
|
+
itemParam.typeAnnotation = b.tsTypeAnnotation(b.tsAnyKeyword());
|
|
381
|
+
}
|
|
382
|
+
if (callback.params.length >= 2) {
|
|
383
|
+
const idxParam = callback.params[1];
|
|
384
|
+
if (idxParam && !idxParam.typeAnnotation) {
|
|
385
|
+
idxParam.typeAnnotation = b.tsTypeAnnotation(b.tsNumberKeyword());
|
|
386
|
+
}
|
|
387
|
+
}
|
|
340
388
|
}
|
|
341
389
|
|
|
342
390
|
const itemRoot = findElementByLoc(ast, transform.loc);
|
|
@@ -355,6 +403,11 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
355
403
|
b.jsxAttribute(b.jsxIdentifier('data-preview-style-type'), b.stringLiteral('card'))
|
|
356
404
|
);
|
|
357
405
|
}
|
|
406
|
+
if (!hasJsxAttribute(itemRoot.node, 'key')) {
|
|
407
|
+
itemRoot.node.openingElement.attributes.push(
|
|
408
|
+
buildCompositeKeyAttribute(binding, indexName)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
358
411
|
|
|
359
412
|
markItemFields(callback, { listPath, binding, indexName });
|
|
360
413
|
markComponentRefItemsStatic(callback, binding);
|
|
@@ -374,7 +427,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
|
|
|
374
427
|
);
|
|
375
428
|
}
|
|
376
429
|
|
|
377
|
-
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef));
|
|
430
|
+
return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef), isTypeScript);
|
|
378
431
|
}
|
|
379
432
|
|
|
380
433
|
function findMapCall(ast, loc) {
|
|
@@ -395,7 +448,7 @@ function findMapCall(ast, loc) {
|
|
|
395
448
|
hit = true;
|
|
396
449
|
return false;
|
|
397
450
|
}
|
|
398
|
-
|
|
451
|
+
this.traverse(inner);
|
|
399
452
|
},
|
|
400
453
|
});
|
|
401
454
|
if (hit) {
|
|
@@ -543,7 +596,7 @@ function findEnclosingFunction(pathNode) {
|
|
|
543
596
|
return null;
|
|
544
597
|
}
|
|
545
598
|
|
|
546
|
-
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false) {
|
|
599
|
+
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false, isTypeScript = false) {
|
|
547
600
|
const arrayName = mapCallPath.node.callee.object?.name;
|
|
548
601
|
if (!arrayName) return false;
|
|
549
602
|
let bound = false;
|
|
@@ -568,9 +621,13 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, merg
|
|
|
568
621
|
const body = fnPath.node.body.body;
|
|
569
622
|
const already = body.some((stmt) => recast.print(stmt).code.includes(`const ${arrayName} =`));
|
|
570
623
|
if (!already) {
|
|
624
|
+
const localId = b.identifier(arrayName);
|
|
625
|
+
if (isTypeScript) {
|
|
626
|
+
localId.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
627
|
+
}
|
|
571
628
|
const localDecl = b.variableDeclaration('const', [
|
|
572
629
|
b.variableDeclarator(
|
|
573
|
-
|
|
630
|
+
localId,
|
|
574
631
|
mergeDefaultRefs
|
|
575
632
|
? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
|
|
576
633
|
: siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
@@ -588,6 +645,9 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, merg
|
|
|
588
645
|
return false;
|
|
589
646
|
}
|
|
590
647
|
|
|
648
|
+
if (isTypeScript && node.id?.type === 'Identifier' && !node.id.typeAnnotation) {
|
|
649
|
+
node.id.typeAnnotation = b.tsTypeAnnotation(b.tsArrayType(b.tsAnyKeyword()));
|
|
650
|
+
}
|
|
591
651
|
node.init = siteDataListBinding(listPath.split('.'), node.init);
|
|
592
652
|
bound = true;
|
|
593
653
|
return false;
|
|
@@ -656,15 +716,41 @@ function resolveSiteDataRuntimeSpecifier(profile) {
|
|
|
656
716
|
return '@deneb-ui/ui';
|
|
657
717
|
}
|
|
658
718
|
|
|
719
|
+
function functionReferencesSiteData(fn) {
|
|
720
|
+
if (!fn || !fn.body) return false;
|
|
721
|
+
let found = false;
|
|
722
|
+
recast.types.visit(fn.body, {
|
|
723
|
+
visitIdentifier(pathNode) {
|
|
724
|
+
if (pathNode.node.name === 'siteData') {
|
|
725
|
+
found = true;
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
this.traverse(pathNode);
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
return found;
|
|
732
|
+
}
|
|
733
|
+
|
|
659
734
|
function injectSiteDataHook(ast) {
|
|
660
|
-
const program = ast.program || ast;
|
|
661
735
|
let injected = false;
|
|
736
|
+
let anyFunctionReferencedSiteData = false;
|
|
737
|
+
let alreadyHasUseSiteData = false;
|
|
738
|
+
|
|
739
|
+
recast.types.visit(ast, {
|
|
740
|
+
visitCallExpression(pathNode) {
|
|
741
|
+
if (pathNode.node.callee && pathNode.node.callee.name === 'useSiteData') {
|
|
742
|
+
alreadyHasUseSiteData = true;
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
this.traverse(pathNode);
|
|
746
|
+
},
|
|
747
|
+
});
|
|
662
748
|
|
|
663
749
|
function injectIntoFunction(fn) {
|
|
664
750
|
if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
|
|
665
751
|
const body = fn.body.body;
|
|
666
752
|
const already = body.some((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
667
|
-
if (already) return
|
|
753
|
+
if (already) return false;
|
|
668
754
|
const hook = b.variableDeclaration('const', [
|
|
669
755
|
b.variableDeclarator(
|
|
670
756
|
b.identifier('siteData'),
|
|
@@ -672,40 +758,70 @@ function injectSiteDataHook(ast) {
|
|
|
672
758
|
),
|
|
673
759
|
]);
|
|
674
760
|
body.unshift(hook);
|
|
761
|
+
alreadyHasUseSiteData = true;
|
|
675
762
|
return true;
|
|
676
763
|
}
|
|
677
764
|
|
|
765
|
+
// Pass 1: Inject useSiteData() into EVERY component function that references siteData
|
|
678
766
|
recast.types.visit(ast, {
|
|
679
|
-
|
|
680
|
-
if (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
injected = injectIntoFunction(decl);
|
|
767
|
+
visitFunctionDeclaration(pathNode) {
|
|
768
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
769
|
+
anyFunctionReferencedSiteData = true;
|
|
770
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
684
771
|
}
|
|
685
772
|
this.traverse(pathNode);
|
|
686
773
|
},
|
|
687
|
-
|
|
688
|
-
if (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
injected = injectIntoFunction(pathNode.node);
|
|
692
|
-
return false;
|
|
774
|
+
visitFunctionExpression(pathNode) {
|
|
775
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
776
|
+
anyFunctionReferencedSiteData = true;
|
|
777
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
693
778
|
}
|
|
694
779
|
this.traverse(pathNode);
|
|
695
780
|
},
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
701
|
-
injected = injectIntoFunction(init);
|
|
702
|
-
return false;
|
|
781
|
+
visitArrowFunctionExpression(pathNode) {
|
|
782
|
+
if (functionReferencesSiteData(pathNode.node)) {
|
|
783
|
+
anyFunctionReferencedSiteData = true;
|
|
784
|
+
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
703
785
|
}
|
|
704
786
|
this.traverse(pathNode);
|
|
705
787
|
},
|
|
706
788
|
});
|
|
707
789
|
|
|
708
|
-
|
|
790
|
+
// Pass 2: If no component references siteData yet (e.g. before subsequent AST edits),
|
|
791
|
+
// ensure the primary component receives useSiteData()
|
|
792
|
+
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
793
|
+
recast.types.visit(ast, {
|
|
794
|
+
visitExportDefaultDeclaration(pathNode) {
|
|
795
|
+
if (injected) return false;
|
|
796
|
+
const decl = pathNode.node.declaration;
|
|
797
|
+
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
798
|
+
injected = injectIntoFunction(decl);
|
|
799
|
+
}
|
|
800
|
+
this.traverse(pathNode);
|
|
801
|
+
},
|
|
802
|
+
visitFunctionDeclaration(pathNode) {
|
|
803
|
+
if (injected) return false;
|
|
804
|
+
const name = pathNode.node.id && pathNode.node.id.name;
|
|
805
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
806
|
+
injected = injectIntoFunction(pathNode.node);
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
this.traverse(pathNode);
|
|
810
|
+
},
|
|
811
|
+
visitVariableDeclarator(pathNode) {
|
|
812
|
+
if (injected) return false;
|
|
813
|
+
const id = pathNode.node.id;
|
|
814
|
+
const init = pathNode.node.init;
|
|
815
|
+
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
816
|
+
injected = injectIntoFunction(init);
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
this.traverse(pathNode);
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
709
825
|
recast.types.visit(ast, {
|
|
710
826
|
visitFunctionDeclaration(pathNode) {
|
|
711
827
|
if (injected) return false;
|
|
@@ -719,13 +835,15 @@ function injectSiteDataHook(ast) {
|
|
|
719
835
|
}
|
|
720
836
|
|
|
721
837
|
function resolveSiteDataSpecifier(profile, fromRelativeFile) {
|
|
722
|
-
const aliases = profile.aliasMap || {};
|
|
838
|
+
const aliases = (profile && profile.aliasMap) || {};
|
|
723
839
|
const hasAt = Object.keys(aliases).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
724
|
-
const
|
|
725
|
-
|
|
840
|
+
const hasSrc = Boolean(profile && profile.hasSrc);
|
|
841
|
+
const siteDataRel = hasSrc ? 'src/data/site-data.json' : 'data/site-data.json';
|
|
842
|
+
if (hasAt && hasSrc) return '@/data/site-data.json';
|
|
726
843
|
|
|
727
|
-
const
|
|
728
|
-
const
|
|
844
|
+
const root = profile && profile.root ? profile.root : process.cwd();
|
|
845
|
+
const fromAbs = path.join(root, fromRelativeFile || 'page.tsx');
|
|
846
|
+
const toAbs = path.join(root, siteDataRel);
|
|
729
847
|
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
730
848
|
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
731
849
|
return relSpec;
|
|
@@ -749,11 +867,14 @@ function applyFilePlan(filePlan, profile) {
|
|
|
749
867
|
'extract-alt',
|
|
750
868
|
'extract-placeholder',
|
|
751
869
|
'extract-text',
|
|
870
|
+
'extract-prop',
|
|
752
871
|
'wrap-text-span',
|
|
753
872
|
'collection-conversion',
|
|
754
873
|
'style-bind',
|
|
755
874
|
]);
|
|
756
875
|
|
|
876
|
+
const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
|
|
877
|
+
|
|
757
878
|
// Collections run first: they rewrite the array declaration and add an index
|
|
758
879
|
// parameter, and later per-element edits must observe that shape.
|
|
759
880
|
const ordered = [...filePlan.transformations].sort(
|
|
@@ -768,7 +889,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
768
889
|
|
|
769
890
|
if (transform.operation === 'collection-conversion') {
|
|
770
891
|
try {
|
|
771
|
-
if (applyCollectionTransform(ast, transform, isClient)) applied++;
|
|
892
|
+
if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
|
|
772
893
|
else failures.push({ loc: transform.loc, reason: 'collection-not-bindable' });
|
|
773
894
|
} catch (err) {
|
|
774
895
|
failures.push({ loc: transform.loc, reason: err.message });
|
|
@@ -796,9 +917,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
796
917
|
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
797
918
|
if (isClient) {
|
|
798
919
|
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
799
|
-
|
|
800
|
-
injectSiteDataHook(ast);
|
|
801
|
-
}
|
|
920
|
+
injectSiteDataHook(ast);
|
|
802
921
|
ensureImport(ast, runtimeSpecifier, ['useSiteData']);
|
|
803
922
|
} else {
|
|
804
923
|
ensureDefaultImport(ast, siteDataImport, 'siteData');
|
|
@@ -1048,11 +1167,98 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
|
|
|
1048
1167
|
|
|
1049
1168
|
ensureProviderInitialData(ast, jsonIdent);
|
|
1050
1169
|
injectPlatformAdditionalPages(ast, providerImport);
|
|
1170
|
+
injectDualModeTheme(ast, jsonIdent, providerImport);
|
|
1051
1171
|
sanitizeDuplicateBindings(ast);
|
|
1052
1172
|
const next = printSource(ast, code);
|
|
1053
1173
|
return { code: next, updated: next !== code };
|
|
1054
1174
|
}
|
|
1055
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* Injects <ThemeStyles /> and <ThemeToggle /> into the layout JSX tree.
|
|
1178
|
+
* ThemeStyles applies light/dark variables and auto-contrast rules.
|
|
1179
|
+
* ThemeToggle provides an out-of-the-box floating theme switcher.
|
|
1180
|
+
*/
|
|
1181
|
+
function injectDualModeTheme(ast, jsonIdent, providerImport = '@deneb-ui/ui') {
|
|
1182
|
+
if (!ast) return;
|
|
1183
|
+
let hasThemeStyles = false;
|
|
1184
|
+
let hasThemeToggle = false;
|
|
1185
|
+
recast.types.visit(ast, {
|
|
1186
|
+
visitJSXIdentifier(pathNode) {
|
|
1187
|
+
if (pathNode.node.name === 'ThemeStyles') hasThemeStyles = true;
|
|
1188
|
+
if (pathNode.node.name === 'ThemeToggle') hasThemeToggle = true;
|
|
1189
|
+
this.traverse(pathNode);
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
const importsToAdd = [];
|
|
1194
|
+
if (!hasThemeStyles) importsToAdd.push('ThemeStyles');
|
|
1195
|
+
if (!hasThemeToggle) importsToAdd.push('ThemeToggle');
|
|
1196
|
+
if (importsToAdd.length === 0) return;
|
|
1197
|
+
|
|
1198
|
+
if (!hasThemeStyles) {
|
|
1199
|
+
let stylesInjected = false;
|
|
1200
|
+
const snippet = parseSource(
|
|
1201
|
+
`<ThemeStyles theme={${jsonIdent}?.template?.structure?.theme || ${jsonIdent}?.theme} enableDualMode />`,
|
|
1202
|
+
'snippet.tsx'
|
|
1203
|
+
);
|
|
1204
|
+
const stylesEl = snippet.program.body[0].expression;
|
|
1205
|
+
|
|
1206
|
+
// Try <head> first
|
|
1207
|
+
recast.types.visit(ast, {
|
|
1208
|
+
visitJSXElement(pathNode) {
|
|
1209
|
+
if (stylesInjected) return false;
|
|
1210
|
+
const name = getJsxName(pathNode.node);
|
|
1211
|
+
if (name === 'head' || name === 'Head') {
|
|
1212
|
+
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1213
|
+
stylesInjected = true;
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
this.traverse(pathNode);
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// If no <head>, inject inside SiteDataProvider or <body>
|
|
1221
|
+
if (!stylesInjected) {
|
|
1222
|
+
recast.types.visit(ast, {
|
|
1223
|
+
visitJSXElement(pathNode) {
|
|
1224
|
+
if (stylesInjected) return false;
|
|
1225
|
+
const name = getJsxName(pathNode.node);
|
|
1226
|
+
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1227
|
+
pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
|
|
1228
|
+
stylesInjected = true;
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
this.traverse(pathNode);
|
|
1232
|
+
},
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (!hasThemeToggle) {
|
|
1238
|
+
let toggleInjected = false;
|
|
1239
|
+
const snippet = parseSource(
|
|
1240
|
+
'<ThemeToggle showLabel className="fixed bottom-6 left-6 z-40" />',
|
|
1241
|
+
'snippet.tsx'
|
|
1242
|
+
);
|
|
1243
|
+
const toggleEl = snippet.program.body[0].expression;
|
|
1244
|
+
|
|
1245
|
+
recast.types.visit(ast, {
|
|
1246
|
+
visitJSXElement(pathNode) {
|
|
1247
|
+
if (toggleInjected) return false;
|
|
1248
|
+
const name = getJsxName(pathNode.node);
|
|
1249
|
+
if (name === 'SiteDataProvider' || name === 'body') {
|
|
1250
|
+
pathNode.node.children = [...(pathNode.node.children || []), b.jsxText('\n '), toggleEl];
|
|
1251
|
+
toggleInjected = true;
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
this.traverse(pathNode);
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
ensureImport(ast, providerImport || '@deneb-ui/ui', importsToAdd);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1056
1262
|
/**
|
|
1057
1263
|
* Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
|
|
1058
1264
|
* This component handles all additionalPages visual-editing markers correctly
|
|
@@ -1137,20 +1343,69 @@ function ensureJsonModule(tsconfig) {
|
|
|
1137
1343
|
function sanitizeContradictoryMarkers(ast) {
|
|
1138
1344
|
let cleaned = 0;
|
|
1139
1345
|
recast.types.visit(ast, {
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1346
|
+
visitJSXElement(pathNode) {
|
|
1347
|
+
const opening = pathNode.node.openingElement;
|
|
1348
|
+
if (!opening || !Array.isArray(opening.attributes)) {
|
|
1349
|
+
this.traverse(pathNode);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const attrs = opening.attributes;
|
|
1353
|
+
const hasStatic = attrs.some(
|
|
1354
|
+
(a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
|
|
1355
|
+
);
|
|
1356
|
+
if (!hasStatic) {
|
|
1357
|
+
this.traverse(pathNode);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 0. Broad container collision: broad content containers cannot carry data-preview-static
|
|
1362
|
+
const tag = getJsxName(pathNode.node);
|
|
1363
|
+
const lower = String(tag || '').toLowerCase();
|
|
1364
|
+
if (BROAD_CONTENT_CONTAINERS.has(tag) || BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
1365
|
+
opening.attributes = attrs.filter(
|
|
1366
|
+
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
1367
|
+
);
|
|
1368
|
+
cleaned++;
|
|
1369
|
+
this.traverse(pathNode);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 1. Direct collision: cannot share element with editable markers
|
|
1374
|
+
const hasDirectEditable = attrs.some(
|
|
1143
1375
|
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
1144
1376
|
a.name.name === 'data-preview-field-path' ||
|
|
1145
1377
|
a.name.name === 'data-preview-list-path' ||
|
|
1146
1378
|
a.name.name === 'data-preview-item-path'
|
|
1147
1379
|
)
|
|
1148
1380
|
);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (
|
|
1153
|
-
pathNode.node
|
|
1381
|
+
|
|
1382
|
+
// 2. Hierarchical collision: static element cannot enclose editable descendants
|
|
1383
|
+
let hasNestedEditable = false;
|
|
1384
|
+
if (!hasDirectEditable) {
|
|
1385
|
+
recast.types.visit(pathNode.node, {
|
|
1386
|
+
visitJSXElement(inner) {
|
|
1387
|
+
if (inner.node === pathNode.node) {
|
|
1388
|
+
this.traverse(inner);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const innerAttrs = inner.node.openingElement?.attributes || [];
|
|
1392
|
+
if (innerAttrs.some(
|
|
1393
|
+
(a) => a.type === 'JSXAttribute' && a.name && (
|
|
1394
|
+
a.name.name === 'data-preview-field-path' ||
|
|
1395
|
+
a.name.name === 'data-preview-list-path' ||
|
|
1396
|
+
a.name.name === 'data-preview-item-path'
|
|
1397
|
+
)
|
|
1398
|
+
)) {
|
|
1399
|
+
hasNestedEditable = true;
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
this.traverse(inner);
|
|
1403
|
+
},
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (hasDirectEditable || hasNestedEditable) {
|
|
1408
|
+
opening.attributes = attrs.filter(
|
|
1154
1409
|
(a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
|
|
1155
1410
|
);
|
|
1156
1411
|
cleaned++;
|
|
@@ -1162,7 +1417,7 @@ function sanitizeContradictoryMarkers(ast) {
|
|
|
1162
1417
|
}
|
|
1163
1418
|
|
|
1164
1419
|
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
1165
|
-
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path')) {
|
|
1420
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path') && !code.includes('overflow-hidden')) {
|
|
1166
1421
|
return { code, updated: false };
|
|
1167
1422
|
}
|
|
1168
1423
|
let ast;
|
|
@@ -1175,8 +1430,9 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
|
1175
1430
|
const healedBroad = healBroadContainerMarkers(ast);
|
|
1176
1431
|
const healedEmpty = healEmptyStateConditionals(ast);
|
|
1177
1432
|
const healedHidden = healHiddenPreviewMarkers(ast);
|
|
1178
|
-
|
|
1179
|
-
|
|
1433
|
+
const healedOverflow = healSectionOverflowHidden(ast);
|
|
1434
|
+
if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0 && healedOverflow === 0) return { code, updated: false };
|
|
1435
|
+
return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden + healedOverflow };
|
|
1180
1436
|
}
|
|
1181
1437
|
|
|
1182
1438
|
function healBroadContainerMarkers(ast) {
|
|
@@ -1415,8 +1671,75 @@ function healLegacyProductDetailLinks(ast) {
|
|
|
1415
1671
|
return healed;
|
|
1416
1672
|
}
|
|
1417
1673
|
|
|
1674
|
+
function healSectionOverflowHidden(ast) {
|
|
1675
|
+
let healed = 0;
|
|
1676
|
+
recast.types.visit(ast, {
|
|
1677
|
+
visitJSXOpeningElement(pathNode) {
|
|
1678
|
+
const node = pathNode.node;
|
|
1679
|
+
const tag = getJsxName(node);
|
|
1680
|
+
const lower = String(tag || '').toLowerCase();
|
|
1681
|
+
const isContainer = lower === 'section' || lower === 'div' || lower === 'main' || lower === 'article';
|
|
1682
|
+
if (!isContainer) {
|
|
1683
|
+
this.traverse(pathNode);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const classAttr = (node.attributes || []).find(
|
|
1687
|
+
(a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class')
|
|
1688
|
+
);
|
|
1689
|
+
if (!classAttr || !classAttr.value) {
|
|
1690
|
+
this.traverse(pathNode);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
let modified = false;
|
|
1694
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
1695
|
+
const val = String(classAttr.value.value || '');
|
|
1696
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
1697
|
+
classAttr.value.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1698
|
+
modified = true;
|
|
1699
|
+
}
|
|
1700
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer') {
|
|
1701
|
+
const expr = classAttr.value.expression;
|
|
1702
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
1703
|
+
const val = String(expr.value || '');
|
|
1704
|
+
if (/\boverflow-hidden\b/.test(val)) {
|
|
1705
|
+
expr.value = val.replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1706
|
+
modified = true;
|
|
1707
|
+
}
|
|
1708
|
+
} else if (expr && expr.type === 'TemplateLiteral') {
|
|
1709
|
+
for (const quasi of expr.quasis || []) {
|
|
1710
|
+
if (quasi.value && /\boverflow-hidden\b/.test(quasi.value.raw || '')) {
|
|
1711
|
+
quasi.value.raw = (quasi.value.raw || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1712
|
+
if (quasi.value.cooked) {
|
|
1713
|
+
quasi.value.cooked = (quasi.value.cooked || '').replace(/\boverflow-hidden\b/g, 'overflow-clip');
|
|
1714
|
+
}
|
|
1715
|
+
modified = true;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (modified) healed++;
|
|
1721
|
+
this.traverse(pathNode);
|
|
1722
|
+
},
|
|
1723
|
+
});
|
|
1724
|
+
return healed;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
1728
|
+
if (!code.includes('siteData')) return code;
|
|
1729
|
+
try {
|
|
1730
|
+
const ast = parseSource(code, filePath);
|
|
1731
|
+
const injected = injectSiteDataHook(ast);
|
|
1732
|
+
if (injected) {
|
|
1733
|
+
ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
|
|
1734
|
+
return printSource(ast, code);
|
|
1735
|
+
}
|
|
1736
|
+
} catch {}
|
|
1737
|
+
return code;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1418
1740
|
module.exports = {
|
|
1419
1741
|
applyFilePlan,
|
|
1742
|
+
applyCollectionTransform,
|
|
1420
1743
|
instrumentLayoutSource,
|
|
1421
1744
|
instrumentPageKey,
|
|
1422
1745
|
resolveSiteDataSpecifier,
|
|
@@ -1429,6 +1752,9 @@ module.exports = {
|
|
|
1429
1752
|
healBroadContainerMarkers,
|
|
1430
1753
|
healEmptyStateConditionals,
|
|
1431
1754
|
healHiddenPreviewMarkers,
|
|
1755
|
+
healSectionOverflowHidden,
|
|
1432
1756
|
healLegacyProductDetailLinks,
|
|
1757
|
+
healMissingSiteDataHooks,
|
|
1758
|
+
injectSiteDataHook,
|
|
1433
1759
|
};
|
|
1434
1760
|
|
|
@@ -2178,7 +2178,7 @@ function auditSensitiveAttributes(
|
|
|
2178
2178
|
|
|
2179
2179
|
function isHiddenHtmlElement(token: string, tag: string) {
|
|
2180
2180
|
if (
|
|
2181
|
-
|
|
2181
|
+
/(?:^|\s)hidden(?:\s|=|\/?>)/i.test(token) ||
|
|
2182
2182
|
/\baria-hidden\s*=\s*(?:"true"|'true')/i.test(token)
|
|
2183
2183
|
) {
|
|
2184
2184
|
return true;
|