@deneb-ui/cli 2.0.71 → 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 +34 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +82 -0
- package/src/arc/manifest.cjs +37 -3
- package/src/arc/planner.cjs +10 -0
- package/src/arc/residual.cjs +25 -5
- package/src/arc/semantic.cjs +51 -0
- package/src/arc/transformer.cjs +108 -6
package/bin/index.js
CHANGED
|
@@ -467,15 +467,49 @@ function getDefaultSiteData(projectName, pages) {
|
|
|
467
467
|
secondaryColor: '#0a1931',
|
|
468
468
|
accentColor: '#00adb5',
|
|
469
469
|
backgroundColor: '#ffffff',
|
|
470
|
+
cardBackgroundColor: '#f8fafc',
|
|
470
471
|
textColor: '#0f172a',
|
|
472
|
+
borderColor: '#e2e8f0',
|
|
471
473
|
headingFont: 'Inter',
|
|
472
474
|
bodyFont: 'Inter',
|
|
473
475
|
baseSize: '16px',
|
|
474
476
|
heroMinHeight: '70vh',
|
|
475
477
|
sectionPadding: '4rem',
|
|
478
|
+
dark: {
|
|
479
|
+
primaryColor: '#00adb5',
|
|
480
|
+
secondaryColor: '#f8fafc',
|
|
481
|
+
accentColor: '#016a7e',
|
|
482
|
+
backgroundColor: '#0b0f19',
|
|
483
|
+
cardBackgroundColor: '#111827',
|
|
484
|
+
textColor: '#f9fafb',
|
|
485
|
+
borderColor: '#1f2937',
|
|
486
|
+
},
|
|
476
487
|
},
|
|
477
488
|
},
|
|
478
489
|
},
|
|
490
|
+
theme: {
|
|
491
|
+
primaryColor: '#016a7e',
|
|
492
|
+
secondaryColor: '#0a1931',
|
|
493
|
+
accentColor: '#00adb5',
|
|
494
|
+
backgroundColor: '#ffffff',
|
|
495
|
+
cardBackgroundColor: '#f8fafc',
|
|
496
|
+
textColor: '#0f172a',
|
|
497
|
+
borderColor: '#e2e8f0',
|
|
498
|
+
headingFont: 'Inter',
|
|
499
|
+
bodyFont: 'Inter',
|
|
500
|
+
baseSize: '16px',
|
|
501
|
+
heroMinHeight: '70vh',
|
|
502
|
+
sectionPadding: '4rem',
|
|
503
|
+
dark: {
|
|
504
|
+
primaryColor: '#00adb5',
|
|
505
|
+
secondaryColor: '#f8fafc',
|
|
506
|
+
accentColor: '#016a7e',
|
|
507
|
+
backgroundColor: '#0b0f19',
|
|
508
|
+
cardBackgroundColor: '#111827',
|
|
509
|
+
textColor: '#f9fafb',
|
|
510
|
+
borderColor: '#1f2937',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
479
513
|
requirements: {
|
|
480
514
|
requiredPages: pages.filter((p) => p.required).map((p) => p.id),
|
|
481
515
|
requiredFeatures: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.72",
|
|
4
4
|
"description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deneb": "bin/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@babel/parser": "^7.28.0",
|
|
52
|
-
"@deneb-ui/core": "^2.0.
|
|
52
|
+
"@deneb-ui/core": "^2.0.72",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -1627,6 +1627,88 @@ test('learning loadFingerprintBoost returns verified boost for baseline trained
|
|
|
1627
1627
|
assert.equal(boost3.boost, 0.08);
|
|
1628
1628
|
});
|
|
1629
1629
|
|
|
1630
|
+
test('instrumentLayoutSource injects ThemeStyles and ThemeToggle with dual mode support into root layout', () => {
|
|
1631
|
+
const { instrumentLayoutSource } = require('../transformer.cjs');
|
|
1632
|
+
const code = `
|
|
1633
|
+
import React from 'react';
|
|
1634
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1635
|
+
return (
|
|
1636
|
+
<html lang="en">
|
|
1637
|
+
<body>
|
|
1638
|
+
<main>{children}</main>
|
|
1639
|
+
</body>
|
|
1640
|
+
</html>
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
`;
|
|
1644
|
+
const result = instrumentLayoutSource(code, '@/data/site-data.json', '@deneb-ui/ui');
|
|
1645
|
+
assert.equal(result.updated, true);
|
|
1646
|
+
assert.ok(result.code.includes('ThemeStyles'));
|
|
1647
|
+
assert.ok(result.code.includes('ThemeToggle'));
|
|
1648
|
+
assert.ok(result.code.includes('SiteDataProvider'));
|
|
1649
|
+
assert.ok(result.code.includes('enableDualMode'));
|
|
1650
|
+
assert.ok(result.code.includes('fixed bottom-6 left-6 z-40'));
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test('semantic prop harvester detects user-facing copy props and transformer binds them to siteData', () => {
|
|
1654
|
+
const { analyzeFile } = require('../semantic.cjs');
|
|
1655
|
+
const { planTransformations } = require('../planner.cjs');
|
|
1656
|
+
const { applyFilePlan } = require('../transformer.cjs');
|
|
1657
|
+
const code = `
|
|
1658
|
+
export function Showcase() {
|
|
1659
|
+
return (
|
|
1660
|
+
<div className="features">
|
|
1661
|
+
<FeatureCard title="Lightning Fast" description="Instant response times" />
|
|
1662
|
+
</div>
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
`;
|
|
1666
|
+
const profile = { root: '/tmp/test', hasSrc: true, jsxFiles: ['Showcase.tsx'], routes: [{ id: 'home', route: '/' }] };
|
|
1667
|
+
const analysis = analyzeFile({
|
|
1668
|
+
code,
|
|
1669
|
+
relativeFile: 'Showcase.tsx',
|
|
1670
|
+
profile,
|
|
1671
|
+
graph: { routesByFile: { 'Showcase.tsx': ['home'] } },
|
|
1672
|
+
ownerScope: 'home',
|
|
1673
|
+
});
|
|
1674
|
+
const propCandidates = analysis.candidates.filter((c) => c.operation === 'extract-prop');
|
|
1675
|
+
assert.ok(propCandidates.length >= 2);
|
|
1676
|
+
assert.ok(propCandidates.some((c) => c.value === 'Lightning Fast' && c.extra?.propName === 'title'));
|
|
1677
|
+
assert.ok(propCandidates.some((c) => c.value === 'Instant response times' && c.extra?.propName === 'description'));
|
|
1678
|
+
|
|
1679
|
+
const plan = planTransformations({
|
|
1680
|
+
profile,
|
|
1681
|
+
analyses: [{ ...analysis, relativeFile: 'Showcase.tsx', code }],
|
|
1682
|
+
});
|
|
1683
|
+
const filePlan = plan.files[0];
|
|
1684
|
+
const transformed = applyFilePlan(filePlan, profile);
|
|
1685
|
+
assert.equal(transformed.changed, true);
|
|
1686
|
+
assert.ok(transformed.code.includes('title={siteData?.content?.home'));
|
|
1687
|
+
assert.ok(transformed.code.includes('description={siteData?.content?.home'));
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
test('manifest buildSiteDataAndManifest creates default dual-mode palette with dark mode overrides', () => {
|
|
1691
|
+
const { buildSiteDataAndManifest } = require('../manifest.cjs');
|
|
1692
|
+
const profile = {
|
|
1693
|
+
packageName: 'test-store',
|
|
1694
|
+
hasSrc: true,
|
|
1695
|
+
routes: [{ id: 'home', label: 'Home', route: '/', required: true }],
|
|
1696
|
+
};
|
|
1697
|
+
const plan = { files: [], usedPaths: [] };
|
|
1698
|
+
const bundle = buildSiteDataAndManifest({
|
|
1699
|
+
projectDir: '/tmp/test',
|
|
1700
|
+
projectName: 'test-store',
|
|
1701
|
+
profile,
|
|
1702
|
+
plan,
|
|
1703
|
+
});
|
|
1704
|
+
assert.ok(bundle.siteData.theme);
|
|
1705
|
+
assert.ok(bundle.siteData.theme.dark);
|
|
1706
|
+
assert.equal(bundle.siteData.theme.backgroundColor, '#ffffff');
|
|
1707
|
+
assert.equal(bundle.siteData.theme.dark.backgroundColor, '#0b0f19');
|
|
1708
|
+
assert.equal(bundle.siteData.theme.dark.textColor, '#f9fafb');
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
|
|
1630
1712
|
|
|
1631
1713
|
|
|
1632
1714
|
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -62,6 +62,37 @@ function setDeep(target, pathStr, value) {
|
|
|
62
62
|
if (curr[last] === undefined) curr[last] = value;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function defaultDualModeTheme(existingTheme = {}) {
|
|
66
|
+
const base = {
|
|
67
|
+
primaryColor: '#0284c7',
|
|
68
|
+
secondaryColor: '#0f172a',
|
|
69
|
+
accentColor: '#38bdf8',
|
|
70
|
+
backgroundColor: '#ffffff',
|
|
71
|
+
cardBackgroundColor: '#f8fafc',
|
|
72
|
+
textColor: '#0f172a',
|
|
73
|
+
borderColor: '#e2e8f0',
|
|
74
|
+
headingFont: 'Outfit, sans-serif',
|
|
75
|
+
bodyFont: 'Inter, sans-serif',
|
|
76
|
+
borderRadius: '12px',
|
|
77
|
+
dark: {
|
|
78
|
+
primaryColor: '#38bdf8',
|
|
79
|
+
secondaryColor: '#f8fafc',
|
|
80
|
+
accentColor: '#0284c7',
|
|
81
|
+
backgroundColor: '#0b0f19',
|
|
82
|
+
cardBackgroundColor: '#111827',
|
|
83
|
+
textColor: '#f9fafb',
|
|
84
|
+
borderColor: '#1f2937',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (isPlainObject(existingTheme)) {
|
|
88
|
+
Object.assign(base, existingTheme);
|
|
89
|
+
if (isPlainObject(existingTheme.dark)) {
|
|
90
|
+
base.dark = { ...base.dark, ...existingTheme.dark };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return base;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
function getDeep(target, pathStr) {
|
|
66
97
|
const parts = String(pathStr).split('.').filter(Boolean);
|
|
67
98
|
let curr = target;
|
|
@@ -384,7 +415,7 @@ function ensurePlatformSections(sections, content, boundListPaths = []) {
|
|
|
384
415
|
{ key: 'businessSummary', type: 'textarea', label: 'Business summary' },
|
|
385
416
|
{ key: 'additionalBusinessDetails', type: 'textarea', label: 'Additional business details' },
|
|
386
417
|
{ key: 'referenceWebsiteUrl', type: 'url', label: 'Reference website URL' },
|
|
387
|
-
{ key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes' },
|
|
418
|
+
{ key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes', fields: [] },
|
|
388
419
|
],
|
|
389
420
|
});
|
|
390
421
|
}
|
|
@@ -572,7 +603,10 @@ function buildSiteDataAndManifest({
|
|
|
572
603
|
id: `${projectName}-template`,
|
|
573
604
|
name: projectName,
|
|
574
605
|
engine: 'NEXT_STATIC_EXPORT',
|
|
575
|
-
structure: {
|
|
606
|
+
structure: {
|
|
607
|
+
pages: routes.map((p) => p.id),
|
|
608
|
+
theme: defaultDualModeTheme(existingSiteData?.template?.structure?.theme || existingSiteData?.theme),
|
|
609
|
+
},
|
|
576
610
|
},
|
|
577
611
|
requirements: existingSiteData?.requirements || {
|
|
578
612
|
requiredPages: routes.filter((p) => p.required).map((p) => p.id),
|
|
@@ -580,7 +614,7 @@ function buildSiteDataAndManifest({
|
|
|
580
614
|
},
|
|
581
615
|
content,
|
|
582
616
|
styles: collectStylesFromPlan(plan, existingSiteData?.styles),
|
|
583
|
-
|
|
617
|
+
theme: defaultDualModeTheme(existingSiteData?.theme || existingSiteData?.template?.structure?.theme),
|
|
584
618
|
};
|
|
585
619
|
|
|
586
620
|
applyFontTheme(siteData, collectFontIdsFromSiteData(siteData));
|
package/src/arc/planner.cjs
CHANGED
|
@@ -159,6 +159,16 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
159
159
|
field: inferFieldName('placeholder', candidate.tag, candidate.value, extra),
|
|
160
160
|
used: usedPaths,
|
|
161
161
|
});
|
|
162
|
+
} else if (candidate.operation === 'extract-prop') {
|
|
163
|
+
const propName = extra.propName || 'text';
|
|
164
|
+
transform.field = buildFieldPath({
|
|
165
|
+
scope,
|
|
166
|
+
section,
|
|
167
|
+
field: inferFieldName(propName, candidate.tag, candidate.value, extra),
|
|
168
|
+
used: usedPaths,
|
|
169
|
+
});
|
|
170
|
+
transform.propName = propName;
|
|
171
|
+
transform.fieldType = classifyFieldType('text', candidate.value);
|
|
162
172
|
} else if (candidate.operation === 'collection-conversion') {
|
|
163
173
|
// A collection is named after the developer's own array variable so the
|
|
164
174
|
// merchant sees "products", not "items2".
|
package/src/arc/residual.cjs
CHANGED
|
@@ -97,7 +97,7 @@ function inMapCallback(pathNode) {
|
|
|
97
97
|
|
|
98
98
|
function isMeaningfulVisibleText(text) {
|
|
99
99
|
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
100
|
-
if (!value || value.length
|
|
100
|
+
if (!value || value.length < 2) return false;
|
|
101
101
|
if (!/\p{L}/u.test(value)) return false;
|
|
102
102
|
if (isStaticSkipText(value)) return false;
|
|
103
103
|
if (CHROME_TEXT_RE.test(value)) return false;
|
|
@@ -372,10 +372,30 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
|
|
|
372
372
|
fields.push({ path: field, type: fieldType, value: text });
|
|
373
373
|
applied++;
|
|
374
374
|
}
|
|
375
|
-
} else
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
375
|
+
} else {
|
|
376
|
+
const nextChildren = [];
|
|
377
|
+
let wrapped = false;
|
|
378
|
+
for (const child of node.children || []) {
|
|
379
|
+
if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
|
|
380
|
+
const leading = child.value.match(/^\s*/)?.[0] || '';
|
|
381
|
+
const trailing = child.value.match(/\s*$/)?.[0] || '';
|
|
382
|
+
if (leading) nextChildren.push(b.jsxText(leading));
|
|
383
|
+
nextChildren.push(wrapTextInEditableSpan(field, text.trim(), fieldType));
|
|
384
|
+
if (trailing) nextChildren.push(b.jsxText(trailing));
|
|
385
|
+
wrapped = true;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
nextChildren.push(child);
|
|
389
|
+
}
|
|
390
|
+
if (wrapped) {
|
|
391
|
+
node.children = nextChildren;
|
|
392
|
+
fields.push({ path: field, type: fieldType, value: text.trim() });
|
|
393
|
+
applied++;
|
|
394
|
+
} else if (!BROAD_CONTENT_CONTAINERS.has(lower)) {
|
|
395
|
+
if (ensureStaticOnLeaf(node, 'non-leaf-copy')) applied++;
|
|
396
|
+
} else if (wrapFirstLiteralAsStatic(node, 'container-copy')) {
|
|
397
|
+
applied++;
|
|
398
|
+
}
|
|
379
399
|
}
|
|
380
400
|
this.traverse(pathNode);
|
|
381
401
|
return;
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -33,6 +33,20 @@ const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
|
33
33
|
const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
|
|
34
34
|
const ARIA_ONLY_RE = /^(aria-|data-state|data-slot|data-orientation)/;
|
|
35
35
|
const SKIP_ATTR_NAMES = new Set(['className', 'class', 'style', 'key', 'id', 'role', 'type', 'name', 'htmlFor', 'suppressHydrationWarning']);
|
|
36
|
+
const USER_FACING_PROP_NAMES = new Set([
|
|
37
|
+
'title',
|
|
38
|
+
'heading',
|
|
39
|
+
'subheading',
|
|
40
|
+
'subtitle',
|
|
41
|
+
'label',
|
|
42
|
+
'description',
|
|
43
|
+
'caption',
|
|
44
|
+
'badge',
|
|
45
|
+
'buttonText',
|
|
46
|
+
'ctaText',
|
|
47
|
+
'helperText',
|
|
48
|
+
'summary',
|
|
49
|
+
]);
|
|
36
50
|
|
|
37
51
|
function fingerprintCandidate(features) {
|
|
38
52
|
return shortHash(JSON.stringify(features));
|
|
@@ -569,6 +583,43 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
569
583
|
});
|
|
570
584
|
}
|
|
571
585
|
|
|
586
|
+
const isCustomComponent = Boolean(name && ((name[0] >= 'A' && name[0] <= 'Z') || name.includes('.')));
|
|
587
|
+
if (isCustomComponent && !apiOwned && node.openingElement && Array.isArray(node.openingElement.attributes)) {
|
|
588
|
+
for (const attr of node.openingElement.attributes) {
|
|
589
|
+
if (attr.type === 'JSXAttribute' && attr.name && USER_FACING_PROP_NAMES.has(attr.name.name)) {
|
|
590
|
+
const propName = attr.name.name;
|
|
591
|
+
let propValue = '';
|
|
592
|
+
if (attr.value) {
|
|
593
|
+
if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
|
|
594
|
+
propValue = String(attr.value.value || '');
|
|
595
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
596
|
+
const expr = attr.value.expression;
|
|
597
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
598
|
+
propValue = String(expr.value || '');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (propValue && !isStaticSkipText(propValue)) {
|
|
603
|
+
const propLoc = `${loc}:${propName}`;
|
|
604
|
+
if (!usedLocs.has(propLoc)) {
|
|
605
|
+
usedLocs.add(propLoc);
|
|
606
|
+
candidates.push({
|
|
607
|
+
...baseMeta,
|
|
608
|
+
loc: propLoc,
|
|
609
|
+
kind: 'text',
|
|
610
|
+
operation: 'extract-prop',
|
|
611
|
+
value: propValue,
|
|
612
|
+
extra: { propName, tag: name },
|
|
613
|
+
confidence: 0.88,
|
|
614
|
+
reason: `component-prop-${propName}`,
|
|
615
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'prop', propName }),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
572
623
|
const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
|
|
573
624
|
|
|
574
625
|
const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
|
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;
|
|
@@ -823,13 +835,15 @@ function injectSiteDataHook(ast) {
|
|
|
823
835
|
}
|
|
824
836
|
|
|
825
837
|
function resolveSiteDataSpecifier(profile, fromRelativeFile) {
|
|
826
|
-
const aliases = profile.aliasMap || {};
|
|
838
|
+
const aliases = (profile && profile.aliasMap) || {};
|
|
827
839
|
const hasAt = Object.keys(aliases).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
828
|
-
const
|
|
829
|
-
|
|
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';
|
|
830
843
|
|
|
831
|
-
const
|
|
832
|
-
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);
|
|
833
847
|
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
834
848
|
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
835
849
|
return relSpec;
|
|
@@ -853,6 +867,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
853
867
|
'extract-alt',
|
|
854
868
|
'extract-placeholder',
|
|
855
869
|
'extract-text',
|
|
870
|
+
'extract-prop',
|
|
856
871
|
'wrap-text-span',
|
|
857
872
|
'collection-conversion',
|
|
858
873
|
'style-bind',
|
|
@@ -1152,11 +1167,98 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
|
|
|
1152
1167
|
|
|
1153
1168
|
ensureProviderInitialData(ast, jsonIdent);
|
|
1154
1169
|
injectPlatformAdditionalPages(ast, providerImport);
|
|
1170
|
+
injectDualModeTheme(ast, jsonIdent, providerImport);
|
|
1155
1171
|
sanitizeDuplicateBindings(ast);
|
|
1156
1172
|
const next = printSource(ast, code);
|
|
1157
1173
|
return { code: next, updated: next !== code };
|
|
1158
1174
|
}
|
|
1159
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
|
+
|
|
1160
1262
|
/**
|
|
1161
1263
|
* Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
|
|
1162
1264
|
* This component handles all additionalPages visual-editing markers correctly
|