diff --git a/src/cli.ts b/src/cli.ts index fafdfc84..7de3985b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,12 +36,14 @@ program .description('upgrade dependencies and devDependencies of all packages') .option('--dry-run', 'no actual upgrading (just the fetching process)', false) .option('--registry ', 'npm registry to use') - .action(async (targetPath: string, { dryRun, registry }) => { + .option('--no-color', 'disable colored output', true) + .action(async (targetPath: string, { dryRun, registry, color }) => { const { upgrade } = await import('./commands/upgrade.js'); await upgrade({ directoryPath: path.resolve(targetPath || ''), dryRun, + color, registryUrl: registry, }); }); diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts index 510b3ec7..caf86fcb 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/upgrade.ts @@ -8,6 +8,7 @@ import { createCliProgressBar } from '../utils/cli-progress-bar.js'; import { loadPlebConfig, normalizePinnedPackages } from '../utils/config.js'; import { loadEnvNpmConfig } from '../utils/npm-config.js'; import { NpmRegistry, officialNpmRegistryUrl, uriToIdentifier } from '../utils/npm-registry.js'; +import { getChangeType, colorizeChangeType, type ChangeType, groupByChangeType } from '../utils/get-change-type.js'; const { gt, coerce } = semver; @@ -15,6 +16,7 @@ export interface UpgradeOptions { directoryPath: string; dryRun?: boolean; registryUrl?: string; + color?: boolean; log?: (message: unknown) => void; logError?: (message: unknown) => void; } @@ -23,6 +25,7 @@ export async function upgrade({ directoryPath, registryUrl, dryRun, + color = true, log = console.log, logError = console.error, }: UpgradeOptions): Promise { @@ -80,24 +83,32 @@ export async function upgrade({ } }; - const replacements = new Map(); - const skipped = new Map(); + const replacements = new Map(); + const skipped = new Map< + string, + { originalValue: string; newValue: string; reason: string; changeType: ChangeType } + >(); function mapDependencies(obj: Partial>): Partial> { const newObj: Partial> = {}; for (const [packageName, request] of Object.entries(obj)) { const newVersionRequest = getVersionRequest(packageName, request!); newObj[packageName] = request; - + const changeType = getChangeType(request!, newVersionRequest); if (newVersionRequest !== request) { if (pinnedPackages.has(packageName)) { skipped.set(packageName, { originalValue: request!, newValue: newVersionRequest, reason: pinnedPackages.get(packageName)!, + changeType, }); } else { - replacements.set(packageName, { originalValue: request!, newValue: newVersionRequest }); + replacements.set(packageName, { + originalValue: request!, + newValue: newVersionRequest, + changeType, + }); newObj[packageName] = newVersionRequest; } } @@ -127,19 +138,32 @@ export async function upgrade({ if (replacements.size) { log('Changes:'); const maxKeyLength = Array.from(replacements.keys()).reduce((acc, key) => Math.max(acc, key.length), 0); - for (const [key, { originalValue, newValue }] of replacements) { - log(` ${key.padEnd(maxKeyLength + 2)} ${originalValue.padStart(8)} -> ${newValue}`); + for (const changeGroup of Object.values(groupByChangeType(replacements))) { + for (const [key, { originalValue, newValue, changeType }] of changeGroup) { + log( + colorizeChangeType( + color, + changeType, + ` ${key.padEnd(maxKeyLength + 2)} ${originalValue.padStart(8)} -> ${newValue}`, + ), + ); + } } } if (skipped.size) { log('Skipped:'); const maxKeyLength = Array.from(skipped.keys()).reduce((acc, key) => Math.max(acc, key.length), 0); - for (const [key, { originalValue, reason, newValue }] of skipped) { - log( - ` ${key.padEnd(maxKeyLength + 2)} ${originalValue.padStart(8)} -> ${newValue}` + - (reason ? ` (${reason})` : ``), - ); + for (const skippedGroup of Object.values(groupByChangeType(skipped))) { + for (const [key, { originalValue, reason, newValue, changeType }] of skippedGroup) { + log( + colorizeChangeType( + color, + changeType, + ` ${key.padEnd(maxKeyLength + 2)} ${originalValue.padStart(8)} -> ${newValue}`, + ) + (reason ? ` (${reason})` : ``), + ); + } } } diff --git a/src/utils/get-change-type.ts b/src/utils/get-change-type.ts new file mode 100644 index 00000000..8a85c272 --- /dev/null +++ b/src/utils/get-change-type.ts @@ -0,0 +1,66 @@ +import semver from 'semver'; + +const colors = { + bgWhite: (text: string) => '\x1b[47m' + text + '\x1b[0m', + green: (text: string) => '\x1b[32m' + text + '\x1b[0m', + red: (text: string) => '\x1b[31m' + text + '\x1b[0m', + yellow: (text: string) => '\x1b[33m' + text + '\x1b[0m', +}; + +export type ChangeType = semver.ReleaseType | 'unknown'; + +/** + * Returns a colorized message based on the type of change. + */ +export function colorizeChangeType(color: boolean, changeType: ChangeType, message: string) { + if (!color) { + return message; + } + switch (changeType) { + case 'unknown': + return message; + case 'prepatch': + case 'preminor': + case 'premajor': + case 'prerelease': + return colors.bgWhite(colors.red(message)); + case 'major': + return colors.red(message); + case 'minor': + return colors.yellow(message); + case 'patch': + return colors.green(message); + } +} + +/** + * Returns the type of change between two versions. + */ +export function getChangeType(oldVersion: string, newVersion: string): ChangeType { + const oldVer = semver.coerce(oldVersion); + const newVer = semver.coerce(newVersion); + if (!oldVer || !newVer) { + return 'unknown'; + } + return semver.diff(oldVer, newVer) ?? 'unknown'; +} + +/** + * Groups a map of packages by their change type. into a constant order of change types. + */ +export function groupByChangeType(map: Map) { + const groups: Record = { + prerelease: [], + premajor: [], + major: [], + preminor: [], + minor: [], + prepatch: [], + patch: [], + unknown: [], + }; + for (const [key, value] of map) { + groups[value.changeType].push([key, value]); + } + return groups; +}