@deneb-ui/cli 2.0.84 → 2.0.85
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 +43 -10
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +106 -0
- package/src/arc/fs-utils.cjs +3 -0
- package/src/arc/index.cjs +8 -1
- package/src/arc/transformer.cjs +192 -11
- package/src/arc/version.cjs +1 -1
- package/src/tools/deneb-doctor.cjs +53 -9
package/bin/index.js
CHANGED
|
@@ -112,19 +112,47 @@ function isForbiddenFile(filename) {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function
|
|
116
|
-
return
|
|
115
|
+
function getVisualWidth(str) {
|
|
116
|
+
if (!str) return 0;
|
|
117
|
+
const stripped = String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
118
|
+
let width = 0;
|
|
119
|
+
for (const char of stripped) {
|
|
120
|
+
const code = char.codePointAt(0);
|
|
121
|
+
if (
|
|
122
|
+
(code >= 0x1100 && code <= 0x115F) ||
|
|
123
|
+
(code >= 0x2E80 && code <= 0xA4CF) ||
|
|
124
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
125
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
126
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
127
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
128
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
129
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
130
|
+
(code >= 0x1F000 && code <= 0x1FAFF) ||
|
|
131
|
+
(code >= 0x2600 && code <= 0x27BF)
|
|
132
|
+
) {
|
|
133
|
+
width += 2;
|
|
134
|
+
} else if (code > 0xFFFF) {
|
|
135
|
+
width += 2;
|
|
136
|
+
} else {
|
|
137
|
+
width += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return width;
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
function createBox(lines,
|
|
143
|
+
function createBox(lines, preferredWidth = 58) {
|
|
120
144
|
const cyan = '\x1b[36m';
|
|
121
145
|
const reset = '\x1b[0m';
|
|
122
|
-
const
|
|
123
|
-
const
|
|
146
|
+
const visualLengths = lines.map((l) => getVisualWidth(l));
|
|
147
|
+
const maxLineVisual = Math.max(...visualLengths, 0);
|
|
148
|
+
const boxWidth = Math.max(preferredWidth, maxLineVisual + 4);
|
|
124
149
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
const top = ` ${cyan}╔${'═'.repeat(boxWidth)}╗${reset}`;
|
|
151
|
+
const bottom = ` ${cyan}╚${'═'.repeat(boxWidth)}╝${reset}`;
|
|
152
|
+
|
|
153
|
+
const rows = lines.map((line, idx) => {
|
|
154
|
+
const rawLen = visualLengths[idx];
|
|
155
|
+
const padTotal = Math.max(0, boxWidth - rawLen);
|
|
128
156
|
const padLeft = Math.floor(padTotal / 2);
|
|
129
157
|
const padRight = padTotal - padLeft;
|
|
130
158
|
return ` ${cyan}║${reset}${' '.repeat(padLeft)}${line}${' '.repeat(padRight)}${cyan}║${reset}`;
|
|
@@ -1210,11 +1238,16 @@ async function initProject(targetInput, options = {}) {
|
|
|
1210
1238
|
}
|
|
1211
1239
|
}
|
|
1212
1240
|
|
|
1213
|
-
if (!skipInstall) {
|
|
1241
|
+
if (!skipInstall && !process.env.DENEB_SKIP_FONTS) {
|
|
1214
1242
|
try {
|
|
1215
1243
|
const { runFontsInstall } = require('../src/tools/deneb-fonts.cjs');
|
|
1244
|
+
const plannedFontArgs = [];
|
|
1245
|
+
const fontList = conversionRes && Array.isArray(conversionRes.fontIds) ? conversionRes.fontIds : [];
|
|
1246
|
+
for (const id of fontList) {
|
|
1247
|
+
plannedFontArgs.push('--font', id);
|
|
1248
|
+
}
|
|
1216
1249
|
console.log(`\n\x1b[36mAa Downloading and configuring DENEB Google Fonts...\x1b[0m`);
|
|
1217
|
-
runFontsInstall(targetDir,
|
|
1250
|
+
runFontsInstall(targetDir, plannedFontArgs);
|
|
1218
1251
|
} catch (err) {
|
|
1219
1252
|
console.log(`\n\x1b[33m! Fonts were not installed automatically: ${err.message}. Run \x1b[1mdeneb fonts install .\x1b[0m\x1b[0m`);
|
|
1220
1253
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.85",
|
|
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.85",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -6,6 +6,8 @@ const fs = require('fs');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
|
+
process.env.DENEB_HOME = path.join(os.tmpdir(), 'deneb-test-home-' + Date.now());
|
|
10
|
+
|
|
9
11
|
const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
|
|
10
12
|
const { analyzeFile } = require('../semantic.cjs');
|
|
11
13
|
const { planTransformations } = require('../planner.cjs');
|
|
@@ -1708,8 +1710,112 @@ test('manifest buildSiteDataAndManifest creates default dual-mode palette with d
|
|
|
1708
1710
|
assert.equal(bundle.siteData.theme.dark.textColor, '#f9fafb');
|
|
1709
1711
|
});
|
|
1710
1712
|
|
|
1713
|
+
test('healEmptyStateConditionals preserves modal, null-check, and state-dependent conditional guards', () => {
|
|
1714
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1715
|
+
const { healEmptyStateConditionals } = require('../transformer.cjs');
|
|
1716
|
+
const code = `
|
|
1717
|
+
export function Showcase() {
|
|
1718
|
+
const [lightboxIndex, setLightboxIndex] = useState(null);
|
|
1719
|
+
const [selectedDish, setSelectedDish] = useState(null);
|
|
1720
|
+
return (
|
|
1721
|
+
<div>
|
|
1722
|
+
<AnimatePresence>
|
|
1723
|
+
{lightboxIndex !== null && filteredItems[lightboxIndex] && (
|
|
1724
|
+
<div className="fixed inset-0 z-50">
|
|
1725
|
+
<span data-preview-field-path="home.ofLabel">Caption</span>
|
|
1726
|
+
</div>
|
|
1727
|
+
)}
|
|
1728
|
+
</AnimatePresence>
|
|
1729
|
+
<AnimatePresence>
|
|
1730
|
+
{selectedDish && (
|
|
1731
|
+
<div className="fixed inset-0 z-50">
|
|
1732
|
+
<h3 data-preview-field-path="home.dishLabel">{selectedDish.name}</h3>
|
|
1733
|
+
</div>
|
|
1734
|
+
)}
|
|
1735
|
+
</AnimatePresence>
|
|
1736
|
+
</div>
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
`;
|
|
1740
|
+
const ast = parseSource(code, 'src/components/Showcase.tsx');
|
|
1741
|
+
const healed = healEmptyStateConditionals(ast);
|
|
1742
|
+
const result = printSource(ast, code);
|
|
1743
|
+
assert.equal(healed, 0, 'No modal guards should be stripped');
|
|
1744
|
+
assert.match(result, /lightboxIndex !== null/);
|
|
1745
|
+
assert.match(result, /selectedDish &&/);
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
test('M1: auditFivoraContract flags uncovered visible text as a contract violation', () => {
|
|
1749
|
+
const { auditFivoraContract } = require('../index.cjs');
|
|
1750
|
+
const profile = { routes: [{ id: 'home', file: 'src/app/page.tsx' }] };
|
|
1751
|
+
const siteData = { content: { home: { heroTitle: 'Welcome' } } };
|
|
1752
|
+
const manifest = {
|
|
1753
|
+
pages: [{ id: 'home', route: '/' }],
|
|
1754
|
+
editorSchema: {
|
|
1755
|
+
sections: [
|
|
1756
|
+
{
|
|
1757
|
+
id: 'home',
|
|
1758
|
+
path: 'home',
|
|
1759
|
+
type: 'object',
|
|
1760
|
+
fields: [{ key: 'heroTitle', type: 'text' }],
|
|
1761
|
+
},
|
|
1762
|
+
],
|
|
1763
|
+
},
|
|
1764
|
+
};
|
|
1765
|
+
const inventory = {
|
|
1766
|
+
sources: [
|
|
1767
|
+
{
|
|
1768
|
+
rel: 'src/app/page.tsx',
|
|
1769
|
+
code: 'export default function Page() { return <div><h1><span data-preview-field-path="home.heroTitle">Welcome</span></h1><p>Uncovered paragraph copy here</p></div>; }',
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
pageKeysByFile: { 'src/app/page.tsx': ['home'] },
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
const audit = auditFivoraContract({ profile, siteData, manifest, inventory });
|
|
1776
|
+
assert.equal(audit.passed, false, 'Audit must fail when uncovered visible text exists');
|
|
1777
|
+
assert.ok(audit.uncoveredVisibleText.length > 0, 'Uncovered visible text must be recorded');
|
|
1778
|
+
assert.match(audit.uncoveredVisibleText[0].text, /Uncovered paragraph copy here/);
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
test('M2: ensureStyleAttrs is idempotent and does not duplicate style attributes', () => {
|
|
1782
|
+
const { parseSource, printSource, ensureStyleAttrs } = require('../ast.cjs');
|
|
1783
|
+
const code = '<span data-preview-field-path="home.title">Hello</span>';
|
|
1784
|
+
const ast = parseSource(code, 'test.tsx');
|
|
1785
|
+
const node = ast.program.body[0].expression;
|
|
1711
1786
|
|
|
1787
|
+
ensureStyleAttrs(node, 'home.title', 'text');
|
|
1788
|
+
const firstPass = printSource(ast, code);
|
|
1789
|
+
assert.match(firstPass, /data-preview-style-target="home\.title"/);
|
|
1790
|
+
assert.match(firstPass, /data-preview-style-type="text"/);
|
|
1712
1791
|
|
|
1792
|
+
// Second pass: must not duplicate attributes
|
|
1793
|
+
ensureStyleAttrs(node, 'home.title', 'text');
|
|
1794
|
+
const secondPass = printSource(ast, code);
|
|
1795
|
+
assert.equal(secondPass, firstPass, 'Second call must be strictly idempotent');
|
|
1796
|
+
});
|
|
1713
1797
|
|
|
1798
|
+
test('M3: collectFontIdsFromSiteData resolves planned fonts and theme accurately', () => {
|
|
1799
|
+
const { collectFontIdsFromSiteData, applyFontTheme } = require('../font-plan.cjs');
|
|
1800
|
+
const siteData = {
|
|
1801
|
+
theme: {
|
|
1802
|
+
headingFont: 'Playfair Display',
|
|
1803
|
+
bodyFont: 'Plus Jakarta Sans',
|
|
1804
|
+
},
|
|
1805
|
+
styles: {
|
|
1806
|
+
'home.accent': { fontFamily: 'Space Grotesk' },
|
|
1807
|
+
},
|
|
1808
|
+
};
|
|
1714
1809
|
|
|
1810
|
+
const fontIds = collectFontIdsFromSiteData(siteData);
|
|
1811
|
+
assert.ok(Array.isArray(fontIds));
|
|
1812
|
+
assert.ok(fontIds.includes('playfair-display'));
|
|
1813
|
+
assert.ok(fontIds.includes('plus-jakarta-sans'));
|
|
1814
|
+
assert.ok(fontIds.includes('space-grotesk'));
|
|
1815
|
+
|
|
1816
|
+
const siteDataEmpty = {};
|
|
1817
|
+
applyFontTheme(siteDataEmpty, ['inter', 'playfair-display']);
|
|
1818
|
+
assert.equal(siteDataEmpty.theme.headingFont, 'Playfair Display');
|
|
1819
|
+
assert.equal(siteDataEmpty.theme.bodyFont, 'Inter');
|
|
1820
|
+
});
|
|
1715
1821
|
|
package/src/arc/fs-utils.cjs
CHANGED
package/src/arc/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDat
|
|
|
21
21
|
const { applyResidualPass } = require('./residual.cjs');
|
|
22
22
|
const { parseSource } = require('./ast.cjs');
|
|
23
23
|
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
|
|
24
|
+
const { collectFontIdsFromSiteData } = require('./font-plan.cjs');
|
|
24
25
|
const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
|
|
25
26
|
const { recordExperience, registryArchitecture } = require('./learning.cjs');
|
|
26
27
|
const { matchRecipeV2 } = require('./recipes-v2.cjs');
|
|
@@ -785,7 +786,11 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
785
786
|
}
|
|
786
787
|
}
|
|
787
788
|
outcome = 'rolled-back';
|
|
788
|
-
|
|
789
|
+
const reasons = [];
|
|
790
|
+
if (!fivoraAudit.passed) reasons.push(`${fivoraAudit.errors.length} Fivora contract error(s)`);
|
|
791
|
+
if (fivoraAudit.uncoveredVisibleText.length > 0) reasons.push(`${fivoraAudit.uncoveredVisibleText.length} uncovered visible text node(s)`);
|
|
792
|
+
if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 98)`);
|
|
793
|
+
printer.printRollback(`Strict Fivora contract failed: ${reasons.join(', ')}`);
|
|
789
794
|
} else if (contractFailure) {
|
|
790
795
|
outcome = 'contract-failed';
|
|
791
796
|
} else if (designFailure) {
|
|
@@ -862,6 +867,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
862
867
|
designPreservation: design.score,
|
|
863
868
|
validation,
|
|
864
869
|
outcome,
|
|
870
|
+
fontIds: collectFontIdsFromSiteData(dataBundle.siteData),
|
|
865
871
|
};
|
|
866
872
|
|
|
867
873
|
const report = buildReport({
|
|
@@ -954,6 +960,7 @@ module.exports = {
|
|
|
954
960
|
runDenebArc,
|
|
955
961
|
parseArcOptions,
|
|
956
962
|
scanProject,
|
|
963
|
+
auditFivoraContract,
|
|
957
964
|
ENGINE_ID,
|
|
958
965
|
ARC_VERSION,
|
|
959
966
|
};
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -1624,6 +1624,108 @@ function findFirstLeafChild(node) {
|
|
|
1624
1624
|
return null;
|
|
1625
1625
|
}
|
|
1626
1626
|
|
|
1627
|
+
function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
|
|
1628
|
+
if (!leftExpr || !rightExpr) return true;
|
|
1629
|
+
|
|
1630
|
+
// 1. Collect all identifiers used on the left-hand side
|
|
1631
|
+
const leftIdentifiers = new Set();
|
|
1632
|
+
recast.types.visit(leftExpr, {
|
|
1633
|
+
visitIdentifier(idPath) {
|
|
1634
|
+
leftIdentifiers.add(idPath.node.name);
|
|
1635
|
+
return false;
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
if (leftIdentifiers.size === 0) return true;
|
|
1640
|
+
|
|
1641
|
+
// 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
|
|
1642
|
+
const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
|
|
1643
|
+
for (const id of leftIdentifiers) {
|
|
1644
|
+
if (stateKeywordRegex.test(id)) {
|
|
1645
|
+
return true; // NEVER strip modal/state/selection guards
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// 3. Null / undefined / equality / comparison checks
|
|
1650
|
+
let hasComparisonOrNullCheck = false;
|
|
1651
|
+
recast.types.visit(leftExpr, {
|
|
1652
|
+
visitBinaryExpression(bPath) {
|
|
1653
|
+
const op = bPath.node.operator;
|
|
1654
|
+
if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
|
|
1655
|
+
hasComparisonOrNullCheck = true;
|
|
1656
|
+
}
|
|
1657
|
+
this.traverse(bPath);
|
|
1658
|
+
},
|
|
1659
|
+
visitUnaryExpression(uPath) {
|
|
1660
|
+
if (uPath.node.operator === '!') {
|
|
1661
|
+
hasComparisonOrNullCheck = true;
|
|
1662
|
+
}
|
|
1663
|
+
this.traverse(uPath);
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
if (hasComparisonOrNullCheck) return true;
|
|
1667
|
+
|
|
1668
|
+
// 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
|
|
1669
|
+
let rightReferencesLeftId = false;
|
|
1670
|
+
recast.types.visit(rightExpr, {
|
|
1671
|
+
visitIdentifier(idPath) {
|
|
1672
|
+
if (leftIdentifiers.has(idPath.node.name)) {
|
|
1673
|
+
rightReferencesLeftId = true;
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
this.traverse(idPath);
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1679
|
+
if (rightReferencesLeftId) return true;
|
|
1680
|
+
|
|
1681
|
+
// 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
|
|
1682
|
+
let curr = pathNode;
|
|
1683
|
+
while (curr && curr.parent) {
|
|
1684
|
+
const parentNode = curr.parent.node;
|
|
1685
|
+
if (parentNode && parentNode.type === 'JSXElement') {
|
|
1686
|
+
const parentTag = getJsxName(parentNode);
|
|
1687
|
+
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(parentTag)) {
|
|
1688
|
+
return true;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
curr = curr.parent;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
let isModalOrOverlay = false;
|
|
1695
|
+
recast.types.visit(rightExpr, {
|
|
1696
|
+
visitJSXElement(elPath) {
|
|
1697
|
+
const node = elPath.node;
|
|
1698
|
+
const tag = getJsxName(node);
|
|
1699
|
+
if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(tag)) {
|
|
1700
|
+
isModalOrOverlay = true;
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
|
|
1704
|
+
if (classAttr && classAttr.value) {
|
|
1705
|
+
const val = classAttr.value.value || '';
|
|
1706
|
+
if (typeof val === 'string' && (/fixed\s+inset-0/i.test(val) || /z-(?:50|\[50\]|40)/i.test(val) || /backdrop-blur/i.test(val))) {
|
|
1707
|
+
isModalOrOverlay = true;
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const roleAttr = findJsxAttribute(node, 'role');
|
|
1712
|
+
if (roleAttr && roleAttr.value && /^(?:dialog|alertdialog)$/i.test(String(roleAttr.value.value || ''))) {
|
|
1713
|
+
isModalOrOverlay = true;
|
|
1714
|
+
return false;
|
|
1715
|
+
}
|
|
1716
|
+
this.traverse(elPath);
|
|
1717
|
+
},
|
|
1718
|
+
});
|
|
1719
|
+
if (isModalOrOverlay) return true;
|
|
1720
|
+
|
|
1721
|
+
// 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
|
|
1722
|
+
const leftCode = recast.print(leftExpr).code;
|
|
1723
|
+
const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
|
|
1724
|
+
if (!isDirectContentCheck) return true;
|
|
1725
|
+
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1627
1729
|
function healEmptyStateConditionals(ast) {
|
|
1628
1730
|
let healed = 0;
|
|
1629
1731
|
|
|
@@ -1650,9 +1752,11 @@ function healEmptyStateConditionals(ast) {
|
|
|
1650
1752
|
visitLogicalExpression(pathNode) {
|
|
1651
1753
|
const expr = pathNode.node;
|
|
1652
1754
|
if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1755
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
1756
|
+
pathNode.replace(expr.right);
|
|
1757
|
+
healed++;
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1656
1760
|
}
|
|
1657
1761
|
this.traverse(pathNode);
|
|
1658
1762
|
},
|
|
@@ -1663,20 +1767,24 @@ function healEmptyStateConditionals(ast) {
|
|
|
1663
1767
|
return;
|
|
1664
1768
|
}
|
|
1665
1769
|
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1770
|
+
if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
|
|
1771
|
+
pathNode.node.expression = expr.right;
|
|
1772
|
+
healed++;
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1669
1775
|
}
|
|
1670
1776
|
if (
|
|
1671
1777
|
expr.type === 'ConditionalExpression' &&
|
|
1672
1778
|
jsxHasPreviewMarker(expr.consequent) &&
|
|
1673
1779
|
(isNullish(expr.alternate) || isNullish(expr.consequent))
|
|
1674
1780
|
) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1781
|
+
if (!isUnsafeToStripCondition(expr.test, expr.consequent, pathNode)) {
|
|
1782
|
+
pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
|
|
1783
|
+
? expr.consequent
|
|
1784
|
+
: expr.alternate;
|
|
1785
|
+
healed++;
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1680
1788
|
}
|
|
1681
1789
|
this.traverse(pathNode);
|
|
1682
1790
|
},
|
|
@@ -1928,6 +2036,77 @@ function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
|
1928
2036
|
return code;
|
|
1929
2037
|
}
|
|
1930
2038
|
|
|
2039
|
+
function healUnguardedModalConditionals(ast) {
|
|
2040
|
+
let healed = 0;
|
|
2041
|
+
const stateVariables = [];
|
|
2042
|
+
recast.types.visit(ast, {
|
|
2043
|
+
visitVariableDeclarator(pathNode) {
|
|
2044
|
+
const node = pathNode.node;
|
|
2045
|
+
if (node.id && node.id.type === 'ArrayPattern') {
|
|
2046
|
+
const first = node.id.elements[0];
|
|
2047
|
+
if (first && first.type === 'Identifier') {
|
|
2048
|
+
stateVariables.push(first.name);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
this.traverse(pathNode);
|
|
2052
|
+
},
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
if (stateVariables.length === 0) return 0;
|
|
2056
|
+
|
|
2057
|
+
recast.types.visit(ast, {
|
|
2058
|
+
visitJSXElement(pathNode) {
|
|
2059
|
+
const tag = getJsxName(pathNode.node);
|
|
2060
|
+
if (tag === 'AnimatePresence') {
|
|
2061
|
+
for (const child of pathNode.node.children || []) {
|
|
2062
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
2063
|
+
let inner = child.expression;
|
|
2064
|
+
if (inner && inner.type === 'ParenthesizedExpression') {
|
|
2065
|
+
inner = inner.expression;
|
|
2066
|
+
}
|
|
2067
|
+
if (inner && (inner.type === 'JSXElement' || inner.type === 'JSXFragment')) {
|
|
2068
|
+
const innerIds = new Set();
|
|
2069
|
+
recast.types.visit(inner, {
|
|
2070
|
+
visitIdentifier(idPath) {
|
|
2071
|
+
innerIds.add(idPath.node.name);
|
|
2072
|
+
return false;
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
const matchingVar = stateVariables.find((sv) => innerIds.has(sv));
|
|
2077
|
+
if (matchingVar) {
|
|
2078
|
+
let guardExpr;
|
|
2079
|
+
if (/index/i.test(matchingVar)) {
|
|
2080
|
+
guardExpr = b.binaryExpression('!==', b.identifier(matchingVar), b.nullLiteral());
|
|
2081
|
+
} else {
|
|
2082
|
+
guardExpr = b.identifier(matchingVar);
|
|
2083
|
+
}
|
|
2084
|
+
child.expression = b.logicalExpression('&&', guardExpr, inner);
|
|
2085
|
+
healed++;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
this.traverse(pathNode);
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
return healed;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function healUnguardedModalConditionalsInSource(code, filePath = 'file.tsx') {
|
|
2099
|
+
if (!code.includes('AnimatePresence') && !code.includes('fixed inset-0')) return { code, updated: false };
|
|
2100
|
+
try {
|
|
2101
|
+
const ast = parseSource(code, filePath);
|
|
2102
|
+
const healed = healUnguardedModalConditionals(ast);
|
|
2103
|
+
if (healed > 0) {
|
|
2104
|
+
return { code: printSource(ast, code), updated: true, count: healed };
|
|
2105
|
+
}
|
|
2106
|
+
} catch {}
|
|
2107
|
+
return { code, updated: false };
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1931
2110
|
module.exports = {
|
|
1932
2111
|
applyFilePlan,
|
|
1933
2112
|
applyCollectionTransform,
|
|
@@ -1947,6 +2126,8 @@ module.exports = {
|
|
|
1947
2126
|
healDecorativeOverlays,
|
|
1948
2127
|
healLegacyProductDetailLinks,
|
|
1949
2128
|
healMissingSiteDataHooks,
|
|
2129
|
+
healUnguardedModalConditionals,
|
|
2130
|
+
healUnguardedModalConditionalsInSource,
|
|
1950
2131
|
injectSiteDataHook,
|
|
1951
2132
|
};
|
|
1952
2133
|
|
package/src/arc/version.cjs
CHANGED
|
@@ -13,17 +13,49 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { spawnSync } = require('child_process');
|
|
15
15
|
const { matchRecipeForProject, loadAllRecipes } = require('./recipe-engine.cjs');
|
|
16
|
-
const { healMissingSiteDataHooks, healBroadContainerMarkers, healSectionOverflowHidden } = require('../arc/transformer.cjs');
|
|
16
|
+
const { healMissingSiteDataHooks, healBroadContainerMarkers, healSectionOverflowHidden, healUnguardedModalConditionalsInSource } = require('../arc/transformer.cjs');
|
|
17
|
+
|
|
18
|
+
function getVisualWidth(str) {
|
|
19
|
+
if (!str) return 0;
|
|
20
|
+
const stripped = String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
21
|
+
let width = 0;
|
|
22
|
+
for (const char of stripped) {
|
|
23
|
+
const code = char.codePointAt(0);
|
|
24
|
+
if (
|
|
25
|
+
(code >= 0x1100 && code <= 0x115F) ||
|
|
26
|
+
(code >= 0x2E80 && code <= 0xA4CF) ||
|
|
27
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
28
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
29
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
30
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
31
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
32
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
33
|
+
(code >= 0x1F000 && code <= 0x1FAFF) ||
|
|
34
|
+
(code >= 0x2600 && code <= 0x27BF)
|
|
35
|
+
) {
|
|
36
|
+
width += 2;
|
|
37
|
+
} else if (code > 0xFFFF) {
|
|
38
|
+
width += 2;
|
|
39
|
+
} else {
|
|
40
|
+
width += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return width;
|
|
44
|
+
}
|
|
17
45
|
|
|
18
|
-
function createBox(lines,
|
|
19
|
-
const
|
|
46
|
+
function createBox(lines, preferredWidth = 66) {
|
|
47
|
+
const visualLengths = lines.map((l) => getVisualWidth(l));
|
|
48
|
+
const maxLineVisual = Math.max(...visualLengths, 0);
|
|
49
|
+
const boxWidth = Math.max(preferredWidth, maxLineVisual + 6);
|
|
50
|
+
const innerWidth = boxWidth - 4;
|
|
51
|
+
|
|
52
|
+
const horizontal = '═'.repeat(boxWidth - 2);
|
|
20
53
|
const top = ` ╔${horizontal}╗`;
|
|
21
54
|
const bottom = ` ╚${horizontal}╝`;
|
|
22
55
|
|
|
23
|
-
const content = lines.map((line) => {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const padding = Math.max(0, width - 4 - stripped.length);
|
|
56
|
+
const content = lines.map((line, idx) => {
|
|
57
|
+
const vLen = visualLengths[idx];
|
|
58
|
+
const padding = Math.max(0, innerWidth - vLen);
|
|
27
59
|
const leftPad = Math.floor(padding / 2);
|
|
28
60
|
const rightPad = padding - leftPad;
|
|
29
61
|
return ` ║ ${' '.repeat(leftPad)}${line}${' '.repeat(rightPad)} ║`;
|
|
@@ -289,17 +321,29 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
289
321
|
addCheck(suite2, 'err', 'Platform Engine Compatibility', 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.', { code: 'DNB-ENG-001' }) //, 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.');
|
|
290
322
|
}
|
|
291
323
|
|
|
292
|
-
// Pre-heal missing useSiteData hooks before tsc runs when --fix is enabled
|
|
324
|
+
// Pre-heal missing useSiteData hooks and unguarded modals before tsc runs when --fix is enabled
|
|
293
325
|
if (shouldFix) {
|
|
294
326
|
for (const file of sourceFiles) {
|
|
295
327
|
if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
|
|
296
328
|
let c = fs.readFileSync(file, 'utf-8');
|
|
329
|
+
let modified = false;
|
|
297
330
|
if (c.includes('siteData')) {
|
|
298
331
|
const healed = healMissingSiteDataHooks(c, file);
|
|
299
332
|
if (healed && healed !== c) {
|
|
300
|
-
|
|
333
|
+
c = healed;
|
|
334
|
+
modified = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (c.includes('AnimatePresence') || c.includes('fixed inset-0')) {
|
|
338
|
+
const modalHealed = healUnguardedModalConditionalsInSource(c, file);
|
|
339
|
+
if (modalHealed && modalHealed.updated) {
|
|
340
|
+
c = modalHealed.code;
|
|
341
|
+
modified = true;
|
|
301
342
|
}
|
|
302
343
|
}
|
|
344
|
+
if (modified) {
|
|
345
|
+
fs.writeFileSync(file, c, 'utf8');
|
|
346
|
+
}
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
349
|
|