@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
|
@@ -13,6 +13,7 @@ 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
17
|
|
|
17
18
|
function createBox(lines, width = 60) {
|
|
18
19
|
const horizontal = '═'.repeat(width - 2);
|
|
@@ -286,6 +287,34 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
286
287
|
} else {
|
|
287
288
|
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.');
|
|
288
289
|
}
|
|
290
|
+
|
|
291
|
+
// Check TypeScript compilation preflight
|
|
292
|
+
const tsconfigPath = path.join(targetDir, 'tsconfig.json');
|
|
293
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
294
|
+
const tscBin = process.platform === 'win32'
|
|
295
|
+
? path.join(targetDir, 'node_modules', '.bin', 'tsc.cmd')
|
|
296
|
+
: path.join(targetDir, 'node_modules', '.bin', 'tsc');
|
|
297
|
+
|
|
298
|
+
let tscRes = null;
|
|
299
|
+
if (fs.existsSync(tscBin)) {
|
|
300
|
+
tscRes = spawnSync(tscBin, ['--noEmit'], { cwd: targetDir, encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 10 * 1024 * 1024 });
|
|
301
|
+
} else {
|
|
302
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
303
|
+
tscRes = spawnSync(npxCmd, ['--no-install', 'tsc', '--noEmit'], { cwd: targetDir, encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 10 * 1024 * 1024 });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (tscRes && tscRes.status === 0) {
|
|
307
|
+
addCheck(suite2, 'pass', 'TypeScript Compilation Preflight', '0 type errors detected (tsc --noEmit clean)', { code: 'DNB-TYP-001' });
|
|
308
|
+
} else if (tscRes && tscRes.status !== null) {
|
|
309
|
+
const rawErr = (tscRes.stdout || tscRes.stderr || '').trim();
|
|
310
|
+
const lines = rawErr.split(/\r?\n/).filter(Boolean);
|
|
311
|
+
const errLines = lines.filter((l) => l.includes('error TS'));
|
|
312
|
+
const summary = errLines.length > 0 ? `${errLines.length} type error(s) (e.g. ${errLines[0].trim()})` : (lines[0] || 'Type errors found');
|
|
313
|
+
addCheck(suite2, 'err', 'TypeScript Compilation Preflight', summary, { code: 'DNB-TYP-001', action: 'Fix TypeScript compilation errors shown by tsc --noEmit' });
|
|
314
|
+
} else {
|
|
315
|
+
addCheck(suite2, 'warn', 'TypeScript Compilation Preflight', 'Could not execute tsc to verify types', { code: 'DNB-TYP-001' });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
289
318
|
} catch (e) {
|
|
290
319
|
addCheck(suite2, 'err', 'package.json Syntax', e.message);
|
|
291
320
|
}
|
|
@@ -419,6 +448,37 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
419
448
|
addCheck(suite5, 'warn', 'site-data.json', 'src/data/site-data.json not found');
|
|
420
449
|
}
|
|
421
450
|
|
|
451
|
+
// Root Layout SiteDataProvider Instrumentation Check
|
|
452
|
+
const doctorLayoutFiles = [
|
|
453
|
+
path.join(targetDir, 'src', 'app', 'layout.tsx'),
|
|
454
|
+
path.join(targetDir, 'src', 'app', 'layout.jsx'),
|
|
455
|
+
path.join(targetDir, 'app', 'layout.tsx'),
|
|
456
|
+
path.join(targetDir, 'app', 'layout.jsx'),
|
|
457
|
+
];
|
|
458
|
+
const docLayout = doctorLayoutFiles.find((f) => fs.existsSync(f));
|
|
459
|
+
if (docLayout) {
|
|
460
|
+
const layoutSrc = fs.readFileSync(docLayout, 'utf8');
|
|
461
|
+
const hasSiteProvider = /SiteDataProvider|DenebDataProvider|<Providers\b/.test(layoutSrc);
|
|
462
|
+
if (hasSiteProvider) {
|
|
463
|
+
addCheck(suite5, 'pass', 'Root Layout Provider', `<SiteDataProvider> mounted in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
|
|
464
|
+
} else if (isFix) {
|
|
465
|
+
try {
|
|
466
|
+
const { instrumentLayoutSource } = require('../arc/transformer.cjs');
|
|
467
|
+
const instrumented = instrumentLayoutSource(layoutSrc, '@/data/site-data.json', '@deneb-ui/ui');
|
|
468
|
+
if (instrumented.updated && instrumented.code !== layoutSrc) {
|
|
469
|
+
fs.writeFileSync(docLayout, instrumented.code, 'utf8');
|
|
470
|
+
addCheck(suite5, 'fixed', 'Root Layout Provider', `Instrumented <SiteDataProvider> into ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
|
|
471
|
+
} else {
|
|
472
|
+
addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)} (Run with --fix to instrument automatically)`, { code: 'DNB-LAY-001' });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
422
482
|
// Scan all source files for visual editing contract compliance
|
|
423
483
|
const sourceFiles = findSourceFiles(path.join(targetDir, 'src'));
|
|
424
484
|
const foundFieldPaths = new Set();
|
|
@@ -503,8 +563,8 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
503
563
|
}
|
|
504
564
|
}
|
|
505
565
|
|
|
506
|
-
// 4. Broad
|
|
507
|
-
// Fivora forbids data-preview-static on broad layout containers like <div>, <section>, <nav>, <main>, <header>
|
|
566
|
+
// 4. Broad Container Detection
|
|
567
|
+
// Fivora forbids data-preview-static and data-preview-field-path on broad layout containers like <div>, <section>, <nav>, <main>, <header>
|
|
508
568
|
const broadStaticMatches = code.matchAll(/<(div|section|nav|main|header|article|aside)(\s+[^>]*data-preview-static="[^"]*"[^>]*)>/gi);
|
|
509
569
|
let fileNeedsBroadStaticFix = false;
|
|
510
570
|
let newCodeBroad = fs.readFileSync(file, 'utf-8');
|
|
@@ -513,7 +573,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
513
573
|
const tag = bsm[1].toLowerCase();
|
|
514
574
|
// Only flag if it's not a pure leaf element
|
|
515
575
|
if (['div', 'section', 'nav', 'main', 'header', 'article', 'aside'].includes(tag)) {
|
|
516
|
-
broadStaticContainers.push({ tag, file: path.relative(targetDir, file) });
|
|
576
|
+
broadStaticContainers.push({ tag, file: path.relative(targetDir, file), type: 'static' });
|
|
517
577
|
if (shouldFix) {
|
|
518
578
|
const originalTag = `<${bsm[1]}${bsm[2]}>`;
|
|
519
579
|
const cleanTag = originalTag.replace(/\s*data-preview-static="[^"]*"/g, '');
|
|
@@ -523,6 +583,26 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
523
583
|
}
|
|
524
584
|
}
|
|
525
585
|
|
|
586
|
+
const broadFieldMatches = code.matchAll(/<(div|section|nav|main|header|article|aside)(\s+[^>]*data-preview-field-path="[^"]*"[^>]*)>/gi);
|
|
587
|
+
for (const bfm of broadFieldMatches) {
|
|
588
|
+
const tag = bfm[1].toLowerCase();
|
|
589
|
+
if (['div', 'section', 'nav', 'main', 'header', 'article', 'aside'].includes(tag)) {
|
|
590
|
+
broadStaticContainers.push({ tag, file: path.relative(targetDir, file), type: 'field' });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (shouldFix && [...broadFieldMatches].length > 0) {
|
|
595
|
+
try {
|
|
596
|
+
const { parseSource, printSource } = require('../arc/ast.cjs');
|
|
597
|
+
const ast = parseSource(newCodeBroad, file);
|
|
598
|
+
const healed = healBroadContainerMarkers(ast);
|
|
599
|
+
if (healed > 0) {
|
|
600
|
+
newCodeBroad = printSource(ast, newCodeBroad);
|
|
601
|
+
fileNeedsBroadStaticFix = true;
|
|
602
|
+
}
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
|
|
526
606
|
if (shouldFix && fileNeedsBroadStaticFix) {
|
|
527
607
|
fs.writeFileSync(file, newCodeBroad, 'utf8');
|
|
528
608
|
}
|
|
@@ -595,11 +675,11 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
595
675
|
}
|
|
596
676
|
|
|
597
677
|
if (broadStaticContainers.length === 0) {
|
|
598
|
-
addCheck(suite5, 'pass', 'Granular Static Markup', 'Zero broad layout containers (div/nav/section) marked static', { code: 'DNB-STC-006' })
|
|
678
|
+
addCheck(suite5, 'pass', 'Granular Static Markup', 'Zero broad layout containers (div/nav/section) marked static or field-bound', { code: 'DNB-STC-006' });
|
|
599
679
|
} else if (shouldFix) {
|
|
600
|
-
addCheck(suite5, 'fixed', 'Granular Static Markup', `
|
|
680
|
+
addCheck(suite5, 'fixed', 'Granular Static Markup', `Repaired/demoted ${broadStaticContainers.length} broad container marker(s)`, { code: 'DNB-STC-006' });
|
|
601
681
|
} else {
|
|
602
|
-
addCheck(suite5, 'warn', 'Granular Static Markup', `${broadStaticContainers.length} broad container(s) marked with
|
|
682
|
+
addCheck(suite5, 'warn', 'Granular Static Markup', `${broadStaticContainers.length} broad container(s) marked with static or field paths (Fivora requires marking only smallest leaf elements). Run with --fix to repair.`, { code: 'DNB-STC-006' });
|
|
603
683
|
}
|
|
604
684
|
|
|
605
685
|
if (dynamicVariableMarkers.length === 0) {
|
|
@@ -608,6 +688,73 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
608
688
|
addCheck(suite5, 'warn', 'Literal Marker Standard', `${dynamicVariableMarkers.length} dynamic variable marker(s) detected`, { code: 'DNB-AST-002' }) //, `${dynamicVariableMarkers.length} dynamic variable marker(s) detected (e.g. ${dynamicVariableMarkers[0].expr})`);
|
|
609
689
|
}
|
|
610
690
|
|
|
691
|
+
// Check: Control-Only & Platform Contract Path Integrity (DNB-CTL-001)
|
|
692
|
+
if (manifestData) {
|
|
693
|
+
manifestData.visualEditing = manifestData.visualEditing || {};
|
|
694
|
+
const existingControlOnly = new Set(manifestData.visualEditing.controlOnlyPaths || []);
|
|
695
|
+
const missingControlOnly = [];
|
|
696
|
+
|
|
697
|
+
// 1. Platform contract paths: if __fivoraIntake or additionalPages exist in editorSchema,
|
|
698
|
+
// ensure their paths are in controlOnlyPaths
|
|
699
|
+
const schemaSections = manifestData.editorSchema?.sections || [];
|
|
700
|
+
const hasIntake = schemaSections.some((s) => s.id === '__fivoraIntake');
|
|
701
|
+
const hasPages = schemaSections.some((s) => s.id === 'additionalPages');
|
|
702
|
+
|
|
703
|
+
if (hasIntake) {
|
|
704
|
+
const intakePaths = [
|
|
705
|
+
'__fivoraIntake.tone',
|
|
706
|
+
'__fivoraIntake.outputLanguage',
|
|
707
|
+
'__fivoraIntake.businessSummary',
|
|
708
|
+
'__fivoraIntake.additionalBusinessDetails',
|
|
709
|
+
'__fivoraIntake.referenceWebsiteUrl',
|
|
710
|
+
];
|
|
711
|
+
for (const p of intakePaths) {
|
|
712
|
+
if (!existingControlOnly.has(p)) missingControlOnly.push(p);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (hasPages) {
|
|
717
|
+
const pagesPaths = [
|
|
718
|
+
'additionalPages[*].id',
|
|
719
|
+
'additionalPages[*].route',
|
|
720
|
+
'additionalPages[*].label',
|
|
721
|
+
];
|
|
722
|
+
for (const p of pagesPaths) {
|
|
723
|
+
if (!existingControlOnly.has(p)) missingControlOnly.push(p);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 2. Modal & unrendered form fields in siteData.content (e.g. *.form.*)
|
|
728
|
+
if (siteData && siteData.content) {
|
|
729
|
+
function collectModalFormPaths(obj, prefix = '') {
|
|
730
|
+
if (!obj || typeof obj !== 'object') return;
|
|
731
|
+
if (Array.isArray(obj)) return;
|
|
732
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
733
|
+
const p = prefix ? `${prefix}.${k}` : k;
|
|
734
|
+
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
735
|
+
collectModalFormPaths(v, p);
|
|
736
|
+
} else {
|
|
737
|
+
if (/\.form\./i.test(p) || /^form\./i.test(p) || /\.modal\./i.test(p) || /Modal\./i.test(p)) {
|
|
738
|
+
if (!existingControlOnly.has(p)) missingControlOnly.push(p);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
collectModalFormPaths(siteData.content);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (missingControlOnly.length === 0) {
|
|
747
|
+
addCheck(suite5, 'pass', 'Control-Only Path Contract', 'All platform-managed & modal form paths declared in controlOnlyPaths', { code: 'DNB-CTL-001' });
|
|
748
|
+
} else if (shouldFix) {
|
|
749
|
+
for (const p of missingControlOnly) existingControlOnly.add(p);
|
|
750
|
+
manifestData.visualEditing.controlOnlyPaths = [...existingControlOnly].sort();
|
|
751
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf8');
|
|
752
|
+
addCheck(suite5, 'fixed', 'Control-Only Path Contract', `Added ${missingControlOnly.length} platform & modal field(s) to controlOnlyPaths`, { code: 'DNB-CTL-001' });
|
|
753
|
+
} else {
|
|
754
|
+
addCheck(suite5, 'warn', 'Control-Only Path Contract', `${missingControlOnly.length} platform or modal path(s) missing from controlOnlyPaths (Run with --fix to register automatically)`, { code: 'DNB-CTL-001' });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
611
758
|
// =========================================================================
|
|
612
759
|
// SUITE 6: Multi-Niche Storefront Architecture & Security Preflight
|
|
613
760
|
// =========================================================================
|
|
@@ -647,17 +794,33 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
647
794
|
// Check: Empty-State Array & Out-of-Range Guards (DNB-ARR-003)
|
|
648
795
|
let unguardedArrayCount = 0;
|
|
649
796
|
for (const file of sourceFiles) {
|
|
650
|
-
|
|
797
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
798
|
+
let fileModified = false;
|
|
651
799
|
if (c.includes('data-preview-list-path')) {
|
|
652
800
|
if (/\?\s*[A-Z0-9_]+\s*:\s*[A-Z0-9_]+/i.test(c) && !c.includes('Array.isArray')) {
|
|
653
801
|
unguardedArrayCount++;
|
|
802
|
+
if (shouldFix) {
|
|
803
|
+
const repaired = c.replace(
|
|
804
|
+
/([a-zA-Z0-9_$]+)\s*(?:&&|\?\.)\s*length(?:\s*>\s*0)?\s*\?\s*\1\s*:\s*([a-zA-Z0-9_$]+)/g,
|
|
805
|
+
'Array.isArray($1) ? $1 : ($1 || $2)'
|
|
806
|
+
);
|
|
807
|
+
if (repaired !== c) {
|
|
808
|
+
c = repaired;
|
|
809
|
+
fileModified = true;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
654
812
|
}
|
|
655
813
|
}
|
|
814
|
+
if (shouldFix && fileModified) {
|
|
815
|
+
fs.writeFileSync(file, c, 'utf8');
|
|
816
|
+
}
|
|
656
817
|
}
|
|
657
818
|
if (unguardedArrayCount === 0) {
|
|
658
819
|
addCheck(suite5, 'pass', 'Empty-State Array Guards', 'All list collections guarded against empty state ([]) out-of-range elements', { code: 'DNB-ARR-003' });
|
|
820
|
+
} else if (shouldFix) {
|
|
821
|
+
addCheck(suite5, 'fixed', 'Empty-State Array Guards', `Injected Array.isArray safe empty-state guard into ${unguardedArrayCount} component(s)`, { code: 'DNB-ARR-003' });
|
|
659
822
|
} else {
|
|
660
|
-
addCheck(suite5, 'warn', 'Empty-State Array Guards', `${unguardedArrayCount} list component(s) should verify Array.isArray(liveList) ? liveList : DEFAULT_LIST
|
|
823
|
+
addCheck(suite5, 'warn', 'Empty-State Array Guards', `${unguardedArrayCount} list component(s) should verify Array.isArray(liveList) ? liveList : (liveList || DEFAULT_LIST). Run with --fix to repair.`, { code: 'DNB-ARR-003' });
|
|
661
824
|
}
|
|
662
825
|
|
|
663
826
|
// Check: Empty-State Singleton Persistence Guard (DNB-EMP-002)
|
|
@@ -701,6 +864,120 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
701
864
|
addCheck(suite5, 'warn', 'WhatsApp Action Live Sync', `${disconnectedWhatsAppCount} WhatsApp trigger(s) using disconnected fallback instead of whatsappOrderUrl. Run with --fix to repair.`, { code: 'DNB-WHA-008' });
|
|
702
865
|
}
|
|
703
866
|
|
|
867
|
+
// Check: Multi-Component Scope & useSiteData Injection (DNB-SCP-001)
|
|
868
|
+
let missingSiteDataHookCount = 0;
|
|
869
|
+
for (const file of sourceFiles) {
|
|
870
|
+
if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
|
|
871
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
872
|
+
if (c.includes('siteData') && (c.includes('useSiteData') || c.includes('@deneb-ui/ui'))) {
|
|
873
|
+
const healed = healMissingSiteDataHooks(c, file);
|
|
874
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
875
|
+
if (healed && norm(healed) !== norm(c)) {
|
|
876
|
+
missingSiteDataHookCount++;
|
|
877
|
+
if (shouldFix) {
|
|
878
|
+
fs.writeFileSync(file, healed, 'utf8');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (missingSiteDataHookCount === 0) {
|
|
884
|
+
addCheck(suite5, 'pass', 'Component Scope siteData Binding', 'All components referencing siteData have local useSiteData() hook binding', { code: 'DNB-SCP-001' });
|
|
885
|
+
} else if (shouldFix) {
|
|
886
|
+
addCheck(suite5, 'fixed', 'Component Scope siteData Binding', `Injected missing useSiteData() hook into ${missingSiteDataHookCount} component(s)`, { code: 'DNB-SCP-001' });
|
|
887
|
+
} else {
|
|
888
|
+
addCheck(suite5, 'err', 'Component Scope siteData Binding', `${missingSiteDataHookCount} component(s) reference siteData without calling useSiteData(). Run with --fix to inject.`, { code: 'DNB-SCP-001' });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Check: Collection Callback TypeScript Typing & Collision Guard (DNB-TYP-002)
|
|
892
|
+
let collParamsFixed = 0;
|
|
893
|
+
for (const file of sourceFiles) {
|
|
894
|
+
if (!/\.(tsx|ts)$/.test(file)) continue;
|
|
895
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
896
|
+
let fileModified = false;
|
|
897
|
+
|
|
898
|
+
// 1. Fix 3-parameter collision: .map((stat, idx, index) =>
|
|
899
|
+
const trippleMatches = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*,\s*([a-zA-Z0-9_$]+)\s*,\s*index\s*\)\s*=>/g)];
|
|
900
|
+
if (trippleMatches.length > 0) {
|
|
901
|
+
collParamsFixed += trippleMatches.length;
|
|
902
|
+
if (shouldFix) {
|
|
903
|
+
for (const m of trippleMatches) {
|
|
904
|
+
const p1 = m[1];
|
|
905
|
+
const p2 = m[2];
|
|
906
|
+
c = c.replace(m[0], `.map((${p1}: any, ${p2}: number) =>`);
|
|
907
|
+
c = c.replace(/\[\$\{index\}\]/g, `[\${${p2}}]`);
|
|
908
|
+
}
|
|
909
|
+
fileModified = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 2. Fix leftover [${index}] when callback uses idx
|
|
914
|
+
if (c.includes('[${index}]') && c.includes('idx') && !c.includes('index) =>') && !c.includes('index,') && !c.includes('index:')) {
|
|
915
|
+
c = c.replace(/\[\$\{index\}\]/g, '[${idx}]');
|
|
916
|
+
fileModified = true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 3. Fix untyped parameters in .map() on data-preview or siteData collections causing TS7006
|
|
920
|
+
if (c.includes('data-preview-') || c.includes('siteData')) {
|
|
921
|
+
const untypedMap2 = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*,\s*([a-zA-Z0-9_$]+)\s*\)\s*=>/g)];
|
|
922
|
+
for (const m of untypedMap2) {
|
|
923
|
+
const p1 = m[1];
|
|
924
|
+
const p2 = m[2];
|
|
925
|
+
if (!p1.includes(':') && !p2.includes(':')) {
|
|
926
|
+
collParamsFixed++;
|
|
927
|
+
if (shouldFix) {
|
|
928
|
+
c = c.replace(m[0], `.map((${p1}: any, ${p2}: number) =>`);
|
|
929
|
+
fileModified = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const untypedMap1 = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*\)\s*=>/g)];
|
|
935
|
+
for (const m of untypedMap1) {
|
|
936
|
+
const p1 = m[1];
|
|
937
|
+
if (!p1.includes(':')) {
|
|
938
|
+
collParamsFixed++;
|
|
939
|
+
if (shouldFix) {
|
|
940
|
+
c = c.replace(m[0], `.map((${p1}: any) =>`);
|
|
941
|
+
fileModified = true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (shouldFix && fileModified) {
|
|
948
|
+
fs.writeFileSync(file, c, 'utf8');
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (collParamsFixed === 0) {
|
|
953
|
+
addCheck(suite5, 'pass', 'Collection Callback TypeScript Contracts', 'Zero parameter collisions or untyped callback parameters in collection maps', { code: 'DNB-TYP-002' });
|
|
954
|
+
} else if (shouldFix) {
|
|
955
|
+
addCheck(suite5, 'fixed', 'Collection Callback TypeScript Contracts', `Repaired and type-annotated ${collParamsFixed} collection map callback(s)`, { code: 'DNB-TYP-002' });
|
|
956
|
+
} else {
|
|
957
|
+
addCheck(suite5, 'warn', 'Collection Callback TypeScript Contracts', `${collParamsFixed} collection callback(s) have untyped parameters or parameter collisions. Run with --fix to repair.`, { code: 'DNB-TYP-002' });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Check: JSON Module Direct Import Type Narrowing Guard (DNB-TYP-003)
|
|
961
|
+
let staticJsonImportCount = 0;
|
|
962
|
+
for (const file of sourceFiles) {
|
|
963
|
+
if (!/\.(tsx|ts)$/.test(file)) continue;
|
|
964
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
965
|
+
if (c.includes('import siteData from') && c.includes('site-data.json')) {
|
|
966
|
+
staticJsonImportCount++;
|
|
967
|
+
if (shouldFix) {
|
|
968
|
+
c = c.replace(/import\s+siteData\s+from\s+['"]([^'"]*site-data\.json)['"];?/, 'import rawSiteData from "$1";\nconst siteData: any = rawSiteData;');
|
|
969
|
+
fs.writeFileSync(file, c, 'utf8');
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (staticJsonImportCount === 0) {
|
|
974
|
+
addCheck(suite5, 'pass', 'JSON Import Type Safety', 'No statically locked site-data.json imports causing TS2339 schema collisions', { code: 'DNB-TYP-003' });
|
|
975
|
+
} else if (shouldFix) {
|
|
976
|
+
addCheck(suite5, 'fixed', 'JSON Import Type Safety', `Safely typed ${staticJsonImportCount} static site-data.json import(s)`, { code: 'DNB-TYP-003' });
|
|
977
|
+
} else {
|
|
978
|
+
addCheck(suite5, 'warn', 'JSON Import Type Safety', `${staticJsonImportCount} file(s) statically import site-data.json without any-casting, which can trigger TS2339 under resolveJsonModule. Run with --fix to heal.`, { code: 'DNB-TYP-003' });
|
|
979
|
+
}
|
|
980
|
+
|
|
704
981
|
// Check: Next.js Image Empty String Guard (DNB-IMG-002)
|
|
705
982
|
let emptyImageSrcCount = 0;
|
|
706
983
|
for (const file of sourceFiles) {
|
|
@@ -895,9 +1172,20 @@ function runDoctor(targetDirInput = '.', options = {}) {
|
|
|
895
1172
|
fs.existsSync(path.join(targetDir, 'public', 'preview.png'));
|
|
896
1173
|
|
|
897
1174
|
if (previewExists) {
|
|
898
|
-
addCheck(suite6, 'pass', 'Storefront Preview Graphic', 'preview.png / thumbnail.png verified for Fivora gallery', { code: 'DNB-PRV-001' })
|
|
1175
|
+
addCheck(suite6, 'pass', 'Storefront Preview Graphic', 'preview.png / thumbnail.png verified for Fivora gallery', { code: 'DNB-PRV-001' });
|
|
1176
|
+
} else if (shouldFix) {
|
|
1177
|
+
try {
|
|
1178
|
+
const minimalPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
|
|
1179
|
+
fs.writeFileSync(path.join(targetDir, 'preview.png'), minimalPng);
|
|
1180
|
+
if (fs.existsSync(path.join(targetDir, 'public'))) {
|
|
1181
|
+
fs.writeFileSync(path.join(targetDir, 'public', 'preview.png'), minimalPng);
|
|
1182
|
+
}
|
|
1183
|
+
addCheck(suite6, 'fixed', 'Storefront Preview Graphic', 'Created placeholder preview.png for Fivora marketplace preflight', { code: 'DNB-PRV-001' });
|
|
1184
|
+
} catch {
|
|
1185
|
+
addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder', { code: 'DNB-PRV-001' });
|
|
1186
|
+
}
|
|
899
1187
|
} else {
|
|
900
|
-
addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder', { code: 'DNB-PRV-001' })
|
|
1188
|
+
addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder (Run with --fix to scaffold placeholder)', { code: 'DNB-PRV-001' });
|
|
901
1189
|
}
|
|
902
1190
|
|
|
903
1191
|
// Large assets audit (> 4MB)
|