libyear
Version:
A simple measure of software dependency freshness
138 lines (137 loc) • 5.5 kB
JavaScript
import { styleText } from "node:util";
import { omit, partial } from "lodash-es";
import { METRICS } from "../constants.js";
import { getTotals, getViolations } from "../validate.js";
import { clipFloat, printFloat } from "./style/number.js";
import { styleTable } from "./style/table.js";
const COLUMN_FILTERS = {
major: 0,
minor: 0,
patch: 0,
deprecated: false,
latest: "—",
};
const getMetricFormat = ({ dependency, metric, violations, }) => {
const isBreach = dependency != null
? violations.individual[metric]?.[dependency]
: violations.collective[metric];
return isBreach ? "red" : undefined;
};
const getFormattedMetrics = ({ dependency, metrics, violations, }) => Object.fromEntries(Object.entries(metrics).map(([key, value]) => [
key,
{
format: getMetricFormat({
dependency,
metric: key,
violations,
}),
value: clipFloat(value),
},
]));
const ntext = (text, plural, count) => Math.abs(count) === 1 ? text : plural;
const getMetricUnit = (metric, count) => {
switch (metric) {
case "releases":
case "major":
case "minor":
case "patch":
return ntext("release", "releases", count);
case "drift":
case "pulse":
default:
return ntext("libyear", "libyears", count);
}
};
const printIndividual = (violations) => {
Object.entries(violations).forEach(([metric, dependencies]) => {
Object.entries(dependencies).forEach(([dependency, { limit, value }]) => {
console.error(`${styleText("magenta", metric)}: ${styleText("cyan", dependency)} is ${styleText("red", `${printFloat(value)} ${getMetricUnit(metric, value)}`)} behind; limit is ${styleText("yellow", printFloat(limit))}.`);
});
});
};
const printCollective = (totals, violations, limit) => {
const isBreach = (metric) => Object.hasOwn(violations, metric);
const logger = (metric) => isBreach(metric) ? console.error : console.log;
const message = (metric, value, limit) => {
const valueStyler = isBreach(metric)
? partial(styleText, "red")
: partial(styleText, "green");
const valueMessage = `${styleText("magenta", metric)}: ${{
drift: "package is",
pulse: "dependencies are",
releases: "dependencies are",
major: "dependencies are",
minor: "dependencies are",
patch: "dependencies are",
}[metric]} ${valueStyler(`${printFloat(value)} ${getMetricUnit(metric, value)}`)} behind`;
const limitMessage = `limit is ${styleText("yellow", limit != null ? printFloat(limit) : String(limit))}`;
return isBreach(metric)
? `${valueMessage}; ${limitMessage}.`
: `${valueMessage}.`;
};
METRICS.forEach((metric) => {
logger(metric)(message(metric, totals[metric] ?? 0, limit?.[`${metric}Collective`]));
});
};
const printDeprecations = (deprecations) => {
Object.entries(deprecations).forEach(([dependency, deprecation]) => {
console.warn(`${styleText("yellow", dependency)}: ${deprecation}`);
});
};
export const print = (dependencies, limit, overrides) => {
const totals = getTotals(dependencies);
const violations = getViolations(dependencies, totals, limit, overrides);
const table = [
...dependencies.map(({ dependency, deprecated, latest, ...metrics }) => ({
dependency: {
href: `https://npm.im/${dependency}`,
value: dependency,
},
...getFormattedMetrics({
dependency,
metrics,
violations,
}),
deprecated: deprecated ? { format: "red", value: true } : false,
latest: latest ?? "—",
})),
{
dependency: "total",
...getFormattedMetrics({
metrics: totals,
violations,
}),
deprecated: false,
latest: "—",
},
];
const emptyColumns = new Set(Object.keys(COLUMN_FILTERS));
table.forEach((row) => {
emptyColumns.forEach((column) => {
if ((row[column]?.value ?? row[column]) !==
COLUMN_FILTERS[column]) {
emptyColumns.delete(column);
}
});
});
console.log(styleTable(table.map((row) => omit(row, Array.from(emptyColumns)))));
const hasIndividualViolations = Object.values(violations.individual).reduce((acc, cur) => acc + Object.keys(cur).length, 0) > 0;
const hasCollectiveViolations = Object.keys(violations.collective).length > 0;
const deprecations = dependencies.reduce((accumulator, { dependency, deprecated }) => deprecated ? { ...accumulator, [dependency]: deprecated } : accumulator, {});
const hasDeprecations = Object.keys(deprecations).length > 0;
if (hasIndividualViolations) {
console.log(`\n${styleText("bold", "# Individual")}`);
printIndividual(violations.individual);
}
if (hasCollectiveViolations) {
console.log(`\n${styleText("bold", "# Collective")}`);
printCollective(totals, violations.collective, limit);
}
if (hasDeprecations) {
console.log(`\n${styleText("bold", "# Deprecations")}`);
printDeprecations(deprecations);
}
if (hasIndividualViolations || hasCollectiveViolations) {
process.exit(1);
}
};