From 9bd074429006d781000d724278132b103e7b0b64 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Mon, 9 Mar 2026 22:23:59 +0800 Subject: [PATCH] chore(workspace): thanks to Discord user saltstar, here is some util script for loc on package level --- scripts/list-module-loc.mjs | 370 ++++++++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 scripts/list-module-loc.mjs diff --git a/scripts/list-module-loc.mjs b/scripts/list-module-loc.mjs new file mode 100644 index 000000000..0156c71fb --- /dev/null +++ b/scripts/list-module-loc.mjs @@ -0,0 +1,370 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +import fs from 'node:fs' +import path from 'node:path' + +import { spawnSync } from 'node:child_process' +import { argv, cwd } from 'node:process' + +const ROOT = cwd() +const SEARCH_ROOTS = ['apps', 'packages', 'plugins', 'services', 'integrations', 'docs'] +const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.turbo', 'coverage', '.cache', 'out', '.vite']) + +function walk(dir, out) { + let entries = [] + try { + entries = fs.readdirSync(dir, { withFileTypes: true }) + } + catch { + return + } + + for (const entry of entries) { + if (entry.name.startsWith('.')) { + if (entry.name !== '.vitepress') + continue + } + + const full = path.join(dir, entry.name) + + if (entry.isDirectory()) { + if (IGNORE_DIRS.has(entry.name)) + continue + walk(full, out) + continue + } + + if (entry.isFile() && entry.name === 'package.json') + out.push(full) + } +} + +function collectPackageJsonFiles() { + const files = [path.join(ROOT, 'package.json')] + for (const base of SEARCH_ROOTS) { + const full = path.join(ROOT, base) + if (fs.existsSync(full)) + walk(full, files) + } + return files +} + +function safeReadJson(file) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')) + } + catch { + return null + } +} + +function collectFromExportsValue(value, out, source) { + if (typeof value === 'string') { + out.push({ source, raw: value }) + return + } + if (Array.isArray(value)) { + for (const item of value) + collectFromExportsValue(item, out, source) + return + } + if (value && typeof value === 'object') { + for (const [k, v] of Object.entries(value)) + collectFromExportsValue(v, out, `${source}.${k}`) + } +} + +function collectEntryCandidates(pkg) { + const out = [] + const pushIfString = (source, value) => { + if (typeof value === 'string' && value.trim()) + out.push({ source, raw: value.trim() }) + } + + pushIfString('main', pkg.main) + pushIfString('module', pkg.module) + pushIfString('types', pkg.types) + pushIfString('typings', pkg.typings) + + if (typeof pkg.bin === 'string') { + pushIfString('bin', pkg.bin) + } + else if (pkg.bin && typeof pkg.bin === 'object') { + for (const [k, v] of Object.entries(pkg.bin)) + pushIfString(`bin.${k}`, v) + } + + if (typeof pkg.exports === 'string') { + pushIfString('exports', pkg.exports) + } + else if (pkg.exports && typeof pkg.exports === 'object') { + for (const [k, v] of Object.entries(pkg.exports)) + collectFromExportsValue(v, out, `exports.${k}`) + } + + return out +} + +function cleanupExportTarget(raw) { + if (!raw) + return '' + + const q = raw.indexOf('?') + const h = raw.indexOf('#') + let end = raw.length + if (q >= 0) + end = Math.min(end, q) + if (h >= 0) + end = Math.min(end, h) + + return raw.slice(0, end).trim() +} + +function resolveParentDir(pkgDir, rawTarget) { + const target = cleanupExportTarget(rawTarget) + // eslint-disable-next-line regexp/no-unused-capturing-group + if (!target || /^(https?:|npm:|node:|data:)/.test(target)) + return null + + const wildcardIndex = target.indexOf('*') + const logicalTarget = wildcardIndex >= 0 ? target.slice(0, wildcardIndex) : target + const abs = path.resolve(pkgDir, logicalTarget) + const ext = path.extname(logicalTarget) + + let parentDir = abs + if (ext) + parentDir = path.dirname(abs) + else if (logicalTarget.endsWith('/')) + parentDir = abs + else if (fs.existsSync(abs) && fs.statSync(abs).isFile()) + parentDir = path.dirname(abs) + + return { + raw: rawTarget, + cleaned: target, + parentDir, + exists: fs.existsSync(parentDir) && fs.statSync(parentDir).isDirectory(), + } +} + +function runScc(dir) { + const proc = spawnSync('scc', ['-a', '-f', 'json', dir], { encoding: 'utf8' }) + if (proc.status !== 0) { + return { + ok: false, + error: (proc.stderr || proc.stdout || `exit ${proc.status}`).trim(), + } + } + + let parsed + try { + parsed = JSON.parse(proc.stdout) + } + catch (err) { + return { + ok: false, + error: `invalid scc json: ${String(err)}`, + } + } + + const sum = key => parsed.reduce((acc, row) => acc + Number(row[key] || 0), 0) + return { + ok: true, + files: sum('Count'), + lines: sum('Lines'), + code: sum('Code'), + comments: sum('Comment'), + blanks: sum('Blank'), + uloc: sum('ULOC'), + } +} + +function rel(p) { + const rp = path.relative(ROOT, p) + return rp || '.' +} + +function toSccCommand(absDir) { + return `scc -a -f json "${rel(absDir)}"` +} + +function printGroup(title) { + console.log(`\n=== ${title} ===`) +} + +function printTable(rows, columns) { + const widths = {} + for (const col of columns) + widths[col.key] = col.title.length + + for (const row of rows) { + for (const col of columns) { + const value = String(row[col.key] ?? '') + widths[col.key] = Math.max(widths[col.key], value.length) + } + } + + const line = row => columns.map(col => String(row[col.key] ?? '').padEnd(widths[col.key])).join(' ') + console.log(line(Object.fromEntries(columns.map(c => [c.key, c.title])))) + console.log(columns.map(col => '-'.repeat(widths[col.key])).join(' ')) + for (const row of rows) + console.log(line(row)) +} + +function main() { + const asJson = argv.includes('--json') + const pkgJsonFiles = collectPackageJsonFiles() + const records = [] + + for (const pkgFile of pkgJsonFiles) { + const pkg = safeReadJson(pkgFile) + if (!pkg) + continue + + const pkgDir = path.dirname(pkgFile) + const pkgName = pkg.name || rel(pkgDir) + const candidates = collectEntryCandidates(pkg) + + for (const candidate of candidates) { + const resolved = resolveParentDir(pkgDir, candidate.raw) + if (!resolved) + continue + records.push({ + packageName: pkgName, + packageDir: pkgDir, + packageJson: pkgFile, + source: candidate.source, + entryRaw: resolved.raw, + entryClean: resolved.cleaned, + parentDir: resolved.parentDir, + parentDirRel: rel(resolved.parentDir), + parentExists: resolved.exists, + }) + } + } + + const uniqueByPackageAndDir = new Map() + for (const rec of records) { + const key = `${rec.packageName}::${rec.parentDir}` + const existing = uniqueByPackageAndDir.get(key) + if (!existing) { + uniqueByPackageAndDir.set(key, { + ...rec, + occurrenceCount: 1, + sources: new Set([rec.source]), + entries: new Set([rec.entryClean]), + }) + } + else { + existing.occurrenceCount += 1 + existing.sources.add(rec.source) + existing.entries.add(rec.entryClean) + } + } + + const sccCache = new Map() + const deduped = Array.from(uniqueByPackageAndDir.values()) + for (const rec of deduped) { + if (!rec.parentExists) { + rec.scc = { ok: false, error: 'directory_not_found' } + continue + } + if (!sccCache.has(rec.parentDir)) + sccCache.set(rec.parentDir, runScc(rec.parentDir)) + rec.scc = sccCache.get(rec.parentDir) + } + + const aggregateByParentDir = new Map() + for (const rec of deduped) { + const key = rec.parentDir + const existing = aggregateByParentDir.get(key) + if (!existing) { + aggregateByParentDir.set(key, { + parentDir: rec.parentDir, + parentDirRel: rec.parentDirRel, + packageCount: 1, + packageNames: new Set([rec.packageName]), + totalOccurrenceCount: rec.occurrenceCount, + scc: rec.scc, + }) + } + else { + existing.packageCount += 1 + existing.totalOccurrenceCount += rec.occurrenceCount + existing.packageNames.add(rec.packageName) + } + } + + const byPackageRows = deduped + .sort((a, b) => (b.occurrenceCount - a.occurrenceCount) || a.parentDirRel.localeCompare(b.parentDirRel)) + .map(rec => ({ + package: rec.packageName, + parent_dir: rec.parentDirRel, + occurrences: rec.occurrenceCount, + sources: Array.from(rec.sources).sort().join(','), + code: rec.scc.ok ? rec.scc.code : '', + uloc: rec.scc.ok ? rec.scc.uloc : '', + files: rec.scc.ok ? rec.scc.files : '', + status: rec.scc.ok ? 'ok' : rec.scc.error, + scc_cmd: toSccCommand(rec.parentDir), + })) + + const aggregateRows = Array.from(aggregateByParentDir.values()) + .sort((a, b) => (b.packageCount - a.packageCount) || a.parentDirRel.localeCompare(b.parentDirRel)) + .map(rec => ({ + parent_dir: rec.parentDirRel, + package_count: rec.packageCount, + packages: Array.from(rec.packageNames).sort().join(','), + total_occurrences: rec.totalOccurrenceCount, + code: rec.scc.ok ? rec.scc.code : '', + uloc: rec.scc.ok ? rec.scc.uloc : '', + files: rec.scc.ok ? rec.scc.files : '', + status: rec.scc.ok ? 'ok' : rec.scc.error, + scc_cmd: toSccCommand(rec.parentDir), + })) + + if (asJson) { + const output = { + generatedAt: new Date().toISOString(), + root: ROOT, + packageJsonScanned: pkgJsonFiles.map(rel), + uniquePackageEntrypointParents: byPackageRows, + dedupedParentDirsAcrossPackages: aggregateRows, + } + console.log(JSON.stringify(output, null, 2)) + return + } + + printGroup('Per Package Entrypoint Parent (Deduped)') + printTable( + byPackageRows, + [ + { key: 'package', title: 'package' }, + { key: 'parent_dir', title: 'parent_dir' }, + { key: 'occurrences', title: 'occ' }, + { key: 'code', title: 'code' }, + { key: 'uloc', title: 'uloc' }, + { key: 'files', title: 'files' }, + { key: 'status', title: 'status' }, + { key: 'scc_cmd', title: 'scc_cmd' }, + ], + ) + + printGroup('Parent Dir Aggregation Across Packages (Deduped)') + printTable( + aggregateRows, + [ + { key: 'parent_dir', title: 'parent_dir' }, + { key: 'package_count', title: 'pkgs' }, + { key: 'total_occurrences', title: 'occ_total' }, + { key: 'code', title: 'code' }, + { key: 'uloc', title: 'uloc' }, + { key: 'files', title: 'files' }, + { key: 'status', title: 'status' }, + { key: 'scc_cmd', title: 'scc_cmd' }, + ], + ) +} + +main()