Files
stats/tooling/unused-deps.mjs
Carl-Gerhard Lindesvärd 81a7e5d62e feat: dashboard v2, esm, upgrades (#211)
* esm

* wip

* wip

* wip

* wip

* wip

* wip

* subscription notice

* wip

* wip

* wip

* fix envs

* fix: update docker build

* fix

* esm/types

* delete dashboard :D

* add patches to dockerfiles

* update packages + catalogs + ts

* wip

* remove native libs

* ts

* improvements

* fix redirects and fetching session

* try fix favicon

* fixes

* fix

* order and resize reportds within a dashboard

* improvements

* wip

* added userjot to dashboard

* fix

* add op

* wip

* different cache key

* improve date picker

* fix table

* event details loading

* redo onboarding completely

* fix login

* fix

* fix

* extend session, billing and improve bars

* fix

* reduce price on 10M
2025-10-16 12:27:44 +02:00

202 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import url from 'node:url';
// Lazy import depcheck to avoid hard crash if not installed
async function loadDepcheck() {
try {
const mod = await import('depcheck');
return mod.default ?? mod;
} catch (err) {
console.error(
'depcheck is not installed. Install it with: pnpm -w add -D depcheck',
);
process.exitCode = 1;
process.exit(1);
}
}
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const DEFAULT_IGNORE_DIRS = [
'node_modules',
'dist',
'build',
'.next',
'.nuxt',
'.svelte-kit',
'.output',
'.turbo',
'coverage',
'.vercel',
'.cache',
'.astro',
'.pnpm',
];
function parseArgs() {
const args = new Set(process.argv.slice(2));
return {
json: args.has('--json') || args.has('-j'),
};
}
async function fileExists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function readWorkspacePatterns() {
const workspaceFile = path.join(repoRoot, 'pnpm-workspace.yaml');
if (!(await fileExists(workspaceFile))) return [];
const content = await fs.readFile(workspaceFile, 'utf8');
const patterns = [];
for (const line of content.split(/\r?\n/)) {
const m = line.match(/^\s*-\s*"?([^"#]+)"?\s*(?:#.*)?$/);
if (m) {
patterns.push(m[1].trim());
}
}
return patterns.filter(Boolean);
}
async function isPackageDir(dir) {
const pkgPath = path.join(dir, 'package.json');
return fileExists(pkgPath);
}
async function listSubdirs(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory())
.map((e) => path.join(dir, e.name));
}
async function findPackagesFromPattern(pattern) {
// Normalize pattern relative to repo root
const absolutePattern = path.resolve(repoRoot, pattern);
if (pattern.endsWith('/**')) {
const baseDir = absolutePattern.slice(0, -3); // remove /**
return await findPackagesRecursively(baseDir);
}
if (pattern.endsWith('/*')) {
const baseDir = absolutePattern.slice(0, -2); // remove /*
return (await listSubdirs(baseDir)).filterAsync(isPackageDir);
}
// Direct path
return (await isPackageDir(absolutePattern)) ? [absolutePattern] : [];
}
async function findPackagesRecursively(startDir) {
const results = [];
async function walk(dir) {
if (!(await fileExists(dir))) return;
if (await isPackageDir(dir)) results.push(dir);
const subdirs = await listSubdirs(dir);
for (const sub of subdirs) {
await walk(sub);
}
}
await walk(startDir);
return results;
}
// Add filterAsync utility on arrays
Object.defineProperty(Array.prototype, 'filterAsync', {
value: async function (predicate) {
const results = await Promise.all(this.map(predicate));
return this.filter((_, i) => results[i]);
},
enumerable: false,
});
async function discoverWorkspacePackageDirs() {
const patterns = await readWorkspacePatterns();
const discovered = new Set();
for (const pattern of patterns) {
const pkgs = await findPackagesFromPattern(pattern);
for (const p of pkgs) discovered.add(path.resolve(p));
}
// Always include repo root as well
discovered.add(repoRoot);
return Array.from(discovered);
}
async function runDepcheckOnDir(depcheck, dir) {
const result = await depcheck(dir, {
ignoreBinPackage: false,
ignoreDirs: DEFAULT_IGNORE_DIRS,
ignoreMatches: [],
specials: [
depcheck.special.eslint,
depcheck.special.typescript,
depcheck.special.webpack,
depcheck.special.rollup,
depcheck.special.babel,
depcheck.special.postcss,
],
});
return {
dir,
unused: {
dependencies: result.dependencies ?? [],
devDependencies: result.devDependencies ?? [],
},
};
}
function relative(p) {
return path.relative(repoRoot, p) || '.';
}
function printHuman(results) {
let any = false;
for (const { dir, unused } of results) {
if (unused.dependencies.length + unused.devDependencies.length === 0)
continue;
any = true;
console.log(`\n${relative(dir)}:`);
if (unused.dependencies.length) {
console.log(` unused dependencies (${unused.dependencies.length}):`);
for (const d of unused.dependencies) console.log(` - ${d}`);
}
if (unused.devDependencies.length) {
console.log(
` unused devDependencies (${unused.devDependencies.length}):`,
);
for (const d of unused.devDependencies) console.log(` - ${d}`);
}
}
if (!any) console.log('No unused dependencies found.');
}
async function main() {
const args = parseArgs();
const depcheck = await loadDepcheck();
const packageDirs = await discoverWorkspacePackageDirs();
const checks = await Promise.all(
packageDirs.map((dir) => runDepcheckOnDir(depcheck, dir)),
);
if (args.json) {
const compact = checks
.map(({ dir, unused }) => ({ dir: relative(dir), ...unused }))
.filter((r) => r.dependencies.length || r.devDependencies.length);
console.log(JSON.stringify(compact, null, 2));
} else {
printHuman(checks);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});