mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
* Add cruft baseline snapshot * pr feedback * rename cruft to tech deb * improve baseline
326 lines
8.5 KiB
JavaScript
326 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
||
import { promises as fs } from 'fs';
|
||
import path from 'path';
|
||
|
||
const ROOT_DIR = process.cwd();
|
||
const ROOT_PACKAGE_JSON_PATH = path.join(ROOT_DIR, 'package.json');
|
||
const OUTPUT_JSON_PATH = path.join(
|
||
ROOT_DIR,
|
||
'docs',
|
||
'maintenance',
|
||
'tech-debt-baseline.json',
|
||
);
|
||
const OUTPUT_MARKDOWN_PATH = path.join(
|
||
ROOT_DIR,
|
||
'docs',
|
||
'maintenance',
|
||
'tech-debt-baseline.md',
|
||
);
|
||
|
||
const IGNORED_DIRECTORIES = new Set([
|
||
'__generated__',
|
||
'.cache',
|
||
'.git',
|
||
'.gradle',
|
||
'.next',
|
||
'.turbo',
|
||
'.yarn',
|
||
'android',
|
||
'artifacts',
|
||
'build',
|
||
'cache',
|
||
'Carthage',
|
||
'coverage',
|
||
'DerivedData',
|
||
'dist',
|
||
'generated',
|
||
'ios',
|
||
'node_modules',
|
||
'out',
|
||
'Pods',
|
||
'typechain-types',
|
||
'vendor',
|
||
]);
|
||
|
||
const SOURCE_EXTENSIONS = new Set([
|
||
'.cjs',
|
||
'.circom',
|
||
'.css',
|
||
'.go',
|
||
'.h',
|
||
'.hpp',
|
||
'.java',
|
||
'.js',
|
||
'.jsx',
|
||
'.kt',
|
||
'.kts',
|
||
'.mjs',
|
||
'.noir',
|
||
'.py',
|
||
'.rb',
|
||
'.rs',
|
||
'.sh',
|
||
'.sol',
|
||
'.swift',
|
||
'.ts',
|
||
'.tsx',
|
||
'.vue',
|
||
]);
|
||
|
||
function sortObjectKeys(obj = {}) {
|
||
const sortedEntries = Object.entries(obj).sort(([a], [b]) =>
|
||
a.localeCompare(b),
|
||
);
|
||
return Object.fromEntries(sortedEntries);
|
||
}
|
||
|
||
function wildcardToRegex(segment) {
|
||
const escaped = segment
|
||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||
.replace(/\*/g, '[^/]*');
|
||
return new RegExp(`^${escaped}$`);
|
||
}
|
||
|
||
async function expandWorkspacePattern(rootDir, pattern) {
|
||
const segments = pattern.split('/').filter(Boolean);
|
||
|
||
async function walkSegments(currentDir, segmentIndex) {
|
||
if (segmentIndex >= segments.length) {
|
||
return [currentDir];
|
||
}
|
||
|
||
const currentSegment = segments[segmentIndex];
|
||
const hasWildcard = currentSegment.includes('*');
|
||
|
||
if (!hasWildcard) {
|
||
const nextDir = path.join(currentDir, currentSegment);
|
||
try {
|
||
const stat = await fs.stat(nextDir);
|
||
if (!stat.isDirectory()) return [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
return walkSegments(nextDir, segmentIndex + 1);
|
||
}
|
||
|
||
const matcher = wildcardToRegex(currentSegment);
|
||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||
const matches = entries
|
||
.filter(entry => entry.isDirectory() && matcher.test(entry.name))
|
||
.map(entry => path.join(currentDir, entry.name));
|
||
|
||
const expanded = await Promise.all(
|
||
matches.map(matchedDir => walkSegments(matchedDir, segmentIndex + 1)),
|
||
);
|
||
|
||
return expanded.flat();
|
||
}
|
||
|
||
return walkSegments(rootDir, 0);
|
||
}
|
||
|
||
async function getWorkspaceDirectories(rootDir, workspacePatterns) {
|
||
const allMatches = await Promise.all(
|
||
workspacePatterns.map(pattern => expandWorkspacePattern(rootDir, pattern)),
|
||
);
|
||
|
||
const candidateDirs = [...new Set(allMatches.flat())];
|
||
const workspaceDirs = [];
|
||
|
||
for (const dir of candidateDirs) {
|
||
const packageJsonPath = path.join(dir, 'package.json');
|
||
try {
|
||
await fs.access(packageJsonPath);
|
||
workspaceDirs.push(dir);
|
||
} catch {
|
||
// Skip directories without package.json.
|
||
}
|
||
}
|
||
|
||
return workspaceDirs.sort((a, b) => a.localeCompare(b));
|
||
}
|
||
|
||
async function collectSourceFileCounts(workspaceDir) {
|
||
const extensionCounts = {};
|
||
|
||
async function walk(currentDir) {
|
||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(currentDir, entry.name);
|
||
|
||
if (entry.isDirectory()) {
|
||
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
||
await walk(fullPath);
|
||
continue;
|
||
}
|
||
|
||
if (!entry.isFile()) continue;
|
||
|
||
const extension = path.extname(entry.name).toLowerCase();
|
||
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
||
|
||
extensionCounts[extension] = (extensionCounts[extension] || 0) + 1;
|
||
}
|
||
}
|
||
|
||
await walk(workspaceDir);
|
||
|
||
const sortedExtensionCounts = sortObjectKeys(extensionCounts);
|
||
const totalSourceFiles = Object.values(sortedExtensionCounts).reduce(
|
||
(sum, count) => sum + count,
|
||
0,
|
||
);
|
||
|
||
return { extensionCounts: sortedExtensionCounts, totalSourceFiles };
|
||
}
|
||
|
||
function buildMarkdownReport(report) {
|
||
const lines = [];
|
||
const topLargest = [...report.workspaces]
|
||
.sort((a, b) => b.sourceFiles.total - a.sourceFiles.total)
|
||
.slice(0, 10);
|
||
|
||
const noTestScript = report.workspaces.filter(
|
||
workspace => !workspace.scripts.includes('test'),
|
||
);
|
||
|
||
const averageDeps =
|
||
report.workspaces.reduce((sum, ws) => sum + ws.dependencyCount.total, 0) /
|
||
Math.max(report.workspaces.length, 1);
|
||
|
||
const variance =
|
||
report.workspaces.reduce(
|
||
(sum, ws) => sum + (ws.dependencyCount.total - averageDeps) ** 2,
|
||
0,
|
||
) / Math.max(report.workspaces.length, 1);
|
||
|
||
const standardDeviation = Math.sqrt(variance);
|
||
const unusualThreshold = Math.max(
|
||
50,
|
||
Math.round(averageDeps + standardDeviation),
|
||
);
|
||
|
||
const unusuallyLargeDeps = report.workspaces.filter(
|
||
workspace => workspace.dependencyCount.total >= unusualThreshold,
|
||
);
|
||
|
||
lines.push('# Tech Debt Baseline Snapshot');
|
||
lines.push('');
|
||
lines.push(
|
||
'Generated from `package.json` workspaces. This file is intended as an immutable baseline for cleanup PRs.',
|
||
);
|
||
lines.push('');
|
||
|
||
lines.push('## Top 10 largest workspaces by source-file count');
|
||
lines.push('');
|
||
for (const workspace of topLargest) {
|
||
lines.push(
|
||
`- \`${workspace.path}\` (${workspace.sourceFiles.total} source files, ${workspace.dependencyCount.total} deps)`,
|
||
);
|
||
}
|
||
|
||
lines.push('');
|
||
lines.push('## Workspaces with no `test` script');
|
||
lines.push('');
|
||
if (noTestScript.length === 0) {
|
||
lines.push('- None');
|
||
} else {
|
||
for (const workspace of noTestScript) {
|
||
lines.push(`- \`${workspace.path}\``);
|
||
}
|
||
}
|
||
|
||
lines.push('');
|
||
lines.push('## Workspaces with unusually large dependency sets');
|
||
lines.push('');
|
||
lines.push(
|
||
`- Threshold: >= ${unusualThreshold} total dependencies (mean + 1σ, minimum 50).`,
|
||
);
|
||
if (unusuallyLargeDeps.length === 0) {
|
||
lines.push('- None');
|
||
} else {
|
||
for (const workspace of unusuallyLargeDeps) {
|
||
lines.push(
|
||
`- \`${workspace.path}\`: ${workspace.dependencyCount.total} total (${workspace.dependencyCount.dependencies} deps, ${workspace.dependencyCount.devDependencies} devDeps, ${workspace.dependencyCount.peerDependencies} peerDeps)`,
|
||
);
|
||
}
|
||
}
|
||
|
||
lines.push('');
|
||
|
||
return `${lines.join('\n')}\n`;
|
||
}
|
||
|
||
async function main() {
|
||
const rootPackageJson = JSON.parse(
|
||
await fs.readFile(ROOT_PACKAGE_JSON_PATH, 'utf8'),
|
||
);
|
||
const workspacePatterns = rootPackageJson.workspaces?.packages;
|
||
|
||
if (!Array.isArray(workspacePatterns) || workspacePatterns.length === 0) {
|
||
throw new Error('Root package.json does not define workspaces.packages.');
|
||
}
|
||
|
||
const workspaceDirs = await getWorkspaceDirectories(
|
||
ROOT_DIR,
|
||
workspacePatterns,
|
||
);
|
||
|
||
const workspaces = [];
|
||
for (const workspaceDir of workspaceDirs) {
|
||
const packageJsonPath = path.join(workspaceDir, 'package.json');
|
||
const packageData = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||
const relativePath = path.relative(ROOT_DIR, workspaceDir) || '.';
|
||
const sourceFiles = await collectSourceFileCounts(workspaceDir);
|
||
|
||
const dependencies = sortObjectKeys(packageData.dependencies || {});
|
||
const devDependencies = sortObjectKeys(packageData.devDependencies || {});
|
||
const peerDependencies = sortObjectKeys(packageData.peerDependencies || {});
|
||
|
||
workspaces.push({
|
||
name: packageData.name || relativePath,
|
||
path: relativePath,
|
||
dependencies,
|
||
devDependencies,
|
||
peerDependencies,
|
||
dependencyCount: {
|
||
dependencies: Object.keys(dependencies).length,
|
||
devDependencies: Object.keys(devDependencies).length,
|
||
peerDependencies: Object.keys(peerDependencies).length,
|
||
total:
|
||
Object.keys(dependencies).length +
|
||
Object.keys(devDependencies).length +
|
||
Object.keys(peerDependencies).length,
|
||
},
|
||
scripts: Object.keys(packageData.scripts || {}).sort((a, b) =>
|
||
a.localeCompare(b),
|
||
),
|
||
sourceFiles: {
|
||
byExtension: sourceFiles.extensionCounts,
|
||
total: sourceFiles.totalSourceFiles,
|
||
},
|
||
});
|
||
}
|
||
|
||
const report = {
|
||
workspacePatterns,
|
||
workspaceCount: workspaces.length,
|
||
workspaces,
|
||
};
|
||
|
||
const markdown = buildMarkdownReport(report);
|
||
|
||
await fs.mkdir(path.dirname(OUTPUT_JSON_PATH), { recursive: true });
|
||
await fs.writeFile(OUTPUT_JSON_PATH, `${JSON.stringify(report, null, 2)}\n`);
|
||
await fs.writeFile(OUTPUT_MARKDOWN_PATH, markdown);
|
||
|
||
console.log(`Wrote ${path.relative(ROOT_DIR, OUTPUT_JSON_PATH)}`);
|
||
console.log(`Wrote ${path.relative(ROOT_DIR, OUTPUT_MARKDOWN_PATH)}`);
|
||
}
|
||
|
||
main().catch(error => {
|
||
console.error(error);
|
||
process.exitCode = 1;
|
||
});
|