├── .github
└── workflows
│ ├── automated-lint.yaml
│ ├── automated-release.yaml
│ └── automated-tests.yaml
├── .gitignore
├── .npmrc
├── LICENSE.md
├── README.md
├── assets
└── preview.png
├── bin
└── ndc.mjs
├── build.config.ts
├── eslint.config.js
├── package.json
├── pnpm-lock.yaml
├── scripts
└── node_releases.ts
├── src
├── chatgpt.ts
├── check.ts
├── cli.ts
├── filter.ts
├── io
│ ├── config.ts
│ ├── current.ts
│ ├── global.ts
│ ├── node.ts
│ └── package.ts
├── packages
│ ├── lockfiles.ts
│ └── package_json.ts
├── schedule.json
├── shared.ts
├── types.ts
└── utils
│ ├── console.ts
│ ├── exec.ts
│ ├── format.ts
│ ├── object.ts
│ └── spinner.ts
├── test
├── config.spec.js
├── current.spec.js
├── examples
│ ├── deprecated.json
│ └── normal.json
├── global.spec.js
├── help.spec.js
├── node.spec.js
└── package.spec.js
└── tsconfig.json
/.github/workflows/automated-lint.yaml:
--------------------------------------------------------------------------------
1 | name: Automated Lint and Type Check
2 |
3 | on:
4 | push:
5 | branches: [main, develop]
6 | pull_request:
7 | branches: [main, develop]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | lint:
14 | runs-on: ubuntu-latest
15 | timeout-minutes: 10
16 |
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 |
21 | - name: Use pnpm
22 | uses: pnpm/action-setup@v4
23 | with:
24 | version: latest
25 |
26 | - name: Use Node.js
27 | uses: actions/setup-node@v4
28 | with:
29 | cache-dependency-path: pnpm-lock.yaml
30 | node-version: lts/*
31 | check-latest: true
32 | cache: pnpm
33 |
34 | - name: Install dependencies
35 | run: pnpm install
36 |
37 | - name: Check linting
38 | run: pnpm lint
39 |
40 | - name: Run TypeScript Type Check
41 | run: pnpm typecheck
42 |
--------------------------------------------------------------------------------
/.github/workflows/automated-release.yaml:
--------------------------------------------------------------------------------
1 | name: Automated Release
2 |
3 | permissions:
4 | id-token: write
5 | contents: write
6 |
7 | on:
8 | push:
9 | branches:
10 | - release
11 |
12 | jobs:
13 | release:
14 | runs-on: ubuntu-latest
15 | timeout-minutes: 30
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 | - uses: pnpm/action-setup@v4
22 | with:
23 | version: 9.15.0
24 | - uses: actions/setup-node@v4
25 | with:
26 | node-version: lts/*
27 | registry-url: https://registry.npmjs.org/
28 |
29 | - name: Install dependencies
30 | run: pnpm install
31 |
32 | - name: Build the project
33 | run: pnpm run build
34 |
35 | - name: Publish to npm
36 | run: pnpm publish -r --access public --publish-branch release
37 | env:
38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
39 | NPM_CONFIG_PROVENANCE: true
40 |
41 | - name: Get version from package.json
42 | id: get_version
43 | run: |
44 | VERSION=$(node -p "require('./package.json').version")
45 | echo "VERSION=$VERSION" >> $GITHUB_ENV
46 |
47 | - name: Create Git Tag
48 | run: |
49 | git config user.name "github-actions[bot]"
50 | git config user.email "github-actions[bot]@users.noreply.github.com"
51 | git tag -a "v${VERSION}" -m "Release v${VERSION}"
52 | git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/KID-joker/npm-deprecated-check.git "v${VERSION}"
53 |
--------------------------------------------------------------------------------
/.github/workflows/automated-tests.yaml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, run tests across different versions of node.
2 |
3 | name: Automated Tests
4 |
5 | on:
6 | push:
7 | branches: [main, develop]
8 | pull_request:
9 | branches: [main, develop]
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | timeout-minutes: 30
18 | strategy:
19 | matrix:
20 | node-version: [22]
21 |
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@v4
25 |
26 | - name: Install pnpm
27 | uses: pnpm/action-setup@v2
28 | with:
29 | version: latest
30 | run_install: false
31 |
32 | - name: Use Node.js ${{ matrix.node-version }}
33 | uses: actions/setup-node@v4
34 | with:
35 | cache-dependency-path: pnpm-lock.yaml
36 | node-version: ${{ matrix.node-version }}
37 | check-latest: true
38 | cache: pnpm
39 |
40 | - name: Install dependencies
41 | run: pnpm install
42 |
43 | - name: Build project
44 | run: pnpm run build
45 |
46 | - name: Save build artifacts
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: build-artifacts
50 | path: dist
51 |
52 | test:
53 | runs-on: ubuntu-latest
54 | needs: build
55 | timeout-minutes: 30
56 | strategy:
57 | matrix:
58 | node-version: [22, 20, 18]
59 |
60 | steps:
61 | - name: Checkout code
62 | uses: actions/checkout@v4
63 |
64 | - name: Use Node.js ${{ matrix.node-version }}
65 | uses: actions/setup-node@v4
66 | with:
67 | node-version: ${{ matrix.node-version }}
68 | check-latest: true
69 |
70 | - name: Download build artifacts
71 | uses: actions/download-artifact@v4
72 | with:
73 | name: build-artifacts
74 | path: dist
75 |
76 | - name: Install pnpm
77 | uses: pnpm/action-setup@v2
78 | with:
79 | version: latest
80 | run_install: false
81 |
82 | - name: Check versions
83 | run: |
84 | yarn --version
85 | pnpm --version
86 |
87 | - name: Check global dependencies
88 | run: |
89 | npm ls -g --depth=0 --json
90 | yarn global list --depth=0
91 | pnpm list -g --depth=0 --json
92 |
93 | - name: Install production dependencies
94 | run: pnpm install --prod
95 |
96 | - name: Run tests
97 | run: pnpm test
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | test/playground
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-workspace-root-check=true
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright © 2022 KID-joker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
🐦 npm-deprecated-check
2 | Check for deprecated packages
3 |
4 | ## Preview
5 |
6 | 
7 |
8 | ## Requirements
9 |
10 | Since version 1.4.0, `npm-deprecated-check` requires Node.js 18 or higher.
11 |
12 | ## Install
13 |
14 | ```bash
15 | npm install -g npm-deprecated-check
16 | ```
17 |
18 | ## Features
19 |
20 | - Check the packages of current project, global or specified is deprecated.
21 | - According to the version range of lockfile and package.json.
22 | - Recommend alternative packages through OpenAI.
23 | - Additionally checks if the running node version reached End Of Life.
24 |
25 | ## Usage
26 |
27 | ```bash
28 | Usage: ndc [options]
29 |
30 | Options:
31 | -V, --version output the version number
32 | -h, --help display help for command
33 |
34 | Commands:
35 | current [options] check the packages of the current project
36 | global [options] check global packages, default: npm
37 | package [options] check for specified package
38 | node check if used node version is deprecated (reached End Of Life)
39 | config [options] inspect and modify the config
40 | help [command] display help for command
41 | ```
42 |
43 | `OpenAI` config:
44 |
45 | ```bash
46 | Options:
47 | --openaiKey recommend alternative packages via ChatGPT
48 | --openaiModel ChatGPT model (choices: "gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "gpt-4o-mini", "gpt-4o")
49 | --openaiBaseURL override the default base URL for the API
50 | ```
51 |
52 | For `current`:
53 |
54 | ```bash
55 | Options:
56 | --registry specify registry URL, default: https://registry.npmjs.org/
57 | --ignore ignore specific packages, example: request,tslint
58 | --failfast exit the program if it has been deprecated
59 | --deep deep inspection for monorepo projects
60 | ```
61 |
62 | For `global`:
63 |
64 | ```bash
65 | Options:
66 | -m, --manager check specified package manager (choices: "npm", "yarn", "pnpm")
67 | --registry specify registry URL, default: https://registry.npmjs.org/
68 | --ignore ignore specific packages, example: request,tslint
69 | --failfast exit the program if it has been deprecated
70 | ```
71 |
72 | For `package`:
73 |
74 | ```bash
75 | -r, --range check specified versions
76 | --registry specify registry URL, default: https://registry.npmjs.org/
77 | --failfast exit the program if it has been deprecated
78 | ```
79 |
80 | You can also save them to global configuration:
81 |
82 | ```bash
83 | Usage: ndc config [options]
84 |
85 | inspect and modify the config
86 |
87 | Options:
88 | -g, --get get value from option
89 | -s, --set set option value
90 | -d, --delete delete option from config
91 | -l, --list list all options
92 | ```
93 |
94 | The path should be `openaiKey`, `openaiModel`, `openaiBaseURL`.
95 |
96 | ## Credits
97 |
98 | `npm-deprecated-check` is inspired by [`check-is-deprecated`](https://github.com/awesome-cli/check-is-deprecated).
99 |
--------------------------------------------------------------------------------
/assets/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KID-joker/npm-deprecated-check/913504e02a1be841a7fb5d4444c4cf5391353119/assets/preview.png
--------------------------------------------------------------------------------
/bin/ndc.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict'
3 | import '../dist/cli.mjs'
4 |
--------------------------------------------------------------------------------
/build.config.ts:
--------------------------------------------------------------------------------
1 | import { defineBuildConfig } from 'unbuild'
2 |
3 | export default defineBuildConfig({
4 | entries: ['src/cli'],
5 | declaration: true,
6 | rollup: {
7 | inlineDependencies: true,
8 | esbuild: {
9 | minify: true,
10 | },
11 | },
12 | clean: true,
13 | })
14 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import antfu from '@antfu/eslint-config'
2 |
3 | export default antfu({
4 | rules: {
5 | 'test/no-import-node-test': 'off',
6 | },
7 | })
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "npm-deprecated-check",
3 | "type": "module",
4 | "version": "1.5.0",
5 | "description": "Check for deprecated packages",
6 | "author": "KID-joker ",
7 | "license": "MIT",
8 | "homepage": "https://github.com/KID-joker/npm-deprecated-check#readme",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/KID-joker/npm-deprecated-check.git"
12 | },
13 | "bugs": "https://github.com/KID-joker/npm-deprecated-check/issues",
14 | "keywords": [
15 | "cli",
16 | "cli-tool",
17 | "dependencies",
18 | "deprecated",
19 | "OpenAI",
20 | "ai-recommend"
21 | ],
22 | "main": "dist/cli.mjs",
23 | "module": "dist/cli.mjs",
24 | "bin": {
25 | "ndc": "bin/ndc.mjs"
26 | },
27 | "files": [
28 | "dist"
29 | ],
30 | "engines": {
31 | "node": ">=18"
32 | },
33 | "preferGlobal": true,
34 | "scripts": {
35 | "build": "pnpm build:releases && unbuild",
36 | "build:releases": "tsx ./scripts/node_releases.ts",
37 | "dev": "tsx ./src/cli.ts",
38 | "lint": "eslint",
39 | "lint:fix": "eslint --fix",
40 | "test": "node --test",
41 | "typecheck": "tsc --noEmit"
42 | },
43 | "dependencies": {
44 | "@pnpm/lockfile-file": "^9.1.3",
45 | "@pnpm/logger": "1001.0.0",
46 | "@yarnpkg/parsers": "^3.0.3",
47 | "ansis": "^4.0.0",
48 | "commander": "^13.1.0",
49 | "semver": "^7.7.2",
50 | "yocto-spinner": "^0.2.2"
51 | },
52 | "devDependencies": {
53 | "@antfu/eslint-config": "^4.13.1",
54 | "@types/node": "^22.15.18",
55 | "@types/semver": "^7.7.0",
56 | "@types/yarnpkg__lockfile": "^1.1.9",
57 | "eslint": "^9.27.0",
58 | "lint-staged": "^16.0.0",
59 | "simple-git-hooks": "^2.13.0",
60 | "tsx": "^4.19.4",
61 | "typescript": "^5.8.3",
62 | "unbuild": "^3.5.0"
63 | },
64 | "simple-git-hooks": {
65 | "pre-commit": "npx lint-staged"
66 | },
67 | "lint-staged": {
68 | "*.{js,ts,md}": [
69 | "eslint --fix"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/scripts/node_releases.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from 'node:fs'
2 | import path from 'node:path'
3 | import { fileURLToPath } from 'node:url'
4 |
5 | const scheduleURL = 'https://raw.githubusercontent.com/nodejs/Release/master/schedule.json'
6 | const __filename = fileURLToPath(import.meta.url)
7 | const __dirname = path.dirname(__filename)
8 | const filePath = path.resolve(__dirname, '../src/schedule.json')
9 |
10 | async function fetchAndSaveSchedule() {
11 | const res = await fetch(scheduleURL)
12 | const schedule = await res.json()
13 | writeFileSync(filePath, `${JSON.stringify(schedule, null, 2)}\n`, 'utf-8')
14 | }
15 |
16 | fetchAndSaveSchedule().catch((error) => {
17 | console.error('Error fetching and saving node.js release schedule:', error)
18 | })
19 |
--------------------------------------------------------------------------------
/src/chatgpt.ts:
--------------------------------------------------------------------------------
1 | import type { OpenaiOption } from './types'
2 | import { getGlobalConfig, openaiBaseURL, openaiModels } from './shared'
3 | import { log, warn } from './utils/console'
4 | import { safeJSON } from './utils/object'
5 |
6 | const defaultConfig = {
7 | openaiModel: openaiModels[0],
8 | openaiBaseURL,
9 | }
10 | const globalConfig = getGlobalConfig()
11 |
12 | export async function recommendDependencies(packageName: string, openaiOptions: OpenaiOption) {
13 | const config = Object.assign(defaultConfig, globalConfig, openaiOptions)
14 |
15 | if (!config.openaiKey)
16 | return null
17 |
18 | for (let i = openaiModels.indexOf(config.openaiModel); i > -1; i--) {
19 | const openaiModel = openaiModels[i]
20 | const { url, req } = buildRequest(packageName, config.openaiKey, openaiModel, config.openaiBaseURL)
21 |
22 | try {
23 | const response = await fetch(url, req)
24 |
25 | if (!response.ok) {
26 | const errText = await response.text().catch(() => 'Unknown')
27 | const errJSON = safeJSON(errText)
28 | const errMessage = errJSON ? undefined : errText
29 | throw new Error(errJSON?.error?.message ? errJSON?.error?.message : errMessage || 'Unknown error occurred')
30 | }
31 |
32 | const resJSON: Record = await response.json().catch(() => null) as Record
33 |
34 | const content = resJSON.choices[0]?.message?.content
35 | const recommendedList = safeJSON(content) || content
36 |
37 | return recommendedList?.length ? recommendedList : null
38 | }
39 | catch (e: any) {
40 | log()
41 | warn(e)
42 | log()
43 | }
44 | }
45 |
46 | return null
47 | }
48 |
49 | function buildRequest(packageName: string, openaiKey: string | undefined, openaiModel: string, openaiBaseURL: string) {
50 | const url = `${openaiBaseURL}/chat/completions`
51 | const req = {
52 | method: 'post',
53 | body: JSON.stringify({
54 | messages: [{ role: 'user', content: `The npm package - ${packageName} is deprecated, please suggest some alternative packages, only return an array of the package names.` }],
55 | model: openaiModel,
56 | }, null, 2),
57 | headers: {
58 | 'Accept': 'application/json',
59 | 'Content-Type': 'application/json',
60 | 'Authorization': `Bearer ${openaiKey}`,
61 | },
62 | }
63 | return {
64 | url,
65 | req,
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/check.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOption, PackageInfo, PackageVersions, VersionOrRange } from './types'
2 | import process from 'node:process'
3 | import ansis from 'ansis'
4 | import semver from 'semver'
5 | import { recommendDependencies } from './chatgpt'
6 | import { getGlobalConfig } from './shared'
7 | import { error, log, ok, warn } from './utils/console'
8 | import { getRegistry } from './utils/exec'
9 | import { startSpinner, stopSpinner } from './utils/spinner'
10 |
11 | export async function checkDependencies(dependencies: Record, config: CommonOption) {
12 | const packageList = Object.keys(dependencies)
13 | const resultList = []
14 | let haveDeprecated = false
15 | let haveErrors = false
16 | for (const packageName of packageList) {
17 | startSpinner()
18 | const result = await getPackageInfo(packageName, dependencies[packageName], config)
19 | stopSpinner()
20 | resultList.push(result)
21 | if (result.error) {
22 | haveErrors = true
23 | error(result.error)
24 | log()
25 | }
26 |
27 | if (result.deprecated) {
28 | haveDeprecated = true
29 | warn(`${result.name}@${result.version}: ${result.time}\ndeprecated: ${result.deprecated}`)
30 |
31 | if (result.recommend) {
32 | log(ansis.green('recommended: '))
33 | if (Array.isArray(result.recommend)) {
34 | for (const packageName of result.recommend)
35 | log(`[${ansis.magenta(packageName)}](https://www.npmjs.com/package/${packageName})`)
36 | }
37 | else {
38 | log(result.recommend)
39 | }
40 | }
41 | log()
42 |
43 | if (config.failfast) {
44 | process.exit(1)
45 | }
46 | }
47 | }
48 |
49 | if (!haveErrors)
50 | ok(`All dependencies retrieved successfully.${haveDeprecated ? '' : ' There are no deprecated dependencies.'}`)
51 |
52 | return resultList
53 | }
54 |
55 | const globalConfig = getGlobalConfig()
56 | async function getPackageInfo(packageName: string, versionOrRange: VersionOrRange, config: CommonOption) {
57 | let packageRes
58 | try {
59 | const registry = config.registry || globalConfig.registry || getRegistry()
60 | const _registry = registry.endsWith('/') ? registry : `${registry}/`
61 | const response = await fetch(_registry + packageName)
62 | packageRes = await response.json() as PackageVersions
63 |
64 | if (!packageRes)
65 | return { name: packageName, error: `${packageName}: Could not find the package!` }
66 | }
67 | catch (e: any) {
68 | return { name: packageName, error: `${packageName}: ${e.message}` }
69 | }
70 |
71 | if (!packageRes['dist-tags'])
72 | return { name: packageName, error: `${packageName}: Could not find the package!` }
73 |
74 | const version: string | null = versionOrRange.version || (versionOrRange.range
75 | ? packageRes['dist-tags'][versionOrRange.range] || semver.maxSatisfying(Object.keys(packageRes.versions), versionOrRange.range || '*') || null
76 | : packageRes['dist-tags'].latest
77 | ? packageRes['dist-tags'].latest
78 | : error(`${packageName}: 'latest' dist-tag does not exist!`) as unknown as string)
79 |
80 | if (!version || !packageRes.versions[version])
81 | return { name: packageName, error: `${packageName}: Please enter the correct range!` }
82 |
83 | const deprecated = packageRes.versions[version].deprecated
84 | const recommend = deprecated ? await recommendDependencies(packageRes.name, config) : null
85 |
86 | const packageInfo: PackageInfo = {
87 | name: packageRes.name,
88 | version,
89 | time: packageRes.time[version],
90 | deprecated,
91 | recommend,
92 | }
93 |
94 | return packageInfo
95 | }
96 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | import type { Command } from 'commander'
2 | import type { CommonOption, ConfigOption, CurrentOption, GlobalOption, PackageOption } from './types'
3 | import process from 'node:process'
4 | import { Option, program } from 'commander'
5 | import { version } from '../package.json'
6 | import checkConfig from './io/config'
7 | import checkCurrent from './io/current'
8 | import checkGlobal from './io/global'
9 | import checkNode from './io/node'
10 | import checkPackage from './io/package'
11 | import { openaiModels } from './shared'
12 |
13 | export { checkCurrent, checkGlobal, checkNode, checkPackage }
14 |
15 | const registryOption = new Option('--registry ', 'specify registry URL')
16 | const gptOption = new Option('--openaiKey ', 'recommend alternative packages via ChatGPT')
17 | const gptModelOption = new Option('--openaiModel ', 'ChatGPT model').choices(openaiModels)
18 | const gptBaseURL = new Option('--openaiBaseURL ', 'override the default base URL for the API')
19 |
20 | program
21 | .version(`npm-deprecated-check ${version}`)
22 | .usage(' [options]')
23 |
24 | program
25 | .command('node')
26 | .description('check if used node version is deprecated (reached End Of Life)')
27 | .action(() => {
28 | checkNode()
29 | })
30 |
31 | program
32 | .command('current')
33 | .description('check the packages of the current project')
34 | .addOption(new Option('--ignore ', 'ignore specific packages'))
35 | .addOption(new Option('--failfast', 'exit the program if it has been deprecated'))
36 | .addOption(new Option('--deep', 'deep inspection for monorepo projects'))
37 | .addOption(registryOption)
38 | .addOption(gptOption)
39 | .addOption(gptModelOption)
40 | .addOption(gptBaseURL)
41 | .action((option: CurrentOption) => {
42 | checkNode()
43 | checkCurrent(option)
44 | })
45 |
46 | program
47 | .command('global')
48 | .description('check global packages, default: npm')
49 | .addOption(new Option('-m, --manager ', 'check specified package manager').choices(['npm', 'yarn', 'pnpm']).default('npm'))
50 | .addOption(new Option('--ignore ', 'ignore specific packages'))
51 | .addOption(new Option('--failfast', 'exit the program if it has been deprecated'))
52 | .addOption(registryOption)
53 | .addOption(gptOption)
54 | .addOption(gptModelOption)
55 | .addOption(gptBaseURL)
56 | .action((globalOption: GlobalOption) => {
57 | checkNode()
58 | checkGlobal(globalOption)
59 | })
60 |
61 | program
62 | .command('package ')
63 | .description('check for specified package')
64 | .addOption(new Option('-r, --range ', 'check specified versions'))
65 | .addOption(new Option('--failfast', 'exit the program if it has been deprecated'))
66 | .addOption(registryOption)
67 | .addOption(gptOption)
68 | .addOption(gptModelOption)
69 | .addOption(gptBaseURL)
70 | .action((packageName: string, option: { range?: string } & CommonOption) => {
71 | const packageOption: PackageOption = {
72 | packageName,
73 | ...option,
74 | }
75 | checkPackage(packageOption)
76 | })
77 |
78 | program
79 | .command('config')
80 | .description('inspect and modify the config')
81 | .addOption(new Option('-g, --get ', 'get value from option'))
82 | .addOption(new Option('-s, --set ', 'set option value'))
83 | .addOption(new Option('-d, --delete ', 'delete option from config'))
84 | .addOption(new Option('-l, --list', 'list all options'))
85 | .action((option: Record, command: Command) => {
86 | if (Object.keys(option).length === 0) {
87 | command.outputHelp()
88 | process.exit(0)
89 | }
90 |
91 | const configOption: ConfigOption = {}
92 | for (const key in option) {
93 | if (key === 'set')
94 | configOption.set = [option.set, command.args[0]]
95 | else
96 | configOption[key as keyof ConfigOption] = option[key]
97 | }
98 | checkConfig(configOption)
99 | })
100 |
101 | program.parse(process.argv)
102 |
--------------------------------------------------------------------------------
/src/filter.ts:
--------------------------------------------------------------------------------
1 | export function isLocalPackage(versionRange: string) {
2 | const localPackagePrefix = [
3 | 'link:',
4 | 'file:',
5 | 'workspace:',
6 | ]
7 | return localPackagePrefix.some(prefix => versionRange.startsWith(prefix))
8 | }
9 |
10 | export function isURLPackage(versionRange: string) {
11 | return /^https?:\/\//.test(versionRange)
12 | }
13 |
14 | export function isGitPackage(versionRange: string) {
15 | return /\.git$/.test(versionRange)
16 | }
17 |
--------------------------------------------------------------------------------
/src/io/config.ts:
--------------------------------------------------------------------------------
1 | import type { ConfigOption } from '../types'
2 | import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3 | import process from 'node:process'
4 | import { version } from '../../package.json'
5 | import { openaiModels, rcPath } from '../shared'
6 | import { error, log } from '../utils/console'
7 | import { get, set, unset } from '../utils/object'
8 |
9 | export default function configure(options: ConfigOption) {
10 | if (!existsSync(rcPath))
11 | writeFileSync(rcPath, JSON.stringify({ latestVersion: version, lastChecked: Date.now() }, null, 2), 'utf-8')
12 |
13 | let config: Record = {}
14 | try {
15 | const fileContent = readFileSync(rcPath, 'utf-8')
16 | config = JSON.parse(fileContent)
17 | }
18 | catch {}
19 |
20 | if (options.get) {
21 | const value = get(config, options.get)
22 | log(value)
23 | }
24 |
25 | if (options.set) {
26 | const [path, value] = options.set
27 |
28 | if (path === 'openaiModel' && !openaiModels.includes(value)) {
29 | error(`error: option '--openaiModel ' argument '${value}' is invalid. Allowed choices are ${openaiModels.join(', ')}.`)
30 | process.exit(1)
31 | }
32 |
33 | let formatValue: any
34 | if (!Number.isNaN(Number.parseInt(value)))
35 | formatValue = Number.parseInt(value)
36 | else if (value === 'true')
37 | formatValue = true
38 | else if (value === 'false')
39 | formatValue = false
40 | else
41 | formatValue = value
42 |
43 | set(config, path, formatValue)
44 |
45 | writeFileSync(rcPath, JSON.stringify(config, null, 2), 'utf-8')
46 | }
47 |
48 | if (options.delete) {
49 | unset(config, options.delete)
50 | writeFileSync(rcPath, JSON.stringify(config, null, 2), 'utf-8')
51 | }
52 |
53 | if (options.list)
54 | log(JSON.stringify(config, null, 2))
55 | }
56 |
--------------------------------------------------------------------------------
/src/io/current.ts:
--------------------------------------------------------------------------------
1 | import type { CurrentOption, VersionOrRange } from '../types'
2 | import { existsSync, readdirSync, statSync } from 'node:fs'
3 | import { join } from 'node:path'
4 | import process from 'node:process'
5 | import { checkDependencies } from '../check'
6 | import { isGitPackage, isLocalPackage, isURLPackage } from '../filter'
7 | import { getDependenciesOfLockfile } from '../packages/lockfiles'
8 | import { getDependenciesOfPackageJson } from '../packages/package_json'
9 | import { error, log } from '../utils/console'
10 |
11 | export default async function checkCurrent(options: CurrentOption) {
12 | const currentPath = process.cwd()
13 | const pkgPaths = options.deep ? findPackageJsonDirs(currentPath) : [currentPath]
14 | for (const pkgPath of pkgPaths) {
15 | log(`> ${pkgPath}`)
16 | await checkCurrentPackageJson(pkgPath, options)
17 | log()
18 | }
19 | }
20 |
21 | function findPackageJsonDirs(dir: string, results: Array = []) {
22 | const pkgPath = join(dir, 'package.json')
23 | if (existsSync(pkgPath)) {
24 | results.push(dir)
25 | }
26 |
27 | let files
28 | try {
29 | files = readdirSync(dir)
30 | }
31 | catch {
32 | return results
33 | }
34 |
35 | for (const file of files) {
36 | if (file === 'node_modules') {
37 | continue
38 | }
39 | const dirPath = join(dir, file)
40 | try {
41 | const stat = statSync(dirPath)
42 | if (stat.isDirectory()) {
43 | findPackageJsonDirs(dirPath, results)
44 | }
45 | }
46 | catch {
47 | }
48 | }
49 |
50 | return results
51 | }
52 |
53 | async function checkCurrentPackageJson(pkgPath: string, options: CurrentOption) {
54 | const packageJsonPath = join(pkgPath, 'package.json')
55 | const dependenciesOfPackageJson = getDependenciesOfPackageJson(packageJsonPath)
56 |
57 | if (!dependenciesOfPackageJson)
58 | return
59 |
60 | try {
61 | const ignores = options.ignore?.split(',') || []
62 |
63 | const npmDependencies: Record = {}
64 |
65 | for (const name in dependenciesOfPackageJson) {
66 | const versionInfo = dependenciesOfPackageJson[name]
67 | if (!ignores.includes(name) && !isLocalPackage(versionInfo.range as string) && !isURLPackage(versionInfo.range as string) && !isGitPackage(versionInfo.range as string)) {
68 | npmDependencies[name] = versionInfo
69 | }
70 | }
71 |
72 | const dependenciesOfLockfile = await getDependenciesOfLockfile(npmDependencies)
73 |
74 | const dependencies = Object.assign(npmDependencies, dependenciesOfLockfile)
75 |
76 | return checkDependencies(dependencies, options)
77 | }
78 | catch (e: any) {
79 | error(e.message)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/io/global.ts:
--------------------------------------------------------------------------------
1 | import type { GlobalOption } from '../types'
2 | import { checkDependencies } from '../check'
3 | import { isLocalPackage } from '../filter'
4 | import { error } from '../utils/console'
5 | import { execCommand } from '../utils/exec'
6 |
7 | const yarnRegexp = /"((?:@[a-z][a-z0-9-_.]*\/)?[a-z][a-z0-9-_.]*)@(\d+\.\d+\.\d+(?:-[a-z0-9-]+(?:\.[a-z0-9-]+)*)?)"/g
8 |
9 | export default function checkGlobal(options: GlobalOption) {
10 | const { manager, ...openaiOptions } = options
11 | try {
12 | let dependencies: Record = {}
13 | if (manager === 'pnpm') {
14 | const result = JSON.parse(execCommand('pnpm list -g --depth=0 --json'))
15 | dependencies = result
16 | .map((ele: { dependencies?: object }) => ele.dependencies)
17 | .reduce((previousValue: object, currentValue?: object) => Object.assign(previousValue, currentValue), {})
18 | }
19 | else if (manager === 'yarn') {
20 | const result = execCommand('yarn global list --depth=0')
21 | const iterator = Array.from(result.matchAll(yarnRegexp), (m: string[]) => [m[1], m[2]])
22 | for (const dependency of iterator) {
23 | const [packageName, version] = dependency
24 | dependencies[packageName] = { version }
25 | }
26 | }
27 | else {
28 | const result = JSON.parse(execCommand('npm ls -g --depth=0 --json'))
29 | dependencies = result.dependencies
30 | }
31 |
32 | const ignores = options.ignore?.split(',') || []
33 |
34 | return checkDependencies(Object.fromEntries(Object.entries(dependencies).filter(([key, { version }]) => !ignores.includes(key) && !isLocalPackage(version))), openaiOptions)
35 | }
36 | catch (e: any) {
37 | error(e.message)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/io/node.ts:
--------------------------------------------------------------------------------
1 | import process from 'node:process'
2 | import { coerce, gt, major } from 'semver'
3 | import nodeReleases from '../schedule.json' assert { type: 'json' }
4 | import { ok, warn } from '../utils/console'
5 |
6 | interface versionInfo {
7 | start: string
8 | lts?: string
9 | maintenance?: string
10 | end: string
11 | codename?: string
12 | }
13 |
14 | function getLatestNodeVersion(nodeReleases: Record) {
15 | const versions = Object.keys(nodeReleases)
16 | const latestVersion = versions.reduce((_prev, _curr) => {
17 | const prev = coerce(_prev)!
18 | const curr = coerce(_curr)!
19 | return gt(curr, prev) ? _curr : _prev
20 | })
21 | return latestVersion
22 | }
23 |
24 | function checkNode() {
25 | const nodeVersion = coerce(process.version)!
26 | const latestNodeVersion = coerce(getLatestNodeVersion(nodeReleases))!
27 | const nodeVersionData = nodeReleases[`v${major(nodeVersion)}` as keyof typeof nodeReleases]
28 | if (nodeVersionData) {
29 | const endDate = new Date(nodeVersionData.end)
30 | const currentDate = new Date()
31 | const isNodeVersionSupported = currentDate < endDate
32 | if (isNodeVersionSupported) {
33 | ok(`Your node version (${nodeVersion}) is supported until ${nodeVersionData.end}.`)
34 | }
35 | else {
36 | warn(`Your node version (${nodeVersion}) is no longer supported since ${nodeVersionData.end}.`)
37 | }
38 | }
39 | else if (gt(nodeVersion, latestNodeVersion)) {
40 | warn(`Your node version (${nodeVersion}) is higher than the latest version ${latestNodeVersion}. Please update 'npm-deprecated-check'.`)
41 | }
42 | else {
43 | warn(`Your node version (${nodeVersion}) can't be found in the release schedule. Please update 'npm-deprecated-check'.`)
44 | }
45 |
46 | return {
47 | version: nodeVersion,
48 | latestVersion: latestNodeVersion,
49 | releases: nodeReleases,
50 | }
51 | }
52 |
53 | export default checkNode
54 |
--------------------------------------------------------------------------------
/src/io/package.ts:
--------------------------------------------------------------------------------
1 | import type { PackageOption } from '../types'
2 | import { checkDependencies } from '../check'
3 |
4 | export default function checkSpecified(options: PackageOption) {
5 | const { packageName, range, ...openaiOptions } = options
6 |
7 | return checkDependencies({ [packageName]: { range } }, openaiOptions)
8 | }
9 |
--------------------------------------------------------------------------------
/src/packages/lockfiles.ts:
--------------------------------------------------------------------------------
1 | import type { VersionOrRange } from '../types'
2 | import fs from 'node:fs'
3 | import { resolve } from 'node:path'
4 | import { readWantedLockfile } from '@pnpm/lockfile-file'
5 | import { parseSyml } from '@yarnpkg/parsers'
6 |
7 | const npmLockPath = resolve('./package-lock.json')
8 | const yarnLockPath = resolve('./yarn.lock')
9 | const pnpmLockPath = resolve('./pnpm-lock.yaml')
10 |
11 | export function getDependenciesOfLockfile(packages: { [k: string]: VersionOrRange }) {
12 | const npmLock = {
13 | path: npmLockPath,
14 | read() {
15 | const lockfileContent = JSON.parse(fs.readFileSync(this.path, 'utf-8'))
16 | let dependencies = lockfileContent.dependencies
17 | let packageNamePrefix = ''
18 | if (lockfileContent.lockfileVersion > 1) {
19 | dependencies = lockfileContent.packages
20 | packageNamePrefix = 'node_modules/'
21 | }
22 | const result: Record = {}
23 | for (const packageName in packages) {
24 | const dependencyKey = packageNamePrefix + packageName
25 | if (dependencies[dependencyKey])
26 | result[packageName] = { version: dependencies[dependencyKey].version }
27 | }
28 |
29 | return result
30 | },
31 | }
32 | const yarnLock = {
33 | path: yarnLockPath,
34 | read() {
35 | const content = fs.readFileSync(this.path, 'utf-8')
36 | const json = parseSyml(content)
37 | const result: Record = {}
38 | for (const packageName in packages)
39 | json[`${packageName}@${packages[packageName].range}`] && (result[packageName] = { version: json[`${packageName}@${packages[packageName].range}`].version })
40 |
41 | return result
42 | },
43 | }
44 | const pnpmLock = {
45 | path: pnpmLockPath,
46 | async read() {
47 | const content = await readWantedLockfile(resolve(this.path, '..'), { ignoreIncompatible: false })
48 | if (content && content.packages) {
49 | const packageNames = Object.keys(packages)
50 | const result: Record = {}
51 | for (const depPath in content.packages) {
52 | const info = (content.packages as Record)[depPath]
53 | packageNames.includes(info.name as string) && (result[info.name as string] = { version: info.version })
54 | }
55 | return result
56 | }
57 | else {
58 | return {}
59 | }
60 | },
61 | }
62 |
63 | const existsLock = [npmLock, yarnLock, pnpmLock]
64 | .filter(ele => fs.existsSync(ele.path))
65 | .sort((a, b) => fs.lstatSync(b.path).mtimeMs - fs.lstatSync(a.path).mtimeMs)
66 |
67 | return existsLock.length > 0 ? existsLock[0].read() : {}
68 | }
69 |
--------------------------------------------------------------------------------
/src/packages/package_json.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync } from 'node:fs'
2 | import { error } from '../utils/console'
3 | import { formatDependencies } from '../utils/format'
4 |
5 | export function getDependenciesOfPackageJson(packageJsonPath: string) {
6 | if (!existsSync(packageJsonPath))
7 | return error('package.json does not exist in the current path, please execute it under the correct project path.')
8 |
9 | const packageJsonContent = readFileSync(packageJsonPath, 'utf-8')
10 | const { dependencies, devDependencies } = JSON.parse(packageJsonContent)
11 |
12 | return {
13 | ...formatDependencies(dependencies),
14 | ...formatDependencies(devDependencies),
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/schedule.json:
--------------------------------------------------------------------------------
1 | {
2 | "v0.8": {
3 | "start": "2012-06-25",
4 | "end": "2014-07-31"
5 | },
6 | "v0.10": {
7 | "start": "2013-03-11",
8 | "end": "2016-10-31"
9 | },
10 | "v0.12": {
11 | "start": "2015-02-06",
12 | "end": "2016-12-31"
13 | },
14 | "v4": {
15 | "start": "2015-09-08",
16 | "lts": "2015-10-12",
17 | "maintenance": "2017-04-01",
18 | "end": "2018-04-30",
19 | "codename": "Argon"
20 | },
21 | "v5": {
22 | "start": "2015-10-29",
23 | "maintenance": "2016-04-30",
24 | "end": "2016-06-30"
25 | },
26 | "v6": {
27 | "start": "2016-04-26",
28 | "lts": "2016-10-18",
29 | "maintenance": "2018-04-30",
30 | "end": "2019-04-30",
31 | "codename": "Boron"
32 | },
33 | "v7": {
34 | "start": "2016-10-25",
35 | "maintenance": "2017-04-30",
36 | "end": "2017-06-30"
37 | },
38 | "v8": {
39 | "start": "2017-05-30",
40 | "lts": "2017-10-31",
41 | "maintenance": "2019-01-01",
42 | "end": "2019-12-31",
43 | "codename": "Carbon"
44 | },
45 | "v9": {
46 | "start": "2017-10-01",
47 | "maintenance": "2018-04-01",
48 | "end": "2018-06-30"
49 | },
50 | "v10": {
51 | "start": "2018-04-24",
52 | "lts": "2018-10-30",
53 | "maintenance": "2020-05-19",
54 | "end": "2021-04-30",
55 | "codename": "Dubnium"
56 | },
57 | "v11": {
58 | "start": "2018-10-23",
59 | "maintenance": "2019-04-22",
60 | "end": "2019-06-01"
61 | },
62 | "v12": {
63 | "start": "2019-04-23",
64 | "lts": "2019-10-21",
65 | "maintenance": "2020-11-30",
66 | "end": "2022-04-30",
67 | "codename": "Erbium"
68 | },
69 | "v13": {
70 | "start": "2019-10-22",
71 | "maintenance": "2020-04-01",
72 | "end": "2020-06-01"
73 | },
74 | "v14": {
75 | "start": "2020-04-21",
76 | "lts": "2020-10-27",
77 | "maintenance": "2021-10-19",
78 | "end": "2023-04-30",
79 | "codename": "Fermium"
80 | },
81 | "v15": {
82 | "start": "2020-10-20",
83 | "maintenance": "2021-04-01",
84 | "end": "2021-06-01"
85 | },
86 | "v16": {
87 | "start": "2021-04-20",
88 | "lts": "2021-10-26",
89 | "maintenance": "2022-10-18",
90 | "end": "2023-09-11",
91 | "codename": "Gallium"
92 | },
93 | "v17": {
94 | "start": "2021-10-19",
95 | "maintenance": "2022-04-01",
96 | "end": "2022-06-01"
97 | },
98 | "v18": {
99 | "start": "2022-04-19",
100 | "lts": "2022-10-25",
101 | "maintenance": "2023-10-18",
102 | "end": "2025-04-30",
103 | "codename": "Hydrogen"
104 | },
105 | "v19": {
106 | "start": "2022-10-18",
107 | "maintenance": "2023-04-01",
108 | "end": "2023-06-01"
109 | },
110 | "v20": {
111 | "start": "2023-04-18",
112 | "lts": "2023-10-24",
113 | "maintenance": "2024-10-22",
114 | "end": "2026-04-30",
115 | "codename": "Iron"
116 | },
117 | "v21": {
118 | "start": "2023-10-17",
119 | "maintenance": "2024-04-01",
120 | "end": "2024-06-01"
121 | },
122 | "v22": {
123 | "start": "2024-04-24",
124 | "lts": "2024-10-29",
125 | "maintenance": "2025-10-21",
126 | "end": "2027-04-30",
127 | "codename": "Jod"
128 | },
129 | "v23": {
130 | "start": "2024-10-16",
131 | "maintenance": "2025-04-01",
132 | "end": "2025-06-01"
133 | },
134 | "v24": {
135 | "start": "2025-05-06",
136 | "lts": "2025-10-28",
137 | "maintenance": "2026-10-20",
138 | "end": "2028-04-30",
139 | "codename": ""
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/shared.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'node:fs'
2 | import os from 'node:os'
3 | import path from 'node:path'
4 |
5 | const homedir = os.homedir()
6 |
7 | export const rcPath = path.resolve(homedir, '.ndcrc')
8 |
9 | export function getGlobalConfig() {
10 | try {
11 | const data = readFileSync(rcPath, 'utf-8')
12 | return JSON.parse(data) || {}
13 | }
14 | catch {
15 | return {}
16 | }
17 | }
18 |
19 | export const openaiModels = ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o-mini', 'gpt-4o']
20 |
21 | export const openaiBaseURL = 'https://api.openai.com/v1'
22 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface OpenaiOption {
2 | openaiKey?: string
3 | openaiModel?: string
4 | openaiBaseURL?: string
5 | }
6 |
7 | export interface CommonOption extends OpenaiOption {
8 | registry: string
9 | failfast: boolean
10 | }
11 |
12 | export interface CurrentOption extends CommonOption {
13 | ignore: string
14 | deep: boolean
15 | }
16 |
17 | export interface GlobalOption extends CommonOption {
18 | manager: string
19 | ignore: string
20 | }
21 |
22 | export interface PackageOption extends CommonOption {
23 | packageName: string
24 | range?: string
25 | }
26 |
27 | export interface ConfigOption {
28 | get?: string
29 | set?: Array
30 | delete?: string
31 | list?: boolean
32 | }
33 |
34 | export interface PackageInfo {
35 | name: string
36 | version?: string
37 | time?: string
38 | deprecated?: string | undefined
39 | recommend?: Array | string | null
40 | error?: string
41 | }
42 |
43 | export interface PackageVersions {
44 | 'name': string
45 | 'time': Record
46 | 'dist-tags': Record
47 | 'versions': {
48 | [version: string]: {
49 | name: string
50 | version: string
51 | deprecated?: string
52 | }
53 | }
54 | }
55 |
56 | export interface VersionOrRange {
57 | version?: string
58 | range?: string
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils/console.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import ansis from 'ansis'
3 |
4 | export function error(text?: string) {
5 | console.error(`${ansis.bgRed(' ERROR ')} ${ansis.red(text ?? '')}`)
6 | }
7 |
8 | export function log(text?: string) {
9 | console.log(text ?? '')
10 | }
11 |
12 | export function ok(text?: string) {
13 | console.log(`${ansis.bgGreen(' OK ')} ${text ?? ''}`)
14 | }
15 |
16 | export function warn(text?: string) {
17 | console.warn(`${ansis.bgYellowBright(' WARN ')} ${ansis.yellow(text ?? '')}`)
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/exec.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process'
2 |
3 | export function execCommand(command: string) {
4 | return execSync(command).toString()
5 | }
6 |
7 | let registry = ''
8 | export function getRegistry() {
9 | if (registry)
10 | return registry
11 |
12 | try {
13 | registry = execCommand('npm config get registry').trim()
14 | }
15 | catch {
16 | registry = 'https://registry.npmjs.org/'
17 | }
18 |
19 | return registry
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | import type { VersionOrRange } from '../types'
2 |
3 | export function formatDependencies(dependencies: Record): Record {
4 | const newDependencies: Record = {}
5 | for (const packageName in dependencies) {
6 | if (dependencies[packageName].includes('@')) {
7 | const idx = dependencies[packageName].lastIndexOf('@')
8 | dependencies[packageName] = dependencies[packageName].slice(idx + 1)
9 | }
10 | newDependencies[packageName] = {
11 | range: dependencies[packageName],
12 | }
13 | }
14 | return newDependencies
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/object.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-shared-utils/lib/object.js
2 | export function set(target: Record, path: string, value: any) {
3 | const fields = path.split('.')
4 | let obj = target
5 | const l = fields.length
6 | for (let i = 0; i < l - 1; i++) {
7 | const key = fields[i]
8 | if (!obj[key])
9 | obj[key] = {}
10 |
11 | obj = obj[key]
12 | }
13 | obj[fields[l - 1]] = value
14 | }
15 |
16 | export function get(target: Record, path: string) {
17 | const fields = path.split('.')
18 | let obj = target
19 | const l = fields.length
20 | for (let i = 0; i < l - 1; i++) {
21 | const key = fields[i]
22 | if (!obj[key])
23 | return undefined
24 |
25 | obj = obj[key]
26 | }
27 | return obj[fields[l - 1]]
28 | }
29 |
30 | export function unset(target: Record, path: string) {
31 | const fields = path.split('.')
32 | let obj = target
33 | const l = fields.length
34 | const objs = []
35 | for (let i = 0; i < l - 1; i++) {
36 | const key = fields[i]
37 | if (!obj[key])
38 | return
39 |
40 | objs.unshift({ parent: obj, key, value: obj[key] })
41 | obj = obj[key]
42 | }
43 | delete obj[fields[l - 1]]
44 | // Clear empty objects
45 | for (const { parent, key, value } of objs) {
46 | if (!Object.keys(value).length)
47 | delete parent[key]
48 | }
49 | }
50 |
51 | export function safeJSON(text: string) {
52 | try {
53 | return JSON.parse(text)
54 | }
55 | catch {
56 | return undefined
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/utils/spinner.ts:
--------------------------------------------------------------------------------
1 | import yoctoSpinner from 'yocto-spinner'
2 |
3 | const spinner = yoctoSpinner({ text: 'Checking…' })
4 | let timer: NodeJS.Timeout
5 |
6 | export function startSpinner() {
7 | spinner.color = 'green'
8 | spinner.start()
9 | timer = setTimeout(() => {
10 | spinner.color = 'yellow'
11 |
12 | timer = setTimeout(() => {
13 | spinner.color = 'red'
14 | }, 30000)
15 | }, 30000)
16 | }
17 |
18 | export function stopSpinner() {
19 | clearTimeout(timer)
20 | spinner.stop()
21 | }
22 |
--------------------------------------------------------------------------------
/test/config.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec } from 'node:child_process'
3 | import path from 'node:path'
4 | import { test } from 'node:test'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
10 |
11 | test('config tests', async (t) => {
12 | await t.test('check config', (_t, done) => {
13 | exec(`node ${cli} config`, (_error, stdout, _stderr) => {
14 | assert.ok(/inspect and modify the config/.test(stdout), 'Expected "inspect and modify the config" to be mentioned in config list.')
15 | done()
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/test/current.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec, execSync } from 'node:child_process'
3 | import fs from 'node:fs'
4 | import path from 'node:path'
5 | import { test } from 'node:test'
6 | import { fileURLToPath } from 'node:url'
7 |
8 | const __filename = fileURLToPath(import.meta.url)
9 | const __dirname = path.dirname(__filename)
10 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
11 |
12 | const managers = ['npm', 'yarn', 'pnpm']
13 | const cases = ['deprecated', 'normal']
14 | const playgroundDir = path.join(__dirname, 'playground')
15 |
16 | async function check(manager, t) {
17 | const normalDir = path.join(playgroundDir, manager, 'normal')
18 | const deprecatedDir = path.join(playgroundDir, manager, 'deprecated')
19 |
20 | await t.test(`check ${manager} that no deprecation warning is shown`, (_t, done) => {
21 | exec(`cd ${normalDir} && node ${cli} current`, (_error, _stdout, stderr) => {
22 | assert.ok(!/deprecated/.test(stderr), 'Not expected "deprecated" to be mentioned in deprecation warning.')
23 | done()
24 | })
25 | })
26 |
27 | await t.test(`check ${manager} that deprecation warning is shown if deprecated package is installed`, (_t, done) => {
28 | exec(`cd ${deprecatedDir} && node ${cli} current`, { timeout: 160000 }, (_error, _stdout, stderr) => {
29 | assert.ok(/deprecated/.test(stderr), 'Expected "deprecated" to be mentioned in deprecation warning.')
30 | done()
31 | })
32 | })
33 |
34 | await t.test(`check ${manager} that no deprecation warning is shown if ignore deprecated package`, (_t, done) => {
35 | exec(`cd ${deprecatedDir} && node ${cli} current --ignore request,tslint`, { timeout: 160000 }, (_error, _stdout, stderr) => {
36 | assert.ok(!/deprecated/.test(stderr), 'Not expected "deprecated" to be mentioned in deprecation warning.')
37 | done()
38 | })
39 | })
40 |
41 | await t.test(`check ${manager} that exit the program if the package is deprecated`, (_t, done) => {
42 | exec(`cd ${deprecatedDir} && node ${cli} current --failfast`, { timeout: 160000 }, (error, _stdout, stderr) => {
43 | // eslint-disable-next-line no-control-regex
44 | assert.ok(error.code === 1 && (stderr.match(/^\u001B\[33mdeprecated:/gm) || []).length === 1, 'Expected "WARN" to be mentioned once in deprecation warning, and process.exit(1).')
45 | done()
46 | })
47 | })
48 | }
49 |
50 | test('current tests', async (t) => {
51 | await Promise.all(
52 | managers.map(async (manager) => {
53 | cases.forEach((caseName) => {
54 | const caseDir = path.join(playgroundDir, manager, caseName)
55 |
56 | fs.mkdirSync(caseDir, { recursive: true })
57 |
58 | const srcFile = path.join(__dirname, 'examples', `${caseName}.json`)
59 | const destFile = path.join(caseDir, 'package.json')
60 | fs.copyFileSync(srcFile, destFile)
61 | execSync(`${manager} install --quiet`, { cwd: caseDir })
62 | })
63 |
64 | await check(manager, t)
65 | }),
66 | ).then(async () => {
67 | await t.test(`deep inspection of deprecated dependencies`, (_t, done) => {
68 | exec(`cd ${playgroundDir} && node ${cli} current --deep`, { timeout: 160000 }, (_error, _stdout, stderr) => {
69 | // eslint-disable-next-line no-control-regex
70 | assert.ok((stderr.match(/^\u001B\[33mdeprecated:/gm) || []).length === 6, 'Expected "WARN" to be mentioned six times in deprecation warning).')
71 | done()
72 | })
73 | })
74 | }).finally(() => {
75 | fs.rmSync(playgroundDir, { recursive: true, force: true })
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/test/examples/deprecated.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deprecated",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "dependencies": {
6 | "eslint": "^9.25.1",
7 | "request": "^2.88.2",
8 | "tslint": "^6.1.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/examples/normal.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deprecated",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "dependencies": {
6 | "eslint": "^9.25.1"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/global.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec, execSync } from 'node:child_process'
3 | import path from 'node:path'
4 | import { test } from 'node:test'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
10 |
11 | const managers = ['npm', 'yarn', 'pnpm']
12 | const installCommands = {
13 | npm: 'npm i -g --force',
14 | yarn: 'yarn global add --force',
15 | pnpm: 'pnpm add -g --force',
16 | }
17 | const uninstallCommands = {
18 | npm: 'npm un -g',
19 | yarn: 'yarn global remove',
20 | pnpm: 'pnpm remove -g',
21 | }
22 |
23 | async function check(manager, t) {
24 | try {
25 | execSync(`${installCommands[manager]} eslint`)
26 |
27 | await t.test(`check ${manager} that no deprecation warning is shown`, (_t, done) => {
28 | exec(`node ${cli} global --manager ${manager}`, (_error, _stdout, stderr) => {
29 | assert.ok(!/deprecated/.test(stderr), 'Not expected "deprecated" to be mentioned in deprecation warning.')
30 | done()
31 | })
32 | })
33 |
34 | execSync(`${installCommands[manager]} tslint vue-cli`)
35 |
36 | await t.test(`check ${manager} that deprecation warning is shown if deprecated package is installed`, (_t, done) => {
37 | exec(`node ${cli} global --manager ${manager}`, { timeout: 160000 }, (_error, _stdout, stderr) => {
38 | assert.ok(/deprecated/.test(stderr), 'Expected "deprecated" to be mentioned in deprecation warning.')
39 | done()
40 | })
41 | })
42 |
43 | await t.test(`check ${manager} that no deprecation warning is shown if ignore deprecated package`, (_t, done) => {
44 | exec(`node ${cli} global --manager ${manager} --ignore tslint,vue-cli`, { timeout: 160000 }, (_error, _stdout, stderr) => {
45 | assert.ok(!/deprecated/.test(stderr), 'Not expected "deprecated" to be mentioned in deprecation warning.')
46 | done()
47 | })
48 | })
49 |
50 | await t.test(`check ${manager} that exit the program if the package is deprecated`, (_t, done) => {
51 | exec(`node ${cli} global --manager ${manager} --failfast`, { timeout: 160000 }, (error, _stdout, stderr) => {
52 | // eslint-disable-next-line no-control-regex
53 | assert.ok(error.code === 1 && (stderr.match(/^\u001B\[33mdeprecated:/gm) || []).length === 1, 'Expected "WARN" to be mentioned once in deprecation warning, and process.exit(1).')
54 | done()
55 | })
56 | })
57 | }
58 | finally {
59 | ['eslint', 'tslint', 'vue-cli'].forEach((dep) => {
60 | try {
61 | execSync(`${uninstallCommands[manager]} ${dep}`)
62 | }
63 | catch {}
64 | })
65 | }
66 | }
67 |
68 | test('global tests', async (t) => {
69 | await Promise.all(managers.map(async (manager) => {
70 | await check(manager, t)
71 | }))
72 | })
73 |
--------------------------------------------------------------------------------
/test/help.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec } from 'node:child_process'
3 | import path from 'node:path'
4 | import { test } from 'node:test'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
10 |
11 | test('help tests', async (t) => {
12 | await t.test('check help', (_t, done) => {
13 | exec(`node ${cli} help`, (_error, stdout, _stderr) => {
14 | assert.ok(/display help for command/.test(stdout), 'Expected "display help for command" to be mentioned in help.')
15 | done()
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/test/node.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec } from 'node:child_process'
3 | import path from 'node:path'
4 | import { test } from 'node:test'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
10 |
11 | test('node tests', async (t) => {
12 | await t.test('test node version deprecation check', (_t, done) => {
13 | exec(`node ${cli} node`, (_error, stdout, stderr) => {
14 | assert.ok(/node version/.test(stdout) || /node version/.test(stderr), 'Expected "node version" to be mentioned in output.')
15 | done()
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/test/package.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { exec } from 'node:child_process'
3 | import path from 'node:path'
4 | import { test } from 'node:test'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 | const cli = path.resolve(__dirname, '../dist/cli.mjs')
10 |
11 | test('package tests', async (t) => {
12 | await t.test('check that a deprecated package is detected', (_t, done) => {
13 | exec(`node ${cli} package request`, (_error, _stdout, stderr) => {
14 | assert.ok(/has been deprecated/.test(stderr), 'Expected "has been deprecated" to be mentioned in deprecation warning.')
15 | done()
16 | })
17 | })
18 |
19 | await t.test('check that a non-deprecated package is not detected as deprecated', (_t, done) => {
20 | exec(`node ${cli} package eslint`, (_error, _stdout, stderr) => {
21 | assert.ok(!/has been deprecated/.test(stderr), 'Not expected "has been deprecated" to be mentioned in deprecation warning.')
22 | done()
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "lib": ["esnext"],
5 | "rootDir": ".",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "resolveJsonModule": true,
9 | "strict": true,
10 | "strictNullChecks": true,
11 | "esModuleInterop": true,
12 | "skipLibCheck": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------