UNPKG

npm-deprecated-check

Version:
667 lines (646 loc) 21.1 kB
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 };