npm-deprecated-check
Version:
Check for deprecated packages
667 lines (646 loc) • 21.1 kB
JavaScript
import process from 'node:process';
import { Option, program } from 'commander';
import fs from 'fs-extra';
import os from 'node:os';
import path, { resolve } from 'node:path';
import ansis from 'ansis';
import fetch from 'node-fetch';
import semver, { coerce, major, gt } from 'semver';
import { execSync } from 'node:child_process';
import yoctoSpinner from 'yocto-spinner';
import { readWantedLockfile } from '@pnpm/lockfile-file';
import lockfile from '@yarnpkg/lockfile';
const version = "1.3.0";
const homedir = os.homedir();
const rcPath = path.resolve(homedir, ".ndcrc");
const getGlobalConfig = function() {
try {
return fs.readJSONSync(rcPath) || {};
} catch {
return {};
}
};
const openaiModels = ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "gpt-4o-mini", "gpt-4o"];
const openaiBaseURL = "https://api.openai.com/v1";
function error(text) {
console.error(`${ansis.bgRed(" ERROR ")} ${ansis.red(text ?? "")}`);
}
function log(text) {
console.log(text ?? "");
}
function ok(text) {
console.log(`${ansis.bgGreen(" OK ")} ${text ?? ""}`);
}
function warn(text) {
console.warn(`${ansis.bgYellowBright(" WARN ")} ${ansis.yellow(text ?? "")}`);
}
function set(target, path, value) {
const fields = path.split(".");
let obj = target;
const l = fields.length;
for (let i = 0; i < l - 1; i++) {
const key = fields[i];
if (!obj[key])
obj[key] = {};
obj = obj[key];
}
obj[fields[l - 1]] = value;
}
function get(target, path) {
const fields = path.split(".");
let obj = target;
const l = fields.length;
for (let i = 0; i < l - 1; i++) {
const key = fields[i];
if (!obj[key])
return void 0;
obj = obj[key];
}
return obj[fields[l - 1]];
}
function unset(target, path) {
const fields = path.split(".");
let obj = target;
const l = fields.length;
const objs = [];
for (let i = 0; i < l - 1; i++) {
const key = fields[i];
if (!obj[key])
return;
objs.unshift({ parent: obj, key, value: obj[key] });
obj = obj[key];
}
delete obj[fields[l - 1]];
for (const { parent, key, value } of objs) {
if (!Object.keys(value).length)
delete parent[key];
}
}
function safeJSON(text) {
try {
return JSON.parse(text);
} catch {
return void 0;
}
}
function configure(options) {
if (!fs.existsSync(rcPath))
fs.writeFileSync(rcPath, JSON.stringify({ latestVersion: version, lastChecked: Date.now() }, null, 2), "utf-8");
let config = {};
try {
config = fs.readJsonSync(rcPath);
} catch {
}
if (options.get) {
const value = get(config, options.get);
log(value);
}
if (options.set) {
const [path, value] = options.set;
if (path === "openaiModel" && !openaiModels.includes(value)) {
error(`error: option '--openaiModel <value>' argument '${value}' is invalid. Allowed choices are ${openaiModels.join(", ")}.`);
process.exit(1);
}
let formatValue;
if (!Number.isNaN(Number.parseInt(value)))
formatValue = Number.parseInt(value);
else if (value === "true")
formatValue = true;
else if (value === "false")
formatValue = false;
else
formatValue = value;
set(config, path, formatValue);
fs.writeFileSync(rcPath, JSON.stringify(config, null, 2), "utf-8");
}
if (options.delete) {
unset(config, options.delete);
fs.writeFileSync(rcPath, JSON.stringify(config, null, 2), "utf-8");
}
if (options.list)
log(JSON.stringify(config, null, 2));
}
const defaultConfig = {
openaiModel: openaiModels[0],
openaiBaseURL
};
const globalConfig$1 = getGlobalConfig();
async function recommendDependencies(packageName, openaiOptions) {
const config = Object.assign(defaultConfig, globalConfig$1, openaiOptions);
if (!config.openaiKey)
return null;
for (let i = openaiModels.indexOf(config.openaiModel); i > -1; i--) {
const openaiModel = openaiModels[i];
const { url, req } = buildRequest(packageName, config.openaiKey, openaiModel, config.openaiBaseURL);
try {
const response = await fetch(url, req);
if (!response.ok) {
const errText = await response.text().catch(() => "Unknown");
const errJSON = safeJSON(errText);
const errMessage = errJSON ? void 0 : errText;
throw new Error(errJSON?.error?.message ? errJSON?.error?.message : errMessage || "Unknown error occurred");
}
const resJSON = await response.json().catch(() => null);
const content = resJSON.choices[0]?.message?.content;
const recommendedList = safeJSON(content) || content;
return recommendedList?.length ? recommendedList : null;
} catch (e) {
log();
warn(e);
log();
}
}
return null;
}
function buildRequest(packageName, openaiKey, openaiModel, openaiBaseURL2) {
const url = `${openaiBaseURL2}/chat/completions`;
const req = {
method: "post",
body: JSON.stringify({
messages: [{ role: "user", content: `The npm package - ${packageName} is deprecated, please suggest some alternative packages, only return an array of the package names.` }],
model: openaiModel
}, null, 2),
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": `Bearer ${openaiKey}`
}
};
return {
url,
req
};
}
function execCommand(command) {
return execSync(command).toString();
}
let registry = "";
function getRegistry() {
if (registry)
return registry;
try {
registry = execCommand("npm config get registry").trim();
} catch {
registry = "https://registry.npmjs.org/";
}
return registry;
}
const spinner = yoctoSpinner({ text: "Checking\u2026" });
let timer;
function startSpinner() {
spinner.color = "green";
spinner.start();
timer = setTimeout(() => {
spinner.color = "yellow";
timer = setTimeout(() => {
spinner.color = "red";
}, 3e4);
}, 3e4);
}
function stopSpinner() {
clearTimeout(timer);
spinner.stop();
}
async function checkDependencies(dependencies, config) {
const packageList = Object.keys(dependencies);
const resultList = [];
let haveDeprecated = false;
let haveErrors = false;
for (const packageName of packageList) {
startSpinner();
const result = await getPackageInfo(packageName, dependencies[packageName], config);
stopSpinner();
resultList.push(result);
if (result.error) {
haveErrors = true;
error(result.error);
log();
}
if (result.deprecated) {
haveDeprecated = true;
warn(`${result.name}@${result.version}: ${result.time}
deprecated: ${result.deprecated}`);
if (result.recommend) {
log(ansis.green("recommended: "));
if (Array.isArray(result.recommend)) {
for (const packageName2 of result.recommend)
log(`[${ansis.magenta(packageName2)}](https://www.npmjs.com/package/${packageName2})`);
} else {
log(result.recommend);
}
}
log();
if (config.failfast) {
process.exit(1);
}
}
}
if (!haveErrors)
ok(`All dependencies retrieved successfully.${haveDeprecated ? "" : " There are no deprecated dependencies."}`);
return resultList;
}
const globalConfig = getGlobalConfig();
async function getPackageInfo(packageName, versionOrRange, config) {
let packageRes;
try {
const registry = config.registry || globalConfig.registry || getRegistry();
const _registry = registry.endsWith("/") ? registry : `${registry}/`;
const response = await fetch(_registry + packageName);
packageRes = await response.json();
if (!packageRes)
return { name: packageName, error: `${packageName}: Could not find the package!` };
} catch (e) {
return { name: packageName, error: `${packageName}: ${e.message}` };
}
if (!packageRes["dist-tags"])
return { name: packageName, error: `${packageName}: Could not find the package!` };
const version = versionOrRange.version || (versionOrRange.range ? packageRes["dist-tags"][versionOrRange.range] || semver.maxSatisfying(Object.keys(packageRes.versions), versionOrRange.range || "*") || null : packageRes["dist-tags"].latest ? packageRes["dist-tags"].latest : error(`${packageName}: 'latest' dist-tag does not exist!`));
if (!version || !packageRes.versions[version])
return { name: packageName, error: `${packageName}: Please enter the correct range!` };
const deprecated = packageRes.versions[version].deprecated;
const recommend = deprecated ? await recommendDependencies(packageRes.name, config) : null;
const packageInfo = {
name: packageRes.name,
version,
time: packageRes.time[version],
deprecated,
recommend
};
return packageInfo;
}
function isLocalPackage(versionRange) {
const localPackagePrefix = [
"link:",
"file:",
"workspace:"
];
return localPackagePrefix.some((prefix) => versionRange.startsWith(prefix));
}
function isURLPackage(versionRange) {
return /^https?:\/\//.test(versionRange);
}
function isGitPackage(versionRange) {
return /\.git$/.test(versionRange);
}
const npmLockPath = resolve("./package-lock.json");
const yarnLockPath = resolve("./yarn.lock");
const pnpmLockPath = resolve("./pnpm-lock.yaml");
function getDependenciesOfLockfile(packages) {
const npmLock = {
path: npmLockPath,
read() {
const lockfileContent = fs.readJsonSync(this.path);
let dependencies = lockfileContent.dependencies;
let packageNamePrefix = "";
if (lockfileContent.lockfileVersion > 1) {
dependencies = lockfileContent.packages;
packageNamePrefix = "node_modules/";
}
const result = {};
for (const packageName in packages) {
const dependencyKey = packageNamePrefix + packageName;
if (dependencies[dependencyKey])
result[packageName] = { version: dependencies[dependencyKey].version };
}
return result;
}
};
const yarnLock = {
path: yarnLockPath,
read() {
const content = fs.readFileSync(this.path).toString("utf-8");
const json = lockfile.parse(content);
const result = {};
for (const packageName in packages)
json.object[`${packageName}@${packages[packageName].range}`] && (result[packageName] = { version: json.object[`${packageName}@${packages[packageName].range}`].version });
return result;
}
};
const pnpmLock = {
path: pnpmLockPath,
async read() {
const content = await readWantedLockfile(resolve(this.path, ".."), { ignoreIncompatible: false });
if (content && content.packages) {
const packageNames = Object.keys(packages);
const result = {};
for (const depPath in content.packages) {
const info = content.packages[depPath];
packageNames.includes(info.name) && (result[info.name] = { version: info.version });
}
return result;
} else {
return {};
}
}
};
const existsLock = [npmLock, yarnLock, pnpmLock].filter((ele) => fs.existsSync(ele.path)).sort((a, b) => fs.lstatSync(b.path).mtimeMs - fs.lstatSync(a.path).mtimeMs);
return existsLock.length > 0 ? existsLock[0].read() : {};
}
function formatDependencies(dependencies) {
const newDependencies = {};
for (const packageName in dependencies) {
if (dependencies[packageName].includes("@")) {
const idx = dependencies[packageName].lastIndexOf("@");
dependencies[packageName] = dependencies[packageName].slice(idx + 1);
}
newDependencies[packageName] = {
range: dependencies[packageName]
};
}
return newDependencies;
}
const packageJsonPath = resolve("./package.json");
function getDependenciesOfPackageJson() {
if (!fs.existsSync(packageJsonPath))
return error("package.json does not exist in the current path, please execute it under the correct project path.");
const { dependencies, devDependencies } = fs.readJsonSync(packageJsonPath);
return {
...formatDependencies(dependencies),
...formatDependencies(devDependencies)
};
}
async function checkCurrent(options) {
try {
const dependenciesOfPackageJson = getDependenciesOfPackageJson();
if (!dependenciesOfPackageJson)
return;
const ignores = options.ignore?.split(",") || [];
const npmDependencies = {};
for (const name in dependenciesOfPackageJson) {
const versionInfo = dependenciesOfPackageJson[name];
if (!ignores.includes(name) && !isLocalPackage(versionInfo.range) && !isURLPackage(versionInfo.range) && !isGitPackage(versionInfo.range)) {
npmDependencies[name] = versionInfo;
}
}
const dependenciesOfLockfile = await getDependenciesOfLockfile(npmDependencies);
const dependencies = Object.assign(npmDependencies, dependenciesOfLockfile);
return checkDependencies(dependencies, options);
} catch (e) {
error(e.message);
}
}
const yarnRegexp = /info "(.+)" has binaries/g;
function checkGlobal(options) {
const { manager, ...openaiOptions } = options;
try {
let dependencies = {};
if (manager === "pnpm") {
const result = JSON.parse(execCommand("pnpm list -g --depth=0 --json"));
dependencies = result.map((ele) => ele.dependencies).reduce((previousValue, currentValue) => Object.assign(previousValue, currentValue), {});
} else if (manager === "yarn") {
const result = execCommand("yarn global list --depth=0");
const iterator = Array.from(result.matchAll(yarnRegexp), (m) => m[1]);
for (const dependency of iterator) {
const index = dependency.lastIndexOf("@");
const packageName = dependency.slice(0, index);
const version = dependency.slice(index + 1);
dependencies[packageName] = { version };
}
} else {
const result = JSON.parse(execCommand("npm ls -g --depth=0 --json"));
dependencies = result.dependencies;
}
const ignores = options.ignore?.split(",") || [];
return checkDependencies(Object.fromEntries(Object.entries(dependencies).filter(([key, { version }]) => !ignores.includes(key) && !isLocalPackage(version))), openaiOptions);
} catch (e) {
error(e.message);
}
}
const v4 = {
start: "2015-09-08",
lts: "2015-10-12",
maintenance: "2017-04-01",
end: "2018-04-30",
codename: "Argon"
};
const v5 = {
start: "2015-10-29",
maintenance: "2016-04-30",
end: "2016-06-30"
};
const v6 = {
start: "2016-04-26",
lts: "2016-10-18",
maintenance: "2018-04-30",
end: "2019-04-30",
codename: "Boron"
};
const v7 = {
start: "2016-10-25",
maintenance: "2017-04-30",
end: "2017-06-30"
};
const v8 = {
start: "2017-05-30",
lts: "2017-10-31",
maintenance: "2019-01-01",
end: "2019-12-31",
codename: "Carbon"
};
const v9 = {
start: "2017-10-01",
maintenance: "2018-04-01",
end: "2018-06-30"
};
const v10 = {
start: "2018-04-24",
lts: "2018-10-30",
maintenance: "2020-05-19",
end: "2021-04-30",
codename: "Dubnium"
};
const v11 = {
start: "2018-10-23",
maintenance: "2019-04-22",
end: "2019-06-01"
};
const v12 = {
start: "2019-04-23",
lts: "2019-10-21",
maintenance: "2020-11-30",
end: "2022-04-30",
codename: "Erbium"
};
const v13 = {
start: "2019-10-22",
maintenance: "2020-04-01",
end: "2020-06-01"
};
const v14 = {
start: "2020-04-21",
lts: "2020-10-27",
maintenance: "2021-10-19",
end: "2023-04-30",
codename: "Fermium"
};
const v15 = {
start: "2020-10-20",
maintenance: "2021-04-01",
end: "2021-06-01"
};
const v16 = {
start: "2021-04-20",
lts: "2021-10-26",
maintenance: "2022-10-18",
end: "2023-09-11",
codename: "Gallium"
};
const v17 = {
start: "2021-10-19",
maintenance: "2022-04-01",
end: "2022-06-01"
};
const v18 = {
start: "2022-04-19",
lts: "2022-10-25",
maintenance: "2023-10-18",
end: "2025-04-30",
codename: "Hydrogen"
};
const v19 = {
start: "2022-10-18",
maintenance: "2023-04-01",
end: "2023-06-01"
};
const v20 = {
start: "2023-04-18",
lts: "2023-10-24",
maintenance: "2024-10-22",
end: "2026-04-30",
codename: "Iron"
};
const v21 = {
start: "2023-10-17",
maintenance: "2024-04-01",
end: "2024-06-01"
};
const v22 = {
start: "2024-04-24",
lts: "2024-10-29",
maintenance: "2025-10-21",
end: "2027-04-30",
codename: "Jod"
};
const v23 = {
start: "2024-10-16",
maintenance: "2025-04-01",
end: "2025-06-01"
};
const v24 = {
start: "2025-04-22",
lts: "2025-10-28",
maintenance: "2026-10-20",
end: "2028-04-30",
codename: ""
};
const nodeReleases = {
"v0.8": {
start: "2012-06-25",
end: "2014-07-31"
},
"v0.10": {
start: "2013-03-11",
end: "2016-10-31"
},
"v0.12": {
start: "2015-02-06",
end: "2016-12-31"
},
v4: v4,
v5: v5,
v6: v6,
v7: v7,
v8: v8,
v9: v9,
v10: v10,
v11: v11,
v12: v12,
v13: v13,
v14: v14,
v15: v15,
v16: v16,
v17: v17,
v18: v18,
v19: v19,
v20: v20,
v21: v21,
v22: v22,
v23: v23,
v24: v24
};
function getLatestNodeVersion(nodeReleases2) {
const versions = Object.keys(nodeReleases2);
const latestVersion = versions.reduce((_prev, _curr) => {
const prev = coerce(_prev);
const curr = coerce(_curr);
return gt(curr, prev) ? _curr : _prev;
});
return latestVersion;
}
function checkNode() {
const nodeVersion = coerce(process.version);
const latestNodeVersion = coerce(getLatestNodeVersion(nodeReleases));
const nodeVersionData = nodeReleases[`v${major(nodeVersion)}`];
if (nodeVersionData) {
const endDate = new Date(nodeVersionData.end);
const currentDate = /* @__PURE__ */ new Date();
const isNodeVersionSupported = currentDate < endDate;
if (isNodeVersionSupported) {
ok(`Your node version (${nodeVersion}) is supported until ${nodeVersionData.end}.`);
} else {
warn(`Your node version (${nodeVersion}) is no longer supported since ${nodeVersionData.end}.`);
}
} else if (gt(nodeVersion, latestNodeVersion)) {
warn(`Your node version (${nodeVersion}) is higher than the latest version ${latestNodeVersion}. Please update 'npm-deprecated-check'.`);
} else {
warn(`Your node version (${nodeVersion}) can't be found in the release schedule. Please update 'npm-deprecated-check'.`);
}
return {
version: nodeVersion,
latestVersion: latestNodeVersion,
releases: nodeReleases
};
}
function checkSpecified(options) {
const { packageName, range, ...openaiOptions } = options;
return checkDependencies({ [packageName]: { range } }, openaiOptions);
}
const registryOption = new Option("--registry <value>", "specify registry URL");
const gptOption = new Option("--openaiKey <value>", "recommend alternative packages via ChatGPT");
const gptModelOption = new Option("--openaiModel <value>", "ChatGPT model").choices(openaiModels);
const gptBaseURL = new Option("--openaiBaseURL <value>", "override the default base URL for the API");
program.version(`npm-deprecated-check ${version}`).usage("<command> [options]");
program.command("node").description("check if used node version is deprecated (reached End Of Life)").action(() => {
checkNode();
});
program.command("current").description("check the packages of the current project").addOption(new Option("--ignore <value>", "ignore specific packages")).addOption(new Option("--failfast", "exit the program if it has been deprecated")).addOption(registryOption).addOption(gptOption).addOption(gptModelOption).addOption(gptBaseURL).action((option) => {
checkNode();
checkCurrent(option);
});
program.command("global").description("check global packages, default: npm").addOption(new Option("-m, --manager <value>", "check specified package manager").choices(["npm", "yarn", "pnpm"]).default("npm")).addOption(new Option("--ignore <value>", "ignore specific packages")).addOption(new Option("--failfast", "exit the program if it has been deprecated")).addOption(registryOption).addOption(gptOption).addOption(gptModelOption).addOption(gptBaseURL).action((globalOption) => {
checkNode();
checkGlobal(globalOption);
});
program.command("package <packageName>").description("check for specified package").addOption(new Option("-r, --range <value>", "check specified versions")).addOption(new Option("--failfast", "exit the program if it has been deprecated")).addOption(registryOption).addOption(gptOption).addOption(gptModelOption).addOption(gptBaseURL).action((packageName, option) => {
const packageOption = {
packageName,
...option
};
checkSpecified(packageOption);
});
program.command("config").description("inspect and modify the config").addOption(new Option("-g, --get <path>", "get value from option")).addOption(new Option("-s, --set <path> <value>", "set option value")).addOption(new Option("-d, --delete <path>", "delete option from config")).addOption(new Option("-l, --list", "list all options")).action((option, command) => {
if (Object.keys(option).length === 0) {
command.outputHelp();
process.exit(0);
}
const configOption = {};
for (const key in option) {
if (key === "set")
configOption.set = [option.set, command.args[0]];
else
configOption[key] = option[key];
}
configure(configOption);
});
program.parse(process.argv);
export { checkCurrent, checkGlobal, checkNode, checkSpecified as checkPackage };