@deneb-ui/cli 2.0.83 → 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 +52 -2
- package/src/arc/learning.cjs +4 -0
- package/src/arc/residual.cjs +9 -1
- package/src/arc/semantic.cjs +43 -0
- package/src/arc/transformer.cjs +202 -16
- package/src/arc/version.cjs +1 -1
- package/src/tools/deneb-doctor.cjs +68 -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');
|
|
@@ -613,6 +614,23 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
613
614
|
}
|
|
614
615
|
}
|
|
615
616
|
|
|
617
|
+
// Mandatory Post-Pass: Guarantee all files referencing siteData have useSiteData hook injected
|
|
618
|
+
const { healMissingSiteDataHooks } = require('./transformer.cjs');
|
|
619
|
+
for (const relativeFile of profile.jsxFiles || []) {
|
|
620
|
+
const abs = path.join(projectDir, relativeFile);
|
|
621
|
+
if (!fs.existsSync(abs)) continue;
|
|
622
|
+
const currentCode = fs.readFileSync(abs, 'utf8');
|
|
623
|
+
if (currentCode.includes('siteData')) {
|
|
624
|
+
const healed = healMissingSiteDataHooks(currentCode, relativeFile);
|
|
625
|
+
if (healed && healed !== currentCode) {
|
|
626
|
+
backupFile(projectDir, backupDir, abs);
|
|
627
|
+
fs.writeFileSync(abs, healed, 'utf8');
|
|
628
|
+
afterFiles[relativeFile] = healed;
|
|
629
|
+
if (!changedFiles.includes(relativeFile)) changedFiles.push(relativeFile);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
616
634
|
if (profile.framework === 'nextjs') {
|
|
617
635
|
const before = findNextConfig(projectDir);
|
|
618
636
|
if (before) backupFile(projectDir, backupDir, before.abs);
|
|
@@ -682,13 +700,39 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
682
700
|
const alreadyEditable = analyses.filter((a) => a.alreadyEditable).length;
|
|
683
701
|
const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
|
|
684
702
|
|
|
685
|
-
|
|
703
|
+
let fivoraAudit = auditFivoraContract({
|
|
686
704
|
profile,
|
|
687
705
|
siteData: dataBundle.siteData,
|
|
688
706
|
manifest: dataBundle.manifest,
|
|
689
707
|
inventory: collectMarkerInventory(projectDir, profile, graph),
|
|
690
708
|
});
|
|
691
709
|
|
|
710
|
+
// Auto-reconciliation: If fivoraAudit flagged unknown paths, auto-register them to controlOnlyPaths
|
|
711
|
+
if (!fivoraAudit.passed && fivoraAudit.errors?.length > 0) {
|
|
712
|
+
let manifestModified = false;
|
|
713
|
+
for (const err of fivoraAudit.errors) {
|
|
714
|
+
const unknownMatch = err.match(/references unknown path "([^"]+)"/);
|
|
715
|
+
if (unknownMatch) {
|
|
716
|
+
const unknownPath = unknownMatch[1];
|
|
717
|
+
dataBundle.manifest.visualEditing = dataBundle.manifest.visualEditing || {};
|
|
718
|
+
dataBundle.manifest.visualEditing.controlOnlyPaths = dataBundle.manifest.visualEditing.controlOnlyPaths || [];
|
|
719
|
+
if (!dataBundle.manifest.visualEditing.controlOnlyPaths.includes(unknownPath)) {
|
|
720
|
+
dataBundle.manifest.visualEditing.controlOnlyPaths.push(unknownPath);
|
|
721
|
+
manifestModified = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (manifestModified) {
|
|
726
|
+
writeDataBank(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
727
|
+
fivoraAudit = auditFivoraContract({
|
|
728
|
+
profile,
|
|
729
|
+
siteData: dataBundle.siteData,
|
|
730
|
+
manifest: dataBundle.manifest,
|
|
731
|
+
inventory: collectMarkerInventory(projectDir, profile, graph),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
692
736
|
const coverage = coverageMetrics({
|
|
693
737
|
analyses,
|
|
694
738
|
plan,
|
|
@@ -742,7 +786,11 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
742
786
|
}
|
|
743
787
|
}
|
|
744
788
|
outcome = 'rolled-back';
|
|
745
|
-
|
|
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(', ')}`);
|
|
746
794
|
} else if (contractFailure) {
|
|
747
795
|
outcome = 'contract-failed';
|
|
748
796
|
} else if (designFailure) {
|
|
@@ -819,6 +867,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
819
867
|
designPreservation: design.score,
|
|
820
868
|
validation,
|
|
821
869
|
outcome,
|
|
870
|
+
fontIds: collectFontIdsFromSiteData(dataBundle.siteData),
|
|
822
871
|
};
|
|
823
872
|
|
|
824
873
|
const report = buildReport({
|
|
@@ -911,6 +960,7 @@ module.exports = {
|
|
|
911
960
|
runDenebArc,
|
|
912
961
|
parseArcOptions,
|
|
913
962
|
scanProject,
|
|
963
|
+
auditFivoraContract,
|
|
914
964
|
ENGINE_ID,
|
|
915
965
|
ARC_VERSION,
|
|
916
966
|
};
|
package/src/arc/learning.cjs
CHANGED
|
@@ -108,6 +108,10 @@ const BASELINE_VERIFIED_FINGERPRINTS = {
|
|
|
108
108
|
'empty-state-array-guard': { id: 'empty-state-array-guard', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
109
109
|
'action-label-split': { id: 'action-label-split', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
110
110
|
'section-overflow-clip': { id: 'section-overflow-clip', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
111
|
+
'auto-injected-site-data-hook': { id: 'auto-injected-site-data-hook', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
112
|
+
'hidden-preview-marker-strip': { id: 'hidden-preview-marker-strip', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
113
|
+
'unknown-marker-auto-prune': { id: 'unknown-marker-auto-prune', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
114
|
+
'hidden-element-strict-contract': { id: 'hidden-element-strict-contract', state: 'deprecated', successfulApplications: 0, failedApplications: 50 },
|
|
111
115
|
};
|
|
112
116
|
|
|
113
117
|
function loadFingerprintBoost(fingerprint) {
|
package/src/arc/residual.cjs
CHANGED
|
@@ -427,8 +427,16 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
|
|
|
427
427
|
return { code, changed: false, fields: [], applied: 0 };
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
let finalCode = printSource(ast, code);
|
|
431
|
+
if (finalCode.includes('siteData')) {
|
|
432
|
+
try {
|
|
433
|
+
const { healMissingSiteDataHooks } = require('./transformer.cjs');
|
|
434
|
+
finalCode = healMissingSiteDataHooks(finalCode, file);
|
|
435
|
+
} catch {}
|
|
436
|
+
}
|
|
437
|
+
|
|
430
438
|
return {
|
|
431
|
-
code:
|
|
439
|
+
code: finalCode,
|
|
432
440
|
changed: true,
|
|
433
441
|
fields,
|
|
434
442
|
applied,
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -286,6 +286,34 @@ function hasStaticMarker(node) {
|
|
|
286
286
|
return hasJsxAttribute(node, 'data-preview-static');
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function isElementOrParentHidden(pathNode) {
|
|
290
|
+
let current = pathNode;
|
|
291
|
+
while (current) {
|
|
292
|
+
const node = current.node || current.value || current;
|
|
293
|
+
if (node && (node.type === 'JSXElement' || node.type === 'JSXOpeningElement')) {
|
|
294
|
+
const attrs = (node.type === 'JSXElement' ? node.openingElement?.attributes : node.attributes) || [];
|
|
295
|
+
if (attrs.some((a) => a.type === 'JSXAttribute' && a.name && (
|
|
296
|
+
a.name.name === 'hidden' ||
|
|
297
|
+
(a.name.name === 'aria-hidden' && /true/.test(recast.print(a).code))
|
|
298
|
+
))) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const classAttr = attrs.find((a) => a.type === 'JSXAttribute' && a.name && (a.name.name === 'className' || a.name.name === 'class'));
|
|
302
|
+
if (classAttr) {
|
|
303
|
+
const rawClass = recast.print(classAttr).code;
|
|
304
|
+
if (/(?<![\w-])hidden(?![a-zA-Z0-9_-])/.test(rawClass)) return true;
|
|
305
|
+
}
|
|
306
|
+
const styleAttr = attrs.find((a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'style');
|
|
307
|
+
if (styleAttr) {
|
|
308
|
+
const rawStyle = recast.print(styleAttr).code;
|
|
309
|
+
if (/(?:display\s*:\s*['"]none['"]|visibility\s*:\s*['"]hidden['"])/.test(rawStyle)) return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
current = current.parentPath || current.parent;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
289
317
|
function parentNames(pathNode) {
|
|
290
318
|
const names = [];
|
|
291
319
|
let current = pathNode.parent;
|
|
@@ -523,6 +551,21 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
523
551
|
return;
|
|
524
552
|
}
|
|
525
553
|
|
|
554
|
+
if (isElementOrParentHidden(pathNode)) {
|
|
555
|
+
candidates.push({
|
|
556
|
+
loc,
|
|
557
|
+
tag: name,
|
|
558
|
+
kind: 'decoration',
|
|
559
|
+
confidence: 0.1,
|
|
560
|
+
skip: true,
|
|
561
|
+
reason: 'hidden-element-strict-contract',
|
|
562
|
+
file: relativeFile,
|
|
563
|
+
ownerScope,
|
|
564
|
+
});
|
|
565
|
+
this.traverse(pathNode);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
526
569
|
const importSource = imports[name.split('.')[0]];
|
|
527
570
|
const recognition = recognizeWithAdapters(node, { profile, imports }, adapters);
|
|
528
571
|
const mapInfo = inMapCallback(pathNode);
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -1537,7 +1537,7 @@ function sanitizeContradictoryMarkers(ast) {
|
|
|
1537
1537
|
}
|
|
1538
1538
|
|
|
1539
1539
|
function sanitizeContradictoryMarkersInSource(code, relativeFile) {
|
|
1540
|
-
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path') && !code.includes('overflow-hidden')) {
|
|
1540
|
+
if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path') && !code.includes('overflow-hidden') && !code.includes('hidden')) {
|
|
1541
1541
|
return { code, updated: false };
|
|
1542
1542
|
}
|
|
1543
1543
|
let ast;
|
|
@@ -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
|
},
|
|
@@ -1720,10 +1828,15 @@ function healHiddenPreviewMarkers(ast) {
|
|
|
1720
1828
|
if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
|
|
1721
1829
|
}
|
|
1722
1830
|
}
|
|
1723
|
-
const
|
|
1724
|
-
(attr) => attr.type === 'JSXAttribute' && attr.name && (
|
|
1725
|
-
|
|
1726
|
-
|
|
1831
|
+
const isHidden =
|
|
1832
|
+
attrs.some((attr) => attr.type === 'JSXAttribute' && attr.name && (
|
|
1833
|
+
attr.name.name === 'hidden' ||
|
|
1834
|
+
(attr.name.name === 'aria-hidden' && /true/.test(recast.print(attr).code))
|
|
1835
|
+
)) ||
|
|
1836
|
+
/(?<![\w-])hidden(?![a-zA-Z0-9_-])/.test(classText) ||
|
|
1837
|
+
attrs.some((attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'style' && /(?:display\s*:\s*['"]none['"]|visibility\s*:\s*['"]hidden['"])/.test(recast.print(attr).code));
|
|
1838
|
+
|
|
1839
|
+
if (isHidden) {
|
|
1727
1840
|
pathNode.node.attributes = attrs.filter(
|
|
1728
1841
|
(attr) =>
|
|
1729
1842
|
!(
|
|
@@ -1923,6 +2036,77 @@ function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
|
1923
2036
|
return code;
|
|
1924
2037
|
}
|
|
1925
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
|
+
|
|
1926
2110
|
module.exports = {
|
|
1927
2111
|
applyFilePlan,
|
|
1928
2112
|
applyCollectionTransform,
|
|
@@ -1942,6 +2126,8 @@ module.exports = {
|
|
|
1942
2126
|
healDecorativeOverlays,
|
|
1943
2127
|
healLegacyProductDetailLinks,
|
|
1944
2128
|
healMissingSiteDataHooks,
|
|
2129
|
+
healUnguardedModalConditionals,
|
|
2130
|
+
healUnguardedModalConditionalsInSource,
|
|
1945
2131
|
injectSiteDataHook,
|
|
1946
2132
|
};
|
|
1947
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)} ║`;
|
|
@@ -201,6 +233,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
201
233
|
// =========================================================================
|
|
202
234
|
if (!isJson) console.log('\n\x1b[1m[2/7] Project Package Configuration:\x1b[0m');
|
|
203
235
|
const suite2 = 'Package Configuration';
|
|
236
|
+
const sourceFiles = findSourceFiles(path.join(targetDir, 'src'));
|
|
204
237
|
|
|
205
238
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
206
239
|
let pkg = null;
|
|
@@ -288,6 +321,32 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
288
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.');
|
|
289
322
|
}
|
|
290
323
|
|
|
324
|
+
// Pre-heal missing useSiteData hooks and unguarded modals before tsc runs when --fix is enabled
|
|
325
|
+
if (shouldFix) {
|
|
326
|
+
for (const file of sourceFiles) {
|
|
327
|
+
if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
|
|
328
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
329
|
+
let modified = false;
|
|
330
|
+
if (c.includes('siteData')) {
|
|
331
|
+
const healed = healMissingSiteDataHooks(c, file);
|
|
332
|
+
if (healed && healed !== c) {
|
|
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;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (modified) {
|
|
345
|
+
fs.writeFileSync(file, c, 'utf8');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
291
350
|
// Check TypeScript compilation preflight
|
|
292
351
|
const tsconfigPath = path.join(targetDir, 'tsconfig.json');
|
|
293
352
|
if (fs.existsSync(tsconfigPath)) {
|
|
@@ -480,7 +539,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
480
539
|
}
|
|
481
540
|
|
|
482
541
|
// Scan all source files for visual editing contract compliance
|
|
483
|
-
|
|
542
|
+
// (sourceFiles already loaded)
|
|
484
543
|
const foundFieldPaths = new Set();
|
|
485
544
|
const orphanPaths = [];
|
|
486
545
|
const missingInSchema = [];
|
|
@@ -869,7 +928,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
869
928
|
for (const file of sourceFiles) {
|
|
870
929
|
if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
|
|
871
930
|
let c = fs.readFileSync(file, 'utf-8');
|
|
872
|
-
if (c.includes('siteData')
|
|
931
|
+
if (c.includes('siteData')) {
|
|
873
932
|
const healed = healMissingSiteDataHooks(c, file);
|
|
874
933
|
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
875
934
|
if (healed && norm(healed) !== norm(c)) {
|