mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
feat(manascore): add 5 new ecosystem metrics
Expand ecosystem-audit with: - Error Boundaries (54%) — +error.svelte + offline page per app - TypeScript Strict (100%) — strict mode in all apps - Test Coverage (72%) — apps with at least one test (111 files total) - PWA Support (2%) — manifest + service worker - Maintainability (0%) — files under 500 lines (38 files exceed limit) Dashboard shows file size top offenders and apps without tests. Overall score adjusted from 76 to 70 with rebalanced weights. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5fa773d400
commit
0c89400b5b
3 changed files with 514 additions and 41 deletions
|
|
@ -1,28 +1,38 @@
|
|||
{
|
||||
"generatedAt": "2026-03-31T11:53:48.040Z",
|
||||
"overallScore": 76,
|
||||
"generatedAt": "2026-03-31T12:04:45.568Z",
|
||||
"overallScore": 70,
|
||||
"scores": {
|
||||
"sharedPackages": 90,
|
||||
"iconConsistency": 89,
|
||||
"modalConsistency": 24,
|
||||
"modalConsistency": 19,
|
||||
"errorHandling": 20,
|
||||
"i18nCoverage": 86,
|
||||
"localFirst": 93,
|
||||
"styleConsistency": 87
|
||||
"styleConsistency": 87,
|
||||
"errorBoundaries": 54,
|
||||
"typescriptStrict": 100,
|
||||
"testCoverage": 72,
|
||||
"pwaSupport": 2,
|
||||
"maintainability": 0
|
||||
},
|
||||
"weights": {
|
||||
"sharedPackages": 25,
|
||||
"iconConsistency": 15,
|
||||
"modalConsistency": 10,
|
||||
"errorHandling": 10,
|
||||
"i18nCoverage": 15,
|
||||
"localFirst": 10,
|
||||
"styleConsistency": 15
|
||||
"sharedPackages": 20,
|
||||
"iconConsistency": 10,
|
||||
"modalConsistency": 5,
|
||||
"errorHandling": 5,
|
||||
"i18nCoverage": 10,
|
||||
"localFirst": 8,
|
||||
"styleConsistency": 10,
|
||||
"errorBoundaries": 8,
|
||||
"typescriptStrict": 7,
|
||||
"testCoverage": 7,
|
||||
"pwaSupport": 5,
|
||||
"maintainability": 5
|
||||
},
|
||||
"details": {
|
||||
"icons": {
|
||||
"adoption": 89,
|
||||
"phosphorFiles": 353,
|
||||
"phosphorFiles": 354,
|
||||
"inlineSvgFiles": 45,
|
||||
"perApp": {
|
||||
"calc": {
|
||||
|
|
@ -30,7 +40,7 @@
|
|||
"inlineSvg": 0
|
||||
},
|
||||
"calendar": {
|
||||
"phosphor": 34,
|
||||
"phosphor": 35,
|
||||
"inlineSvg": 0
|
||||
},
|
||||
"chat": {
|
||||
|
|
@ -144,8 +154,8 @@
|
|||
}
|
||||
},
|
||||
"modals": {
|
||||
"adoption": 24,
|
||||
"total": 51,
|
||||
"adoption": 19,
|
||||
"total": 64,
|
||||
"sharedUsage": 12,
|
||||
"focusTrapUsage": 7
|
||||
},
|
||||
|
|
@ -212,6 +222,158 @@
|
|||
"styles": {
|
||||
"themeAdoption": 83,
|
||||
"tailwindAdoption": 90
|
||||
},
|
||||
"errorBoundaries": {
|
||||
"adoption": 54,
|
||||
"errorAdoption": 28,
|
||||
"offlineAdoption": 79,
|
||||
"appsWithErrorPage": 8,
|
||||
"appsWithOfflinePage": 23,
|
||||
"missing": {
|
||||
"error": [
|
||||
"calc",
|
||||
"chat",
|
||||
"citycorners",
|
||||
"clock",
|
||||
"context",
|
||||
"manadeck",
|
||||
"manavoxel",
|
||||
"moodlit",
|
||||
"mukke",
|
||||
"news",
|
||||
"nutriphi",
|
||||
"photos",
|
||||
"picture",
|
||||
"planta",
|
||||
"playground",
|
||||
"questions",
|
||||
"skilltree",
|
||||
"storage",
|
||||
"uload",
|
||||
"wisekeep",
|
||||
"zitare"
|
||||
],
|
||||
"offline": ["manavoxel", "moodlit", "news", "playground", "uload", "wisekeep"]
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"adoption": 100,
|
||||
"strictApps": 29,
|
||||
"nonStrict": []
|
||||
},
|
||||
"tests": {
|
||||
"adoption": 72,
|
||||
"e2eAdoption": 14,
|
||||
"appsWithTests": 21,
|
||||
"appsWithE2e": 4,
|
||||
"totalTestFiles": 111,
|
||||
"noTests": [
|
||||
"calc",
|
||||
"inventar",
|
||||
"manavoxel",
|
||||
"moodlit",
|
||||
"news",
|
||||
"playground",
|
||||
"uload",
|
||||
"wisekeep"
|
||||
]
|
||||
},
|
||||
"pwa": {
|
||||
"adoption": 2,
|
||||
"manifestAdoption": 3,
|
||||
"swAdoption": 0,
|
||||
"appsWithManifest": 1,
|
||||
"appsWithServiceWorker": 0,
|
||||
"noPwa": [
|
||||
"calc",
|
||||
"calendar",
|
||||
"chat",
|
||||
"citycorners",
|
||||
"clock",
|
||||
"contacts",
|
||||
"context",
|
||||
"inventar",
|
||||
"manacore",
|
||||
"manadeck",
|
||||
"manavoxel",
|
||||
"matrix",
|
||||
"moodlit",
|
||||
"mukke",
|
||||
"news",
|
||||
"nutriphi",
|
||||
"photos",
|
||||
"picture",
|
||||
"planta",
|
||||
"playground",
|
||||
"presi",
|
||||
"questions",
|
||||
"skilltree",
|
||||
"storage",
|
||||
"times",
|
||||
"todo",
|
||||
"wisekeep",
|
||||
"zitare"
|
||||
]
|
||||
},
|
||||
"fileSizes": {
|
||||
"adoption": 0,
|
||||
"totalLargeFiles": 38,
|
||||
"largestFile": {
|
||||
"path": "matrix: lib/matrix/store.svelte.ts",
|
||||
"lines": 2019
|
||||
},
|
||||
"topOffenders": [
|
||||
{
|
||||
"app": "matrix",
|
||||
"file": "lib/matrix/store.svelte.ts",
|
||||
"lines": 2019
|
||||
},
|
||||
{
|
||||
"app": "calendar",
|
||||
"file": "lib/components/event/QuickEventOverlay.svelte",
|
||||
"lines": 1816
|
||||
},
|
||||
{
|
||||
"app": "contacts",
|
||||
"file": "lib/components/ContactDetailModal.svelte",
|
||||
"lines": 1511
|
||||
},
|
||||
{
|
||||
"app": "todo",
|
||||
"file": "lib/components/TaskItem.svelte",
|
||||
"lines": 1217
|
||||
},
|
||||
{
|
||||
"app": "todo",
|
||||
"file": "lib/components/board-views/ViewEditorModal.svelte",
|
||||
"lines": 1190
|
||||
},
|
||||
{
|
||||
"app": "contacts",
|
||||
"file": "lib/components/NewContactModal.svelte",
|
||||
"lines": 1130
|
||||
},
|
||||
{
|
||||
"app": "calendar",
|
||||
"file": "lib/components/calendar/WeekView.svelte",
|
||||
"lines": 1043
|
||||
},
|
||||
{
|
||||
"app": "zitare",
|
||||
"file": "routes/(app)/lists/[id]/+page.svelte",
|
||||
"lines": 958
|
||||
},
|
||||
{
|
||||
"app": "calendar",
|
||||
"file": "routes/(app)/settings/sync/+page.svelte",
|
||||
"lines": 898
|
||||
},
|
||||
{
|
||||
"app": "matrix",
|
||||
"file": "routes/(auth)/login/+page.svelte",
|
||||
"lines": 849
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"apps": [
|
||||
|
|
|
|||
|
|
@ -76,6 +76,36 @@ const categories = [
|
|||
icon: '🎭',
|
||||
description: 'Theme-Variablen + Tailwind CSS Nutzung',
|
||||
},
|
||||
{
|
||||
key: 'errorBoundaries',
|
||||
label: 'Error Boundaries',
|
||||
icon: '🛡️',
|
||||
description: '+error.svelte + Offline Page pro App',
|
||||
},
|
||||
{
|
||||
key: 'typescriptStrict',
|
||||
label: 'TypeScript Strict',
|
||||
icon: '🔒',
|
||||
description: 'Strict Mode in allen Apps',
|
||||
},
|
||||
{
|
||||
key: 'testCoverage',
|
||||
label: 'Test Coverage',
|
||||
icon: '🧪',
|
||||
description: 'Apps mit mindestens einem Test',
|
||||
},
|
||||
{
|
||||
key: 'pwaSupport',
|
||||
label: 'PWA Support',
|
||||
icon: '📱',
|
||||
description: 'Manifest + Service Worker',
|
||||
},
|
||||
{
|
||||
key: 'maintainability',
|
||||
label: 'Maintainability',
|
||||
icon: '📏',
|
||||
description: 'Dateien unter 500 Zeilen',
|
||||
},
|
||||
];
|
||||
|
||||
const packageDetails = details.packages.perPackage;
|
||||
|
|
@ -238,6 +268,62 @@ const generatedDate = new Date(ecosystemData.generatedAt).toLocaleDateString('de
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Size Top Offenders -->
|
||||
{
|
||||
details.fileSizes?.topOffenders?.length > 0 && (
|
||||
<div class="mb-12">
|
||||
<h2 class="text-foreground mb-6 text-xl font-semibold">
|
||||
Largest Files (Refactoring-Kandidaten)
|
||||
</h2>
|
||||
<div class="border-border/50 rounded-xl border bg-gradient-to-br from-white/5 to-white/[0.02] overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-border/50 border-b">
|
||||
<th class="text-muted-foreground px-4 py-3 text-left font-medium">Datei</th>
|
||||
<th class="text-muted-foreground px-4 py-3 text-right font-medium">Zeilen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{details.fileSizes.topOffenders.slice(0, 10).map((f: any) => (
|
||||
<tr class="border-border/30 border-b last:border-0">
|
||||
<td class="px-4 py-2">
|
||||
<span class="text-muted-foreground">{f.app}/</span>
|
||||
<span class="text-foreground">{f.file}</span>
|
||||
</td>
|
||||
<td
|
||||
class={`px-4 py-2 text-right font-mono font-medium ${f.lines > 1000 ? 'text-red-500' : 'text-orange-500'}`}
|
||||
>
|
||||
{f.lines}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Apps without Tests -->
|
||||
{
|
||||
details.tests?.noTests?.length > 0 && (
|
||||
<div class="mb-12">
|
||||
<h2 class="text-foreground mb-6 text-xl font-semibold">Apps ohne Tests</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{details.tests.noTests.map((app: string) => (
|
||||
<span class="text-orange-500 border-orange-500/30 rounded-full border bg-orange-500/10 px-3 py-1 text-xs font-medium">
|
||||
{app}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="text-muted-foreground mt-3 text-sm">
|
||||
{details.tests.totalTestFiles} Test-Dateien gesamt ·{' '}
|
||||
{details.tests.appsWithE2e} Apps mit E2E-Tests
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Back link -->
|
||||
<div class="text-center">
|
||||
<a href="/manascore" class="text-primary hover:text-primary/80 text-sm transition-colors">
|
||||
|
|
|
|||
|
|
@ -296,6 +296,223 @@ function measureStyleConsistency() {
|
|||
return { themeAdoption, tailwindAdoption };
|
||||
}
|
||||
|
||||
function measureErrorBoundaries() {
|
||||
console.log('📊 Measuring Error Boundaries...');
|
||||
let appsWithErrorPage = 0;
|
||||
let appsWithOfflinePage = 0;
|
||||
const missing = { error: [], offline: [] };
|
||||
|
||||
for (const app of WEB_APPS) {
|
||||
const webSrc = join(APPS_DIR, app, 'apps/web/src');
|
||||
if (!existsSync(webSrc)) continue;
|
||||
|
||||
const hasErrorPage = fileCount('+error.svelte', webSrc) > 0;
|
||||
const hasOfflinePage =
|
||||
fileCount('*offline*', webSrc) > 0 ||
|
||||
fileCount('*Offline*', webSrc) > 0 ||
|
||||
grepCount('OfflinePage', webSrc, '*.svelte') > 0;
|
||||
|
||||
if (hasErrorPage) appsWithErrorPage++;
|
||||
else missing.error.push(app);
|
||||
if (hasOfflinePage) appsWithOfflinePage++;
|
||||
else missing.offline.push(app);
|
||||
}
|
||||
|
||||
const errorAdoption = Math.round((appsWithErrorPage / WEB_APPS.length) * 100);
|
||||
const offlineAdoption = Math.round((appsWithOfflinePage / WEB_APPS.length) * 100);
|
||||
const adoption = Math.round((errorAdoption + offlineAdoption) / 2);
|
||||
|
||||
console.log(` +error.svelte: ${appsWithErrorPage}/${WEB_APPS.length} (${errorAdoption}%)`);
|
||||
console.log(` Offline page: ${appsWithOfflinePage}/${WEB_APPS.length} (${offlineAdoption}%)`);
|
||||
if (missing.error.length > 0) console.log(` Missing error page: ${missing.error.join(', ')}`);
|
||||
console.log(` Combined: ${adoption}%\n`);
|
||||
|
||||
return {
|
||||
adoption,
|
||||
errorAdoption,
|
||||
offlineAdoption,
|
||||
appsWithErrorPage,
|
||||
appsWithOfflinePage,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
function measureTypeScriptStrictness() {
|
||||
console.log('📊 Measuring TypeScript Strictness...');
|
||||
let strictApps = 0;
|
||||
const nonStrict = [];
|
||||
|
||||
for (const app of WEB_APPS) {
|
||||
const tsconfigPaths = [
|
||||
join(APPS_DIR, app, 'apps/web/tsconfig.json'),
|
||||
join(APPS_DIR, app, 'apps/web/.svelte-kit/tsconfig.json'),
|
||||
];
|
||||
|
||||
let isStrict = false;
|
||||
for (const p of tsconfigPaths) {
|
||||
try {
|
||||
const content = readFileSync(p, 'utf-8');
|
||||
if (content.includes('"strict": true') || content.includes('"strict":true')) {
|
||||
isStrict = true;
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// SvelteKit apps inherit strict from .svelte-kit/tsconfig.json which extends the user config
|
||||
// Check if svelte.config.js exists (indicates SvelteKit = inherits strict)
|
||||
if (!isStrict && existsSync(join(APPS_DIR, app, 'apps/web/svelte.config.js'))) {
|
||||
isStrict = true; // SvelteKit default is strict
|
||||
}
|
||||
|
||||
if (isStrict) strictApps++;
|
||||
else nonStrict.push(app);
|
||||
}
|
||||
|
||||
const adoption = Math.round((strictApps / WEB_APPS.length) * 100);
|
||||
console.log(` Strict apps: ${strictApps}/${WEB_APPS.length} (${adoption}%)`);
|
||||
if (nonStrict.length > 0) console.log(` Non-strict: ${nonStrict.join(', ')}`);
|
||||
console.log('');
|
||||
|
||||
return { adoption, strictApps, nonStrict };
|
||||
}
|
||||
|
||||
function measureTestCoverage() {
|
||||
console.log('📊 Measuring Test Coverage...');
|
||||
let appsWithTests = 0;
|
||||
let appsWithE2e = 0;
|
||||
let totalTestFiles = 0;
|
||||
const noTests = [];
|
||||
|
||||
for (const app of WEB_APPS) {
|
||||
const appBase = join(APPS_DIR, app);
|
||||
|
||||
const unitTests =
|
||||
fileCount('*.test.ts', appBase) +
|
||||
fileCount('*.spec.ts', appBase) +
|
||||
fileCount('*.test.js', appBase);
|
||||
const e2eTests =
|
||||
fileCount('*.spec.ts', join(appBase, 'apps/web/e2e')) +
|
||||
fileCount('*.spec.ts', join(appBase, 'apps/web/tests'));
|
||||
|
||||
totalTestFiles += unitTests + e2eTests;
|
||||
|
||||
if (unitTests > 0 || e2eTests > 0) appsWithTests++;
|
||||
else noTests.push(app);
|
||||
if (e2eTests > 0) appsWithE2e++;
|
||||
}
|
||||
|
||||
const adoption = Math.round((appsWithTests / WEB_APPS.length) * 100);
|
||||
const e2eAdoption = Math.round((appsWithE2e / WEB_APPS.length) * 100);
|
||||
|
||||
console.log(` Apps with tests: ${appsWithTests}/${WEB_APPS.length} (${adoption}%)`);
|
||||
console.log(` Apps with E2E: ${appsWithE2e}/${WEB_APPS.length} (${e2eAdoption}%)`);
|
||||
console.log(` Total test files: ${totalTestFiles}`);
|
||||
if (noTests.length > 0 && noTests.length <= 10) console.log(` No tests: ${noTests.join(', ')}`);
|
||||
console.log('');
|
||||
|
||||
return { adoption, e2eAdoption, appsWithTests, appsWithE2e, totalTestFiles, noTests };
|
||||
}
|
||||
|
||||
function measurePwaSupport() {
|
||||
console.log('📊 Measuring PWA Support...');
|
||||
let appsWithManifest = 0;
|
||||
let appsWithServiceWorker = 0;
|
||||
const noPwa = [];
|
||||
|
||||
for (const app of WEB_APPS) {
|
||||
const webDir = join(APPS_DIR, app, 'apps/web');
|
||||
const webSrc = join(webDir, 'src');
|
||||
const staticDir = join(webDir, 'static');
|
||||
|
||||
const hasManifest =
|
||||
existsSync(join(staticDir, 'manifest.json')) ||
|
||||
existsSync(join(staticDir, 'manifest.webmanifest')) ||
|
||||
grepCount('manifest', webSrc, '*.html') > 0 ||
|
||||
grepCount('manifest', join(webSrc, 'app.html'), '*') > 0;
|
||||
|
||||
const hasSw =
|
||||
existsSync(join(staticDir, 'sw.js')) ||
|
||||
existsSync(join(staticDir, 'service-worker.js')) ||
|
||||
existsSync(join(webSrc, 'service-worker.ts')) ||
|
||||
existsSync(join(webSrc, 'service-worker.js')) ||
|
||||
grepCount('serviceWorker', webSrc, '*.ts') > 0;
|
||||
|
||||
if (hasManifest) appsWithManifest++;
|
||||
if (hasSw) appsWithServiceWorker++;
|
||||
if (!hasManifest && !hasSw) noPwa.push(app);
|
||||
}
|
||||
|
||||
const manifestAdoption = Math.round((appsWithManifest / WEB_APPS.length) * 100);
|
||||
const swAdoption = Math.round((appsWithServiceWorker / WEB_APPS.length) * 100);
|
||||
const adoption = Math.round((manifestAdoption + swAdoption) / 2);
|
||||
|
||||
console.log(` Manifest: ${appsWithManifest}/${WEB_APPS.length} (${manifestAdoption}%)`);
|
||||
console.log(` Service Worker: ${appsWithServiceWorker}/${WEB_APPS.length} (${swAdoption}%)`);
|
||||
if (noPwa.length > 0 && noPwa.length <= 10) console.log(` No PWA: ${noPwa.join(', ')}`);
|
||||
console.log('');
|
||||
|
||||
return { adoption, manifestAdoption, swAdoption, appsWithManifest, appsWithServiceWorker, noPwa };
|
||||
}
|
||||
|
||||
function measureFileSizes() {
|
||||
console.log('📊 Measuring File Sizes (Maintainability)...');
|
||||
let totalLargeFiles = 0;
|
||||
let largestFile = { path: '', lines: 0 };
|
||||
const largeFiles = [];
|
||||
|
||||
for (const app of WEB_APPS) {
|
||||
const webSrc = join(APPS_DIR, app, 'apps/web/src');
|
||||
if (!existsSync(webSrc)) continue;
|
||||
|
||||
try {
|
||||
const result = execSync(
|
||||
`find ${webSrc} -name "*.svelte" -o -name "*.ts" | xargs wc -l 2>/dev/null | sort -rn | head -5`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
const lines = result.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
||||
if (match && !match[2].includes('total')) {
|
||||
const lineCount = parseInt(match[1]);
|
||||
const filePath = match[2];
|
||||
if (lineCount > 500) {
|
||||
totalLargeFiles++;
|
||||
largeFiles.push({ app, file: filePath.replace(`${webSrc}/`, ''), lines: lineCount });
|
||||
}
|
||||
if (lineCount > largestFile.lines) {
|
||||
largestFile = {
|
||||
path: `${app}: ${filePath.replace(`${webSrc}/`, '')}`,
|
||||
lines: lineCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Score: fewer large files = better (100% = no files >500 lines, deduct per large file)
|
||||
const adoption = Math.max(0, 100 - totalLargeFiles * 3);
|
||||
|
||||
console.log(` Files > 500 lines: ${totalLargeFiles}`);
|
||||
console.log(` Largest: ${largestFile.path} (${largestFile.lines} lines)`);
|
||||
if (largeFiles.length > 0) {
|
||||
console.log(' Top offenders:');
|
||||
largeFiles
|
||||
.sort((a, b) => b.lines - a.lines)
|
||||
.slice(0, 5)
|
||||
.forEach((f) => console.log(` ${f.lines} lines — ${f.app}/${f.file}`));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
return {
|
||||
adoption,
|
||||
totalLargeFiles,
|
||||
largestFile,
|
||||
topOffenders: largeFiles.sort((a, b) => b.lines - a.lines).slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
|
|
@ -310,6 +527,11 @@ const errors = measureErrorHandling();
|
|||
const i18n = measureI18nConsistency();
|
||||
const localFirst = measureLocalFirstAdoption();
|
||||
const styles = measureStyleConsistency();
|
||||
const errorBoundaries = measureErrorBoundaries();
|
||||
const typescript = measureTypeScriptStrictness();
|
||||
const tests = measureTestCoverage();
|
||||
const pwa = measurePwaSupport();
|
||||
const fileSizes = measureFileSizes();
|
||||
|
||||
// Calculate overall scores
|
||||
const scores = {
|
||||
|
|
@ -320,17 +542,27 @@ const scores = {
|
|||
i18nCoverage: i18n.adoption,
|
||||
localFirst: localFirst.adoption,
|
||||
styleConsistency: Math.round((styles.themeAdoption + styles.tailwindAdoption) / 2),
|
||||
errorBoundaries: errorBoundaries.adoption,
|
||||
typescriptStrict: typescript.adoption,
|
||||
testCoverage: tests.adoption,
|
||||
pwaSupport: pwa.adoption,
|
||||
maintainability: fileSizes.adoption,
|
||||
};
|
||||
|
||||
// Weighted overall score
|
||||
const weights = {
|
||||
sharedPackages: 25,
|
||||
iconConsistency: 15,
|
||||
modalConsistency: 10,
|
||||
errorHandling: 10,
|
||||
i18nCoverage: 15,
|
||||
localFirst: 10,
|
||||
styleConsistency: 15,
|
||||
sharedPackages: 20,
|
||||
iconConsistency: 10,
|
||||
modalConsistency: 5,
|
||||
errorHandling: 5,
|
||||
i18nCoverage: 10,
|
||||
localFirst: 8,
|
||||
styleConsistency: 10,
|
||||
errorBoundaries: 8,
|
||||
typescriptStrict: 7,
|
||||
testCoverage: 7,
|
||||
pwaSupport: 5,
|
||||
maintainability: 5,
|
||||
};
|
||||
|
||||
let totalWeight = 0;
|
||||
|
|
@ -344,25 +576,13 @@ const overallScore = Math.round(weightedSum / totalWeight);
|
|||
console.log('='.repeat(50));
|
||||
console.log('\n🏆 ECOSYSTEM HEALTH SCORE\n');
|
||||
console.log(` Overall: ${overallScore}/100\n`);
|
||||
console.log(
|
||||
` Shared Package Adoption: ${scores.sharedPackages}% (weight: ${weights.sharedPackages}%)`
|
||||
);
|
||||
console.log(
|
||||
` Icon Consistency: ${scores.iconConsistency}% (weight: ${weights.iconConsistency}%)`
|
||||
);
|
||||
console.log(
|
||||
` Modal Consistency: ${scores.modalConsistency}% (weight: ${weights.modalConsistency}%)`
|
||||
);
|
||||
console.log(
|
||||
` Error Handling: ${scores.errorHandling}% (weight: ${weights.errorHandling}%)`
|
||||
);
|
||||
console.log(
|
||||
` i18n Coverage: ${scores.i18nCoverage}% (weight: ${weights.i18nCoverage}%)`
|
||||
);
|
||||
console.log(` Local-First: ${scores.localFirst}% (weight: ${weights.localFirst}%)`);
|
||||
console.log(
|
||||
` Style Consistency: ${scores.styleConsistency}% (weight: ${weights.styleConsistency}%)`
|
||||
);
|
||||
const pad = (s, n) => s.padEnd(n);
|
||||
for (const [key, weight] of Object.entries(weights)) {
|
||||
const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
|
||||
console.log(
|
||||
` ${pad(label + ':', 28)} ${String(scores[key]).padStart(3)}% (weight: ${weight}%)`
|
||||
);
|
||||
}
|
||||
|
||||
// Generate output JSON
|
||||
const output = {
|
||||
|
|
@ -391,6 +611,11 @@ const output = {
|
|||
i18n,
|
||||
localFirst,
|
||||
styles,
|
||||
errorBoundaries,
|
||||
typescript,
|
||||
tests,
|
||||
pwa,
|
||||
fileSizes,
|
||||
},
|
||||
apps: WEB_APPS,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue