UNPKG

ditched

Version:

List dependencies that haven't been updated in a long time.

157 lines (156 loc) 5.77 kB
#!/usr/bin/env node import path from "path"; import fs from "fs"; import https from "https"; import CliTable from "cli-table"; import chalk from "chalk"; import yargs from "yargs/yargs"; import { hideBin } from "yargs/helpers"; import { differenceInMilliseconds, formatTimeSince } from "./time.js"; const MS_IN_A_DAY = 1000 * 60 * 60 * 24; const REGISTRY_URL = "https://registry.npmjs.org"; async function parseArgs() { return await yargs(hideBin(process.argv)).options({ all: { type: "boolean", default: false, alias: ["a"], description: "Include all dependencies in the resulting table, not only those that are ditched", }, days: { type: "number", default: 365, alias: ["d"], description: "The number of days since last release needed to consider a package as ditched", }, levels: { type: "number", default: 0, alias: ["l"], description: "How many levels we go down recursively", }, }).argv; } function getJSON(url) { return new Promise((resolve, reject) => { const request = https.get(url, (response) => { if (!response.statusCode || response.statusCode >= 400) { return reject(new Error(`Could not fetch URL ${url} package info. Status code ${response.statusCode}`)); } const body = []; response.on("data", (chunk) => body.push(chunk)); response.on("end", () => resolve(JSON.parse(body.join("")))); request.on("error", (err) => reject(err)); }); }); } function isDitched({ mostRecentReleaseDate }, ditchDays) { if (!mostRecentReleaseDate) return false; const ageDays = differenceInMilliseconds(new Date(), mostRecentReleaseDate) / MS_IN_A_DAY; return ageDays > ditchDays; } function printInfoTable(dataForPackages, showAllPackages, ditchDays) { const packagesToShow = dataForPackages.filter((data) => showAllPackages || isDitched(data, ditchDays)); if (!packagesToShow.length) { return; } const table = new CliTable({ head: [ chalk.gray("Package"), chalk.gray("Latest Release"), chalk.gray("Ditched?"), ], colWidths: [30, 40, 15], }); packagesToShow .sort((a, b) => { if (!a.mostRecentReleaseDate) return -1; if (!b.mostRecentReleaseDate) return 1; return differenceInMilliseconds(b.mostRecentReleaseDate, a.mostRecentReleaseDate); }) .forEach((packageInfo) => { const { name, mostRecentReleaseDate } = packageInfo; const formattedTime = mostRecentReleaseDate ? formatTimeSince(mostRecentReleaseDate) : "No package info found."; let ditchedInfo = chalk.red("?"); if (mostRecentReleaseDate) { ditchedInfo = isDitched(packageInfo, ditchDays) ? chalk.red("Yes") : chalk.green("No"); } table.push([name, formattedTime, ditchedInfo]); }); console.log(table.toString()); } async function getInfoForPackage(packageName, levels) { if (packageName in packageInfoCache) { return packageInfoCache[packageName]; } try { const regUrl = REGISTRY_URL + "/" + packageName; const response = await getJSON(regUrl); const mostRecentReleasedEntry = Object.entries(response.time) .filter(([key]) => key !== "created" && key !== "modified") .reduce((acc, el) => (el[1] > acc[1] ? el : acc)); const mostRecentReleaseDate = new Date(mostRecentReleasedEntry[1]); const mostRecentReleaseVersion = mostRecentReleasedEntry[0]; const mostRecentReleaseVersionDetails = response.versions[mostRecentReleaseVersion]; const { dependencies = {}, devDependencies = {} } = mostRecentReleaseVersionDetails; const dependencyPackages = [ ...Object.keys(dependencies), ...Object.keys(devDependencies), ]; const result = { name: packageName, mostRecentReleaseDate, }; packageInfoCache[packageName] = result; if (levels === 1) { await Promise.all(dependencyPackages.map((pkg) => getInfoForPackage(pkg, 0))); } else if (levels > 1) { for (const dependencyPackage of dependencyPackages) { await getInfoForPackage(dependencyPackage, levels - 1); } } return result; } catch (error) { return { name: packageName, }; } } const packageInfoCache = {}; async function main() { const argv = await parseArgs(); const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJsonStr = fs.readFileSync(packageJsonPath, { encoding: "utf8", }); const { dependencies = {}, devDependencies = {} } = JSON.parse(packageJsonStr); const packages = [ ...Object.keys(dependencies), ...Object.keys(devDependencies), ]; const levels = Number.isSafeInteger(argv.levels) && argv.levels >= 0 ? argv.levels : 0; let dataForPackages = []; if (levels === 0) { dataForPackages = await Promise.all(packages.map((packageName) => getInfoForPackage(packageName, 0))); } else { for (const packageName of packages) { await getInfoForPackage(packageName, levels); } dataForPackages = Object.values(packageInfoCache); } printInfoTable(dataForPackages, argv.all, argv.days); if (dataForPackages.filter(isDitched).length > 0) { process.exit(1); } } main();