├── .prettierignore ├── .npmrc ├── test ├── e2e │ ├── cjs │ │ ├── package.json │ │ └── index.js │ └── esm │ │ ├── package.json │ │ └── index.js ├── test-data │ ├── doctor │ │ ├── nopackagefile │ │ │ └── nil │ │ ├── customtest2 │ │ │ ├── echo.js │ │ │ └── package.json │ │ ├── notestscript │ │ │ └── package.json │ │ ├── pass │ │ │ ├── package.json │ │ │ └── README.md │ │ ├── nolockfile │ │ │ └── package.json │ │ ├── customtest │ │ │ └── package.json │ │ ├── options │ │ │ ├── package.json │ │ │ └── test.js │ │ ├── custominstall │ │ │ └── package.json │ │ └── fail │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ └── test.js │ ├── registry.json │ ├── basic │ │ └── package.json │ ├── deep-ncurc │ │ ├── .ncurc.js │ │ ├── pkg │ │ │ ├── sub1 │ │ │ │ ├── .ncurc.js │ │ │ │ └── package.json │ │ │ ├── sub2 │ │ │ │ ├── .ncurc.js │ │ │ │ ├── sub21 │ │ │ │ │ ├── .ncurc.js │ │ │ │ │ └── package.json │ │ │ │ ├── package.json │ │ │ │ └── sub22 │ │ │ │ │ └── package.json │ │ │ └── sub3 │ │ │ │ ├── sub31 │ │ │ │ ├── .ncurc.js │ │ │ │ └── package.json │ │ │ │ ├── package.json │ │ │ │ └── sub32 │ │ │ │ └── package.json │ │ └── package.json │ ├── ncu │ │ ├── package.json │ │ └── package2.json │ ├── workspace-basic │ │ ├── pkg │ │ │ └── sub │ │ │ │ └── package.json │ │ └── package.json │ ├── workspace-sub-package-names │ │ ├── pkg │ │ │ ├── dirname-will-become-name │ │ │ │ └── package.json │ │ │ ├── unlisted │ │ │ │ └── package.json │ │ │ ├── dirname-matches-name │ │ │ │ └── package.json │ │ │ └── dirname-does-not-match-name │ │ │ │ └── package.json │ │ └── package.json │ ├── peer-post-upgrade-no-upgrades │ │ └── package.json │ ├── workspace-workspace-param-is-array │ │ └── package.json │ ├── workspace-no-sub-packages │ │ └── package.json │ └── peer-post-upgrade │ │ └── package.json ├── bun │ ├── bun.lockb │ ├── package.json │ └── index.test.ts ├── package-managers │ ├── npm │ │ ├── package.json │ │ └── index.test.ts │ ├── yarn │ │ ├── default │ │ │ └── package.json │ │ ├── nolockfile │ │ │ └── package.json │ │ └── index.test.ts │ └── deno │ │ └── index.test.ts ├── getInstalledPackages.test.ts ├── cli-options.test.ts ├── bun-install.sh ├── helpers │ ├── chaiSetup.ts │ └── stubVersions.ts ├── global.test.ts ├── getEnginesNodeFromRegistry.test.ts ├── isUpgradeable.test.ts ├── timeout.test.ts ├── getPeerDependenciesFromRegistry.test.ts ├── enginesNode.test.ts ├── getIgnoredUpgradesDueToEnginesNode.test.ts ├── filterResults.test.ts ├── getPreferredWildcard.test.ts ├── github-urls.test.ts ├── registryType.test.ts ├── upgradeDependencies.test.ts ├── getRepoUrl.test.ts ├── e2e.sh ├── rejectVersion.ts ├── getIgnoredUpgradesDueToPeerDeps.test.ts ├── cache.test.ts ├── determinePackageManager.test.ts └── filterVersion.test.ts ├── .gitattributes ├── .vscode └── settings.json ├── .github ├── screenshot.png ├── workflows │ ├── lint.yml │ ├── codeql.yml │ └── test.yml ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── src ├── types │ ├── libnpmconfig.d.ts │ ├── prompts-ncu.d.ts │ ├── UpgradeGroup.ts │ ├── Maybe.ts │ ├── VersionLevel.ts │ ├── StaticRegistry.ts │ ├── Version.ts │ ├── PackageManagerName.ts │ ├── NpmConfig.ts │ ├── NpmOptions.ts │ ├── PackageFileRepository.ts │ ├── VersionSpec.ts │ ├── IndexType.ts │ ├── SpawnPleaseOptions.ts │ ├── ExtendedHelp.ts │ ├── SpawnOptions.ts │ ├── FilterFunction.ts │ ├── FilterPattern.ts │ ├── TargetFunction.ts │ ├── PackageInfo.ts │ ├── VersionResult.ts │ ├── Cacher.ts │ ├── IgnoredUpgradeDueToEnginesNode.ts │ ├── IgnoredUpgradeDueToPeerDeps.ts │ ├── FilterResultsFunction.ts │ ├── MockedVersions.ts │ ├── GetVersion.ts │ ├── GroupFunction.ts │ ├── RcOptions.ts │ ├── CLIOption.ts │ ├── Packument.ts │ ├── Target.ts │ ├── Options.ts │ ├── PackageFile.ts │ └── PackageManager.ts ├── lib │ ├── exists.ts │ ├── filterObject.ts │ ├── sortBy.ts │ ├── programError.ts │ ├── libnpmconfig │ │ ├── LICENSE │ │ ├── README.md │ │ └── index.js │ ├── figgy-pudding │ │ ├── LICENSE.md │ │ └── index.js │ ├── loadPackageInfoFromFile.ts │ ├── pick.ts │ ├── mergeOptions.ts │ ├── resolveDepSections.ts │ ├── getPackageVersion.ts │ ├── table.ts │ ├── getPackageManager.ts │ ├── determinePackageManager.ts │ ├── getPackageJson.ts │ ├── getPreferredWildcard.ts │ ├── wrap.ts │ ├── getEnginesNodeFromRegistry.ts │ ├── getInstalledPackages.ts │ ├── keyValueBy.ts │ ├── findLockfile.ts │ ├── isUpgradeable.ts │ ├── upgradePackageData.ts │ ├── chalk.ts │ ├── getIgnoredUpgradesDueToEnginesNode.ts │ ├── getRepoUrl.ts │ ├── getPeerDependenciesFromRegistry.ts │ ├── getNcuRc.ts │ ├── getCurrentDependencies.ts │ ├── runGlobal.ts │ ├── findPackage.ts │ ├── cache.ts │ ├── getIgnoredUpgradesDueToPeerDeps.ts │ ├── filterAndReject.ts │ └── upgradeDependencies.ts ├── scripts │ ├── install-hooks │ └── build-options.ts └── package-managers │ ├── index.ts │ ├── README.md │ ├── staticRegistry.ts │ ├── bun.ts │ ├── filters.ts │ ├── pnpm.ts │ └── gitTags.ts ├── tea.yaml ├── Dockerfile ├── .hooks ├── pre-push └── post-commit ├── .editorconfig ├── .gitignore ├── deploy.md ├── .prettierrc.json ├── .markdownlint.js ├── .ncurc.js ├── LICENSE ├── vite.config.mts ├── tsconfig.json └── .eslintrc.js /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | script-shell = bash -------------------------------------------------------------------------------- /test/e2e/cjs/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/test-data/doctor/nopackagefile/nil: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/e2e/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /test/test-data/doctor/customtest2/echo.js: -------------------------------------------------------------------------------- 1 | console.log(process.argv) 2 | -------------------------------------------------------------------------------- /test/test-data/registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "ncu-test-v2": "99.9.9" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode" 3 | } 4 | -------------------------------------------------------------------------------- /test/bun/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edlerd/npm-check-updates/main/test/bun/bun.lockb -------------------------------------------------------------------------------- /test/test-data/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: ['cute-animals'], 3 | } 4 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub1/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: ['fp-and-or'], 3 | } 4 | -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edlerd/npm-check-updates/main/.github/screenshot.png -------------------------------------------------------------------------------- /src/types/libnpmconfig.d.ts: -------------------------------------------------------------------------------- 1 | // add to tsconfig compilerOptions.paths 2 | declare module 'libnpmconfig' 3 | -------------------------------------------------------------------------------- /src/types/prompts-ncu.d.ts: -------------------------------------------------------------------------------- 1 | // add to tsconfig compilerOptions.paths 2 | declare module 'prompts-ncu' 3 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub2/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: ['cute-animals'], 3 | } 4 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub2/sub21/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: ['fp-and-or'], 3 | } 4 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub3/sub31/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: ['fp-and-or'], 3 | } 4 | -------------------------------------------------------------------------------- /src/types/UpgradeGroup.ts: -------------------------------------------------------------------------------- 1 | export type UpgradeGroup = 'major' | 'minor' | 'patch' | 'majorVersionZero' | 'none' 2 | -------------------------------------------------------------------------------- /src/types/Maybe.ts: -------------------------------------------------------------------------------- 1 | /** A value that may be null or undefined. */ 2 | export type Maybe = T | null | undefined 3 | -------------------------------------------------------------------------------- /test/bun/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "^1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/test-data/ncu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "express": "^1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/types/VersionLevel.ts: -------------------------------------------------------------------------------- 1 | /** The three main parts of a SemVer version. */ 2 | export type VersionLevel = 'major' | 'minor' | 'patch' 3 | -------------------------------------------------------------------------------- /test/package-managers/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "express": "^1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/types/StaticRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Version } from './Version' 2 | 3 | export type StaticRegistry = { 4 | [key: string]: Version 5 | } 6 | -------------------------------------------------------------------------------- /test/package-managers/yarn/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "chalk": "^3.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/test-data/doctor/notestscript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "express": "^1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/package-managers/yarn/nolockfile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "chalk": "^3.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/test-data/workspace-basic/pkg/sub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-sub-package", 3 | "license": "MIT", 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /test/test-data/workspace-sub-package-names/pkg/dirname-will-become-name/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x333377f49cBD5396E27f750C9413b5D758c705C2' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | RUN npm install -g npm-check-updates 4 | 5 | WORKDIR /app 6 | 7 | ENTRYPOINT ["npm-check-updates"] 8 | -------------------------------------------------------------------------------- /src/types/Version.ts: -------------------------------------------------------------------------------- 1 | /** An exact version number or value. Includes SemVer and some nonstandard variants for convenience. */ 2 | export type Version = string 3 | -------------------------------------------------------------------------------- /src/types/PackageManagerName.ts: -------------------------------------------------------------------------------- 1 | /** A valid package manager. */ 2 | export type PackageManagerName = 'npm' | 'yarn' | 'pnpm' | 'deno' | 'bun' | 'staticRegistry' 3 | -------------------------------------------------------------------------------- /test/test-data/ncu/package2.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "lodash.map": "2.0.0", 5 | "lodash.filter": "2.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "cute-animals": "^0.1.0", 5 | "fp-and-or": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/workspace-sub-package-names/pkg/unlisted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "this-should-never-be-listed", 3 | "license": "MIT", 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /.hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | fail=0 3 | 4 | npm run lint || fail=1 5 | npm run prettier -- --check || fail=1 6 | 7 | if [ "$fail" -ne 0 ]; then 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /src/types/NpmConfig.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | 3 | export type NpmConfig = Index string | { ca: string[] } | undefined)> 4 | -------------------------------------------------------------------------------- /src/types/NpmOptions.ts: -------------------------------------------------------------------------------- 1 | /** Options that can be provided to npm. */ 2 | export interface NpmOptions { 3 | global?: boolean 4 | prefix?: string 5 | registry?: string 6 | } 7 | -------------------------------------------------------------------------------- /test/test-data/workspace-sub-package-names/pkg/dirname-matches-name/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dirname-matches-name", 3 | "license": "MIT", 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /src/types/PackageFileRepository.ts: -------------------------------------------------------------------------------- 1 | /** Represents the repository field in package.json. */ 2 | export interface PackageFileRepository { 3 | url: string 4 | directory?: string 5 | } 6 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-v2": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-v2": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/workspace-sub-package-names/pkg/dirname-does-not-match-name/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-different-name-to-dirname", 3 | "license": "MIT", 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /src/types/VersionSpec.ts: -------------------------------------------------------------------------------- 1 | /** A version specification or range supported as a value in package.json dependencies. Includes SemVer, git urls, npm urls, etc. */ 2 | export type VersionSpec = string 3 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub2/sub22/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-v2": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub3/sub32/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-v2": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/IndexType.ts: -------------------------------------------------------------------------------- 1 | // This file cannot be named Index.ts as it conflicts with default directory import. 2 | 3 | /** A very generic object. */ 4 | export type Index = Record 5 | -------------------------------------------------------------------------------- /src/types/SpawnPleaseOptions.ts: -------------------------------------------------------------------------------- 1 | export interface SpawnPleaseOptions { 2 | rejectOnError?: boolean 3 | stdin?: string 4 | stdout?: (s: string) => void 5 | stderr?: (s: string) => void 6 | } 7 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-return-version": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/ExtendedHelp.ts: -------------------------------------------------------------------------------- 1 | /** A function that renders extended help for an option. */ 2 | type ExtendedHelp = string | ((options: { markdown?: boolean }) => string) 3 | 4 | export default ExtendedHelp 5 | -------------------------------------------------------------------------------- /src/types/SpawnOptions.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | 3 | /** Options to the spawn node built-in. */ 4 | export interface SpawnOptions { 5 | cwd?: string 6 | env?: Index 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub2/sub21/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-return-version": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/deep-ncurc/pkg/sub3/sub31/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cute-animals": "^0.1.0", 4 | "fp-and-or": "^0.1.0", 5 | "ncu-test-return-version": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test-data/doctor/pass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "~1.0.0" 5 | }, 6 | "scripts": { 7 | "test": "echo Success" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-data/doctor/nolockfile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "express": "^1.0.0" 5 | }, 6 | "scripts": { 7 | "test": "echo 'No test'" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-data/doctor/customtest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "~1.0.0" 5 | }, 6 | "scripts": { 7 | "mytest": "echo Success" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-data/doctor/customtest2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "~1.0.0" 5 | }, 6 | "scripts": { 7 | "mytest": "echo Success" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/FilterFunction.ts: -------------------------------------------------------------------------------- 1 | import { SemVer } from 'semver-utils' 2 | 3 | /** Supported function for the --filter and --reject options. */ 4 | export type FilterFunction = (packageName: string, versionRange: SemVer[]) => boolean 5 | -------------------------------------------------------------------------------- /src/types/FilterPattern.ts: -------------------------------------------------------------------------------- 1 | import { FilterFunction } from './FilterFunction' 2 | 3 | /** Supported patterns for the --filter and --reject options. */ 4 | export type FilterPattern = string | RegExp | (string | RegExp)[] | FilterFunction 5 | -------------------------------------------------------------------------------- /test/test-data/peer-post-upgrade-no-upgrades/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "eslint": "8.57.0", 5 | "eslint-plugin-import": "2.29.1", 6 | "eslint-plugin-unused-imports": "3.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/test-data/workspace-workspace-param-is-array/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-workspace-no-sub-packages", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "0.0.0", 6 | "dependencies": {}, 7 | "workspaces": [] 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/exists.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | 3 | /** Returns true if a file exists. */ 4 | const exists = (path: string) => 5 | fs.stat(path).then( 6 | () => true, 7 | () => false, 8 | ) 9 | 10 | export default exists 11 | -------------------------------------------------------------------------------- /src/types/TargetFunction.ts: -------------------------------------------------------------------------------- 1 | import { SemVer } from 'semver-utils' 2 | 3 | /** A function that can be provided to the --target option for custom filtering. */ 4 | export type TargetFunction = (packageName: string, versionRange: SemVer[]) => string 5 | -------------------------------------------------------------------------------- /test/test-data/doctor/options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "~1.0.0", 5 | "ncu-test-return-version": "~1.0.0" 6 | }, 7 | "scripts": { 8 | "test": "node test.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/test-data/doctor/custominstall/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "ncu-test-v2": "~1.0.0" 5 | }, 6 | "scripts": { 7 | "myinstall": "echo 'Install Success'", 8 | "test": "echo 'Test Success'" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/test-data/workspace-no-sub-packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-workspace-no-sub-packages", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "0.0.0", 6 | "dependencies": {}, 7 | "workspaces": { 8 | "packages": [] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/test-data/doctor/fail/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "emitter20": "1.0.0", 5 | "ncu-test-return-version": "~1.0.0", 6 | "ncu-test-v2": "~1.0.0" 7 | }, 8 | "scripts": { 9 | "test": "node test.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/test-data/workspace-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-test-workspaces", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "0.0.0", 6 | "dependencies": {}, 7 | "workspaces": { 8 | "packages": [ 9 | "pkg/sub" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{md,jade}] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/types/PackageInfo.ts: -------------------------------------------------------------------------------- 1 | import { PackageFile } from './PackageFile' 2 | 3 | /** Describes package data plus it's filepath */ 4 | export interface PackageInfo { 5 | name?: string 6 | pkg: PackageFile 7 | pkgFile: string // the raw file string 8 | filepath: string 9 | } 10 | -------------------------------------------------------------------------------- /test/test-data/doctor/fail/README.md: -------------------------------------------------------------------------------- 1 | - yarn.lock is necessary otherwise yarn sees the package.json in the npm-check-updates directory and throws an error. 2 | - license must be specified in the package.json otherwise yarn will print a warning to stderr, which doctor thinks is a failed test. 3 | -------------------------------------------------------------------------------- /test/test-data/doctor/pass/README.md: -------------------------------------------------------------------------------- 1 | - yarn.lock is necessary otherwise yarn sees the package.json in the npm-check-updates directory and throws an error. 2 | - license must be specified in the package.json otherwise yarn will print a warning to stderr, which doctor thinks is a failed test. 3 | -------------------------------------------------------------------------------- /test/test-data/peer-post-upgrade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "@vitest/ui": "1.3.1", 5 | "vitest": "1.3.1", 6 | "eslint": "8.57.0", 7 | "eslint-plugin-import": "2.29.1", 8 | "eslint-plugin-unused-imports": "3.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | build 17 | 18 | .idea 19 | *.iml 20 | yarn.lock 21 | .DS_store 22 | 23 | # test files 24 | /test/temp_package*.json 25 | /test/.ncurc.json 26 | -------------------------------------------------------------------------------- /src/scripts/install-hooks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Use a simple bash script to install git hooks. 4 | # husky was slow and did not play nice with nvm. 5 | # lefthook suppressed the post-commit notification for some reason. 6 | 7 | # change the git hook directory from .git/hooks to .hooks 8 | git config core.hooksPath .hooks 9 | -------------------------------------------------------------------------------- /deploy.md: -------------------------------------------------------------------------------- 1 | # Deployment Instructions 2 | 3 | - Have you created tests? 4 | - Have you updated the README? 5 | 6 | ```bash 7 | npm version [minor] 8 | git push && git push --tags 9 | npm publish [--tag unstable] 10 | ``` 11 | 12 | - Update the release history 13 | 14 | -------------------------------------------------------------------------------- /test/getInstalledPackages.test.ts: -------------------------------------------------------------------------------- 1 | import getInstalledPackages from '../src/lib/getInstalledPackages' 2 | 3 | // test getInstalledPackages since we cannot test runGlobal without additional code for mocking 4 | describe('getInstalledPackages', () => { 5 | it('execute npm ls', async () => { 6 | await getInstalledPackages() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/types/VersionResult.ts: -------------------------------------------------------------------------------- 1 | import { Version } from './Version' 2 | 3 | /** The result of fetching a version from the package manager, which may include an error. Used to pass errors back up the call chain for better reporting. */ 4 | export interface VersionResult { 5 | version?: Version | null 6 | error?: string 7 | time?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/types/Cacher.ts: -------------------------------------------------------------------------------- 1 | export interface CacheData { 2 | timestamp?: number 3 | packages?: Record 4 | } 5 | 6 | export type Cacher = { 7 | get(name: string, target: string): string | undefined 8 | set(name: string, target: string, version: string): void 9 | save(): Promise 10 | log(): void 11 | } 12 | -------------------------------------------------------------------------------- /src/types/IgnoredUpgradeDueToEnginesNode.ts: -------------------------------------------------------------------------------- 1 | import { Version } from './Version' 2 | import { VersionSpec } from './VersionSpec' 3 | 4 | /** An object that represents an upgrade that was ignored due to mismatch of engines.node */ 5 | export interface IgnoredUpgradeDueToEnginesNode { 6 | from: Version 7 | to: Version 8 | enginesNode: VersionSpec 9 | } 10 | -------------------------------------------------------------------------------- /src/types/IgnoredUpgradeDueToPeerDeps.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | import { Version } from './Version' 3 | 4 | /** An object that represents an upgrade that was ignored due to peer dependencies, along with the reason. */ 5 | export interface IgnoredUpgradeDueToPeerDeps { 6 | from: Version 7 | to: Version 8 | reason: Index 9 | } 10 | -------------------------------------------------------------------------------- /test/test-data/workspace-sub-package-names/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-test-workspaces", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "0.0.0", 6 | "dependencies": {}, 7 | "workspaces": { 8 | "packages": [ 9 | "pkg/a-different-name-to-dirname", 10 | "pkg/dirname-matches-name", 11 | "pkg/dirname-will-become-name" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/cli-options.test.ts: -------------------------------------------------------------------------------- 1 | import cliOptions from '../src/cli-options' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('cli-options', () => { 7 | it('require long and description properties', () => { 8 | cliOptions.forEach(option => { 9 | option.should.have.property('long') 10 | option.should.have.property('description') 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/test-data/doctor/fail/test.js: -------------------------------------------------------------------------------- 1 | const returnVersion = require('ncu-test-return-version') 2 | const v = returnVersion() 3 | 4 | // pass on < 2 5 | if (v.startsWith('0')) { 6 | console.log('Works with v0.x :)') 7 | } else if (v.startsWith('1')) { 8 | console.log('Works with v1.x :)') 9 | } 10 | // break on v2.x 11 | else if (v.startsWith('2')) { 12 | throw new Error('Breaks with v2.x :(') 13 | } 14 | -------------------------------------------------------------------------------- /test/test-data/doctor/options/test.js: -------------------------------------------------------------------------------- 1 | const returnVersion = require('ncu-test-return-version') 2 | const v = returnVersion() 3 | 4 | // pass on < 2 5 | if (v.startsWith('0')) { 6 | console.log('Works with v0.x :)') 7 | } else if (v.startsWith('1')) { 8 | console.log('Works with v1.x :)') 9 | } 10 | // break on v2.x 11 | else if (v.startsWith('2')) { 12 | throw new Error('Breaks with v2.x :(') 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/filterObject.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | import keyValueBy from './keyValueBy' 3 | 4 | /** Filters an object by a predicate. Does not catch exceptions thrown by the predicate. */ 5 | const filterObject = (obj: Index, predicate: (key: string, value: T) => boolean) => 6 | keyValueBy(obj, (key, value) => (predicate(key, value) ? { [key]: value } : null)) 7 | 8 | export default filterObject 9 | -------------------------------------------------------------------------------- /test/bun-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Install bun if not installed. 3 | # Cannot be added to devDependencies as bun currently does not work on Linux. 4 | bun -v &>/dev/null 5 | BUN_EXISTS="$?" 6 | 7 | if [ $BUN_EXISTS -ne 0 ]; then 8 | npm install -g bun 9 | fi 10 | 11 | # Always return success, even if the install script fails. 12 | # Windows is expected to fail and the bun tests will be skipped. 13 | exit 0 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "importOrder": ["^\\.\\./", "^\\./"], 4 | "importOrderSortSpecifiers": true, 5 | "overrides": [{ "files": "*.ts", "options": { "parser": "typescript" } }], 6 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 7 | "printWidth": 120, 8 | "semi": false, 9 | "singleQuote": true, 10 | "tabWidth": 2, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /src/types/FilterResultsFunction.ts: -------------------------------------------------------------------------------- 1 | import { SemVer } from 'semver-utils' 2 | import { Version } from './Version' 3 | import { VersionSpec } from './VersionSpec' 4 | 5 | export type FilterResultsFunction = ( 6 | packageName: string, 7 | versioningMetadata: { 8 | currentVersion: VersionSpec 9 | currentVersionSemver: SemVer[] 10 | upgradedVersion: Version 11 | upgradedVersionSemver: SemVer 12 | }, 13 | ) => boolean 14 | -------------------------------------------------------------------------------- /src/types/MockedVersions.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | import { Options } from './Options' 3 | import { Packument } from './Packument' 4 | import { Version } from './Version' 5 | 6 | /** Parameter type for stubVersions. */ 7 | export type MockedVersions = 8 | | Version 9 | | Partial 10 | | Index 11 | | Index> 12 | | ((options: Options) => Index | Index> | null) 13 | -------------------------------------------------------------------------------- /.markdownlint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // use code indentation rather than code fencing so that extended help can be used for both the CLI and README 3 | 'code-block-style': 0, 4 | 'first-line-heading': 0, 5 | 'line-length': 0, 6 | 'no-bare-urls': 0, 7 | 'no-duplicate-heading': { 8 | siblings_only: true, 9 | }, 10 | // inline HTML used to create tables without headers 11 | 'no-inline-html': 0, 12 | 'commands-show-output': 0, 13 | } 14 | -------------------------------------------------------------------------------- /.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | format: 'group', 3 | reject: [ 4 | // breaking 5 | '@types/chai-as-promised', 6 | '@types/node', 7 | 'chai-as-promised', 8 | 'eslint', 9 | 'eslint-plugin-n', 10 | 'eslint-plugin-promise', 11 | // esm only modules 12 | '@types/chai', 13 | '@types/remote-git-tags', 14 | 'camelcase', 15 | 'find-up', 16 | 'chai', 17 | 'p-map', 18 | 'remote-git-tags', 19 | 'untildify', 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /src/types/GetVersion.ts: -------------------------------------------------------------------------------- 1 | import { NpmConfig } from './NpmConfig' 2 | import { Options } from './Options' 3 | import { Version } from './Version' 4 | import { VersionResult } from './VersionResult' 5 | 6 | /** A function that gets a target version of a dependency. */ 7 | export type GetVersion = ( 8 | packageName: string, 9 | currentVersion: Version, 10 | options?: Options, 11 | npmConfig?: NpmConfig, 12 | npmConfigProject?: NpmConfig, 13 | ) => Promise 14 | -------------------------------------------------------------------------------- /src/types/GroupFunction.ts: -------------------------------------------------------------------------------- 1 | import { SemVer } from 'semver-utils' 2 | import { UpgradeGroup } from './UpgradeGroup' 3 | 4 | /** Customize how packages are divided into groups when using `--format group`. Run "ncu --help --groupFunction" for details. */ 5 | export type GroupFunction = ( 6 | packageName: string, 7 | defaultGroup: UpgradeGroup, 8 | currentVersionSpec: SemVer[], 9 | upgradedVersionSpec: SemVer[], 10 | upgradedVersion: SemVer | null, 11 | ) => UpgradeGroup | string 12 | -------------------------------------------------------------------------------- /src/package-managers/index.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | import { PackageManager } from '../types/PackageManager' 3 | import * as bun from './bun' 4 | import * as gitTags from './gitTags' 5 | import * as npm from './npm' 6 | import * as pnpm from './pnpm' 7 | import * as staticRegistry from './staticRegistry' 8 | import * as yarn from './yarn' 9 | 10 | export default { 11 | npm, 12 | pnpm, 13 | yarn, 14 | bun, 15 | gitTags, 16 | staticRegistry, 17 | } as Index 18 | -------------------------------------------------------------------------------- /test/helpers/chaiSetup.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import chaiString from 'chai-string' 4 | 5 | /** Global chai setup. */ 6 | const chaiSetup = () => { 7 | const should = chai.should() 8 | chai.use(chaiAsPromised) 9 | chai.use(chaiString) 10 | 11 | // do not truncate strings in error messages 12 | chai.config.truncateThreshold = 0 13 | 14 | process.env.NCU_TESTS = 'true' 15 | 16 | return should 17 | } 18 | 19 | export default chaiSetup 20 | -------------------------------------------------------------------------------- /src/types/RcOptions.ts: -------------------------------------------------------------------------------- 1 | import { RunOptions } from './RunOptions' 2 | 3 | /** Options that would make no sense in a .ncurc file */ 4 | type Nonsensical = 'configFileName' | 'configFilePath' | 'cwd' | 'packageData' | 'stdin' 5 | 6 | /** Expected options that might be found in an .ncurc file. Since the config is external, this cannot be guaranteed */ 7 | export type RcOptions = Omit & { 8 | $schema?: string 9 | format?: string | string[] // Format is often set as a string, but needs to be an array 10 | } 11 | -------------------------------------------------------------------------------- /src/types/CLIOption.ts: -------------------------------------------------------------------------------- 1 | import ExtendedHelp from './ExtendedHelp' 2 | 3 | export interface CLIOption { 4 | arg?: string 5 | choices?: T[] 6 | /** If false, the option is only usable in the ncurc file, or when using npm-check-updates as a module, not on the command line. */ 7 | cli?: boolean 8 | default?: T 9 | deprecated?: boolean 10 | description: string 11 | help?: ExtendedHelp 12 | parse?: (s: string, p?: T) => T 13 | long: string 14 | short?: string 15 | type: string 16 | } 17 | 18 | export default CLIOption 19 | -------------------------------------------------------------------------------- /test/global.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import path from 'path' 3 | import spawn from 'spawn-please' 4 | import chaiSetup from './helpers/chaiSetup' 5 | 6 | chaiSetup() 7 | 8 | const bin = path.join(__dirname, '../build/cli.js') 9 | 10 | describe('global', () => { 11 | // TODO: Hangs on Windows 12 | const itSkipWindows = process.platform === 'win32' ? it.skip : it 13 | itSkipWindows('global should run', async () => { 14 | const { stdout } = await spawn('node', [bin, '--jsonUpgraded', '--global', 'npm']) 15 | expect(JSON.parse(stdout)) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Tomas Junnonen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/types/Packument.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | import { Version } from './Version' 3 | 4 | /** A packument result object from npm-registry-fetch. */ 5 | export interface Packument { 6 | name: string 7 | deprecated?: boolean 8 | 'dist-tags': Index 9 | engines: { 10 | node: string 11 | } 12 | // fullMetadata only 13 | // TODO: store only the time of the latest version? 14 | time?: Index 15 | version: Version 16 | versions: Index< 17 | Omit & { 18 | _npmUser?: { 19 | name: string 20 | } 21 | } 22 | > 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/sortBy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an array of elements, sorted in ascending order by the results of 3 | * running each element in a collection through each iteratee. This method 4 | * performs a stable sort, that is, it preserves the original sort order of 5 | * equal elements. The iteratees are invoked with one argument: (value). 6 | */ 7 | export function sortBy(collection: T[] | null | undefined, selector: (item: T) => any): T[] { 8 | if (!collection) return [] 9 | return collection 10 | .map(item => ({ item, key: selector(item) })) 11 | .sort((a, b) => (a.key > b.key ? 1 : a.key < b.key ? -1 : 0)) 12 | .map(({ item }) => item) 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/programError.ts: -------------------------------------------------------------------------------- 1 | import { print } from '../lib/logging' 2 | import { Options } from '../types/Options' 3 | import chalk from './chalk' 4 | 5 | /** Print an error. Exit the process if in CLI mode. */ 6 | function programError( 7 | options: Options, 8 | message: string, 9 | { 10 | color = true, 11 | }: { 12 | // defaults to true, which uses chalk.red on the whole error message. 13 | // set to false to provide your own coloring. 14 | color?: boolean 15 | } = {}, 16 | ): never { 17 | if (options.cli) { 18 | print(options, color ? chalk.red(message) : message, null, 'error') 19 | process.exit(1) 20 | } else { 21 | throw new Error(message) 22 | } 23 | } 24 | 25 | export default programError 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - '!dependabot/**' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | env: 13 | FORCE_COLOR: 2 14 | NODE: 18 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | lint: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ env.NODE }} 31 | cache: npm 32 | 33 | - name: Install npm dependencies 34 | run: npm ci 35 | 36 | - name: Run lint 37 | run: npm run lint 38 | -------------------------------------------------------------------------------- /src/types/Target.ts: -------------------------------------------------------------------------------- 1 | import { TargetFunction } from './TargetFunction' 2 | 3 | /** Valid strings for the --target option. Indicates the desired version to upgrade to. */ 4 | export const supportedVersionTargets = ['latest', 'newest', 'greatest', 'minor', 'patch', 'semver'] as const 5 | 6 | /** A union of supported version target strings. */ 7 | export type TargetString = (typeof supportedVersionTargets)[number] 8 | 9 | /** Upgrading to specific distribution tags can be done by passing @-starting value to --target option. */ 10 | export type TargetDistTag = `@${string}` 11 | 12 | /** The type of the --target option. Specifies the range from which to select the version to upgrade to. */ 13 | export type Target = TargetString | TargetDistTag | TargetFunction 14 | -------------------------------------------------------------------------------- /src/lib/libnpmconfig/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright npm, Inc 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [] I have searched for [similar issues](https://github.com/raineorshine/npm-check-updates/issues) 2 | 3 | --- 4 | 5 | ## Steps to Reproduce 6 | 7 | .ncurc: 8 | 9 | 10 | 11 | ```js 12 | 13 | ``` 14 | 15 | Dependencies: 16 | 17 | 18 | 19 | ```json 20 | 21 | ``` 22 | 23 | Steps: 24 | 25 | 26 | 27 | ## Current Behavior 28 | 29 | 30 | 31 | ## Expected Behavior 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - '!dependabot/**' 8 | pull_request: 9 | # The branches below must be a subset of the branches above 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '0 2 * * 5' 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-latest 19 | permissions: 20 | actions: read 21 | contents: read 22 | security-events: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v2 30 | with: 31 | languages: 'javascript' 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v2 35 | -------------------------------------------------------------------------------- /src/lib/figgy-pudding/LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) npm, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for 6 | any purpose with or without fee is hereby granted, provided that the 7 | above copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE COPYRIGHT HOLDER DISCLAIMS 10 | ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 11 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 12 | COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR 13 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 14 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 15 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE 16 | USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /src/package-managers/README.md: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | 3 | ## How to add a new package manager 4 | 5 | To add support for another package manager, drop in a module with the following interface. 6 | 7 | ```js 8 | { 9 | *list: (npmOptions: {}) => Promise<{ name: version }>, 10 | *latest: (pkgName: string) => Promise version, 11 | newest: (pkgName: string) => Promise version, 12 | greatest: (pkgName: string) => Promise version, 13 | minor: (pkgName: string, String currentVersion) => Promise version, 14 | patch: (pkgName: string, String currentVersion) => Promise version, 15 | } 16 | ``` 17 | 18 | - `list` and `latest` are required. 19 | - Methods corresponding to other `--target` values are optional. 20 | - Methods are expected to reject with `'404 Not Found'` if the package is not found. 21 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { nodeExternals } from 'rollup-plugin-node-externals' 2 | import { defineConfig } from 'vite' 3 | import { analyzer } from 'vite-bundle-analyzer' 4 | import dts from 'vite-plugin-dts' 5 | 6 | export default defineConfig(({ mode }) => ({ 7 | plugins: [ 8 | dts({ 9 | entryRoot: 'src', 10 | rollupTypes: true, 11 | include: ['src'], 12 | }), 13 | nodeExternals(), 14 | process.env.ANALYZER && analyzer(), 15 | ], 16 | ssr: { 17 | // bundle and treeshake everything 18 | noExternal: true, 19 | }, 20 | build: { 21 | ssr: true, 22 | lib: { 23 | entry: ['src/index.ts', 'src/bin/cli.ts'], 24 | formats: ['cjs'], 25 | }, 26 | target: 'node18', 27 | outDir: 'build', 28 | sourcemap: true, 29 | minify: mode === 'production' && 'esbuild', 30 | }, 31 | })) 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "test", "vite.config.mts"], 3 | "exclude": ["test/deep", "test/doctor"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "allowSyntheticDefaultImports": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "isolatedModules": true, 11 | "lib": ["es2021"], 12 | "module": "nodenext", 13 | "moduleResolution": "nodenext", 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "paths": { 17 | "libnpmconfig": ["./src/types/libnpmconfig"], 18 | "prompts-ncu": ["./src/types/prompts-ncu"] 19 | }, 20 | "resolveJsonModule": true, 21 | "outDir": "./build", 22 | "skipLibCheck": true, 23 | "sourceMap": true, 24 | "strict": true, 25 | "target": "es2021" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/types/Options.ts: -------------------------------------------------------------------------------- 1 | import { Cacher } from './Cacher' 2 | import { Index } from './IndexType' 3 | import { RunOptions } from './RunOptions' 4 | import { VersionSpec } from './VersionSpec' 5 | 6 | /** Internal, normalized options for all ncu behavior. Includes RunOptions that are specified in the CLI or passed to the ncu module, as well as meta information including CLI arguments, package information, and ncurc config. */ 7 | export type Options = RunOptions & { 8 | args?: any[] 9 | cacher?: Cacher 10 | cli?: boolean 11 | distTag?: string 12 | json?: boolean 13 | nodeEngineVersion?: VersionSpec 14 | packageData?: string 15 | peerDependencies?: Index 16 | rcConfigPath?: string 17 | // A list of local workspace packages by name. 18 | // This is used to ignore local workspace packages when fetching new versions. 19 | workspacePackages?: string[] 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/loadPackageInfoFromFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { Options } from '../types/Options' 3 | import { PackageFile } from '../types/PackageFile' 4 | import { PackageInfo } from '../types/PackageInfo' 5 | import programError from './programError' 6 | 7 | /** Load and parse a package file. */ 8 | const loadPackageInfoFromFile = async (options: Options, filepath: string): Promise => { 9 | let pkg: PackageFile, pkgFile: string 10 | 11 | // assert package.json 12 | try { 13 | pkgFile = await fs.readFile(filepath, 'utf-8') 14 | pkg = JSON.parse(pkgFile) 15 | } catch (e) { 16 | programError(options, `Missing or invalid ${filepath}`) 17 | } 18 | 19 | return { 20 | name: undefined, // defined by workspace code only 21 | pkg, 22 | pkgFile, 23 | filepath, 24 | } 25 | } 26 | 27 | export default loadPackageInfoFromFile 28 | -------------------------------------------------------------------------------- /src/types/PackageFile.ts: -------------------------------------------------------------------------------- 1 | import { Index } from './IndexType' 2 | import { PackageFileRepository } from './PackageFileRepository' 3 | import { VersionSpec } from './VersionSpec' 4 | 5 | type NestedVersionSpecs = { 6 | [name: string]: VersionSpec | NestedVersionSpecs 7 | } 8 | 9 | /** The relevant bits of a parsed package.json file. */ 10 | export interface PackageFile { 11 | dependencies?: Index 12 | devDependencies?: Index 13 | // deno only 14 | imports?: Index 15 | engines?: Index 16 | name?: string 17 | // https://nodejs.org/api/packages.html#packagemanager 18 | packageManager?: string 19 | optionalDependencies?: Index 20 | overrides?: NestedVersionSpecs 21 | peerDependencies?: Index 22 | repository?: string | PackageFileRepository 23 | scripts?: Index 24 | workspaces?: string[] | { packages: string[] } 25 | version?: string 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/pick.ts: -------------------------------------------------------------------------------- 1 | /** Creates an object composed of the picked `object` properties. */ 2 | export function pick(obj: T, props: U[]): Pick { 3 | const newObject = {} as Pick 4 | 5 | for (const prop of props) { 6 | newObject[prop] = obj[prop] 7 | } 8 | 9 | return newObject 10 | } 11 | 12 | /** 13 | * Creates an object composed of the `object` properties `predicate` returns 14 | * truthy for. The predicate is invoked with two arguments: (value, key). 15 | */ 16 | export function pickBy( 17 | object: R | null | undefined, 18 | predicate: (value: R[K], key: keyof R) => any, 19 | ): Record { 20 | const newObject = {} as Record 21 | 22 | for (const [key, value] of Object.entries(object ?? {})) { 23 | const _key = key as K 24 | if (predicate(value, _key)) { 25 | newObject[_key] = value 26 | } 27 | } 28 | 29 | return newObject 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/mergeOptions.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../types/Options' 2 | 3 | type OptionKey = keyof Options 4 | 5 | /** Merges two arrays into one, removing duplicates. */ 6 | function mergeArrays(arr1: any[], arr2: any[]) { 7 | return Array.from(new Set([...(arr1 || []), ...(arr2 || [])])) 8 | } 9 | 10 | /** 11 | * Shallow merge (specific or all) properties. 12 | * If some properties both are arrays, then merge them also. 13 | */ 14 | function mergeOptions(rawOptions1: Options | null, rawOptions2: Options | null) { 15 | const options1: Options = rawOptions1 || {} 16 | const options2: Options = rawOptions2 || {} 17 | const result = { ...options1, ...options2 } 18 | ;(Object.keys(result) as OptionKey[]).forEach(key => { 19 | if (Array.isArray(options1[key]) && Array.isArray(options2[key])) { 20 | result[key] = mergeArrays(options1[key] as any[], options2[key] as any[]) as any 21 | } 22 | }) 23 | return result 24 | } 25 | 26 | export default mergeOptions 27 | -------------------------------------------------------------------------------- /src/lib/resolveDepSections.ts: -------------------------------------------------------------------------------- 1 | import { cliOptionsMap } from '../cli-options' 2 | import { Index } from '../types/IndexType' 3 | import { PackageFile } from '../types/PackageFile' 4 | 5 | // dependency section aliases that will be resolved to the full name 6 | const depAliases: Index = { 7 | dev: 'devDependencies', 8 | peer: 'peerDependencies', 9 | prod: 'dependencies', 10 | optional: 'optionalDependencies', 11 | } 12 | 13 | /** Gets a list of dependency sections based on options.dep. */ 14 | const resolveDepSections = (dep?: string | string[]): (keyof PackageFile)[] => { 15 | // parse dep string and set default 16 | const depOptions: string[] = dep ? (typeof dep === 'string' ? dep.split(',') : dep) : cliOptionsMap.dep.default 17 | 18 | // map the dependency section option to a full dependency section name 19 | const depSections = depOptions.map(name => depAliases[name] || name) 20 | 21 | return depSections 22 | } 23 | 24 | export default resolveDepSections 25 | -------------------------------------------------------------------------------- /src/lib/getPackageVersion.ts: -------------------------------------------------------------------------------- 1 | import { PackageFile } from '../types/PackageFile' 2 | import getPackageJson from './getPackageJson' 3 | 4 | /** 5 | * @param packageName A package name as listed in package.json's dependencies list 6 | * @param packageJson Optional param to specify a object representation of a package.json file instead of loading from node_modules 7 | * @returns The package version or null if a version could not be determined 8 | */ 9 | async function getPackageVersion( 10 | packageName: string, 11 | packageJson?: PackageFile, 12 | { 13 | pkgFile, 14 | }: { 15 | /** Specify the package file location to add to the node_modules search paths. Needed in workspaces/deep mode. */ 16 | pkgFile?: string 17 | } = {}, 18 | ) { 19 | if (packageJson) { 20 | return packageJson.version 21 | } 22 | 23 | const loadedPackageJson = await getPackageJson(packageName, { pkgFile }) 24 | return loadedPackageJson?.version ?? null 25 | } 26 | 27 | export default getPackageVersion 28 | -------------------------------------------------------------------------------- /src/lib/table.ts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3' 2 | import wrap from './wrap' 3 | 4 | /** Wraps the second column in a list of 2-column cli-table rows. */ 5 | const wrapRows = (rows: string[][]) => rows.map(([col1, col2]) => [col1, wrap(col2)]) 6 | 7 | /** Renders an HTML row. */ 8 | const row = (cells: string[]) => '\n ' + cells.map(cell => `${cell}`).join('') + '' 9 | 10 | /** Renders a table for the CLI or markdown. */ 11 | const table = ({ 12 | colAligns, 13 | markdown, 14 | rows, 15 | }: { 16 | colAligns?: ('left' | 'right')[] 17 | markdown?: boolean 18 | rows: string[][] 19 | }): string => { 20 | // return HTML table for Github-flavored markdown 21 | if (markdown) { 22 | return `${rows.map(row).join('')}\n
` 23 | } 24 | // otherwise use cli-table3 25 | else { 26 | const t = new Table({ ...(colAligns ? { colAligns } : null) }) 27 | t.push(...(markdown ? rows : wrapRows(rows))) 28 | return t.toString() 29 | } 30 | } 31 | 32 | export default table 33 | -------------------------------------------------------------------------------- /test/e2e/esm/index.js: -------------------------------------------------------------------------------- 1 | /** NOTE: This script is copied into a temp directory by the e2e test and dependencies are installed from the local verdaccio registry. */ 2 | import assert from 'assert' 3 | import ncu from 'npm-check-updates' 4 | 5 | const registry = process.env.REGISTRY || 'http://localhost:4873' 6 | 7 | // must exit with error code on unhandledRejection, otherwise script will exit with 0 if an assertion fails in the async block 8 | process.on('unhandledRejection', (reason, p) => { 9 | process.exit(1) 10 | }) 11 | 12 | // test 13 | ;(async () => { 14 | const upgraded = await ncu.run({ 15 | // --pre 1 to ensure that an upgrade is always suggested even if npm-check-updates is on a prerelease version 16 | pre: true, 17 | packageData: JSON.stringify({ 18 | dependencies: { 19 | 'npm-check-updates': '1.0.0', 20 | }, 21 | }), 22 | registry, 23 | }) 24 | 25 | console.info(upgraded) 26 | 27 | assert.notStrictEqual(upgraded['npm-check-updates'], '1.0.0', 'npm-check-updates should be upgraded') 28 | })() 29 | -------------------------------------------------------------------------------- /test/e2e/cjs/index.js: -------------------------------------------------------------------------------- 1 | /** NOTE: This script is copied into a temp directory by the e2e test and dependencies are installed from the local verdaccio registry. */ 2 | const ncu = require('npm-check-updates') 3 | const assert = require('assert') 4 | 5 | const registry = process.env.REGISTRY || 'http://localhost:4873' 6 | 7 | // must exit with error code on unhandledRejection, otherwise script will exit with 0 if an assertion fails in the async block 8 | process.on('unhandledRejection', (reason, p) => { 9 | process.exit(1) 10 | }) 11 | 12 | // test 13 | ;(async () => { 14 | const upgraded = await ncu.run({ 15 | // --pre 1 to ensure that an upgrade is always suggested even if npm-check-updates is on a prerelease version 16 | pre: true, 17 | packageData: JSON.stringify({ 18 | dependencies: { 19 | 'npm-check-updates': '1.0.0', 20 | }, 21 | }), 22 | registry, 23 | }) 24 | 25 | console.info(upgraded) 26 | 27 | assert.notStrictEqual(upgraded['npm-check-updates'], '1.0.0', 'npm-check-updates should be upgraded') 28 | })() 29 | -------------------------------------------------------------------------------- /src/lib/getPackageManager.ts: -------------------------------------------------------------------------------- 1 | import packageManagers from '../package-managers' 2 | import { Maybe } from '../types/Maybe' 3 | import { Options } from '../types/Options' 4 | import { PackageManager } from '../types/PackageManager' 5 | import programError from './programError' 6 | 7 | /** 8 | * Resolves the package manager from a string or object. Throws an error if an invalid packageManager is provided. 9 | * 10 | * @param packageManagerNameOrObject 11 | * @param packageManagerNameOrObject.global 12 | * @param packageManagerNameOrObject.packageManager 13 | * @returns 14 | */ 15 | function getPackageManager(options: Options, name: Maybe): PackageManager { 16 | // default to npm 17 | if (!name || name === 'deno') { 18 | return packageManagers.npm 19 | } else if (options.registryType === 'json') { 20 | return packageManagers.staticRegistry 21 | } 22 | 23 | if (!packageManagers[name]) { 24 | programError(options, `Invalid package manager: ${name}`) 25 | } 26 | 27 | return packageManagers[name] 28 | } 29 | 30 | export default getPackageManager 31 | -------------------------------------------------------------------------------- /test/helpers/stubVersions.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import * as npmPackageManager from '../../src/package-managers/npm' 3 | import { MockedVersions } from '../../src/types/MockedVersions' 4 | 5 | /** Stubs the npmView function from package-managers/npm. Returns the stub object. Call stub.restore() after assertions to restore the original function. Set spawn:true to stub ncu spawned as a child process. */ 6 | const stubVersions = (mockReturnedVersions: MockedVersions, { spawn }: { spawn?: boolean } = {}) => { 7 | // stub child process 8 | // the only way to stub functionality in spawned child processes is to pass data through process.env and stub internally 9 | if (spawn) { 10 | process.env.STUB_VERSIONS = JSON.stringify(mockReturnedVersions) 11 | return { 12 | restore: () => { 13 | process.env.STUB_VERSIONS = '' 14 | }, 15 | } 16 | } 17 | // stub module 18 | else { 19 | return sinon 20 | .stub(npmPackageManager, 'fetchUpgradedPackumentMemo') 21 | .callsFake(npmPackageManager.mockFetchUpgradedPackument(mockReturnedVersions)) 22 | } 23 | } 24 | 25 | export default stubVersions 26 | -------------------------------------------------------------------------------- /test/bun/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as bun from '../../src/package-managers/bun' 2 | import chaiSetup from '../helpers/chaiSetup' 3 | import { testFail, testPass } from '../helpers/doctorHelpers' 4 | import stubVersions from '../helpers/stubVersions' 5 | 6 | chaiSetup() 7 | 8 | const mockNpmVersions = { 9 | emitter20: '2.0.0', 10 | 'ncu-test-return-version': '2.0.0', 11 | 'ncu-test-tag': '1.1.0', 12 | 'ncu-test-v2': '2.0.0', 13 | } 14 | 15 | describe('bun', function () { 16 | it('list', async () => { 17 | const result = await bun.list({ cwd: __dirname }) 18 | result.should.have.property('ncu-test-v2') 19 | }) 20 | 21 | it('latest', async () => { 22 | const { version } = await bun.latest('ncu-test-v2', '1.0.0', { cwd: __dirname }) 23 | version!.should.equal('2.0.0') 24 | }) 25 | 26 | describe('doctor', function () { 27 | this.timeout(3 * 60 * 1000) 28 | 29 | let stub: { restore: () => void } 30 | before(() => (stub = stubVersions(mockNpmVersions, { spawn: true }))) 31 | after(() => stub.restore()) 32 | 33 | testPass({ packageManager: 'bun' }) 34 | testFail({ packageManager: 'bun' }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/lib/determinePackageManager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { Index } from '../types/IndexType' 3 | import { Options } from '../types/Options' 4 | import { PackageManagerName } from '../types/PackageManagerName' 5 | import findLockfile from './findLockfile' 6 | 7 | // map lockfiles to package managers 8 | const packageManagerLockfileMap: Index = { 9 | 'package-lock': 'npm', 10 | yarn: 'yarn', 11 | 'pnpm-lock': 'pnpm', 12 | deno: 'deno', 13 | bun: 'bun', 14 | } 15 | 16 | /** 17 | * If the packageManager option was not provided, look at the lockfiles to 18 | * determine which package manager is being used. 19 | */ 20 | const determinePackageManager = async ( 21 | options: Options, 22 | // only for testing 23 | readdir: (_path: string) => Promise = fs.readdir, 24 | ): Promise => { 25 | if (options.packageManager) return options.packageManager 26 | else if (options.global) return 'npm' 27 | 28 | const lockfileName = (await findLockfile(options, readdir))?.filename 29 | return lockfileName ? packageManagerLockfileMap[lockfileName.split('.')[0]] : 'npm' 30 | } 31 | 32 | export default determinePackageManager 33 | -------------------------------------------------------------------------------- /src/types/PackageManager.ts: -------------------------------------------------------------------------------- 1 | import { GetVersion } from './GetVersion' 2 | import { Index } from './IndexType' 3 | import { NpmConfig } from './NpmConfig' 4 | import { Options } from './Options' 5 | import { Version } from './Version' 6 | import { VersionSpec } from './VersionSpec' 7 | 8 | /** The package manager API that ncu uses to fetch versions and meta information for packages. Includes npm and yarn, and others can be added as needed. */ 9 | export interface PackageManager { 10 | defaultPrefix?: (options: Options) => Promise 11 | list?: (options: Options) => Promise> 12 | latest: GetVersion 13 | minor?: GetVersion 14 | newest?: GetVersion 15 | patch?: GetVersion 16 | greatest?: GetVersion 17 | semver?: GetVersion 18 | packageAuthorChanged?: ( 19 | packageName: string, 20 | from: VersionSpec, 21 | to: VersionSpec, 22 | options?: Options, 23 | ) => Promise 24 | getPeerDependencies?: (packageName: string, version: Version) => Promise> 25 | getEngines?: ( 26 | packageName: string, 27 | version: Version, 28 | options: Options, 29 | npmConfigLocal?: NpmConfig, 30 | ) => Promise> 31 | } 32 | -------------------------------------------------------------------------------- /test/getEnginesNodeFromRegistry.test.ts: -------------------------------------------------------------------------------- 1 | import { chalkInit } from '../src/lib/chalk' 2 | import getEnginesNodeFromRegistry from '../src/lib/getEnginesNodeFromRegistry' 3 | import chaiSetup from './helpers/chaiSetup' 4 | 5 | chaiSetup() 6 | 7 | describe('getEnginesNodeFromRegistry', function () { 8 | it('single package', async () => { 9 | await chalkInit() 10 | const data = await getEnginesNodeFromRegistry({ del: '2.0.0' }, {}) 11 | data.should.deep.equal({ 12 | del: '>=0.10.0', 13 | }) 14 | }) 15 | 16 | it('single package empty', async () => { 17 | await chalkInit() 18 | const data = await getEnginesNodeFromRegistry({ 'ncu-test-return-version': '1.0.0' }, {}) 19 | data.should.deep.equal({ 'ncu-test-return-version': undefined }) 20 | }) 21 | 22 | it('multiple packages', async () => { 23 | await chalkInit() 24 | const data = await getEnginesNodeFromRegistry( 25 | { 26 | 'ncu-test-return-version': '1.0.0', 27 | 'ncu-test-peer': '1.0.0', 28 | del: '2.0.0', 29 | }, 30 | {}, 31 | ) 32 | data.should.deep.equal({ 33 | 'ncu-test-return-version': undefined, 34 | 'ncu-test-peer': undefined, 35 | del: '>=0.10.0', 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /.hooks/post-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SHORT_SHA=$(git rev-parse HEAD) 3 | LINT_LOG="$TMPDIR"/lint."$SHORT_SHA".log 4 | 5 | # strip color 6 | strip() { 7 | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g" 8 | } 9 | 10 | # display a notification 11 | notify() { 12 | # if osascript is not supported, do nothing 13 | if [ -f /usr/bin/osascript ]; then 14 | # read back in the lint errors 15 | ERRORS=$(sed 1,4d "$LINT_LOG") 16 | 17 | # Trigger apple- or OSA-script on supported platforms 18 | /usr/bin/osascript -e "display notification \"$ERRORS\" with title \"$*\"" 19 | fi 20 | 21 | # clean up 22 | rm "$LINT_LOG" 23 | } 24 | 25 | # ensure failed lint exit code passes through sed 26 | set -o pipefail 27 | 28 | # Do NOT run this when rebasing or we can't get the branch 29 | branch=$(git branch --show-current) 30 | if [ -z "$branch" ]; then 31 | exit 0 32 | fi 33 | 34 | # Lint in the background, not blocking the terminal and piping all output to a file. 35 | # If the lint fails, trigger a notification (on supported platforms) with at least the first error shown. 36 | # We pipe output so that the terminal (tmux, vim, emacs etc.) isn't borked by stray output. 37 | npm run lint:src | strip &>"$LINT_LOG" || notify "Lint Error" & 38 | -------------------------------------------------------------------------------- /test/isUpgradeable.test.ts: -------------------------------------------------------------------------------- 1 | import isUpgradeable from '../src/lib/isUpgradeable' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('isUpgradeable', () => { 7 | it('do not upgrade pure wildcards', () => { 8 | isUpgradeable('*', '0.5.1').should.equal(false) 9 | }) 10 | 11 | it('upgrade versions that do not satisfy latest versions', () => { 12 | isUpgradeable('0.1.x', '0.5.1').should.equal(true) 13 | }) 14 | 15 | it('do not upgrade invalid versions', () => { 16 | isUpgradeable('https://github.com/strongloop/express', '4.11.2').should.equal(false) 17 | }) 18 | 19 | it('do not upgrade versions beyond the latest', () => { 20 | isUpgradeable('5.0.0', '4.11.2').should.equal(false) 21 | }) 22 | 23 | it('handle comparison constraints', () => { 24 | isUpgradeable('>1.0', '0.5.1').should.equal(false) 25 | isUpgradeable('<3.0 >0.1', '0.5.1').should.equal(false) 26 | isUpgradeable('>0.1.x', '0.5.1').should.equal(true) 27 | isUpgradeable('<7.0.0', '7.2.0').should.equal(true) 28 | isUpgradeable('<7.0', '7.2.0').should.equal(true) 29 | isUpgradeable('<7', '7.2.0').should.equal(true) 30 | }) 31 | 32 | it('upgrade simple versions', () => { 33 | isUpgradeable('v1', 'v2').should.equal(true) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - '!dependabot/**' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | env: 13 | FORCE_COLOR: 2 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | run: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | 23 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 24 | runs-on: ${{ matrix.os }} 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | node: [18, 20] 30 | os: [ubuntu-latest, windows-latest] 31 | 32 | steps: 33 | - name: Clone repository 34 | uses: actions/checkout@v4 35 | 36 | - name: Set up Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: ${{ matrix.node }} 40 | cache: npm 41 | 42 | - name: Install npm dependencies 43 | run: npm ci 44 | 45 | - name: Build 46 | run: npm run build 47 | 48 | - name: Unit Tests 49 | run: npm run test:unit 50 | 51 | - name: Bun Tests 52 | run: npm run test:bun 53 | 54 | - name: E2E Tests 55 | run: npm run test:e2e 56 | if: startsWith(matrix.os, 'ubuntu') && matrix.node == 20 57 | -------------------------------------------------------------------------------- /src/lib/getPackageJson.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { PackageFile } from '../types/PackageFile' 4 | import exists from './exists' 5 | 6 | /** Gets the package.json contents of an installed package. */ 7 | async function getPackageJson( 8 | packageName: string, 9 | { 10 | pkgFile, 11 | }: { 12 | /** Specify the package file location to add to the node_modules search paths. Needed in workspaces/deep mode. */ 13 | pkgFile?: string 14 | } = {}, 15 | ): Promise { 16 | const requirePaths = require.resolve.paths(packageName) || [] 17 | const pkgFileNodeModules = pkgFile ? [path.join(path.dirname(pkgFile), 'node_modules')] : [] 18 | const localNodeModules = [path.join(process.cwd(), 'node_modules')] 19 | const nodeModulePaths = [...pkgFileNodeModules, ...localNodeModules, ...requirePaths] 20 | 21 | for (const basePath of nodeModulePaths) { 22 | const packageJsonPath = path.join(basePath, packageName, 'package.json') 23 | if (await exists(packageJsonPath)) { 24 | try { 25 | const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')) 26 | return packageJson 27 | } catch (e) {} 28 | } 29 | } 30 | 31 | return null 32 | } 33 | 34 | export default getPackageJson 35 | -------------------------------------------------------------------------------- /test/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import spawn from 'spawn-please' 4 | import ncu from '../src/' 5 | import chaiSetup from './helpers/chaiSetup' 6 | import stubVersions from './helpers/stubVersions' 7 | 8 | chaiSetup() 9 | 10 | const bin = path.join(__dirname, '../build/cli.js') 11 | 12 | describe('timeout', function () { 13 | it('throw an exception instead of printing to the console when timeout is exceeded', async () => { 14 | const pkgPath = path.join(__dirname, './test-data/ncu/package-large.json') 15 | return ncu({ 16 | packageData: await fs.readFile(pkgPath, 'utf-8'), 17 | timeout: 1, 18 | }).should.eventually.be.rejectedWith('Exceeded global timeout of 1ms') 19 | }) 20 | 21 | it('exit with error when timeout is exceeded', async () => { 22 | return spawn('node', [bin, '--timeout', '1'], { 23 | stdin: '{ "dependencies": { "express": "1" } }', 24 | }).should.eventually.be.rejectedWith('Exceeded global timeout of 1ms') 25 | }) 26 | 27 | it('completes successfully with timeout', async () => { 28 | const stub = stubVersions('99.9.9', { spawn: true }) 29 | await spawn('node', [bin, '--timeout', '100000'], { stdin: '{ "dependencies": { "express": "1" } }' }) 30 | stub.restore() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/getPeerDependenciesFromRegistry.test.ts: -------------------------------------------------------------------------------- 1 | import { chalkInit } from '../src/lib/chalk' 2 | import getPeerDependenciesFromRegistry from '../src/lib/getPeerDependenciesFromRegistry' 3 | import chaiSetup from './helpers/chaiSetup' 4 | 5 | chaiSetup() 6 | 7 | describe('getPeerDependenciesFromRegistry', function () { 8 | it('single package', async () => { 9 | await chalkInit() 10 | const data = await getPeerDependenciesFromRegistry({ 'ncu-test-peer': '1.0' }, {}) 11 | data.should.deep.equal({ 12 | 'ncu-test-peer': { 13 | 'ncu-test-return-version': '1.x', 14 | }, 15 | }) 16 | }) 17 | 18 | it('single package empty', async () => { 19 | await chalkInit() 20 | const data = await getPeerDependenciesFromRegistry({ 'ncu-test-return-version': '1.0' }, {}) 21 | data.should.deep.equal({ 'ncu-test-return-version': {} }) 22 | }) 23 | 24 | it('multiple packages', async () => { 25 | await chalkInit() 26 | const data = await getPeerDependenciesFromRegistry( 27 | { 28 | 'ncu-test-return-version': '1.0.0', 29 | 'ncu-test-peer': '1.0.0', 30 | }, 31 | {}, 32 | ) 33 | data.should.deep.equal({ 34 | 'ncu-test-return-version': {}, 35 | 'ncu-test-peer': { 36 | 'ncu-test-return-version': '1.x', 37 | }, 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/lib/getPreferredWildcard.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | import { sortBy } from './sortBy' 3 | import { WILDCARDS } from './version-util' 4 | 5 | /** 6 | * 7 | * @param dependencies A dependencies collection 8 | * @returns Returns whether the user prefers ^, ~, .*, or .x 9 | * (simply counts the greatest number of occurrences) or `null` if 10 | * given no dependencies. 11 | */ 12 | function getPreferredWildcard(dependencies: Index) { 13 | // if there are no dependencies, return null. 14 | if (Object.keys(dependencies).length === 0) { 15 | return null 16 | } 17 | 18 | // group the dependencies by wildcard 19 | const groups = Object.values(dependencies).reduce>((acc, dep) => { 20 | const wildcard = WILDCARDS.find((wildcard: string) => dep && dep.includes(wildcard)) 21 | if (wildcard !== undefined) { 22 | acc[wildcard] ||= [] 23 | acc[wildcard].push(dep) 24 | } 25 | return acc 26 | }, {}) 27 | 28 | const arrOfGroups = Object.entries(groups).map(([wildcard, instances]) => ({ wildcard, instances })) 29 | 30 | // reverse sort the groups so that the wildcard with the most appearances is at the head, then return it. 31 | const sorted = sortBy(arrOfGroups, wildcardObject => -wildcardObject.instances.length) 32 | 33 | return sorted.length > 0 ? sorted[0].wildcard : null 34 | } 35 | 36 | export default getPreferredWildcard 37 | -------------------------------------------------------------------------------- /test/enginesNode.test.ts: -------------------------------------------------------------------------------- 1 | import ncu from '../src/' 2 | import { Index } from '../src/types/IndexType' 3 | import { VersionSpec } from '../src/types/VersionSpec' 4 | import chaiSetup from './helpers/chaiSetup' 5 | 6 | chaiSetup() 7 | 8 | describe('enginesNode', () => { 9 | it("update packages that satisfy the project's engines.node", async () => { 10 | const upgraded = await ncu({ 11 | enginesNode: true, 12 | packageData: { 13 | dependencies: { 14 | del: '3.0.0', 15 | }, 16 | engines: { 17 | node: '>=6', 18 | }, 19 | }, 20 | }) 21 | 22 | upgraded!.should.eql({ 23 | del: '4.1.1', 24 | }) 25 | }) 26 | 27 | it('do not update packages with incompatible engines.node', async () => { 28 | const upgraded = await ncu({ 29 | enginesNode: true, 30 | packageData: { 31 | dependencies: { 32 | del: '3.0.0', 33 | }, 34 | engines: { 35 | node: '>=1', 36 | }, 37 | }, 38 | }) 39 | 40 | upgraded!.should.eql({}) 41 | }) 42 | 43 | it('update packages that do not have engines.node', async () => { 44 | const upgraded = (await ncu({ 45 | enginesNode: true, 46 | packageData: { 47 | dependencies: { 48 | 'ncu-test-v2': '1.0.0', 49 | }, 50 | engines: { 51 | node: '>=6', 52 | }, 53 | }, 54 | })) as Index 55 | 56 | upgraded!.should.eql({ 57 | 'ncu-test-v2': '2.0.0', 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/lib/wrap.ts: -------------------------------------------------------------------------------- 1 | /** Wraps a string by inserting newlines every n characters. Wraps on word break. Default: 92 chars. */ 2 | const wrap = (s: string, maxLineLength = 92) => { 3 | const linesIn = s.split('\n') 4 | const linesOut: string[] = [] 5 | linesIn.forEach(lineIn => { 6 | let i = 0 7 | if (lineIn.length === 0) { 8 | linesOut.push('') 9 | return 10 | } 11 | 12 | while (i < lineIn.length) { 13 | const lineFull = lineIn.slice(i, i + maxLineLength + 1) 14 | 15 | // if the line is within the line length, push it as the last line and break 16 | const lineTrimmed = lineFull.trimEnd() 17 | if (lineTrimmed.length <= maxLineLength) { 18 | linesOut.push(lineTrimmed) 19 | break 20 | } 21 | 22 | // otherwise, wrap before the last word that exceeds the wrap length 23 | // do not wrap in the middle of a word 24 | // reverse the string and use match to find the first non-word character to wrap on 25 | const wrapOffset = 26 | lineFull 27 | .split('') 28 | .reverse() 29 | .join('') 30 | // add [^\W] to not break in the middle of --registry 31 | .match(/[ -][^\W]/)?.index || 0 32 | const line = lineFull.slice(0, lineFull.length - wrapOffset) 33 | 34 | // make sure we do not end up in an infinite loop 35 | if (line.length === 0) break 36 | 37 | linesOut.push(line.trimEnd()) 38 | i += line.length 39 | } 40 | i = 0 41 | }) 42 | return linesOut.join('\n').trim() 43 | } 44 | 45 | export default wrap 46 | -------------------------------------------------------------------------------- /src/lib/getEnginesNodeFromRegistry.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from 'progress' 2 | import { Index } from '../types/IndexType' 3 | import { Options } from '../types/Options' 4 | import { Version } from '../types/Version' 5 | import { VersionSpec } from '../types/VersionSpec' 6 | import getPackageManager from './getPackageManager' 7 | 8 | /** 9 | * Get the engines.node versions from the NPM repository based on the version target. 10 | * 11 | * @param packageMap An object whose keys are package name and values are version 12 | * @param [options={}] Options. 13 | * @returns Promised {packageName: engines.node} collection 14 | */ 15 | async function getEnginesNodeFromRegistry(packageMap: Index, options: Options) { 16 | const packageManager = getPackageManager(options, options.packageManager) 17 | if (!packageManager.getEngines) return {} 18 | 19 | const numItems = Object.keys(packageMap).length 20 | let bar: ProgressBar 21 | if (!options.json && options.loglevel !== 'silent' && options.loglevel !== 'verbose' && numItems > 0) { 22 | bar = new ProgressBar('[:bar] :current/:total :percent', { total: numItems, width: 20 }) 23 | bar.render() 24 | } 25 | 26 | return Object.entries(packageMap).reduce(async (accumPromise, [pkg, version]) => { 27 | const enginesNode = (await packageManager.getEngines!(pkg, version, options)).node 28 | if (bar) { 29 | bar.tick() 30 | } 31 | const accum = await accumPromise 32 | return { ...accum, [pkg]: enginesNode } 33 | }, Promise.resolve>({})) 34 | } 35 | 36 | export default getEnginesNodeFromRegistry 37 | -------------------------------------------------------------------------------- /src/lib/getInstalledPackages.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | import { Options } from '../types/Options' 3 | import { Version } from '../types/Version' 4 | import { VersionSpec } from '../types/VersionSpec' 5 | import filterAndReject from './filterAndReject' 6 | import filterObject from './filterObject' 7 | import getPackageManager from './getPackageManager' 8 | import programError from './programError' 9 | import { isWildPart } from './version-util' 10 | 11 | /** 12 | * @param [options] 13 | * @param options.cwd 14 | * @param options.filter 15 | * @param options.global 16 | * @param options.packageManager 17 | * @param options.prefix 18 | * @param options.reject 19 | */ 20 | async function getInstalledPackages(options: Options = {}) { 21 | const packages = await getPackageManager(options, options.packageManager).list?.({ 22 | cwd: options.cwd, 23 | prefix: options.prefix, 24 | global: options.global, 25 | }) 26 | 27 | if (!packages) { 28 | programError(options, 'Unable to retrieve package list') 29 | } 30 | 31 | // filter out undefined packages or those with a wildcard 32 | const filterFunction = filterAndReject(options.filter, options.reject, options.filterVersion, options.rejectVersion) 33 | let filteredPackages: Index = {} 34 | try { 35 | filteredPackages = filterObject( 36 | packages, 37 | (dep: VersionSpec, version: Version) => !!version && !isWildPart(version) && filterFunction(dep, version), 38 | ) 39 | } catch (err: any) { 40 | programError(options, 'Invalid filter: ' + err.message || err) 41 | } 42 | 43 | return filteredPackages 44 | } 45 | 46 | export default getInstalledPackages 47 | -------------------------------------------------------------------------------- /src/package-managers/staticRegistry.ts: -------------------------------------------------------------------------------- 1 | import memoize from 'fast-memoize' 2 | import fs from 'fs/promises' 3 | import programError from '../lib/programError' 4 | import { GetVersion } from '../types/GetVersion' 5 | import { Options } from '../types/Options' 6 | import { StaticRegistry } from '../types/StaticRegistry' 7 | import { Version } from '../types/Version' 8 | 9 | /** Returns true if a string is a url. */ 10 | const isUrl = (s: string) => (s && s.startsWith('http://')) || s.startsWith('https://') 11 | 12 | /** 13 | * Returns a registry object given a valid file path or url. 14 | * 15 | * @param path 16 | * @returns a registry object 17 | */ 18 | const readStaticRegistry = async (options: Options): Promise => { 19 | const path = options.registry! 20 | let content: string 21 | 22 | // url 23 | if (isUrl(path)) { 24 | const body = await fetch(path) 25 | content = await body.text() 26 | } 27 | // local path 28 | else { 29 | try { 30 | content = await fs.readFile(path, 'utf8') 31 | } catch (err) { 32 | programError(options, `\nThe specified static registry file does not exist: ${options.registry}`) 33 | } 34 | } 35 | 36 | return JSON.parse(content) 37 | } 38 | 39 | const registryMemoized = memoize(readStaticRegistry) 40 | 41 | /** 42 | * Fetches the version in static registry. 43 | * 44 | * @param packageName 45 | * @param currentVersion 46 | * @param options 47 | * @returns A promise that fulfills to string value or null 48 | */ 49 | export const latest: GetVersion = async (packageName: string, currentVersion: Version, options?: Options) => { 50 | const registry: StaticRegistry = await registryMemoized(options || {}) 51 | return { version: registry[packageName] || null } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/keyValueBy.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | 3 | type KeyValueGenerator = (key: K, value: V, accum: Index) => Index | null 4 | type ArrayKeyValueGenerator = KeyValueGenerator 5 | type ObjectKeyValueGenerator = KeyValueGenerator 6 | 7 | export function keyValueBy(arr: T[]): Index 8 | export function keyValueBy(arr: T[], keyValue: KeyValueGenerator, initialValue?: Index): Index 9 | export function keyValueBy( 10 | obj: Index, 11 | keyValue: KeyValueGenerator, 12 | initialValue?: Index, 13 | ): Index 14 | 15 | /** Generates an object from an array or object. Simpler than reduce or _.transform. The KeyValueGenerator passes (key, value) if the input is an object, and (value, i) if it is an array. The return object from each iteration is merged into the accumulated object. Return null to skip an item. */ 16 | export function keyValueBy( 17 | input: T[] | Index, 18 | // if no keyValue is given, sets all values to true 19 | keyValue?: ArrayKeyValueGenerator | ObjectKeyValueGenerator, 20 | accum: Index = {}, 21 | ): Index { 22 | const isArray = Array.isArray(input) 23 | keyValue = keyValue || ((key: T): Index => ({ [key as unknown as string]: true as unknown as R })) 24 | // considerably faster than Array.prototype.reduce 25 | Object.entries(input || {}).forEach(([key, value], i) => { 26 | const o = isArray 27 | ? (keyValue as ArrayKeyValueGenerator)(value, i, accum) 28 | : (keyValue as ObjectKeyValueGenerator)(key, value, accum) 29 | Object.entries(o || {}).forEach(entry => { 30 | accum[entry[0]] = entry[1] 31 | }) 32 | }) 33 | 34 | return accum 35 | } 36 | 37 | export default keyValueBy 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | mocha: true, 5 | node: true, 6 | }, 7 | extends: ['standard', 'eslint:recommended', 'plugin:import/typescript', 'raine', 'prettier'], 8 | overrides: [ 9 | { 10 | files: ['**/*.ts'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: 'module', 15 | project: './tsconfig.json', 16 | }, 17 | extends: ['plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended'], 18 | globals: { 19 | Atomics: 'readonly', 20 | SharedArrayBuffer: 'readonly', 21 | }, 22 | plugins: ['@typescript-eslint'], 23 | rules: { 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | '@typescript-eslint/no-non-null-assertion': 'off', 26 | '@typescript-eslint/no-use-before-define': 'error', 27 | '@typescript-eslint/no-unused-vars': [ 28 | 'error', 29 | { 30 | caughtErrors: 'none', 31 | // using destructuring to omit properties from objects 32 | destructuredArrayIgnorePattern: '^_', 33 | argsIgnorePattern: '^_', 34 | varsIgnorePattern: '^_', 35 | }, 36 | ], 37 | '@typescript-eslint/array-type': [ 38 | 'error', 39 | { 40 | default: 'array', 41 | }, 42 | ], 43 | }, 44 | }, 45 | ], 46 | plugins: ['jsdoc'], 47 | rules: { 48 | 'jsdoc/require-jsdoc': [ 49 | 'error', 50 | { 51 | contexts: ['VariableDeclarator > ArrowFunctionExpression'], 52 | require: { 53 | ClassDeclaration: true, 54 | ClassExpression: true, 55 | }, 56 | }, 57 | ], 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /test/package-managers/npm/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as npm from '../../../src/package-managers/npm' 2 | import chaiSetup from '../../helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('npm', function () { 7 | it('list', async () => { 8 | const versionObject = await npm.list({ cwd: __dirname }) 9 | versionObject.should.have.property('express') 10 | }) 11 | 12 | it('latest', async () => { 13 | const { version } = await npm.latest('express', '', { cwd: __dirname }) 14 | parseInt(version!, 10).should.be.above(1) 15 | }) 16 | 17 | it('greatest', async () => { 18 | const { version } = await npm.greatest('ncu-test-greatest-not-newest', '', { pre: true, cwd: __dirname }) 19 | version!.should.equal('2.0.0-beta') 20 | }) 21 | 22 | it('ownerChanged', async () => { 23 | await npm.packageAuthorChanged('mocha', '^7.1.0', '8.0.1').should.eventually.equal(true) 24 | await npm.packageAuthorChanged('htmlparser2', '^3.10.1', '^4.0.0').should.eventually.equal(false) 25 | await npm.packageAuthorChanged('ncu-test-v2', '^1.0.0', '2.2.0').should.eventually.equal(false) 26 | }) 27 | 28 | it('getPeerDependencies', async () => { 29 | await npm.getPeerDependencies('ncu-test-return-version', '1.0.0').should.eventually.deep.equal({}) 30 | await npm.getPeerDependencies('ncu-test-peer', '1.0.0').should.eventually.deep.equal({ 31 | 'ncu-test-return-version': '1.x', 32 | }) 33 | }) 34 | 35 | it('getEngines', async () => { 36 | await npm.getEngines('del', '2.0.0').should.eventually.deep.equal({ node: '>=0.10.0' }) 37 | await npm.getEngines('ncu-test-return-version', '1.0.0').should.eventually.deep.equal({}) 38 | await npm 39 | .getEngines('ncu-test-return-version', '1.0') 40 | .should.eventually.be.rejectedWith('404 Not Found - GET https://registry.npmjs.org/ncu-test-return-version/1.0') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/getIgnoredUpgradesDueToEnginesNode.test.ts: -------------------------------------------------------------------------------- 1 | import getIgnoredUpgradesDueToEnginesNode from '../src/lib/getIgnoredUpgradesDueToEnginesNode' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | const MOCK_ESLINT_VERSION = '999.0.0' 5 | const MOCK_DEL_VERSION = '999.0.1' 6 | 7 | chaiSetup() 8 | 9 | /* This test needs to be rewritten because it is run against live data that affects the outcome of the test. The eslint and del versions were mocked in order to prevent this, but now the latest del.enginesNode is >=18 which fails the test. This data should either be mocked or the target packages should be entirely replaced by packages under our control. */ 10 | describe.skip('getIgnoredUpgradesDueToEnginesNode', function () { 11 | it('ncu-test-peer-update', async () => { 12 | const data = await getIgnoredUpgradesDueToEnginesNode( 13 | { 14 | 'ncu-test-return-version': '1.0.0', 15 | 'ncu-test-peer': '^1.0.0', 16 | del: '2.2.2', 17 | '@typescript-eslint/eslint-plugin': '^7.18.0', 18 | }, 19 | { 20 | 'ncu-test-return-version': '2.0.0', 21 | 'ncu-test-peer': '^1.1.0', 22 | del: '2.2.2', 23 | '@typescript-eslint/eslint-plugin': '^8.1.0', 24 | }, 25 | { 26 | enginesNode: true, 27 | nodeEngineVersion: `^0.10.0`, 28 | }, 29 | ) 30 | 31 | // override 'to' fields with mock versions since this is live npm data that will change 32 | data['@typescript-eslint/eslint-plugin'].to = MOCK_ESLINT_VERSION 33 | data.del.to = MOCK_DEL_VERSION 34 | 35 | data.should.deep.equal({ 36 | '@typescript-eslint/eslint-plugin': { 37 | enginesNode: '^18.18.0 || ^20.9.0 || >=21.1.0', 38 | from: '^7.18.0', 39 | to: MOCK_ESLINT_VERSION, 40 | }, 41 | del: { 42 | enginesNode: '>=14.16', 43 | from: '2.2.2', 44 | to: MOCK_DEL_VERSION, 45 | }, 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Filing an issue 2 | 3 | Make sure you read the list of [known issues](https://github.com/raineorshine/npm-check-updates#known-issues) and search for [similar issues](https://github.com/raineorshine/npm-check-updates/issues) before filing an issue. 4 | 5 | ## Known Issues 6 | 7 | - If `ncu` prints output that does not seem related to this package, it may be conflicting with another executable such as `ncu-weather-cli` or Nvidia CUDA. Try using the long name instead: `npm-check-updates`. 8 | - Windows: If npm-check-updates hangs, try setting the package file explicitly: `ncu --packageFile package.json`. You can run `ncu --loglevel verbose` to confirm that it was incorrectly waiting for stdin. See [#136](https://github.com/raineorshine/npm-check-updates/issues/136#issuecomment-155721102). 9 | 10 | When filing an issue, please include: 11 | 12 | - node version 13 | - npm version 14 | - npm-check-updates version 15 | - the relevant package names and their specified versions from your package file 16 | - ...or the output from `npm -g ls --depth=0` if using global mode 17 | 18 | ## Design Guidelines 19 | 20 | The _raison d'être_ of npm-check-updates is to upgrade package.json dependencies to the latest versions, ignoring specified versions. Suggested features that do not fit within this objective will be considered out of scope. 21 | 22 | npm-check-updates maintains a balance between minimalism and customizability. The default execution with no options will always produce simple, clean output. If you would like to add additional information to ncu's output, you may propose a new value for the `--format` option. 23 | 24 | ## Adding a new CLI or module option 25 | 26 | All of ncu's options are generated from [/src/cli-options.ts](https://github.com/raineorshine/npm-check-updates/blob/main/src/cli-options.ts). You can add a new option to this file and then run `npm run build` to automatically generate README, CLI help text, and Typescript definitions. 27 | -------------------------------------------------------------------------------- /test/filterResults.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import fs from 'fs/promises' 3 | import os from 'os' 4 | import path from 'path' 5 | import ncu from '../src/' 6 | import chaiSetup from './helpers/chaiSetup' 7 | import stubVersions from './helpers/stubVersions' 8 | 9 | chaiSetup() 10 | 11 | describe('filterResults', () => { 12 | it('should return only major versions updated', async () => { 13 | const dependencies = { 'ncu-test-v2': '2.0.0', 'ncu-test-return-version': '1.0.0', 'ncu-test-tag': '1.0.0' } 14 | const stub = stubVersions( 15 | { 16 | 'ncu-test-v2': '3.0.0', 17 | 'ncu-test-tag': '2.1.0', 18 | 'ncu-test-return-version': '1.2.0', 19 | }, 20 | { spawn: true }, 21 | ) 22 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 23 | const pkgFile = path.join(tempDir, 'package.json') 24 | await fs.writeFile( 25 | pkgFile, 26 | JSON.stringify({ 27 | dependencies, 28 | }), 29 | 'utf-8', 30 | ) 31 | 32 | try { 33 | const upgraded = await ncu({ 34 | packageFile: pkgFile, 35 | filterResults: ( 36 | packageName, 37 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 38 | { currentVersion, currentVersionSemver, upgradedVersion, upgradedVersionSemver }, 39 | ) => { 40 | const currentMajorVersion = currentVersionSemver?.[0]?.major 41 | const upgradedMajorVersion = upgradedVersionSemver?.major 42 | if (currentMajorVersion && upgradedMajorVersion) { 43 | return currentMajorVersion < upgradedMajorVersion 44 | } 45 | return true 46 | }, 47 | }) 48 | expect(upgraded).to.have.property('ncu-test-tag', '2.1.0') 49 | expect(upgraded).to.have.property('ncu-test-v2', '3.0.0') 50 | expect(upgraded).to.not.have.property('ncu-test-return-version') 51 | } finally { 52 | await fs.rm(tempDir, { recursive: true, force: true }) 53 | stub.restore() 54 | } 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/lib/findLockfile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { Options } from '../types/Options' 4 | 5 | /** 6 | * Goes up the filesystem tree until it finds a package-lock.json, yarn.lock, pnpm-lock.yaml, deno.json, deno.jsonc, or bun.lockb file. 7 | * 8 | * @param readdir This is only a parameter so that it can be used in tests. 9 | * @returns The path of the directory that contains the lockfile and the 10 | * filename of the lockfile. 11 | */ 12 | export default async function findLockfile( 13 | options: Pick, 14 | readdir: (_path: string) => Promise = fs.readdir, 15 | ): Promise<{ directoryPath: string; filename: string } | null> { 16 | try { 17 | // 1. explicit cwd 18 | // 2. same directory as package file 19 | // 3. current directory 20 | let currentPath = options.cwd ? options.cwd : options.packageFile ? path.dirname(options.packageFile) : '.' 21 | 22 | while (true) { 23 | const files = await readdir(currentPath) 24 | 25 | if (files.includes('package-lock.json')) { 26 | return { directoryPath: currentPath, filename: 'package-lock.json' } 27 | } else if (files.includes('yarn.lock')) { 28 | return { directoryPath: currentPath, filename: 'yarn.lock' } 29 | } else if (files.includes('pnpm-lock.yaml')) { 30 | return { directoryPath: currentPath, filename: 'pnpm-lock.yaml' } 31 | } else if (files.includes('deno.json')) { 32 | return { directoryPath: currentPath, filename: 'deno.json' } 33 | } else if (files.includes('deno.jsonc')) { 34 | return { directoryPath: currentPath, filename: 'deno.jsonc' } 35 | } else if (files.includes('bun.lockb')) { 36 | return { directoryPath: currentPath, filename: 'bun.lockb' } 37 | } 38 | 39 | const pathParent = path.resolve(currentPath, '..') 40 | if (pathParent === currentPath) break 41 | 42 | currentPath = pathParent 43 | } 44 | } catch (e) { 45 | // if readdirSync fails, return null 46 | } 47 | 48 | return null 49 | } 50 | -------------------------------------------------------------------------------- /test/getPreferredWildcard.test.ts: -------------------------------------------------------------------------------- 1 | import getPreferredWildcard from '../src/lib/getPreferredWildcard' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | const should = chaiSetup() 5 | 6 | describe('getPreferredWildcard', () => { 7 | it('identify ^ when it is preferred', () => { 8 | const deps = { 9 | async: '^0.9.0', 10 | bluebird: '^2.9.27', 11 | cint: '^8.2.1', 12 | commander: '~2.8.1', 13 | lodash: '^3.2.0', 14 | } 15 | getPreferredWildcard(deps)!.should.equal('^') 16 | }) 17 | 18 | it('identify ~ when it is preferred', () => { 19 | const deps = { 20 | async: '~0.9.0', 21 | bluebird: '~2.9.27', 22 | cint: '^8.2.1', 23 | commander: '~2.8.1', 24 | lodash: '^3.2.0', 25 | } 26 | getPreferredWildcard(deps)!.should.equal('~') 27 | }) 28 | 29 | it('identify .x when it is preferred', () => { 30 | const deps = { 31 | async: '0.9.x', 32 | bluebird: '2.9.x', 33 | cint: '^8.2.1', 34 | commander: '~2.8.1', 35 | lodash: '3.x', 36 | } 37 | getPreferredWildcard(deps)!.should.equal('.x') 38 | }) 39 | 40 | it('identify .* when it is preferred', () => { 41 | const deps = { 42 | async: '0.9.*', 43 | bluebird: '2.9.*', 44 | cint: '^8.2.1', 45 | commander: '~2.8.1', 46 | lodash: '3.*', 47 | } 48 | getPreferredWildcard(deps)!.should.equal('.*') 49 | }) 50 | 51 | it('do not allow wildcards to be outnumbered by non-wildcards', () => { 52 | const deps = { 53 | gulp: '^4.0.0', 54 | typescript: '3.3.0', 55 | webpack: '4.30.0', 56 | } 57 | getPreferredWildcard(deps)!.should.equal('^') 58 | }) 59 | 60 | it('use the first wildcard if there is a tie', () => { 61 | const deps = { 62 | async: '0.9.x', 63 | commander: '2.8.*', 64 | } 65 | getPreferredWildcard(deps)!.should.equal('.x') 66 | }) 67 | 68 | it('return null when it cannot be determined from other dependencies', () => { 69 | const deps = { 70 | async: '0.9.0', 71 | commander: '2.8.1', 72 | lodash: '3.2.0', 73 | } 74 | should.equal(getPreferredWildcard(deps), null) 75 | should.equal(getPreferredWildcard({}), null) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/github-urls.test.ts: -------------------------------------------------------------------------------- 1 | import ncu from '../src' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('github urls', () => { 7 | it('upgrade github https urls', async () => { 8 | const upgrades = await ncu({ 9 | packageData: { 10 | dependencies: { 11 | 'ncu-test-v2': 'https://github.com/raineorshine/ncu-test-v2#1.0.0', 12 | }, 13 | }, 14 | }) 15 | upgrades!.should.deep.equal({ 16 | 'ncu-test-v2': 'https://github.com/raineorshine/ncu-test-v2#2.0.0', 17 | }) 18 | }) 19 | 20 | it('upgrade short github urls', async () => { 21 | const upgrades = await ncu({ 22 | packageData: { 23 | dependencies: { 24 | 'ncu-test-v2': 'github:raineorshine/ncu-test-v2#1.0.0', 25 | }, 26 | }, 27 | }) 28 | upgrades!.should.deep.equal({ 29 | 'ncu-test-v2': 'github:raineorshine/ncu-test-v2#2.0.0', 30 | }) 31 | }) 32 | 33 | it('upgrade shortest github urls', async () => { 34 | const upgrades = await ncu({ 35 | packageData: { 36 | dependencies: { 37 | 'ncu-test-v2': 'raineorshine/ncu-test-v2#1.0.0', 38 | }, 39 | }, 40 | }) 41 | upgrades!.should.deep.equal({ 42 | 'ncu-test-v2': 'raineorshine/ncu-test-v2#2.0.0', 43 | }) 44 | }) 45 | 46 | it('upgrade github http urls with semver', async () => { 47 | const upgrades = await ncu({ 48 | packageData: { 49 | dependencies: { 50 | 'ncu-test-v2': 'https://github.com/raineorshine/ncu-test-v2#semver:^1.0.0', 51 | }, 52 | }, 53 | }) 54 | upgrades!.should.deep.equal({ 55 | 'ncu-test-v2': 'https://github.com/raineorshine/ncu-test-v2#semver:^2.0.0', 56 | }) 57 | }) 58 | 59 | // does not work in GitHub actions for some reason 60 | it.skip('upgrade github git+ssh urls with semver', async () => { 61 | const upgrades = await ncu({ 62 | packageData: { 63 | dependencies: { 64 | 'ncu-test-v2': 'git+ssh://git@github.com/raineorshine/ncu-test-v2.git#semver:^1.0.0', 65 | }, 66 | }, 67 | }) 68 | upgrades!.should.deep.equal({ 69 | 'ncu-test-v2': 'git+ssh://git@github.com/raineorshine/ncu-test-v2.git#semver:^2.0.0', 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/lib/isUpgradeable.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver' 2 | import semverutils from 'semver-utils' 3 | import { Version } from '../types/Version' 4 | import { VersionSpec } from '../types/VersionSpec' 5 | import { fixPseudoVersion, isComparable, isWildCard, stringify } from './version-util' 6 | 7 | /** 8 | * Check if a version satisfies the latest, and is not beyond the latest). Ignores `v` prefix. 9 | * 10 | * @param current 11 | * @param latest 12 | * @param downgrade Allow downgrading 13 | * @returns 14 | */ 15 | function isUpgradeable(current: VersionSpec, latest: Version, { downgrade }: { downgrade?: boolean } = {}): boolean { 16 | // do not upgrade non-npm version declarations (such as git tags) 17 | // do not upgrade wildcards 18 | if (!semver.validRange(current) || isWildCard(current)) { 19 | return false 20 | } 21 | 22 | // remove the constraint (e.g. ^1.0.1 -> 1.0.1) to allow upgrades that satisfy the range, but are out of date 23 | const [range] = semverutils.parseRange(current) 24 | if (!range) { 25 | throw new Error( 26 | `"${current}" could not be parsed by semver-utils. This is probably a bug. Please file an issue at https://github.com/raineorshine/npm-check-updates.`, 27 | ) 28 | } 29 | 30 | // allow upgrading of pseudo versions such as "v1" or "1.0" 31 | const latestNormalized = fixPseudoVersion(latest) 32 | 33 | const version = stringify(range) 34 | const isValidCurrent = Boolean(semver.validRange(version)) 35 | const isValidLatest = Boolean(semver.valid(latestNormalized)) 36 | 37 | // make sure it is a valid range 38 | // not upgradeable if the latest version satisfies the current range 39 | // not upgradeable if the specified version is newer than the latest (indicating a prerelease version) 40 | // NOTE: When "<" is specified with a single digit version, e.g. "<7", and has the same major version as the latest, e.g. "7", isSatisfied(latest, version) will return true since it ignores the "<". In this case, test the original range (current) rather than the versionUtil output (version). 41 | return ( 42 | isValidCurrent && 43 | isValidLatest && 44 | // allow an upgrade if two prerelease versions can't be compared by semver 45 | (!isComparable(latestNormalized, version) || 46 | (!semver.satisfies(latestNormalized, range.operator === '<' ? current : version) && 47 | (downgrade || !semver.ltr(latestNormalized, version)))) 48 | ) 49 | } 50 | 51 | export default isUpgradeable 52 | -------------------------------------------------------------------------------- /src/lib/libnpmconfig/README.md: -------------------------------------------------------------------------------- 1 | # libnpmconfig 2 | 3 | [![npm version](https://img.shields.io/npm/v/libnpmconfig.svg)](https://npm.im/libnpmconfig) 4 | [![license](https://img.shields.io/npm/l/libnpmconfig.svg)](https://npm.im/libnpmconfig) 5 | [![Travis](https://img.shields.io/travis/npm/libnpmconfig.svg)](https://travis-ci.org/npm/libnpmconfig) 6 | [![Coverage Status](https://coveralls.io/repos/github/npm/libnpmconfig/badge.svg?branch=latest)](https://coveralls.io/github/npm/libnpmconfig?branch=latest) 7 | 8 | [`libnpmconfig`](https://github.com/npm/libnpmconfig) is a Node.js library for 9 | programmatically managing npm's configuration files and data. 10 | 11 | ## Table of Contents 12 | 13 | - [Example](#example) 14 | - [Install](#install) 15 | - [Contributing](#contributing) 16 | - [API](#api) 17 | 18 | ## Example 19 | 20 | ```js 21 | const config = require('libnpmconfig') 22 | 23 | console.log( 24 | 'configured registry:', 25 | config.read({ 26 | registry: 'https://default.registry/', 27 | }), 28 | ) 29 | // => configured registry: https://registry.npmjs.org 30 | ``` 31 | 32 | ## Install 33 | 34 | `$ npm install libnpmconfig` 35 | 36 | ### Contributing 37 | 38 | The npm team enthusiastically welcomes contributions and project participation! 39 | There's a bunch of things you can do if you want to contribute! The 40 | [Contributor Guide](https://github.com/npm/cli/blob/latest/CONTRIBUTING.md) 41 | outlines the process for community interaction and contribution. Please don't 42 | hesitate to jump in if you'd like to, or even ask us questions if something 43 | isn't clear. 44 | 45 | All participants and maintainers in this project are expected to follow the 46 | [npm Code of Conduct](https://www.npmjs.com/policies/conduct), and just 47 | generally be excellent to each other. 48 | 49 | Please refer to the [Changelog](CHANGELOG.md) for project history details, too. 50 | 51 | Happy hacking! 52 | 53 | ### API 54 | 55 | #### `read(cliOpts, builtinOpts)` 56 | 57 | Reads configurations from the filesystem and the env and returns a 58 | [`figgy-pudding`](https://npm.im/figgy-pudding) object with the configuration 59 | values. 60 | 61 | If `cliOpts` is provided, it will be merged with the returned config pudding, 62 | shadowing any read values. These are intended as CLI-provided options. Do your 63 | own `process.argv` parsing, though. 64 | 65 | If `builtinOpts.cwd` is provided, it will be used instead of `process.cwd()` as 66 | the starting point for config searching. 67 | -------------------------------------------------------------------------------- /src/lib/upgradePackageData.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '../types/IndexType' 2 | import { Options } from '../types/Options' 3 | import { PackageFile } from '../types/PackageFile' 4 | import { VersionSpec } from '../types/VersionSpec' 5 | import resolveDepSections from './resolveDepSections' 6 | 7 | /** 8 | * @returns String safe for use in `new RegExp()` 9 | */ 10 | function escapeRegexp(s: string) { 11 | return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') // Thanks Stack Overflow! 12 | } 13 | 14 | /** 15 | * Upgrade the dependency declarations in the package data. 16 | * 17 | * @param pkgData The package.json data, as utf8 text 18 | * @param oldDependencies Old dependencies {package: range} 19 | * @param newDependencies New dependencies {package: range} 20 | * @returns The updated package data, as utf8 text 21 | * @description Side Effect: prompts 22 | */ 23 | async function upgradePackageData( 24 | pkgData: string, 25 | current: Index, 26 | upgraded: Index, 27 | options: Options, 28 | ) { 29 | // Always include overrides since any upgraded dependencies needed to be upgraded in overrides as well. 30 | // https://github.com/raineorshine/npm-check-updates/issues/1332 31 | const depSections = [...resolveDepSections(options.dep), 'overrides'] 32 | 33 | // iterate through each dependency section 34 | const sectionRegExp = new RegExp(`"(${depSections.join(`|`)})"s*:[^}]*`, 'g') 35 | let newPkgData = pkgData.replace(sectionRegExp, section => { 36 | // replace each upgraded dependency in the section 37 | Object.keys(upgraded).forEach(dep => { 38 | // const expression = `"${dep}"\\s*:\\s*"(${escapeRegexp(current[dep])})"` 39 | const expression = `"${dep}"\\s*:\\s*("|{\\s*"."\\s*:\\s*")(${escapeRegexp(current[dep])})"` 40 | const regExp = new RegExp(expression, 'g') 41 | section = section.replace(regExp, (match, child) => `"${dep}${child ? `": ${child}` : ': '}${upgraded[dep]}"`) 42 | }) 43 | 44 | return section 45 | }) 46 | 47 | if (depSections.includes('packageManager')) { 48 | const pkg = JSON.parse(pkgData) as PackageFile 49 | if (pkg.packageManager) { 50 | const [name] = pkg.packageManager.split('@') 51 | if (upgraded[name]) { 52 | newPkgData = newPkgData.replace( 53 | /"packageManager"\s*:\s*".*?@[^"]*"/, 54 | `"packageManager": "${name}@${upgraded[name]}"`, 55 | ) 56 | } 57 | } 58 | } 59 | 60 | return newPkgData 61 | } 62 | 63 | export default upgradePackageData 64 | -------------------------------------------------------------------------------- /src/lib/chalk.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This chalk wrapper allows synchronous chalk.COLOR(...) syntax with special support for: 4 | 5 | 1) dynamic import as pure ESM module 6 | 2) force color on all instances 7 | 3) disable color on all instances 8 | 9 | Call await chalkInit(color) at the beginning of execution and the chalk instance will be available everywhere. 10 | 11 | It is a hacky solution, but it is the easiest way to import and pass the color option to all chalk instances without brutalizing the syntax. 12 | 13 | */ 14 | import keyValueBy from './keyValueBy' 15 | 16 | type ChalkMethod = ((s: any) => string) & { bold: (s: any) => string } 17 | 18 | const chalkMethods = { 19 | blue: true, 20 | bold: true, 21 | cyan: true, 22 | gray: true, 23 | green: true, 24 | magenta: true, 25 | red: true, 26 | yellow: true, 27 | } 28 | 29 | // A chalk instance that passes strings through as-is, without color. Used with color: null. */ 30 | const chalkNoop = keyValueBy(chalkMethods, name => ({ [name]: (s: any) => s.toString() })) as Record< 31 | keyof typeof chalkMethods, 32 | ChalkMethod 33 | > 34 | 35 | // a global Promise of a chalk instance that can optionally force or ignore color 36 | let chalkInstance: Record 37 | 38 | /** Initializes the global chalk instance with an optional flag for forced color. Idempotent. */ 39 | export const chalkInit = async (color?: boolean | null) => { 40 | const chalkModule = await import('chalk') 41 | const { default: chalkDefault, Chalk } = chalkModule 42 | chalkInstance = color === true ? new Chalk({ level: 1 }) : color === null ? chalkNoop : chalkDefault 43 | } 44 | 45 | /** Asserts that chalk has been imported. */ 46 | const assertChalk = () => { 47 | if (!chalkInstance) { 48 | throw new Error( 49 | `Chalk has not been imported yet. Chalk is a dynamic import and requires that you await { chalkInit } from './lib/chalk'.`, 50 | ) 51 | } 52 | } 53 | 54 | // generate an async method for each chalk method that calls a chalk instance with global.color for forced color 55 | const chalkGlobal = keyValueBy(chalkMethods, name => { 56 | /** Chained bold method. */ 57 | const bold = (s: any) => { 58 | assertChalk() 59 | return chalkInstance[name as keyof typeof chalkInstance].bold(s) 60 | } 61 | /** Chalk method. */ 62 | const method = (s: any) => { 63 | assertChalk() 64 | return chalkInstance[name as keyof typeof chalkInstance](s) 65 | } 66 | method.bold = bold 67 | return { 68 | [name]: method, 69 | } 70 | }) as Record 71 | 72 | export default chalkGlobal 73 | -------------------------------------------------------------------------------- /test/registryType.test.ts: -------------------------------------------------------------------------------- 1 | import ncu from '../src/index' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('staticRegistry', function () { 7 | it('upgrade to the version specified in the static registry file', async () => { 8 | const output = await ncu({ 9 | packageData: { 10 | dependencies: { 11 | 'ncu-test-v2': '1.0.0', 12 | }, 13 | }, 14 | registryType: 'json', 15 | registry: './test/test-data/registry.json', 16 | }) 17 | 18 | output!.should.deep.equal({ 19 | 'ncu-test-v2': '99.9.9', 20 | }) 21 | }) 22 | 23 | it('ignore dependencies that are not in the static registry', async () => { 24 | const output = await ncu({ 25 | packageData: { 26 | dependencies: { 27 | 'ncu-test-tag': '1.0.0', 28 | }, 29 | }, 30 | registryType: 'json', 31 | registry: './test/test-data/registry.json', 32 | }) 33 | 34 | output!.should.deep.equal({}) 35 | }) 36 | 37 | it('fetch static registry from a url', async () => { 38 | const output = await ncu({ 39 | packageData: { 40 | dependencies: { 41 | 'ncu-test-tag': '1.0.0', 42 | }, 43 | }, 44 | registryType: 'json', 45 | registry: 46 | // https://gist.github.com/raineorshine/0802d7388c69193bed49c5ee6ab611b9 47 | 'https://gist.githubusercontent.com/raineorshine/0802d7388c69193bed49c5ee6ab611b9/raw/6f22bfdf19b7596089e56e0b14cd66d077f049d5/staticRegistry.json', 48 | }) 49 | 50 | output!.should.deep.equal({}) 51 | }) 52 | 53 | it('infer registryType json when --registry file path ends in .json', async () => { 54 | const output = await ncu({ 55 | packageData: { 56 | dependencies: { 57 | 'ncu-test-v2': '1.0.0', 58 | }, 59 | }, 60 | registry: './test/test-data/registry.json', 61 | }) 62 | 63 | output!.should.deep.equal({ 64 | 'ncu-test-v2': '99.9.9', 65 | }) 66 | }) 67 | 68 | it('infer registryType json when --registry url ends in .json', async () => { 69 | const output = await ncu({ 70 | packageData: { 71 | dependencies: { 72 | 'ncu-test-v2': '1.0.0', 73 | }, 74 | }, 75 | registry: 76 | // https://gist.github.com/raineorshine/0802d7388c69193bed49c5ee6ab611b9 77 | 'https://gist.githubusercontent.com/raineorshine/0802d7388c69193bed49c5ee6ab611b9/raw/6f22bfdf19b7596089e56e0b14cd66d077f049d5/staticRegistry.json', 78 | }) 79 | 80 | output!.should.deep.equal({ 81 | 'ncu-test-v2': '99.9.9', 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/lib/getIgnoredUpgradesDueToEnginesNode.ts: -------------------------------------------------------------------------------- 1 | import { minVersion, satisfies } from 'semver' 2 | import { IgnoredUpgradeDueToEnginesNode } from '../types/IgnoredUpgradeDueToEnginesNode' 3 | import { Index } from '../types/IndexType' 4 | import { Maybe } from '../types/Maybe' 5 | import { Options } from '../types/Options' 6 | import { Version } from '../types/Version' 7 | import { VersionSpec } from '../types/VersionSpec' 8 | import getEnginesNodeFromRegistry from './getEnginesNodeFromRegistry' 9 | import keyValueBy from './keyValueBy' 10 | import upgradePackageDefinitions from './upgradePackageDefinitions' 11 | 12 | /** Checks if package.json min node version satisfies given package engine.node spec */ 13 | const satisfiesNodeEngine = (enginesNode: Maybe, optionsEnginesNodeMinVersion: Version) => 14 | !enginesNode || satisfies(optionsEnginesNodeMinVersion, enginesNode) 15 | 16 | /** Get all upgrades that are ignored due to incompatible engines.node. */ 17 | export async function getIgnoredUpgradesDueToEnginesNode( 18 | current: Index, 19 | upgraded: Index, 20 | options: Options = {}, 21 | ) { 22 | if (!options.nodeEngineVersion) return {} 23 | const optionsEnginesNodeMinVersion = minVersion(options.nodeEngineVersion)?.version 24 | if (!optionsEnginesNodeMinVersion) return {} 25 | const [upgradedLatestVersions, latestVersionResults] = await upgradePackageDefinitions(current, { 26 | ...options, 27 | enginesNode: false, 28 | nodeEngineVersion: undefined, 29 | loglevel: 'silent', 30 | }) 31 | 32 | // Use the latest versions since getEnginesNodeFromRegistry requires exact versions. 33 | // Filter down to only the upgraded latest versions, as there is no point in checking the engines.node for packages that have been filtered out, e.g. by options.minimal or options.filterResults. 34 | const latestVersions = keyValueBy(latestVersionResults, (dep, result) => 35 | upgradedLatestVersions[dep] && result?.version 36 | ? { 37 | [dep]: result.version, 38 | } 39 | : null, 40 | ) 41 | const enginesNodes = await getEnginesNodeFromRegistry(latestVersions, options) 42 | return Object.entries(upgradedLatestVersions) 43 | .filter( 44 | ([pkgName, newVersion]) => 45 | upgraded[pkgName] !== newVersion && !satisfiesNodeEngine(enginesNodes[pkgName], optionsEnginesNodeMinVersion), 46 | ) 47 | .reduce( 48 | (accum, [pkgName, newVersion]) => ({ 49 | ...accum, 50 | [pkgName]: { 51 | from: current[pkgName], 52 | to: newVersion, 53 | enginesNode: enginesNodes[pkgName]!, 54 | }, 55 | }), 56 | {} as Index, 57 | ) 58 | } 59 | 60 | export default getIgnoredUpgradesDueToEnginesNode 61 | -------------------------------------------------------------------------------- /src/lib/getRepoUrl.ts: -------------------------------------------------------------------------------- 1 | import hostedGitInfo from 'hosted-git-info' 2 | import { URL } from 'url' 3 | import { PackageFile } from '../types/PackageFile' 4 | import { PackageFileRepository } from '../types/PackageFileRepository' 5 | import getPackageJson from './getPackageJson' 6 | 7 | /** Gets the repo url of an installed package. */ 8 | async function getPackageRepo( 9 | packageName: string, 10 | { 11 | pkgFile, 12 | }: { 13 | /** Specify the package file location to add to the node_modules search paths. Needed in workspaces/deep mode. */ 14 | pkgFile?: string 15 | } = {}, 16 | ): Promise { 17 | const packageJson = await getPackageJson(packageName, { pkgFile }) 18 | return packageJson?.repository ?? null 19 | } 20 | 21 | /** 22 | * @param packageName A package name as listed in package.json's dependencies list 23 | * @param packageJson Optional param to specify a object representation of a package.json file instead of loading from node_modules 24 | * @returns A valid url to the root of the package's source or null if a url could not be determined 25 | */ 26 | async function getRepoUrl( 27 | packageName: string, 28 | packageJson?: PackageFile, 29 | { 30 | pkgFile, 31 | }: { 32 | /** See: getPackageRepo pkgFile param. */ 33 | pkgFile?: string 34 | } = {}, 35 | ) { 36 | const repositoryMetadata: string | PackageFileRepository | null = !packageJson 37 | ? await getPackageRepo(packageName, { pkgFile }) 38 | : packageJson.repository 39 | ? packageJson.repository 40 | : null 41 | 42 | if (!repositoryMetadata) return null 43 | 44 | let gitURL 45 | let directory = '' 46 | 47 | // It may be a string instead of an object 48 | if (typeof repositoryMetadata === 'string') { 49 | gitURL = repositoryMetadata 50 | try { 51 | // It may already be a valid Repo URL 52 | const url = new URL(gitURL) 53 | // Some packages put a full URL in this field although it's not spec compliant. Let's detect that and use it if present 54 | if (url.protocol === 'https:' || url.protocol === 'http:') { 55 | return gitURL 56 | } 57 | } catch (e) {} 58 | } else if (typeof repositoryMetadata.url === 'string') { 59 | gitURL = repositoryMetadata.url 60 | if (typeof repositoryMetadata.directory === 'string') { 61 | directory = repositoryMetadata.directory 62 | } 63 | } 64 | 65 | if (typeof gitURL === 'string' && typeof directory === 'string') { 66 | const hostedGitURL = hostedGitInfo.fromUrl(gitURL)?.browse(directory) 67 | if (hostedGitURL !== undefined) { 68 | // Remove the default branch path (/tree/HEAD) from a git url 69 | return hostedGitURL.replace(/\/$/, '').replace(/\/tree\/HEAD$/, '') 70 | } 71 | return gitURL 72 | } 73 | return null 74 | } 75 | 76 | export default getRepoUrl 77 | -------------------------------------------------------------------------------- /src/lib/getPeerDependenciesFromRegistry.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from 'progress' 2 | import { Index } from '../types/IndexType' 3 | import { Options } from '../types/Options' 4 | import { Version } from '../types/Version' 5 | import getPackageManager from './getPackageManager' 6 | 7 | type CircularData = 8 | | { 9 | isCircular: true 10 | offendingPackage: string 11 | } 12 | | { 13 | isCircular: false 14 | } 15 | 16 | /** 17 | * Checks if the specified package will create a loop of peer dependencies by traversing all paths to find a cycle 18 | * 19 | * If a cycle was found, the offending peer dependency of the specified package is returned 20 | */ 21 | function isCircularPeer(peerDependencies: Index>, packageName: string): CircularData { 22 | let queue = [[packageName]] 23 | while (queue.length > 0) { 24 | const nextQueue: string[][] = [] 25 | for (const path of queue) { 26 | const parents = Object.keys(peerDependencies[path[0]] ?? {}) 27 | for (const name of parents) { 28 | if (name === path.at(-1)) { 29 | return { 30 | isCircular: true, 31 | offendingPackage: path[0], 32 | } 33 | } 34 | nextQueue.push([name, ...path]) 35 | } 36 | } 37 | queue = nextQueue 38 | } 39 | return { 40 | isCircular: false, 41 | } 42 | } 43 | 44 | /** 45 | * Get the latest or greatest versions from the NPM repository based on the version target. 46 | * 47 | * @param packageMap An object whose keys are package name and values are version 48 | * @param [options={}] Options. 49 | * @returns Promised {packageName: peer dependencies} collection 50 | */ 51 | async function getPeerDependenciesFromRegistry(packageMap: Index, options: Options) { 52 | const packageManager = getPackageManager(options, options.packageManager) 53 | if (!packageManager.getPeerDependencies) return {} 54 | 55 | const numItems = Object.keys(packageMap).length 56 | let bar: ProgressBar 57 | if (!options.json && options.loglevel !== 'silent' && options.loglevel !== 'verbose' && numItems > 0) { 58 | bar = new ProgressBar('[:bar] :current/:total :percent', { total: numItems, width: 20 }) 59 | bar.render() 60 | } 61 | 62 | return Object.entries(packageMap).reduce(async (accumPromise, [pkg, version]) => { 63 | const dep = await packageManager.getPeerDependencies!(pkg, version) 64 | if (bar) { 65 | bar.tick() 66 | } 67 | const accum = await accumPromise 68 | const newAcc: Index> = { ...accum, [pkg]: dep } 69 | const circularData = isCircularPeer(newAcc, pkg) 70 | if (circularData.isCircular) { 71 | delete newAcc[pkg][circularData.offendingPackage] 72 | } 73 | return newAcc 74 | }, Promise.resolve>>({})) 75 | } 76 | 77 | export default getPeerDependenciesFromRegistry 78 | -------------------------------------------------------------------------------- /test/upgradeDependencies.test.ts: -------------------------------------------------------------------------------- 1 | import upgradeDependencies from '../src/lib/upgradeDependencies' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | describe('upgradeDependencies', () => { 7 | it('upgrade simple, non-semver versions', () => { 8 | upgradeDependencies({ foo: '1' }, { foo: '2' }).should.eql({ foo: '2' }) 9 | upgradeDependencies({ foo: '1.0' }, { foo: '1.1' }).should.eql({ foo: '1.1' }) 10 | upgradeDependencies({ 'ncu-test-simple-tag': 'v1' }, { 'ncu-test-simple-tag': 'v3' }).should.eql({ 11 | 'ncu-test-simple-tag': 'v3', 12 | }) 13 | }) 14 | 15 | it('upgrade github dependencies', () => { 16 | upgradeDependencies({ foo: 'github:foo/bar#v1' }, { foo: 'github:foo/bar#v2' }).should.eql({ 17 | foo: 'github:foo/bar#v2', 18 | }) 19 | upgradeDependencies({ foo: 'github:foo/bar#v1.0' }, { foo: 'github:foo/bar#v2.0' }).should.eql({ 20 | foo: 'github:foo/bar#v2.0', 21 | }) 22 | upgradeDependencies({ foo: 'github:foo/bar#v1.0.0' }, { foo: 'github:foo/bar#v2.0.0' }).should.eql({ 23 | foo: 'github:foo/bar#v2.0.0', 24 | }) 25 | }) 26 | 27 | it('upgrade latest versions that already satisfy the specified version', () => { 28 | upgradeDependencies({ mongodb: '^1.0.0' }, { mongodb: '1.4.30' }).should.eql({ 29 | mongodb: '^1.4.30', 30 | }) 31 | }) 32 | 33 | it('do not downgrade', () => { 34 | upgradeDependencies({ mongodb: '^2.0.7' }, { mongodb: '1.4.30' }).should.eql({}) 35 | }) 36 | 37 | it('allow to update to latest via @latest tag', () => { 38 | upgradeDependencies({ mongodb: '^1.5.0-alpha.1' }, { mongodb: '1.4.30' }, { target: '@latest' }).should.eql({ 39 | mongodb: '^1.4.30', 40 | }) 41 | }) 42 | 43 | it('use the preferred wildcard when converting <, closed, or mixed ranges', () => { 44 | upgradeDependencies({ a: '1.*', mongodb: '<1.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '3.*' }) 45 | upgradeDependencies({ a: '1.x', mongodb: '<1.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '3.x' }) 46 | upgradeDependencies({ a: '~1', mongodb: '<1.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '~3.0' }) 47 | upgradeDependencies({ a: '^1', mongodb: '<1.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '^3.0' }) 48 | 49 | upgradeDependencies({ a: '1.*', mongodb: '1.0 < 2.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '3.*' }) 50 | upgradeDependencies({ mongodb: '1.0 < 2.*' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '3.*' }) 51 | }) 52 | 53 | it('convert closed ranges to caret (^) when preferred wildcard is unknown', () => { 54 | upgradeDependencies({ mongodb: '1.0 < 2.0' }, { mongodb: '3.0.0' }).should.eql({ mongodb: '^3.0' }) 55 | }) 56 | 57 | it('ignore packages with empty values', () => { 58 | upgradeDependencies({ mongodb: null }, { mongodb: '1.4.30' }).should.eql({}) 59 | upgradeDependencies({ mongodb: '' }, { mongodb: '1.4.30' }).should.eql({}) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/lib/getNcuRc.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import path from 'path' 3 | import { rcFile } from 'rc-config-loader' 4 | import { cliOptionsMap } from '../cli-options' 5 | import { Options } from '../types/Options' 6 | import { RcOptions } from '../types/RcOptions' 7 | import programError from './programError' 8 | 9 | /** Loads the .ncurc config file. */ 10 | async function getNcuRc({ 11 | configFileName, 12 | configFilePath, 13 | packageFile, 14 | global, 15 | options, 16 | }: { 17 | configFileName?: string 18 | configFilePath?: string 19 | /** If true, does not look in package directory. */ 20 | global?: boolean 21 | packageFile?: string 22 | options: Options 23 | }) { 24 | const { default: chalkDefault, Chalk } = await import('chalk') 25 | const chalk = options?.color ? new Chalk({ level: 1 }) : chalkDefault 26 | 27 | const rawResult = rcFile('ncurc', { 28 | configFileName: configFileName || '.ncurc', 29 | defaultExtension: ['.json', '.yml', '.js'], 30 | cwd: configFilePath || (global ? os.homedir() : packageFile ? path.dirname(packageFile) : undefined), 31 | }) 32 | 33 | // ensure a file was found if expected 34 | const filePath = rawResult?.filePath 35 | if (configFileName && !filePath) { 36 | programError(options, `Config file ${configFileName} not found in ${configFilePath || process.cwd()}`) 37 | } 38 | 39 | // convert the config to valid options by removing $schema and parsing format 40 | const { $schema: _, ...rawConfig } = rawResult?.config || {} 41 | const config: Options = rawConfig 42 | if (typeof config.format === 'string') config.format = cliOptionsMap.format.parse!(config.format) 43 | 44 | // validate arguments here to provide a better error message 45 | const unknownOptions = Object.keys(config).filter(arg => !cliOptionsMap[arg]) 46 | if (unknownOptions.length > 0) { 47 | console.error( 48 | chalk.red(`Unknown option${unknownOptions.length === 1 ? '' : 's'} found in config file:`), 49 | chalk.gray(unknownOptions.join(', ')), 50 | ) 51 | console.info('Using config file ' + filePath) 52 | console.info(`You can change the config file path with ${chalk.blue('--configFilePath')}`) 53 | } 54 | 55 | // flatten config object into command line arguments to be read by commander 56 | const args = Object.entries(config).flatMap(([name, value]): any[] => { 57 | // render boolean options as a single parameter 58 | // an option is considered boolean if its type is explicitly set to boolean, or if it is has a proper Javascript boolean value 59 | if (typeof value === 'boolean' || cliOptionsMap[name]?.type === 'boolean') { 60 | // if the boolean option is true, include only the nullary option --${name}, otherwise exclude it 61 | return value ? [`--${name}`] : [] 62 | } 63 | // otherwise render as a 2-tuple with name and value 64 | return [`--${name}`, value] 65 | }) 66 | 67 | return { filePath, args, config } 68 | } 69 | 70 | export default getNcuRc 71 | -------------------------------------------------------------------------------- /src/package-managers/bun.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import spawn from 'spawn-please' 3 | import keyValueBy from '../lib/keyValueBy' 4 | import { Index } from '../types/IndexType' 5 | import { NpmOptions } from '../types/NpmOptions' 6 | import { Options } from '../types/Options' 7 | import { SpawnPleaseOptions } from '../types/SpawnPleaseOptions' 8 | 9 | /** Spawn bun. */ 10 | async function spawnBun( 11 | args: string | string[], 12 | npmOptions: NpmOptions = {}, 13 | spawnPleaseOptions: SpawnPleaseOptions = {}, 14 | spawnOptions: Index = {}, 15 | ): Promise<{ stdout: string; stderr: string }> { 16 | const fullArgs = [ 17 | ...(npmOptions.global ? ['--global'] : []), 18 | ...(npmOptions.prefix ? [`--prefix=${npmOptions.prefix}`] : []), 19 | ...(Array.isArray(args) ? args : [args]), 20 | ] 21 | 22 | return spawn('bun', fullArgs, spawnPleaseOptions, spawnOptions) 23 | } 24 | 25 | /** Returns the global directory of bun. */ 26 | export const defaultPrefix = async (options: Options): Promise => 27 | options.global 28 | ? options.prefix || process.env.BUN_INSTALL || path.dirname((await spawn('bun', ['pm', '-g', 'bin'])).stdout) 29 | : undefined 30 | 31 | /** 32 | * (Bun) Fetches the list of all installed packages. 33 | */ 34 | export const list = async (options: Options = {}): Promise> => { 35 | const { default: stripAnsi } = await import('strip-ansi') 36 | 37 | // bun pm ls 38 | const { stdout } = await spawnBun( 39 | ['pm', 'ls'], 40 | { 41 | ...(options.global ? { global: true } : null), 42 | ...(options.prefix ? { prefix: options.prefix } : null), 43 | }, 44 | { 45 | rejectOnError: false, 46 | }, 47 | { 48 | env: { 49 | ...process.env, 50 | // Disable color to ensure the output is parsed correctly. 51 | // However, this may be ineffective in some environments (see stripAnsi below). 52 | // https://bun.sh/docs/runtime/configuration#environment-variables 53 | NO_COLOR: '1', 54 | }, 55 | ...(options.cwd ? { cwd: options.cwd } : null), 56 | }, 57 | ) 58 | 59 | // Parse the output of `bun pm ls` into an object { [name]: version }. 60 | // When bun is spawned in the GitHub Actions environment, it outputs ANSI color. Unfortunately, it does not respect the `NO_COLOR` envirionment variable. Therefore, we have to manually strip ansi. 61 | const lines = stripAnsi(stdout).split('\n') 62 | const dependencies = keyValueBy(lines, line => { 63 | const match = line.match(/.* (.*?)@(.+)/) 64 | if (match) { 65 | const [, name, version] = match 66 | return { [name]: version } 67 | } 68 | return null 69 | }) 70 | 71 | return dependencies 72 | } 73 | 74 | export { 75 | distTag, 76 | getEngines, 77 | getPeerDependencies, 78 | greatest, 79 | latest, 80 | minor, 81 | newest, 82 | packageAuthorChanged, 83 | patch, 84 | semver, 85 | } from './npm' 86 | 87 | export default spawnBun 88 | -------------------------------------------------------------------------------- /test/getRepoUrl.test.ts: -------------------------------------------------------------------------------- 1 | import getRepoUrl from '../src/lib/getRepoUrl' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | const should = chaiSetup() 5 | 6 | describe('getRepoUrl', () => { 7 | it('return null if package is not installed', async () => { 8 | should.equal(await getRepoUrl('not-installed/package'), null) 9 | }) 10 | it('return null repository field is undefined', async () => { 11 | should.equal(await getRepoUrl('package-name', {}), null) 12 | }) 13 | it('return null repository field is unknown type', async () => { 14 | should.equal(await getRepoUrl('package-name', { repository: true as any /* allow to compile */ }), null) 15 | }) 16 | it('return url directly from repository field if valid https url', async () => { 17 | const url = await getRepoUrl('package-name', { repository: 'https://github.com/user/repo' }) 18 | url!.should.equal('https://github.com/user/repo') 19 | }) 20 | it('return url directly from repository field if valid http url', async () => { 21 | const url = await getRepoUrl('package-name', { repository: 'http://anything.com/user/repo' }) 22 | url!.should.equal('http://anything.com/user/repo') 23 | }) 24 | it('return url constructed from github shortcut syntax string', async () => { 25 | const url = await getRepoUrl('package-name', { repository: 'user/repo' }) 26 | url!.should.equal('https://github.com/user/repo') 27 | }) 28 | it('return url constructed from repository specific shortcut syntax string', async () => { 29 | const url = await getRepoUrl('package-name', { repository: 'github:user/repo' }) 30 | url!.should.equal('https://github.com/user/repo') 31 | }) 32 | it('return url directly from url field if not a known git host', async () => { 33 | const url = await getRepoUrl('package-name', { repository: { url: 'https://any.website.com/some/path' } }) 34 | url!.should.equal('https://any.website.com/some/path') 35 | }) 36 | it('return url constructed from git-https protocol', async () => { 37 | const url = await getRepoUrl('package-name', { repository: { url: 'git+https://github.com/user/repo.git' } }) 38 | url!.should.equal('https://github.com/user/repo') 39 | }) 40 | it('return url constructed from git protocol', async () => { 41 | const url = await getRepoUrl('package-name', { repository: { url: 'git://github.com/user/repo.git' } }) 42 | url!.should.equal('https://github.com/user/repo') 43 | }) 44 | it('return url constructed from http protocol', async () => { 45 | const url = await getRepoUrl('package-name', { repository: { url: 'http://github.com/user/repo.git' } }) 46 | url!.should.equal('https://github.com/user/repo') 47 | }) 48 | it('return url with directory path', async () => { 49 | const url = await getRepoUrl('package-name', { 50 | repository: { url: 'http://github.com/user/repo.git', directory: 'packages/specific-package' }, 51 | }) 52 | url!.should.equal('https://github.com/user/repo/tree/HEAD/packages/specific-package') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/lib/getCurrentDependencies.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver' 2 | import { Index } from '../types/IndexType' 3 | import { Options } from '../types/Options' 4 | import { PackageFile } from '../types/PackageFile' 5 | import { VersionSpec } from '../types/VersionSpec' 6 | import filterAndReject from './filterAndReject' 7 | import filterObject from './filterObject' 8 | import { keyValueBy } from './keyValueBy' 9 | import programError from './programError' 10 | import resolveDepSections from './resolveDepSections' 11 | 12 | /** Returns true if spec1 is greater than spec2, ignoring invalid version ranges. */ 13 | const isGreaterThanSafe = (spec1: VersionSpec, spec2: VersionSpec) => 14 | // not a valid range to compare (e.g. github url) 15 | semver.validRange(spec1) && 16 | semver.validRange(spec2) && 17 | // otherwise return true if spec2 is smaller than spec1 18 | semver.gt(semver.minVersion(spec1)!, semver.minVersion(spec2)!) 19 | 20 | /** Parses the packageManager field into a { [name]: version } pair. */ 21 | const parsePackageManager = (pkgData: PackageFile) => { 22 | if (!pkgData.packageManager) return {} 23 | const [name, version] = pkgData.packageManager.split('@') 24 | return { [name]: version } 25 | } 26 | /** 27 | * Get the current dependencies from the package file. 28 | * 29 | * @param [pkgData={}] Object with dependencies, devDependencies, peerDependencies, and/or optionalDependencies properties. 30 | * @param [options={}] 31 | * @param options.dep 32 | * @param options.filter 33 | * @param options.reject 34 | * @returns Promised {packageName: version} collection 35 | */ 36 | function getCurrentDependencies(pkgData: PackageFile = {}, options: Options = {}) { 37 | const depSections = resolveDepSections(options.dep) 38 | 39 | // get all dependencies from the selected sections 40 | // if a dependency appears in more than one section, take the lowest version number 41 | const allDependencies = depSections.reduce((accum, depSection) => { 42 | return { 43 | ...accum, 44 | ...(depSection === 'packageManager' 45 | ? parsePackageManager(pkgData) 46 | : filterObject( 47 | (pkgData[depSection] as Index) || {}, 48 | (dep, spec) => !isGreaterThanSafe(spec, accum[dep]), 49 | )), 50 | } 51 | }, {} as Index) 52 | 53 | // filter & reject dependencies and versions 54 | const workspacePackageMap = keyValueBy(options.workspacePackages || []) 55 | let filteredDependencies: Index = {} 56 | try { 57 | filteredDependencies = filterObject( 58 | filterObject(allDependencies, name => !workspacePackageMap[name]), 59 | filterAndReject( 60 | options.filter || null, 61 | options.reject || null, 62 | options.filterVersion || null, 63 | options.rejectVersion || null, 64 | ), 65 | ) 66 | } catch (err: any) { 67 | programError(options, 'Invalid filter: ' + err.message || err) 68 | } 69 | 70 | return filteredDependencies 71 | } 72 | 73 | export default getCurrentDependencies 74 | -------------------------------------------------------------------------------- /src/lib/runGlobal.ts: -------------------------------------------------------------------------------- 1 | import { print, printJson, printSorted, printUpgrades } from '../lib/logging' 2 | import { Index } from '../types/IndexType' 3 | import { Options } from '../types/Options' 4 | import chalk from './chalk' 5 | import getInstalledPackages from './getInstalledPackages' 6 | import { keyValueBy } from './keyValueBy' 7 | import programError from './programError' 8 | import upgradePackageDefinitions from './upgradePackageDefinitions' 9 | 10 | /** Checks global dependencies for upgrades. */ 11 | async function runGlobal(options: Options): Promise | void> { 12 | print(options, '\nOptions:', 'verbose') 13 | printSorted(options, options, 'verbose') 14 | 15 | print(options, '\nGetting installed packages', 'verbose') 16 | let globalPackages: Index = {} 17 | try { 18 | const { cli, cwd, filter, filterVersion, global, packageManager, prefix, reject, rejectVersion } = options 19 | 20 | globalPackages = await getInstalledPackages({ 21 | cli, 22 | cwd, 23 | filter, 24 | filterVersion, 25 | global, 26 | packageManager, 27 | prefix, 28 | reject, 29 | rejectVersion, 30 | }) 31 | } catch (e: any) { 32 | programError(options, e.message) 33 | } 34 | 35 | print(options, 'globalPackages:', 'verbose') 36 | print(options, globalPackages, 'verbose') 37 | print(options, '', 'verbose') 38 | print(options, `Fetching ${options.target} versions`, 'verbose') 39 | 40 | const [upgraded, latest] = await upgradePackageDefinitions(globalPackages, options) 41 | print(options, latest, 'verbose') 42 | 43 | const time = keyValueBy(latest, (key, result) => (result.time ? { [key]: result.time } : null)) 44 | 45 | const upgradedPackageNames = Object.keys(upgraded) 46 | await printUpgrades(options, { 47 | current: globalPackages, 48 | upgraded, 49 | latest, 50 | total: upgradedPackageNames.length, 51 | time, 52 | }) 53 | 54 | const instruction = upgraded ? upgradedPackageNames.map(pkg => pkg + '@' + upgraded[pkg]).join(' ') : '[package]' 55 | 56 | if (options.json) { 57 | // since global packages do not have a package.json, return the upgraded deps directly (no version range replacements) 58 | printJson(options, upgraded) 59 | } else if (instruction.length) { 60 | const upgradeCmd = 61 | options.packageManager === 'yarn' 62 | ? 'yarn global upgrade' 63 | : options.packageManager === 'pnpm' 64 | ? 'pnpm -g add' 65 | : options.packageManager === 'bun' 66 | ? 'bun add -g' 67 | : 'npm -g install' 68 | 69 | print( 70 | options, 71 | '\n' + 72 | chalk.cyan('ncu') + 73 | ' itself cannot upgrade global packages. Run the following to upgrade all global packages: \n\n' + 74 | chalk.cyan(`${upgradeCmd} ` + instruction) + 75 | '\n', 76 | ) 77 | } 78 | 79 | // if errorLevel is 2, exit with non-zero error code 80 | if (options.cli && options.errorLevel === 2 && upgradedPackageNames.length > 0) { 81 | process.exit(1) 82 | } 83 | return upgraded 84 | } 85 | 86 | export default runGlobal 87 | -------------------------------------------------------------------------------- /src/lib/figgy-pudding/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /* 4 | 5 | This is stripped down version of the deprecated figgy-pudding. It is used by libnpmconfig, which is also deprecated and has been brought into the codebase to avoid deprecation warnings. 6 | 7 | https://github.com/npm/figgy-pudding 8 | 9 | */ 10 | 11 | class FiggyPudding { 12 | constructor(specs, opts, providers) { 13 | this.__specs = specs || {} 14 | this.__opts = opts || {} 15 | this.__providers = reverse(providers.filter(x => x != null && typeof x === 'object')) 16 | this.__isFiggyPudding = true 17 | } 18 | get(key) { 19 | return pudGet(this, key, true) 20 | } 21 | toJSON() { 22 | const obj = {} 23 | this.forEach((val, key) => { 24 | obj[key] = val 25 | }) 26 | return obj 27 | } 28 | forEach(fn, thisArg = this) { 29 | for (let [key, value] of this.entries()) { 30 | fn.call(thisArg, value, key, this) 31 | } 32 | } 33 | *entries(_matcher) { 34 | for (let key of Object.keys(this.__specs)) { 35 | yield [key, this.get(key)] 36 | } 37 | const matcher = _matcher || this.__opts.other 38 | if (matcher) { 39 | const seen = new Set() 40 | for (let p of this.__providers) { 41 | const iter = p.entries ? p.entries(matcher) : entries(p) 42 | for (let [key, val] of iter) { 43 | if (matcher(key) && !seen.has(key)) { 44 | seen.add(key) 45 | yield [key, val] 46 | } 47 | } 48 | } 49 | } 50 | } 51 | concat(...moreConfig) { 52 | return new Proxy( 53 | new FiggyPudding(this.__specs, this.__opts, reverse(this.__providers).concat(moreConfig)), 54 | proxyHandler, 55 | ) 56 | } 57 | } 58 | 59 | function pudGet(pud, key, validate) { 60 | let spec = pud.__specs[key] 61 | if (!spec) { 62 | spec = {} 63 | } 64 | let ret 65 | for (let p of pud.__providers) { 66 | ret = tryGet(key, p) 67 | if (ret !== undefined) { 68 | break 69 | } 70 | } 71 | if (ret === undefined && spec.default !== undefined) { 72 | if (typeof spec.default === 'function') { 73 | return spec.default(pud) 74 | } else { 75 | return spec.default 76 | } 77 | } else { 78 | return ret 79 | } 80 | } 81 | 82 | function tryGet(key, p) { 83 | let ret 84 | if (p.__isFiggyPudding) { 85 | ret = pudGet(p, key, false) 86 | } else { 87 | ret = p[key] 88 | } 89 | return ret 90 | } 91 | 92 | const proxyHandler = { 93 | get(obj, prop) { 94 | if (typeof prop === 'symbol' || prop.slice(0, 2) === '__' || prop in FiggyPudding.prototype) { 95 | return obj[prop] 96 | } 97 | return obj.get(prop) 98 | }, 99 | } 100 | 101 | export default function figgyPudding(specs, opts) { 102 | function factory(...providers) { 103 | return new Proxy(new FiggyPudding(specs, opts, providers), proxyHandler) 104 | } 105 | return factory 106 | } 107 | 108 | function reverse(arr) { 109 | const ret = [] 110 | arr.forEach(x => ret.unshift(x)) 111 | return ret 112 | } 113 | 114 | function entries(obj) { 115 | return Object.keys(obj).map(k => [k, obj[k]]) 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/findPackage.ts: -------------------------------------------------------------------------------- 1 | import findUp from 'find-up' 2 | import fs from 'fs/promises' 3 | import { text } from 'node:stream/consumers' 4 | import path from 'path' 5 | import { print } from '../lib/logging' 6 | import { Options } from '../types/Options' 7 | import chalk from './chalk' 8 | import programError from './programError' 9 | 10 | /** 11 | * Finds the package file and data. 12 | * 13 | * Searches as follows: 14 | * --packageData flag 15 | * --packageFile flag 16 | * --stdin 17 | * --findUp 18 | */ 19 | async function findPackage(options: Options): Promise<{ 20 | pkgData: string | null 21 | pkgFile: string | null 22 | pkgPath: string | null 23 | }> { 24 | let pkgData 25 | let pkgFile = null 26 | const pkgPath = options.packageFile || 'package.json' 27 | 28 | /** Reads the contents of a package file. */ 29 | function getPackageDataFromFile(pkgFile: string | null | undefined, pkgFileName: string): Promise { 30 | // exit if no pkgFile to read from fs 31 | if (pkgFile != null) { 32 | const relPathToPackage = path.resolve(pkgFile) 33 | print(options, `${options.upgrade ? 'Upgrading' : 'Checking'} ${relPathToPackage}`) 34 | } else { 35 | programError( 36 | options, 37 | `${chalk.red( 38 | `No ${pkgFileName}`, 39 | )}\n\nPlease add a ${pkgFileName} to the current directory, specify the ${chalk.cyan( 40 | '--packageFile', 41 | )} or ${chalk.cyan('--packageData')} options, or pipe a ${pkgFileName} to stdin and specify ${chalk.cyan( 42 | '--stdin', 43 | )}.`, 44 | { color: false }, 45 | ) 46 | } 47 | 48 | return fs.readFile(pkgFile!, 'utf-8').catch(e => { 49 | programError(options, e) 50 | }) 51 | } 52 | 53 | print(options, 'Running in local mode', 'verbose') 54 | print(options, 'Finding package file data', 'verbose') 55 | 56 | // get the package data from the various input possibilities 57 | if (options.packageData) { 58 | pkgFile = null 59 | pkgData = Promise.resolve(options.packageData) 60 | } else if (options.packageFile) { 61 | pkgFile = options.packageFile 62 | pkgData = getPackageDataFromFile(pkgFile, pkgPath) 63 | } else if (options.stdin) { 64 | print(options, 'Waiting for package data on stdin', 'verbose') 65 | 66 | // get data from stdin 67 | // trim stdin to account for \r\n 68 | const stdinData = await text(process.stdin) 69 | const data = stdinData.trim().length > 0 ? stdinData : null 70 | 71 | // if no stdin content fall back to searching for package.json from pwd and up to root 72 | pkgFile = data || !pkgPath ? null : await findUp(pkgPath) 73 | pkgData = data || getPackageDataFromFile(await pkgFile, pkgPath) 74 | } else { 75 | // find the closest package starting from the current working directory and going up to the root 76 | pkgFile = pkgPath 77 | ? await findUp( 78 | !options.packageFile && options.packageManager === 'deno' ? ['deno.json', 'deno.jsonc'] : pkgPath, 79 | { 80 | cwd: options.cwd || process.cwd(), 81 | }, 82 | ) 83 | : null 84 | pkgData = getPackageDataFromFile(pkgFile, pkgPath) 85 | } 86 | 87 | const pkgDataResolved = await pkgData 88 | 89 | return { 90 | pkgData: pkgDataResolved, 91 | pkgFile: pkgFile || null, 92 | pkgPath, 93 | } 94 | } 95 | 96 | export default findPackage 97 | -------------------------------------------------------------------------------- /src/package-managers/filters.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | import * as versionUtil from '../lib/version-util' 3 | import { Index } from '../types/IndexType' 4 | import { Maybe } from '../types/Maybe' 5 | import { Options } from '../types/Options' 6 | import { Packument } from '../types/Packument' 7 | import { Version } from '../types/Version' 8 | 9 | /** 10 | * @param versionResult Available version 11 | * @param options Options 12 | * @returns True if deprecated versions are allowed or the version is not deprecated 13 | */ 14 | export function allowDeprecatedOrIsNotDeprecated(versionResult: Partial, options: Options): boolean { 15 | return options.deprecated || !versionResult.deprecated 16 | } 17 | 18 | /** 19 | * @param versionResult Available version 20 | * @param options Options 21 | * @returns True if pre-releases are allowed or the version is not a pre-release 22 | */ 23 | export function allowPreOrIsNotPre(versionResult: Partial, options: Options): boolean { 24 | if (options.pre) return true 25 | return !versionResult.version || !versionUtil.isPre(versionResult.version) 26 | } 27 | 28 | /** 29 | * Returns true if the node engine requirement is satisfied or not specified for a given package version. 30 | * 31 | * @param versionResult Version object returned by packument. 32 | * @param nodeEngineVersion The value of engines.node in the package file. 33 | * @returns True if the node engine requirement is satisfied or not specified. 34 | */ 35 | export function satisfiesNodeEngine(versionResult: Partial, nodeEngineVersion: Maybe): boolean { 36 | if (!nodeEngineVersion) return true 37 | const minVersion = semver.minVersion(nodeEngineVersion)?.version 38 | if (!minVersion) return true 39 | const versionNodeEngine: string | undefined = versionResult?.engines?.node 40 | return !versionNodeEngine || semver.satisfies(minVersion, versionNodeEngine) 41 | } 42 | 43 | /** 44 | * Returns true if the peer dependencies requirement is satisfied or not specified for a given package version. 45 | * 46 | * @param versionResult Version object returned by packument. 47 | * @param peerDependencies The list of peer dependencies. 48 | * @returns True if the peer dependencies are satisfied or not specified. 49 | */ 50 | export function satisfiesPeerDependencies(versionResult: Partial, peerDependencies: Index>) { 51 | if (!peerDependencies) return true 52 | return Object.values(peerDependencies).every( 53 | peers => 54 | peers[versionResult.name!] === undefined || semver.satisfies(versionResult.version!, peers[versionResult.name!]), 55 | ) 56 | } 57 | 58 | /** Returns a composite predicate that filters out deprecated, prerelease, and node engine incompatibilies from version objects returns by packument. */ 59 | export function filterPredicate(options: Options) { 60 | const predicators: (((o: Partial) => boolean) | null)[] = [ 61 | o => allowDeprecatedOrIsNotDeprecated(o, options), 62 | o => allowPreOrIsNotPre(o, options), 63 | options.enginesNode ? o => satisfiesNodeEngine(o, options.nodeEngineVersion) : null, 64 | options.peerDependencies ? o => satisfiesPeerDependencies(o, options.peerDependencies!) : null, 65 | ] 66 | 67 | return (o: Partial) => predicators.every(predicator => (predicator ? predicator(o) : true)) 68 | } 69 | -------------------------------------------------------------------------------- /test/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # These can be set once the fnm permission errors are figured out 4 | # See: https://github.com/Schniz/fnm 5 | # set -e 6 | # set -o pipefail 7 | 8 | cwd=$(pwd) 9 | e2e_dir=$(dirname "$(readlink -f "$0")") 10 | temp_dir=$(mktemp -d) 11 | registry_port=4873 12 | registry_local="http://localhost:$registry_port" 13 | registry_log=$temp_dir/verdaccio.log 14 | verdaccio_config=$temp_dir/verdaccio-config.yaml 15 | 16 | # cleanup on exit 17 | cleanup() { 18 | 19 | exit_status=$? 20 | 21 | # shut down verdaccio 22 | verdaccio_pid=$(lsof -t -i :$registry_port) 23 | if [ -n "$verdaccio_pid" ]; then 24 | echo Shutting down verdaccio 25 | kill -9 $verdaccio_pid 26 | wait $verdaccio_pid 2>/dev/null 27 | fi 28 | 29 | # delete authToken 30 | # WARNING: The original authToken cannot be restored because it is protected and cannot be read with 'npm config get'. 31 | npm config delete "//localhost:$registry_port/:_authToken" 32 | 33 | # remove temp directory 34 | rm -rf $temp_dir 35 | 36 | # return to working directory 37 | cd $cwd 38 | 39 | if [ $exit_status -ne 0 ]; then 40 | echo Error 41 | else 42 | echo Done 43 | fi 44 | } 45 | 46 | trap cleanup EXIT 47 | 48 | # create verdaccio config 49 | # - store packages in temp directory so they are deleted on exit 50 | # - allow anyone to publish to avoid npm login 51 | echo " 52 | storage: $temp_dir/storage 53 | packages: 54 | npm-check-updates: 55 | access: \$all 56 | publish: \$all 57 | '**': 58 | access: \$all 59 | proxy: npmjs 60 | uplinks: 61 | npmjs: 62 | url: https://registry.npmjs.org/ 63 | " >$verdaccio_config 64 | 65 | # start verdaccio and wait for it to boot 66 | echo Starting local registry 67 | nohup verdaccio -l $registry_port -c $verdaccio_config &>$registry_log & 68 | grep -q 'http address' <(tail -f $registry_log) 69 | 70 | # set dummy authToken which is required to publish 71 | # https://github.com/verdaccio/verdaccio/issues/212#issuecomment-308578500 72 | npm config set "//localhost:$registry_port/:_authToken=e2e_dummy" 73 | 74 | # publish to local registry 75 | echo Publishing to local registry 76 | npm publish --registry $registry_local 77 | 78 | # Test: ncu -v 79 | echo ncu -v 80 | npx --registry $registry_local npm-check-updates -v 81 | 82 | # Test: cli 83 | # Create a package.json file with a dependency on npm-check-updates since it is already published to the local registry 84 | echo Test: cli 85 | echo '{ 86 | "dependencies": { 87 | "npm-check-updates": "1.0.0" 88 | } 89 | }' >$temp_dir/package.json 90 | 91 | # --configFilePath to avoid reading the repo .ncurc 92 | # --cwd to point to the temp package file 93 | # --pre 1 to ensure that an upgrade is always suggested even if npm-check-updates is on a prerelease version 94 | npx --registry $registry_local npm-check-updates --configFilePath $temp_dir --cwd $temp_dir --pre 1 --registry $registry_local 95 | 96 | rm $temp_dir/package.json 97 | cp -r $e2e_dir/e2e $temp_dir 98 | 99 | # Test: cjs 100 | echo Test: cjs 101 | cd $temp_dir/e2e/cjs 102 | 103 | echo Installing 104 | npm i npm-check-updates@latest --registry $registry_local 105 | 106 | echo Running test 107 | REGISTRY=$registry_local node $temp_dir/e2e/cjs/index.js || { exit 1; } 108 | 109 | # Test: esm 110 | echo Test: esm 111 | cd $temp_dir/e2e/esm 112 | 113 | echo Installing 114 | npm i npm-check-updates@latest --registry $registry_local 115 | 116 | echo Running test 117 | REGISTRY=$registry_local node $temp_dir/e2e/esm/index.js || { exit 1; } 118 | -------------------------------------------------------------------------------- /src/lib/libnpmconfig/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is a copy of the deprecated libnpmconfig library. It has been brought into the codebase to avoid deprecation warnings. 4 | 5 | https://github.com/npm/libnpmconfig 6 | 7 | */ 8 | import findUp from 'find-up' 9 | import ini from 'ini' 10 | import fs from 'node:fs' 11 | import os from 'node:os' 12 | import path from 'node:path' 13 | import figgyPudding from '../figgy-pudding' 14 | 15 | const NpmConfig = figgyPudding( 16 | {}, 17 | { 18 | // Open up the pudding object. 19 | other() { 20 | return true 21 | }, 22 | }, 23 | ) 24 | 25 | const ConfigOpts = figgyPudding({ 26 | cache: { default: path.join(process.env.HOME || os.homedir(), '.npm') }, 27 | configNames: { default: ['npmrc', '.npmrc'] }, 28 | envPrefix: { default: /^npm_config_/i }, 29 | cwd: { default: () => process.cwd() }, 30 | globalconfig: { 31 | default: () => path.join(getGlobalPrefix(), 'etc', 'npmrc'), 32 | }, 33 | userconfig: { default: path.join(process.env.HOME || os.homedir(), '.npmrc') }, 34 | }) 35 | 36 | /** Gets the npm config. */ 37 | function getNpmConfig(_opts, _builtin) { 38 | const builtin = ConfigOpts(_builtin) 39 | const env = {} 40 | Object.keys(process.env).forEach(key => { 41 | if (!key.match(builtin.envPrefix)) return 42 | const newKey = key 43 | .toLowerCase() 44 | .replace(builtin.envPrefix, '') 45 | .replace(/(?!^)_/g, '-') 46 | env[newKey] = process.env[key] 47 | }) 48 | const cli = NpmConfig(_opts) 49 | const userConfPath = builtin.userconfig || cli.userconfig || env.userconfig 50 | const user = userConfPath && maybeReadIni(userConfPath) 51 | const globalConfPath = builtin.globalconfig || cli.globalconfig || env.globalconfig 52 | const global = globalConfPath && maybeReadIni(globalConfPath) 53 | const projConfPath = findUp.sync(builtin.configNames, { cwd: builtin.cwd }) 54 | let proj = {} 55 | if (projConfPath && projConfPath !== userConfPath) { 56 | proj = maybeReadIni(projConfPath) 57 | } 58 | const newOpts = NpmConfig(builtin, global, user, proj, env, cli) 59 | if (newOpts.cache) { 60 | return newOpts.concat({ 61 | cache: path.resolve( 62 | cli.cache || env.cache 63 | ? builtin.cwd 64 | : proj.cache 65 | ? path.dirname(projConfPath) 66 | : user.cache 67 | ? path.dirname(userConfPath) 68 | : global.cache 69 | ? path.dirname(globalConfPath) 70 | : path.dirname(userConfPath), 71 | newOpts.cache, 72 | ), 73 | }) 74 | } else { 75 | return newOpts 76 | } 77 | } 78 | 79 | /** Try to read the given ini file. */ 80 | function maybeReadIni(f) { 81 | let txt 82 | try { 83 | txt = fs.readFileSync(f, 'utf8') 84 | } catch (err) { 85 | if (err.code === 'ENOENT') { 86 | return '' 87 | } else { 88 | throw err 89 | } 90 | } 91 | return ini.parse(txt) 92 | } 93 | 94 | /** Get the global node PREFIX. */ 95 | function getGlobalPrefix() { 96 | if (process.env.PREFIX) { 97 | return process.env.PREFIX 98 | } else if (process.platform === 'win32') { 99 | // c:\node\node.exe --> prefix=c:\node\ 100 | return path.dirname(process.execPath) 101 | } else { 102 | // /usr/local/bin/node --> prefix=/usr/local 103 | let pref = path.dirname(path.dirname(process.execPath)) 104 | // destdir only is respected on Unix 105 | if (process.env.DESTDIR) { 106 | pref = path.join(process.env.DESTDIR, pref) 107 | } 108 | return pref 109 | } 110 | } 111 | 112 | export default getNpmConfig 113 | -------------------------------------------------------------------------------- /src/lib/cache.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import path from 'path' 4 | import { CacheData, Cacher } from '../types/Cacher' 5 | import { Options } from '../types/Options' 6 | import { print } from './logging' 7 | 8 | export const CACHE_DELIMITER = '___' 9 | 10 | /** 11 | * Check if cache is expired if timestamp is set 12 | * 13 | * @param cacheData 14 | * @param cacheExpiration 15 | * @returns 16 | */ 17 | function checkCacheExpiration(cacheData: CacheData, cacheExpiration = 10) { 18 | if (typeof cacheData.timestamp !== 'number') { 19 | return false 20 | } 21 | 22 | const unixMinuteMS = 60 * 1000 23 | const expirationLimit = cacheData.timestamp + cacheExpiration * unixMinuteMS 24 | return expirationLimit < Date.now() 25 | } 26 | 27 | export const defaultCacheFilename = '.ncu-cache.json' 28 | export const defaultCacheFile = `~/${defaultCacheFilename}` 29 | export const resolvedDefaultCacheFile = path.join(os.homedir(), defaultCacheFilename) 30 | 31 | /** Resolve the cache file path based on os/homedir. */ 32 | export function resolveCacheFile(optionsCacheFile: string) { 33 | return optionsCacheFile === defaultCacheFile ? resolvedDefaultCacheFile : optionsCacheFile 34 | } 35 | 36 | /** Clear the default cache, or the cache file specified by --cacheFile. */ 37 | export async function cacheClear(options: Options) { 38 | if (!options.cacheFile) { 39 | return 40 | } 41 | 42 | await fs.promises.rm(resolveCacheFile(options.cacheFile), { force: true }) 43 | } 44 | 45 | /** 46 | * The cacher stores key (name + target) - value (new version) pairs 47 | * for quick updates across `ncu` calls. 48 | * 49 | * @returns 50 | */ 51 | export default async function cacher(options: Omit): Promise { 52 | if (!options.cache || !options.cacheFile) { 53 | return 54 | } 55 | 56 | const cacheFile = resolveCacheFile(options.cacheFile) 57 | let cacheData: CacheData = {} 58 | const cacheUpdates: Record = {} 59 | 60 | try { 61 | cacheData = JSON.parse(await fs.promises.readFile(cacheFile, 'utf-8')) 62 | 63 | const expired = checkCacheExpiration(cacheData, options.cacheExpiration) 64 | if (expired) { 65 | // reset cache 66 | fs.promises.rm(cacheFile, { force: true }) 67 | cacheData = {} 68 | } 69 | } catch (error) { 70 | // ignore file read/parse/remove errors 71 | } 72 | 73 | if (typeof cacheData.timestamp !== 'number') { 74 | cacheData.timestamp = Date.now() 75 | } 76 | if (!cacheData.packages) { 77 | cacheData.packages = {} 78 | } 79 | 80 | return { 81 | get: (name: string, target: string) => { 82 | const key = `${name}${CACHE_DELIMITER}${target}` 83 | if (!key || !cacheData.packages) return 84 | const cached = cacheData.packages[key] 85 | if (cached && !key.includes(cached)) { 86 | const [name] = key.split(CACHE_DELIMITER) 87 | cacheUpdates[name] = cached 88 | } 89 | return cached 90 | }, 91 | set: (name: string, target: string, version: string) => { 92 | const key = `${name}${CACHE_DELIMITER}${target}` 93 | if (!key || !cacheData.packages) return 94 | cacheData.packages[key] = version 95 | }, 96 | save: async () => { 97 | await fs.promises.writeFile(cacheFile, JSON.stringify(cacheData)) 98 | }, 99 | log: () => { 100 | const cacheCount = Object.keys(cacheUpdates).length 101 | if (cacheCount === 0) return 102 | 103 | print(options, `\nUsing ${cacheCount} cached package version${cacheCount > 1 ? 's' : ''}`, 'warn') 104 | print(options, cacheUpdates, 'verbose') 105 | }, 106 | } as Cacher 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/getIgnoredUpgradesDueToPeerDeps.ts: -------------------------------------------------------------------------------- 1 | import { intersects, minVersion, satisfies, validRange } from 'semver' 2 | import { IgnoredUpgradeDueToPeerDeps } from '../types/IgnoredUpgradeDueToPeerDeps' 3 | import { Index } from '../types/IndexType' 4 | import { Options } from '../types/Options' 5 | import { Version } from '../types/Version' 6 | import { VersionSpec } from '../types/VersionSpec' 7 | import getPeerDependenciesFromRegistry from './getPeerDependenciesFromRegistry' 8 | import upgradePackageDefinitions from './upgradePackageDefinitions' 9 | 10 | /** Get all upgrades that are ignored due to incompatible peer dependencies. */ 11 | export async function getIgnoredUpgradesDueToPeerDeps( 12 | current: Index, 13 | upgraded: Index, 14 | upgradedPeerDependencies: Index>, 15 | options: Options = {}, 16 | ) { 17 | const upgradedPackagesWithPeerRestriction = { 18 | ...current, 19 | ...upgraded, 20 | } 21 | const [upgradedLatestVersions, latestVersionResults] = await upgradePackageDefinitions(current, { 22 | ...options, 23 | peer: false, 24 | peerDependencies: undefined, 25 | loglevel: 'silent', 26 | }) 27 | const upgradedPeerDependenciesLatest = await getPeerDependenciesFromRegistry( 28 | Object.fromEntries( 29 | Object.entries(upgradedLatestVersions).map(([packageName, versionSpec]) => { 30 | return [ 31 | packageName, 32 | // git urls and other non-semver versions are ignored. 33 | // Make sure versionSpec is a valid semver range, otherwise minVersion will throw. 34 | validRange(versionSpec) ? (minVersion(versionSpec)?.version ?? versionSpec) : versionSpec, 35 | ] 36 | }), 37 | ), 38 | options, 39 | ) 40 | return Object.entries(upgradedLatestVersions) 41 | .filter(([pkgName, newVersion]) => upgraded[pkgName] !== newVersion) 42 | .reduce((accum, [pkgName, newVersion]) => { 43 | let reason = Object.entries(upgradedPeerDependencies) 44 | .filter( 45 | ([, peers]) => 46 | peers[pkgName] !== undefined && 47 | latestVersionResults[pkgName]?.version && 48 | !satisfies(latestVersionResults[pkgName].version!, peers[pkgName]), 49 | ) 50 | .reduce( 51 | (accumReason, [peerPkg, peers]) => ({ 52 | ...accumReason, 53 | [peerPkg]: !validRange(peers[pkgName]) 54 | ? `a range that semver does not understand: ${peers[pkgName]}. This range does not work with semver.satisfies or semver.intersects, which npm-check-updates relies on to determine peer dependency compatibility. Either this is a mistake in ${peerPkg}, or it relies on a new syntax that is not compatible with the semver package.` 55 | : peers[pkgName], 56 | }), 57 | {} as Index, 58 | ) 59 | if (Object.keys(reason).length === 0) { 60 | const peersOfPkg = upgradedPeerDependenciesLatest?.[pkgName] || {} 61 | reason = Object.entries(peersOfPkg) 62 | .filter( 63 | ([peer, peerSpec]) => 64 | upgradedPackagesWithPeerRestriction[peer] && 65 | !(!validRange(peerSpec) || intersects(upgradedPackagesWithPeerRestriction[peer], peerSpec)), 66 | ) 67 | .reduce( 68 | (accumReason, [peerPkg, peerSpec]) => ({ ...accumReason, [pkgName]: `${peerPkg} ${peerSpec}` }), 69 | {} as Index, 70 | ) 71 | } 72 | return { 73 | ...accum, 74 | [pkgName]: { 75 | from: current[pkgName], 76 | to: newVersion, 77 | reason, 78 | }, 79 | } 80 | }, {} as Index) 81 | } 82 | 83 | export default getIgnoredUpgradesDueToPeerDeps 84 | -------------------------------------------------------------------------------- /src/lib/filterAndReject.ts: -------------------------------------------------------------------------------- 1 | import { and, or } from 'fp-and-or' 2 | import identity from 'lodash/identity' 3 | import picomatch from 'picomatch' 4 | import { parseRange } from 'semver-utils' 5 | import { FilterPattern } from '../types/FilterPattern' 6 | import { Maybe } from '../types/Maybe' 7 | import { VersionSpec } from '../types/VersionSpec' 8 | 9 | /** 10 | * Creates a filter function from a given filter string. 11 | * Supports strings, wildcards, comma-or-space-delimited lists, and regexes. 12 | * The filter function *may* throw an exception if the filter pattern is invalid. 13 | * 14 | * @param [filterPattern] 15 | * @returns 16 | */ 17 | function composeFilter(filterPattern: FilterPattern): (name: string, versionSpec?: string) => boolean { 18 | let predicate: (name: string, versionSpec?: string) => boolean 19 | 20 | // no filter 21 | if (!filterPattern) { 22 | predicate = identity 23 | } 24 | // string 25 | else if (typeof filterPattern === 'string') { 26 | // RegExp string 27 | if (filterPattern[0] === '/' && filterPattern.at(-1) === '/') { 28 | const regexp = new RegExp(filterPattern.slice(1, -1)) 29 | predicate = (dependencyName: string) => regexp.test(dependencyName) 30 | } 31 | // glob string 32 | else { 33 | const patterns = filterPattern.split(/[\s,]+/) 34 | predicate = (dependencyName: string) => { 35 | /** Returns true if the pattern matches an unscoped dependency name. */ 36 | const matchUnscoped = (pattern: string) => picomatch(pattern)(dependencyName) 37 | 38 | /** Returns true if the pattern matches a scoped dependency name. */ 39 | const matchScoped = (pattern: string) => 40 | !pattern.includes('/') && 41 | dependencyName.includes('/') && 42 | picomatch(pattern)(dependencyName.replace(/\//g, '_')) 43 | 44 | // return true if any of the provided patterns match the dependency name 45 | return patterns.some(or(matchUnscoped, matchScoped)) 46 | } 47 | } 48 | } 49 | // array 50 | else if (Array.isArray(filterPattern)) { 51 | predicate = (dependencyName: string, versionSpec?: string) => 52 | filterPattern.some(subpattern => composeFilter(subpattern)(dependencyName, versionSpec)) 53 | } 54 | // raw RegExp 55 | else if (filterPattern instanceof RegExp) { 56 | predicate = (dependencyName: string) => filterPattern.test(dependencyName) 57 | } 58 | // function 59 | else if (typeof filterPattern === 'function') { 60 | predicate = (dependencyName: string, versionSpec?: string) => 61 | filterPattern(dependencyName, parseRange((versionSpec as string) ?? dependencyName)) 62 | } else { 63 | throw new TypeError('Invalid filter. Must be a RegExp, array, or comma-or-space-delimited list.') 64 | } 65 | 66 | // limit the arity to 1 to avoid passing the value 67 | return predicate 68 | } 69 | 70 | /** 71 | * Composes a filter function from filter, reject, filterVersion, and rejectVersion patterns. The filter function *may* throw an exception if the filter pattern is invalid. 72 | * 73 | * @param [filter] 74 | * @param [reject] 75 | * @param [filterVersion] 76 | * @param [rejectVersion] 77 | */ 78 | function filterAndReject( 79 | filter: Maybe, 80 | reject: Maybe, 81 | filterVersion: Maybe, 82 | rejectVersion: Maybe, 83 | ) { 84 | return and( 85 | // filter dep 86 | (dependencyName: VersionSpec, version: string) => 87 | and(filter ? composeFilter(filter) : true, reject ? (...args) => !composeFilter(reject)(...args) : true)( 88 | dependencyName, 89 | version, 90 | ), 91 | // filter version 92 | (dependencyName: VersionSpec, version: string) => 93 | and( 94 | filterVersion ? composeFilter(filterVersion) : true, 95 | rejectVersion ? (...args) => !composeFilter(rejectVersion)(...args) : true, 96 | )(version), 97 | ) 98 | } 99 | 100 | export default filterAndReject 101 | -------------------------------------------------------------------------------- /test/rejectVersion.ts: -------------------------------------------------------------------------------- 1 | import ncu from '../src' 2 | import chaiSetup from './helpers/chaiSetup' 3 | import stubVersions from './helpers/stubVersions' 4 | 5 | chaiSetup() 6 | 7 | describe('rejectVersion', () => { 8 | it('reject by package version with string', async () => { 9 | const stub = stubVersions({ 10 | 'ncu-test-v2': '2.0.0', 11 | 'ncu-test-return-version': '2.0.0', 12 | }) 13 | 14 | const pkg = { 15 | dependencies: { 16 | 'ncu-test-v2': '1.0.0', 17 | 'ncu-test-return-version': '1.0.1', 18 | }, 19 | } 20 | 21 | const upgraded = await ncu({ 22 | packageData: pkg, 23 | rejectVersion: '1.0.0', 24 | }) 25 | 26 | upgraded!.should.not.have.property('ncu-test-v2') 27 | upgraded!.should.have.property('ncu-test-return-version') 28 | 29 | stub.restore() 30 | }) 31 | 32 | it('reject by package version with space-delimited list of strings', async () => { 33 | const stub = stubVersions({ 34 | 'ncu-test-v2': '2.0.0', 35 | 'ncu-test-return-version': '2.0.0', 36 | 'fp-and-or': '0.1.3', 37 | }) 38 | 39 | const pkg = { 40 | dependencies: { 41 | 'ncu-test-v2': '1.0.0', 42 | 'ncu-test-return-version': '1.0.1', 43 | 'fp-and-or': '0.1.0', 44 | }, 45 | } 46 | 47 | const upgraded = await ncu({ 48 | packageData: pkg, 49 | rejectVersion: '1.0.0 0.1.0', 50 | }) 51 | 52 | upgraded!.should.not.have.property('ncu-test-v2') 53 | upgraded!.should.have.property('ncu-test-return-version') 54 | upgraded!.should.not.have.property('fp-and-or') 55 | 56 | stub.restore() 57 | }) 58 | 59 | it('reject by package version with comma-delimited list of strings', async () => { 60 | const stub = stubVersions({ 61 | 'ncu-test-v2': '2.0.0', 62 | 'ncu-test-return-version': '2.0.0', 63 | 'fp-and-or': '0.1.3', 64 | }) 65 | 66 | const pkg = { 67 | dependencies: { 68 | 'ncu-test-v2': '1.0.0', 69 | 'ncu-test-return-version': '1.0.1', 70 | 'fp-and-or': '0.1.0', 71 | }, 72 | } 73 | 74 | const upgraded = await ncu({ 75 | packageData: pkg, 76 | rejectVersion: '1.0.0,0.1.0', 77 | }) 78 | 79 | upgraded!.should.not.have.property('ncu-test-v2') 80 | upgraded!.should.have.property('ncu-test-return-version') 81 | upgraded!.should.not.have.property('fp-and-or') 82 | 83 | stub.restore() 84 | }) 85 | 86 | it('reject by package version with RegExp', async () => { 87 | const stub = stubVersions({ 88 | 'ncu-test-v2': '2.0.0', 89 | 'ncu-test-return-version': '2.0.0', 90 | 'fp-and-or': '0.1.3', 91 | }) 92 | 93 | const pkg = { 94 | dependencies: { 95 | 'ncu-test-v2': '1.0.0', 96 | 'ncu-test-return-version': '1.0.1', 97 | 'fp-and-or': '0.1.0', 98 | }, 99 | } 100 | 101 | const upgraded = await ncu({ 102 | packageData: pkg, 103 | rejectVersion: /^1/, 104 | }) 105 | 106 | upgraded!.should.not.have.property('ncu-test-v2') 107 | upgraded!.should.not.have.property('ncu-test-return-version') 108 | upgraded!.should.have.property('fp-and-or') 109 | 110 | stub.restore() 111 | }) 112 | 113 | it('reject by package version with RegExp string', async () => { 114 | const stub = stubVersions({ 115 | 'ncu-test-v2': '2.0.0', 116 | 'ncu-test-return-version': '2.0.0', 117 | 'fp-and-or': '0.1.3', 118 | }) 119 | 120 | const pkg = { 121 | dependencies: { 122 | 'ncu-test-v2': '1.0.0', 123 | 'ncu-test-return-version': '1.0.1', 124 | 'fp-and-or': '0.1.0', 125 | }, 126 | } 127 | 128 | const upgraded = await ncu({ 129 | packageData: pkg, 130 | rejectVersion: '/^1/', 131 | }) 132 | 133 | upgraded!.should.not.have.property('ncu-test-v2') 134 | upgraded!.should.not.have.property('ncu-test-return-version') 135 | upgraded!.should.have.property('fp-and-or') 136 | 137 | stub.restore() 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /test/getIgnoredUpgradesDueToPeerDeps.test.ts: -------------------------------------------------------------------------------- 1 | import getIgnoredUpgradesDueToPeerDeps from '../src/lib/getIgnoredUpgradesDueToPeerDeps' 2 | import { Packument } from '../src/types/Packument' 3 | import chaiSetup from './helpers/chaiSetup' 4 | import stubVersions from './helpers/stubVersions' 5 | 6 | chaiSetup() 7 | 8 | describe('getIgnoredUpgradesDueToPeerDeps', function () { 9 | it('ncu-test-peer-update', async () => { 10 | const data = await getIgnoredUpgradesDueToPeerDeps( 11 | { 12 | 'ncu-test-return-version': '1.0.0', 13 | 'ncu-test-peer': '1.0.0', 14 | }, 15 | { 16 | 'ncu-test-return-version': '1.1.0', 17 | 'ncu-test-peer': '1.1.0', 18 | }, 19 | { 20 | 'ncu-test-peer': { 21 | 'ncu-test-return-version': '1.1.x', 22 | }, 23 | }, 24 | {}, 25 | ) 26 | data.should.deep.equal({ 27 | 'ncu-test-return-version': { 28 | from: '1.0.0', 29 | to: '2.0.0', 30 | reason: { 31 | 'ncu-test-peer': '1.1.x', 32 | }, 33 | }, 34 | }) 35 | }) 36 | it('ignored peer after upgrade', async () => { 37 | const stub = stubVersions({ 38 | '@vitest/ui': { 39 | version: '1.6.0', 40 | versions: { 41 | '1.3.1': { 42 | version: '1.3.1', 43 | } as Packument, 44 | '1.6.0': { 45 | version: '1.6.0', 46 | } as Packument, 47 | }, 48 | }, 49 | vitest: { 50 | version: '1.6.0', 51 | versions: { 52 | '1.3.1': { 53 | version: '1.3.1', 54 | } as Packument, 55 | '1.6.0': { 56 | version: '1.6.0', 57 | } as Packument, 58 | }, 59 | }, 60 | eslint: { 61 | version: '9.0.0', 62 | versions: { 63 | '8.57.0': { 64 | version: '8.57.0', 65 | } as Packument, 66 | '9.0.0': { 67 | version: '9.0.0', 68 | } as Packument, 69 | }, 70 | }, 71 | 'eslint-plugin-import': { 72 | version: '2.29.1', 73 | versions: { 74 | '2.29.1': { 75 | version: '2.29.1', 76 | } as Packument, 77 | }, 78 | }, 79 | 'eslint-plugin-unused-imports': { 80 | version: '4.0.0', 81 | versions: { 82 | '4.0.0': { 83 | version: '4.0.0', 84 | } as Packument, 85 | '3.0.0': { 86 | version: '3.0.0', 87 | } as Packument, 88 | }, 89 | }, 90 | }) 91 | const data = await getIgnoredUpgradesDueToPeerDeps( 92 | { 93 | '@vitest/ui': '1.3.1', 94 | vitest: '1.3.1', 95 | eslint: '8.57.0', 96 | 'eslint-plugin-import': '2.29.1', 97 | 'eslint-plugin-unused-imports': '3.0.0', 98 | }, 99 | { 100 | '@vitest/ui': '1.6.0', 101 | vitest: '1.6.0', 102 | }, 103 | { 104 | '@vitest/ui': { 105 | vitest: '1.6.0', 106 | }, 107 | vitest: { 108 | jsdom: '*', 109 | 'happy-dom': '*', 110 | '@vitest/ui': '1.6.0', 111 | '@types/node': '^18.0.0 || >=20.0.0', 112 | '@vitest/browser': '1.6.0', 113 | '@edge-runtime/vm': '*', 114 | }, 115 | eslint: {}, 116 | 'eslint-plugin-import': { 117 | eslint: '^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8', 118 | }, 119 | 'eslint-plugin-unused-imports': { 120 | '@typescript-eslint/eslint-plugin': '^6.0.0', 121 | eslint: '^8.0.0', 122 | }, 123 | }, 124 | { 125 | target: packageName => { 126 | return packageName === 'eslint-plugin-unused-imports' ? 'greatest' : 'minor' 127 | }, 128 | }, 129 | ) 130 | data.should.deep.equal({ 131 | 'eslint-plugin-unused-imports': { 132 | from: '3.0.0', 133 | reason: { 134 | 'eslint-plugin-unused-imports': 'eslint 9', 135 | }, 136 | to: '4.0.0', 137 | }, 138 | }) 139 | stub.restore() 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /test/package-managers/deno/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import jph from 'json-parse-helpfulerror' 3 | import os from 'os' 4 | import path from 'path' 5 | import spawn from 'spawn-please' 6 | import chaiSetup from '../../helpers/chaiSetup' 7 | 8 | chaiSetup() 9 | 10 | const bin = path.join(__dirname, '../../../build/cli.js') 11 | 12 | describe('deno', async function () { 13 | it('handle import map', async () => { 14 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 15 | const pkgFile = path.join(tempDir, 'deno.json') 16 | const pkg = { 17 | imports: { 18 | 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', 19 | }, 20 | } 21 | await fs.writeFile(pkgFile, JSON.stringify(pkg)) 22 | try { 23 | const { stdout } = await spawn( 24 | 'node', 25 | [bin, '--jsonUpgraded', '--packageManager', 'deno', '--packageFile', pkgFile], 26 | undefined, 27 | ) 28 | const pkg = jph.parse(stdout) 29 | pkg.should.have.property('ncu-test-v2') 30 | } finally { 31 | await fs.rm(tempDir, { recursive: true, force: true }) 32 | } 33 | }) 34 | 35 | it('auto detect deno.json', async () => { 36 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 37 | const pkgFile = path.join(tempDir, 'deno.json') 38 | const pkg = { 39 | imports: { 40 | 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', 41 | }, 42 | } 43 | await fs.writeFile(pkgFile, JSON.stringify(pkg)) 44 | try { 45 | const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, { 46 | cwd: tempDir, 47 | }) 48 | const pkg = jph.parse(stdout) 49 | pkg.should.have.property('ncu-test-v2') 50 | } finally { 51 | await fs.rm(tempDir, { recursive: true, force: true }) 52 | } 53 | }) 54 | 55 | it('rewrite deno.json', async () => { 56 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 57 | const pkgFile = path.join(tempDir, 'deno.json') 58 | const pkg = { 59 | imports: { 60 | 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', 61 | }, 62 | } 63 | await fs.writeFile(pkgFile, JSON.stringify(pkg)) 64 | try { 65 | await spawn('node', [bin, '-u'], undefined, { cwd: tempDir }) 66 | const pkgDataNew = await fs.readFile(pkgFile, 'utf-8') 67 | const pkg = jph.parse(pkgDataNew) 68 | pkg.should.deep.equal({ 69 | imports: { 70 | 'ncu-test-v2': 'npm:ncu-test-v2@2.0.0', 71 | }, 72 | }) 73 | } finally { 74 | await fs.rm(tempDir, { recursive: true, force: true }) 75 | } 76 | }) 77 | 78 | it('auto detect deno.jsonc', async () => { 79 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 80 | const pkgFile = path.join(tempDir, 'deno.jsonc') 81 | const pkgString = `{ 82 | "imports": { 83 | // this comment should be ignored in a jsonc file 84 | "ncu-test-v2": "npm:ncu-test-v2@1.0.0" 85 | } 86 | }` 87 | await fs.writeFile(pkgFile, pkgString) 88 | try { 89 | const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, { 90 | cwd: tempDir, 91 | }) 92 | const pkg = jph.parse(stdout) 93 | pkg.should.have.property('ncu-test-v2') 94 | } finally { 95 | await fs.rm(tempDir, { recursive: true, force: true }) 96 | } 97 | }) 98 | 99 | it('rewrite deno.jsonc', async () => { 100 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) 101 | const pkgFile = path.join(tempDir, 'deno.jsonc') 102 | const pkg = { 103 | imports: { 104 | 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', 105 | }, 106 | } 107 | await fs.writeFile(pkgFile, JSON.stringify(pkg)) 108 | try { 109 | await spawn('node', [bin, '-u'], undefined, { cwd: tempDir }) 110 | const pkgDataNew = await fs.readFile(pkgFile, 'utf-8') 111 | const pkg = jph.parse(pkgDataNew) 112 | pkg.should.deep.equal({ 113 | imports: { 114 | 'ncu-test-v2': 'npm:ncu-test-v2@2.0.0', 115 | }, 116 | }) 117 | } finally { 118 | await fs.rm(tempDir, { recursive: true, force: true }) 119 | } 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /src/package-managers/pnpm.ts: -------------------------------------------------------------------------------- 1 | import memoize from 'fast-memoize' 2 | import findUp from 'find-up' 3 | import fs from 'fs/promises' 4 | import ini from 'ini' 5 | import path from 'path' 6 | import spawn from 'spawn-please' 7 | import keyValueBy from '../lib/keyValueBy' 8 | import { print } from '../lib/logging' 9 | import { GetVersion } from '../types/GetVersion' 10 | import { Index } from '../types/IndexType' 11 | import { NpmConfig } from '../types/NpmConfig' 12 | import { NpmOptions } from '../types/NpmOptions' 13 | import { Options } from '../types/Options' 14 | import { SpawnOptions } from '../types/SpawnOptions' 15 | import { SpawnPleaseOptions } from '../types/SpawnPleaseOptions' 16 | import { Version } from '../types/Version' 17 | import * as npm from './npm' 18 | 19 | // return type of pnpm ls --json 20 | type PnpmList = { 21 | path: string 22 | private: boolean 23 | dependencies: Index<{ 24 | from: string 25 | version: Version 26 | resolved: string 27 | }> 28 | }[] 29 | 30 | /** Reads the npmrc config file from the pnpm-workspace.yaml directory. */ 31 | const npmConfigFromPnpmWorkspace = memoize(async (options: Options): Promise => { 32 | const pnpmWorkspacePath = await findUp('pnpm-workspace.yaml') 33 | if (!pnpmWorkspacePath) return {} 34 | 35 | const pnpmWorkspaceDir = path.dirname(pnpmWorkspacePath) 36 | const pnpmWorkspaceConfigPath = path.join(pnpmWorkspaceDir, '.npmrc') 37 | 38 | let pnpmWorkspaceConfig 39 | try { 40 | pnpmWorkspaceConfig = await fs.readFile(pnpmWorkspaceConfigPath, 'utf-8') 41 | } catch (e) { 42 | return {} 43 | } 44 | 45 | print(options, `\nUsing pnpm workspace config at ${pnpmWorkspaceConfigPath}:`, 'verbose') 46 | 47 | const config = npm.normalizeNpmConfig(ini.parse(pnpmWorkspaceConfig), pnpmWorkspaceDir) 48 | 49 | print(options, config, 'verbose') 50 | 51 | return config 52 | }) 53 | 54 | /** Fetches the list of all installed packages. */ 55 | export const list = async (options: Options = {}): Promise> => { 56 | // use npm for local ls for completeness 57 | // this should never happen since list is only called in runGlobal -> getInstalledPackages 58 | if (!options.global) return npm.list(options) 59 | 60 | const cmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' 61 | const { stdout } = await spawn(cmd, ['ls', '-g', '--json']) 62 | const result = JSON.parse(stdout) as PnpmList 63 | const list = keyValueBy(result[0].dependencies || {}, (name, { version }) => ({ 64 | [name]: version, 65 | })) 66 | return list 67 | } 68 | 69 | /** Wraps a GetVersion function and passes the npmrc located next to the pnpm-workspace.yaml if it exists. */ 70 | const withNpmWorkspaceConfig = 71 | (getVersion: GetVersion): GetVersion => 72 | async (packageName, currentVersion, options = {}) => 73 | getVersion(packageName, currentVersion, options, {}, await npmConfigFromPnpmWorkspace(options)) 74 | 75 | export const distTag = withNpmWorkspaceConfig(npm.distTag) 76 | export const greatest = withNpmWorkspaceConfig(npm.greatest) 77 | export const latest = withNpmWorkspaceConfig(npm.latest) 78 | export const minor = withNpmWorkspaceConfig(npm.minor) 79 | export const newest = withNpmWorkspaceConfig(npm.newest) 80 | export const patch = withNpmWorkspaceConfig(npm.patch) 81 | export const semver = withNpmWorkspaceConfig(npm.semver) 82 | 83 | /** 84 | * Spawn pnpm. 85 | * 86 | * @param args 87 | * @param [npmOptions={}] 88 | * @param [spawnOptions={}] 89 | * @returns 90 | */ 91 | async function spawnPnpm( 92 | args: string | string[], 93 | npmOptions: NpmOptions = {}, 94 | spawnOptions?: SpawnOptions, 95 | spawnPleaseOptions?: SpawnPleaseOptions, 96 | ): Promise { 97 | const cmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' 98 | 99 | const fullArgs = [ 100 | ...(npmOptions.global ? 'global' : []), 101 | ...(Array.isArray(args) ? args : [args]), 102 | ...(npmOptions.prefix ? `--prefix=${npmOptions.prefix}` : []), 103 | ] 104 | 105 | const { stdout } = await spawn(cmd, fullArgs, spawnPleaseOptions, spawnOptions) 106 | 107 | return stdout 108 | } 109 | 110 | export { defaultPrefix, getPeerDependencies, getEngines, packageAuthorChanged } from './npm' 111 | 112 | export default spawnPnpm 113 | -------------------------------------------------------------------------------- /test/cache.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import fs from 'fs/promises' 3 | import ncu from '../src/' 4 | import { CACHE_DELIMITER, resolvedDefaultCacheFile } from '../src/lib/cache' 5 | import { CacheData } from '../src/types/Cacher' 6 | import chaiSetup from './helpers/chaiSetup' 7 | import stubVersions from './helpers/stubVersions' 8 | 9 | chaiSetup() 10 | 11 | describe('cache', () => { 12 | it('cache latest versions', async () => { 13 | const stub = stubVersions({ 14 | 'ncu-test-v2': '2.0.0', 15 | 'ncu-test-tag': '1.1.0', 16 | 'ncu-test-alpha': '1.0.0', 17 | }) 18 | try { 19 | const packageData = { 20 | dependencies: { 21 | 'ncu-test-v2': '^1.0.0', 22 | 'ncu-test-tag': '1.0.0', 23 | 'ncu-test-alpha': '1.0.0', 24 | }, 25 | } 26 | 27 | await ncu({ packageData, cache: true }) 28 | 29 | const cacheData: CacheData = await fs.readFile(resolvedDefaultCacheFile, 'utf-8').then(JSON.parse) 30 | 31 | expect(cacheData.timestamp).lessThanOrEqual(Date.now()) 32 | expect(cacheData.packages).deep.eq({ 33 | [`ncu-test-v2${CACHE_DELIMITER}latest`]: '2.0.0', 34 | [`ncu-test-tag${CACHE_DELIMITER}latest`]: '1.1.0', 35 | [`ncu-test-alpha${CACHE_DELIMITER}latest`]: '1.0.0', 36 | }) 37 | } finally { 38 | await fs.rm(resolvedDefaultCacheFile, { recursive: true, force: true }) 39 | stub.restore() 40 | } 41 | }) 42 | 43 | it('use different cache key for different target', async () => { 44 | const stub = stubVersions(options => 45 | options.target === 'latest' 46 | ? { 47 | 'ncu-test-v2': '2.0.0', 48 | 'ncu-test-tag': '1.1.0', 49 | 'ncu-test-alpha': '1.0.0', 50 | } 51 | : options.target === 'greatest' 52 | ? { 53 | 'ncu-test-v2': '2.0.0', 54 | 'ncu-test-tag': '1.2.0-dev.0', 55 | 'ncu-test-alpha': '2.0.0-alpha.2', 56 | } 57 | : null, 58 | ) 59 | try { 60 | const packageData = { 61 | dependencies: { 62 | 'ncu-test-v2': '^1.0.0', 63 | 'ncu-test-tag': '1.0.0', 64 | 'ncu-test-alpha': '1.0.0', 65 | }, 66 | } 67 | 68 | // first run caches latest 69 | await ncu({ packageData, cache: true }) 70 | 71 | const cacheData1: CacheData = await fs.readFile(resolvedDefaultCacheFile, 'utf-8').then(JSON.parse) 72 | 73 | expect(cacheData1.packages).deep.eq({ 74 | [`ncu-test-v2${CACHE_DELIMITER}latest`]: '2.0.0', 75 | [`ncu-test-tag${CACHE_DELIMITER}latest`]: '1.1.0', 76 | [`ncu-test-alpha${CACHE_DELIMITER}latest`]: '1.0.0', 77 | }) 78 | 79 | // second run has a different target so should not use the cache 80 | const result2 = await ncu({ packageData, cache: true, target: 'greatest' }) 81 | expect(result2).deep.eq({ 82 | 'ncu-test-v2': '^2.0.0', 83 | 'ncu-test-tag': '1.2.0-dev.0', 84 | 'ncu-test-alpha': '2.0.0-alpha.2', 85 | }) 86 | 87 | const cacheData2: CacheData = await fs.readFile(resolvedDefaultCacheFile, 'utf-8').then(JSON.parse) 88 | 89 | expect(cacheData2.packages).deep.eq({ 90 | [`ncu-test-v2${CACHE_DELIMITER}latest`]: '2.0.0', 91 | [`ncu-test-tag${CACHE_DELIMITER}latest`]: '1.1.0', 92 | [`ncu-test-alpha${CACHE_DELIMITER}latest`]: '1.0.0', 93 | [`ncu-test-v2${CACHE_DELIMITER}greatest`]: '2.0.0', 94 | [`ncu-test-tag${CACHE_DELIMITER}greatest`]: '1.2.0-dev.0', 95 | [`ncu-test-alpha${CACHE_DELIMITER}greatest`]: '2.0.0-alpha.2', 96 | }) 97 | } finally { 98 | await fs.rm(resolvedDefaultCacheFile, { recursive: true, force: true }) 99 | stub.restore() 100 | } 101 | }) 102 | 103 | it('clears the cache file', async () => { 104 | const stub = stubVersions('99.9.9') 105 | const packageData = { 106 | dependencies: { 107 | 'ncu-test-v2': '^1.0.0', 108 | 'ncu-test-tag': '1.0.0', 109 | 'ncu-test-alpha': '1.0.0', 110 | }, 111 | } 112 | 113 | await ncu({ packageData, cache: true }) 114 | 115 | await ncu({ packageData, cacheClear: true }) 116 | let noCacheFile = false 117 | try { 118 | await fs.stat(resolvedDefaultCacheFile) 119 | } catch (error) { 120 | noCacheFile = true 121 | } 122 | expect(noCacheFile).eq(true) 123 | stub.restore() 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /src/package-managers/gitTags.ts: -------------------------------------------------------------------------------- 1 | /** Fetches package metadata from Github tags. */ 2 | import parseGithubUrl from 'parse-github-url' 3 | import remoteGitTags from 'remote-git-tags' 4 | import { valid } from 'semver' 5 | import { print } from '../lib/logging' 6 | import * as versionUtil from '../lib/version-util' 7 | import { GetVersion } from '../types/GetVersion' 8 | import { Options } from '../types/Options' 9 | import { VersionLevel } from '../types/VersionLevel' 10 | import { VersionResult } from '../types/VersionResult' 11 | import { VersionSpec } from '../types/VersionSpec' 12 | 13 | /** Gets remote versions sorted. */ 14 | const getSortedVersions = async (name: string, declaration: VersionSpec, options?: Options) => { 15 | // if present, github: is parsed as the protocol. This is not valid when passed into remote-git-tags. 16 | declaration = declaration.replace(/^github:/, '') 17 | const { auth, protocol, host, path } = parseGithubUrl(declaration)! 18 | let tagMap = new Map() 19 | let tagsPromise = Promise.resolve(tagMap) 20 | const protocolKnown = protocol != null 21 | if (protocolKnown) { 22 | tagsPromise = tagsPromise.then(() => 23 | remoteGitTags( 24 | `${protocol ? protocol.replace('git+', '') : 'https:'}//${auth ? auth + '@' : ''}${host}/${path?.replace(/^:/, '')}`, 25 | ), 26 | ) 27 | } else { 28 | // try ssh first, then https on failure 29 | tagsPromise = tagsPromise 30 | .then(() => remoteGitTags(`ssh://git@${host}/${path?.replace(/^:/, '')}`)) 31 | .catch(() => remoteGitTags(`https://${auth ? auth + '@' : ''}${host}/${path}`)) 32 | } 33 | 34 | // fetch remote tags 35 | try { 36 | tagMap = await tagsPromise 37 | } catch (e) { 38 | // catch a variety of errors that occur on invalid or private repos 39 | print(options || {}, `Invalid, private repo, or no tags for ${name}: ${declaration}`, 'verbose') 40 | return null 41 | } 42 | 43 | const tags = Array.from(tagMap.keys()) 44 | .map(versionUtil.fixPseudoVersion) 45 | // do not pass semver.valid reference directly since the mapping index will be interpreted as the loose option 46 | // https://github.com/npm/node-semver#functions 47 | .filter(tag => valid(tag)) 48 | .sort(versionUtil.compareVersions) 49 | 50 | return tags 51 | } 52 | 53 | /** Return the highest non-prerelease numbered tag on a remote Git URL. */ 54 | export const latest: GetVersion = async (name: string, declaration: VersionSpec, options?: Options) => { 55 | const versions = await getSortedVersions(name, declaration, options) 56 | if (!versions) return { version: null } 57 | const versionsFiltered = options?.pre ? versions : versions.filter(v => !versionUtil.isPre(v)) 58 | const latestVersion = versionsFiltered[versionsFiltered.length - 1] 59 | return { version: latestVersion ? versionUtil.upgradeGithubUrl(declaration, latestVersion) : null } 60 | } 61 | 62 | /** Return the highest numbered tag on a remote Git URL. */ 63 | export const greatest: GetVersion = async (name: string, declaration: VersionSpec, options?: Options) => { 64 | const versions = await getSortedVersions(name, declaration, options) 65 | if (!versions) return { version: null } 66 | const greatestVersion = versions[versions.length - 1] 67 | return { version: greatestVersion ? versionUtil.upgradeGithubUrl(declaration, greatestVersion) : null } 68 | } 69 | 70 | /** Returns a function that returns the highest version at the given level. */ 71 | export const greatestLevel = 72 | (level: VersionLevel) => 73 | async (name: string, declaration: VersionSpec, options: Options = {}): Promise => { 74 | const version = decodeURIComponent(parseGithubUrl(declaration)!.branch).replace(/^semver:/, '') 75 | const versions = await getSortedVersions(name, declaration, options) 76 | if (!versions) return { version: null } 77 | 78 | const greatestMinor = versionUtil.findGreatestByLevel( 79 | versions.map(v => v.replace(/^v/, '')), 80 | version, 81 | level, 82 | ) 83 | 84 | return { version: greatestMinor ? versionUtil.upgradeGithubUrl(declaration, greatestMinor) : null } 85 | } 86 | 87 | export const minor = greatestLevel('minor') 88 | export const patch = greatestLevel('patch') 89 | 90 | /** All git tags are exact versions, so --target semver should never upgrade git tags. */ 91 | // https://github.com/raineorshine/npm-check-updates/pull/1368 92 | export const semver: GetVersion = async (_name: string, _declaration: VersionSpec, _options?: Options) => { 93 | return { version: null } 94 | } 95 | 96 | // use greatest for newest rather than leaving newest undefined 97 | // this allows a mix of npm and github urls to be used in a package file without causing an "Unsupported target" error 98 | export const newest = greatest 99 | -------------------------------------------------------------------------------- /src/lib/upgradeDependencies.ts: -------------------------------------------------------------------------------- 1 | import flow from 'lodash/flow' 2 | import { parseRange } from 'semver-utils' 3 | import { Index } from '../types/IndexType' 4 | import { Options } from '../types/Options' 5 | import { Version } from '../types/Version' 6 | import { VersionSpec } from '../types/VersionSpec' 7 | import filterObject from './filterObject' 8 | import getPreferredWildcard from './getPreferredWildcard' 9 | import isUpgradeable from './isUpgradeable' 10 | import { pickBy } from './pick' 11 | import * as versionUtil from './version-util' 12 | 13 | interface UpgradeSpec { 14 | current: VersionSpec 15 | currentParsed: VersionSpec | null 16 | latest: Version 17 | latestParsed: Version | null 18 | } 19 | 20 | /** 21 | * Upgrade a dependencies collection based on latest available versions. Supports npm aliases. 22 | * 23 | * @param currentDependencies current dependencies collection object 24 | * @param latestVersions latest available versions collection object 25 | * @param [options={}] 26 | * @returns upgraded dependency collection object 27 | */ 28 | function upgradeDependencies( 29 | currentDependencies: Index, 30 | latestVersions: Index, 31 | options: Options = {}, 32 | ): Index { 33 | const targetOption = options.target || 'latest' 34 | 35 | // filter out dependencies with empty values 36 | currentDependencies = filterObject(currentDependencies, (key, value) => !!value) 37 | 38 | // get the preferred wildcard and bind it to upgradeDependencyDeclaration 39 | const wildcard = getPreferredWildcard(currentDependencies) || versionUtil.DEFAULT_WILDCARD 40 | 41 | /** Upgrades a single dependency. */ 42 | const upgradeDep = (current: VersionSpec, latest: Version) => 43 | versionUtil.upgradeDependencyDeclaration(current, latest, { 44 | wildcard, 45 | removeRange: options.removeRange, 46 | }) 47 | 48 | return flow([ 49 | // only include packages for which a latest version was fetched 50 | (deps: Index): Index => 51 | pickBy(deps, (current, packageName) => packageName in latestVersions), 52 | // unpack npm alias and git urls 53 | (deps: Index): Index => 54 | Object.entries(deps).reduce>((acc, [packageName, current]) => { 55 | const latest = latestVersions[packageName] 56 | let currentParsed = null 57 | let latestParsed = null 58 | 59 | // parse npm alias 60 | if (versionUtil.isNpmAlias(current)) { 61 | currentParsed = versionUtil.parseNpmAlias(current)![1] 62 | } 63 | if (versionUtil.isNpmAlias(latest)) { 64 | latestParsed = versionUtil.parseNpmAlias(latest)![1] 65 | } 66 | 67 | // "branch" is also used for tags (refers to everything after the hash character) 68 | if (versionUtil.isGithubUrl(current)) { 69 | const currentTag = versionUtil.getGithubUrlTag(current)! 70 | const [currentSemver] = parseRange(currentTag) 71 | currentParsed = versionUtil.stringify(currentSemver) 72 | } 73 | 74 | if (versionUtil.isGithubUrl(latest)) { 75 | const latestTag = versionUtil.getGithubUrlTag(latest)! 76 | const [latestSemver] = parseRange(latestTag) 77 | latestParsed = versionUtil.stringify(latestSemver) 78 | } 79 | 80 | acc[packageName] = { current, currentParsed, latest, latestParsed } 81 | return acc 82 | }, {}), 83 | // pick the packages that are upgradeable 84 | (deps: Index): Index => 85 | pickBy(deps, ({ current, currentParsed, latest, latestParsed }: UpgradeSpec, name) => { 86 | // allow downgrades from prereleases when explicit tag is given 87 | const downgrade: boolean = 88 | versionUtil.isPre(current) && 89 | (typeof targetOption === 'string' ? targetOption : targetOption(name, parseRange(current))).startsWith('@') 90 | return isUpgradeable(currentParsed || current, latestParsed || latest, { downgrade }) 91 | }), 92 | // pack embedded versions: npm aliases and git urls 93 | (deps: Index): Index => 94 | Object.entries(deps).reduce>( 95 | (acc, [packageName, { current, currentParsed, latest, latestParsed }]) => { 96 | const upgraded = upgradeDep(currentParsed || current, latestParsed || latest) 97 | 98 | acc[packageName] = versionUtil.isNpmAlias(current) 99 | ? versionUtil.upgradeNpmAlias(current, upgraded) 100 | : versionUtil.isGithubUrl(current) 101 | ? versionUtil.upgradeGithubUrl(current, upgraded) 102 | : upgraded 103 | return acc 104 | }, 105 | {}, 106 | ), 107 | ])(currentDependencies) 108 | } 109 | 110 | export default upgradeDependencies 111 | -------------------------------------------------------------------------------- /test/package-managers/yarn/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import * as yarn from '../../../src/package-managers/yarn' 3 | import { getPathToLookForYarnrc } from '../../../src/package-managers/yarn' 4 | import chaiSetup from '../../helpers/chaiSetup' 5 | 6 | const should = chaiSetup() 7 | 8 | const isWindows = process.platform === 'win32' 9 | 10 | // append the local node_modules bin directory to process.env.PATH so local yarn is used during tests 11 | const localBin = path.resolve(__dirname.replace('build/', ''), '../../../node_modules/.bin') 12 | const localYarnSpawnOptions = { 13 | env: { 14 | ...process.env, 15 | PATH: `${process.env.PATH}:${localBin}`, 16 | }, 17 | } 18 | 19 | describe('yarn', function () { 20 | it('list', async () => { 21 | const testDir = path.join(__dirname, 'default') 22 | const { version } = await yarn.latest('chalk', '', { cwd: testDir }) 23 | parseInt(version!, 10).should.be.above(3) 24 | }) 25 | 26 | it('latest', async () => { 27 | const testDir = path.join(__dirname, 'default') 28 | const { version } = await yarn.latest('chalk', '', { cwd: testDir }) 29 | parseInt(version!, 10).should.be.above(3) 30 | }) 31 | 32 | it('greatest', async () => { 33 | const { version } = await yarn.greatest('ncu-test-greatest-not-newest', '', { pre: true, cwd: __dirname }) 34 | version!.should.equal('2.0.0-beta') 35 | }) 36 | 37 | it('avoids deprecated', async () => { 38 | const testDir = path.join(__dirname, 'default') 39 | const { version } = await yarn.minor('popper.js', '1.15.0', { cwd: testDir, pre: true }) 40 | version!.should.equal('1.16.1-lts') 41 | }) 42 | 43 | it('"No lockfile" error should be thrown on list command when there is no lockfile', async () => { 44 | const testDir = path.join(__dirname, 'nolockfile') 45 | const lockFileErrorMessage = 'No lockfile in this directory. Run `yarn install` to generate one.' 46 | await yarn.list({ cwd: testDir }, localYarnSpawnOptions).should.eventually.be.rejectedWith(lockFileErrorMessage) 47 | }) 48 | 49 | it('getPeerDependencies', async () => { 50 | await yarn.getPeerDependencies('ncu-test-return-version', '1.0.0').should.eventually.deep.equal({}) 51 | await yarn.getPeerDependencies('ncu-test-peer', '1.0.0').should.eventually.deep.equal({ 52 | 'ncu-test-return-version': '1.x', 53 | }) 54 | }) 55 | 56 | describe('npmAuthTokenKeyValue', () => { 57 | it('npmRegistryServer with trailing slash', () => { 58 | const authToken = yarn.npmAuthTokenKeyValue({}, 'fortawesome', { 59 | npmAlwaysAuth: true, 60 | npmAuthToken: 'MY-AUTH-TOKEN', 61 | npmRegistryServer: 'https://npm.fontawesome.com/', 62 | }) 63 | 64 | authToken!.should.deep.equal({ 65 | '//npm.fontawesome.com/:_authToken': 'MY-AUTH-TOKEN', 66 | }) 67 | }) 68 | 69 | it('npmRegistryServer without trailing slash', () => { 70 | const authToken = yarn.npmAuthTokenKeyValue({}, 'fortawesome', { 71 | npmAlwaysAuth: true, 72 | npmAuthToken: 'MY-AUTH-TOKEN', 73 | npmRegistryServer: 'https://npm.fontawesome.com', 74 | }) 75 | 76 | authToken!.should.deep.equal({ 77 | '//npm.fontawesome.com/:_authToken': 'MY-AUTH-TOKEN', 78 | }) 79 | }) 80 | 81 | it('returns null when no npmAlwaysAuth', () => { 82 | const authToken = yarn.npmAuthTokenKeyValue({}, 'fortawesome', { 83 | npmAlwaysAuth: true, 84 | // undefined: npmAuthToken: 'MY-AUTH-TOKEN', 85 | npmRegistryServer: 'https://npm.fontawesome.com/', 86 | }) 87 | 88 | should.equal(authToken, null) 89 | }) 90 | 91 | it('returns null when no registry server', () => { 92 | const authToken = yarn.npmAuthTokenKeyValue({}, 'fortawesome', { 93 | npmAlwaysAuth: true, 94 | npmAuthToken: 'MY-AUTH-TOKEN', 95 | // undefined: npmRegistryServer: 'https://npm.fontawesome.com/', 96 | }) 97 | 98 | should.equal(authToken, null) 99 | }) 100 | }) 101 | 102 | describe('getPathToLookForLocalYarnrc', () => { 103 | it('returns the correct path when using Yarn workspaces', async () => { 104 | /** Mock for filesystem calls. */ 105 | function readdirMock(path: string): Promise { 106 | switch (path) { 107 | case '/home/test-repo/packages/package-a': 108 | case 'C:\\home\\test-repo\\packages\\package-a': 109 | return Promise.resolve(['index.ts']) 110 | case '/home/test-repo/packages': 111 | case 'C:\\home\\test-repo\\packages': 112 | return Promise.resolve([]) 113 | case '/home/test-repo': 114 | case 'C:\\home\\test-repo': 115 | return Promise.resolve(['yarn.lock']) 116 | } 117 | 118 | throw new Error(`Mock cannot handle path: ${path}.`) 119 | } 120 | 121 | const yarnrcPath = await getPathToLookForYarnrc( 122 | { 123 | cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', 124 | }, 125 | readdirMock, 126 | ) 127 | 128 | should.exist(yarnrcPath) 129 | yarnrcPath!.should.equal(isWindows ? 'C:\\home\\test-repo\\.yarnrc.yml' : '/home/test-repo/.yarnrc.yml') 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /test/determinePackageManager.test.ts: -------------------------------------------------------------------------------- 1 | import determinePackageManager from '../src/lib/determinePackageManager' 2 | import chaiSetup from './helpers/chaiSetup' 3 | 4 | chaiSetup() 5 | 6 | const isWindows = process.platform === 'win32' 7 | 8 | describe('determinePackageManager', () => { 9 | it('returns bun if bun.lockb exists in cwd', async () => { 10 | /** Mock for filesystem calls. */ 11 | function readdirMock(path: string): Promise { 12 | switch (path) { 13 | case '/home/test-repo': 14 | case 'C:\\home\\test-repo': 15 | return Promise.resolve(['bun.lockb']) 16 | } 17 | 18 | throw new Error(`Mock cannot handle path: ${path}.`) 19 | } 20 | 21 | const packageManager = await determinePackageManager( 22 | { 23 | cwd: isWindows ? 'C:\\home\\test-repo' : '/home/test-repo', 24 | }, 25 | readdirMock, 26 | ) 27 | packageManager.should.equal('bun') 28 | }) 29 | 30 | it('returns bun if bun.lockb exists in an ancestor directory', async () => { 31 | /** Mock for filesystem calls. */ 32 | function readdirMock(path: string): Promise { 33 | switch (path) { 34 | case '/home/test-repo/packages/package-a': 35 | case 'C:\\home\\test-repo\\packages\\package-a': 36 | return Promise.resolve(['index.ts']) 37 | case '/home/test-repo/packages': 38 | case 'C:\\home\\test-repo\\packages': 39 | return Promise.resolve([]) 40 | case '/home/test-repo': 41 | case 'C:\\home\\test-repo': 42 | return Promise.resolve(['bun.lockb']) 43 | } 44 | 45 | throw new Error(`Mock cannot handle path: ${path}.`) 46 | } 47 | 48 | const packageManager = await determinePackageManager( 49 | { 50 | cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', 51 | }, 52 | readdirMock, 53 | ) 54 | packageManager.should.equal('bun') 55 | }) 56 | 57 | it('returns yarn if yarn.lock exists in cwd', async () => { 58 | /** Mock for filesystem calls. */ 59 | function readdirMock(path: string): Promise { 60 | switch (path) { 61 | case '/home/test-repo': 62 | case 'C:\\home\\test-repo': 63 | return Promise.resolve(['yarn.lock']) 64 | } 65 | 66 | throw new Error(`Mock cannot handle path: ${path}.`) 67 | } 68 | 69 | const packageManager = await determinePackageManager( 70 | { 71 | cwd: isWindows ? 'C:\\home\\test-repo' : '/home/test-repo', 72 | }, 73 | readdirMock, 74 | ) 75 | packageManager.should.equal('yarn') 76 | }) 77 | 78 | it('returns yarn if yarn.lock exists in an ancestor directory', async () => { 79 | /** Mock for filesystem calls. */ 80 | function readdirMock(path: string): Promise { 81 | switch (path) { 82 | case '/home/test-repo/packages/package-a': 83 | case 'C:\\home\\test-repo\\packages\\package-a': 84 | return Promise.resolve(['index.ts']) 85 | case '/home/test-repo/packages': 86 | case 'C:\\home\\test-repo\\packages': 87 | return Promise.resolve([]) 88 | case '/home/test-repo': 89 | case 'C:\\home\\test-repo': 90 | return Promise.resolve(['yarn.lock']) 91 | } 92 | 93 | throw new Error(`Mock cannot handle path: ${path}.`) 94 | } 95 | 96 | const packageManager = await determinePackageManager( 97 | { 98 | cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', 99 | }, 100 | readdirMock, 101 | ) 102 | packageManager.should.equal('yarn') 103 | }) 104 | 105 | it('returns npm if package-lock.json found before yarn.lock', async () => { 106 | /** Mock for filesystem calls. */ 107 | function readdirMock(path: string): Promise { 108 | switch (path) { 109 | case '/home/test-repo/packages/package-a': 110 | case 'C:\\home\\test-repo\\packages\\package-a': 111 | return Promise.resolve(['index.ts']) 112 | case '/home/test-repo/packages': 113 | case 'C:\\home\\test-repo\\packages': 114 | return Promise.resolve(['package-lock.json']) 115 | case '/home/test-repo': 116 | case 'C:\\home\\test-repo': 117 | return Promise.resolve(['yarn.lock']) 118 | } 119 | 120 | throw new Error(`Mock cannot handle path: ${path}.`) 121 | } 122 | 123 | const packageManager = await determinePackageManager( 124 | { 125 | cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', 126 | }, 127 | readdirMock, 128 | ) 129 | packageManager.should.equal('npm') 130 | }) 131 | 132 | it('does not loop infinitely if no lockfile found', async () => { 133 | /** Mock for filesystem calls. */ 134 | function readdirMock(): Promise { 135 | return Promise.resolve([]) 136 | } 137 | 138 | const packageManager = await determinePackageManager( 139 | { 140 | cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', 141 | }, 142 | readdirMock, 143 | ) 144 | packageManager.should.equal('npm') 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /src/scripts/build-options.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import spawn from 'spawn-please' 3 | import cliOptions, { renderExtendedHelp } from '../cli-options' 4 | import { chalkInit } from '../lib/chalk' 5 | import CLIOption from '../types/CLIOption' 6 | 7 | const INJECT_HEADER = 8 | '' 9 | 10 | /** Replaces markdown code ticks with ... tag. */ 11 | const codeHtml = (code: string) => code.replace(/\b`/g, '').replace(/`/g, '') 12 | 13 | /** Replaces the "Options" and "Advanced Options" sections of the README with direct output from "ncu --help". */ 14 | const injectReadme = async () => { 15 | const { default: stripAnsi } = await import('strip-ansi') 16 | let readme = await fs.readFile('README.md', 'utf8') 17 | const optionRows = cliOptions 18 | .map(option => { 19 | return ` 20 | ${option.help ? `` : ''}${option.short ? `-${option.short}, ` : ''}${ 21 | option.cli !== false ? '--' : '' 22 | }${option.long}${option.arg ? ` <${option.arg}>` : ''}${option.help ? '' : ''} 23 | ${codeHtml(option.description)}${option.default ? ` (default: ${JSON.stringify(option.default)})` : ''} 24 | ` 25 | }) 26 | .join('\n') 27 | 28 | // inject options into README 29 | const optionsStart = readme.indexOf('') + ''.length 30 | const optionsEnd = readme.indexOf('', optionsStart) 31 | readme = `${readme.slice(0, optionsStart)} 32 | ${INJECT_HEADER} 33 | 34 | 35 | ${optionRows} 36 |
37 | 38 | ${readme.slice(optionsEnd)}` 39 | 40 | // Inject advanced options into README 41 | // Even though chalkInit has a colorless option, we need stripAnsi to remove the ANSI characters frim the output of cli-table 42 | await chalkInit() 43 | const advancedOptionsStart = 44 | readme.indexOf('') + ''.length 45 | const advancedOptionsEnd = readme.indexOf('', advancedOptionsStart) 46 | readme = `${readme.slice(0, advancedOptionsStart)} 47 | ${INJECT_HEADER} 48 | 49 | ${cliOptions 50 | .filter(option => option.help) 51 | .map( 52 | option => `## ${option.long} 53 | 54 | ${stripAnsi(renderExtendedHelp(option, { markdown: true }))} 55 | `, 56 | ) 57 | .join('\n')} 58 | ${readme.slice(advancedOptionsEnd)}` 59 | 60 | return readme 61 | } 62 | 63 | /** Renders a single CLI option for a type definition file. */ 64 | const renderOption = (option: CLIOption) => { 65 | // deepPatternFix needs to be escaped, otherwise it will break the block comment 66 | const description = option.long === 'deep' ? option.description.replace('**/', '**\\/') : option.description 67 | 68 | // pre must be internally typed as number and externally typed as boolean to maintain compatibility with the CLI option and the RunOption 69 | const type = option.long === 'pre' ? 'boolean' : option.type 70 | 71 | const defaults = 72 | // do not render default empty arrays 73 | option.default && (!Array.isArray(option.default) || option.default.length > 0) 74 | ? `\n *\n * @default ${JSON.stringify(option.default)}\n ` 75 | : '' 76 | 77 | // all options are optional 78 | return ` /** ${description}${option.help ? ` Run "ncu --help --${option.long}" for details.` : ''}${defaults} */ 79 | ${option.long}?: ${type} 80 | ` 81 | } 82 | 83 | /** Generate /src/types/RunOptions from cli-options so there is a single source of truth. */ 84 | const generateRunOptions = (options: CLIOption[]) => { 85 | const header = `/** This file is generated automatically from the options specified in /src/cli-options.ts. Do not edit manually. Run "npm run build" or "npm run build:options" to build. */ 86 | import { FilterFunction } from './FilterFunction' 87 | import { FilterResultsFunction } from './FilterResultsFunction' 88 | import { GroupFunction } from './GroupFunction' 89 | import { PackageFile } from './PackageFile' 90 | import { TargetFunction } from './TargetFunction' 91 | 92 | /** Options that can be given on the CLI or passed to the ncu module to control all behavior. */ 93 | export interface RunOptions { 94 | ` 95 | 96 | const footer = '}\n' 97 | 98 | const optionsTypeCode = options.map(renderOption).join('\n') 99 | 100 | const output = `${header}${optionsTypeCode}${footer}` 101 | 102 | return output 103 | } 104 | 105 | /** Generates a JSON schema for the ncurc file. */ 106 | const generateRunOptionsJsonSchema = async (): Promise => { 107 | // programmatic usage of typescript-json-schema does not work, at least not straightforwardly. 108 | // Use the CLI which works out-of-the-box. 109 | const { stdout } = await spawn('typescript-json-schema', ['tsconfig.json', 'RunOptions']) 110 | return stdout 111 | } 112 | 113 | ;(async () => { 114 | await fs.writeFile('README.md', await injectReadme()) 115 | await fs.writeFile('src/types/RunOptions.ts', generateRunOptions(cliOptions)) 116 | await fs.writeFile('src/types/RunOptions.json', await generateRunOptionsJsonSchema()) 117 | await spawn('prettier', ['-w', 'src/types/RunOptions.json']) 118 | })() 119 | -------------------------------------------------------------------------------- /test/filterVersion.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import spawn from 'spawn-please' 3 | import ncu from '../src' 4 | import chaiSetup from './helpers/chaiSetup' 5 | import stubVersions from './helpers/stubVersions' 6 | 7 | chaiSetup() 8 | 9 | const bin = path.join(__dirname, '../build/cli.js') 10 | 11 | describe('filterVersion', () => { 12 | describe('module', () => { 13 | let stub: { restore: () => void } 14 | before(() => { 15 | stub = stubVersions({ 16 | 'ncu-test-v2': '2.0.0', 17 | 'ncu-test-return-version': '2.0.0', 18 | }) 19 | }) 20 | after(() => { 21 | stub.restore() 22 | }) 23 | 24 | it('filter by package version with string', async () => { 25 | const pkg = { 26 | dependencies: { 27 | 'ncu-test-v2': '1.0.0', 28 | 'ncu-test-return-version': '1.0.1', 29 | }, 30 | } 31 | 32 | const upgraded = await ncu({ 33 | packageData: pkg, 34 | filterVersion: '1.0.0', 35 | }) 36 | 37 | upgraded!.should.have.property('ncu-test-v2') 38 | upgraded!.should.not.have.property('ncu-test-return-version') 39 | 40 | stub.restore() 41 | }) 42 | 43 | it('filter by package version with space-delimited list of strings', async () => { 44 | const pkg = { 45 | dependencies: { 46 | 'ncu-test-v2': '1.0.0', 47 | 'ncu-test-return-version': '1.0.1', 48 | 'fp-and-or': '0.1.0', 49 | }, 50 | } 51 | 52 | const upgraded = await ncu({ 53 | packageData: pkg, 54 | filterVersion: '1.0.0 0.1.0', 55 | }) 56 | 57 | upgraded!.should.have.property('ncu-test-v2') 58 | upgraded!.should.not.have.property('ncu-test-return-version') 59 | upgraded!.should.have.property('fp-and-or') 60 | }) 61 | 62 | it('filter by package version with comma-delimited list of strings', async () => { 63 | const pkg = { 64 | dependencies: { 65 | 'ncu-test-v2': '1.0.0', 66 | 'ncu-test-return-version': '1.0.1', 67 | 'fp-and-or': '0.1.0', 68 | }, 69 | } 70 | 71 | const upgraded = await ncu({ 72 | packageData: pkg, 73 | filterVersion: '1.0.0,0.1.0', 74 | }) 75 | 76 | upgraded!.should.have.property('ncu-test-v2') 77 | upgraded!.should.not.have.property('ncu-test-return-version') 78 | upgraded!.should.have.property('fp-and-or') 79 | }) 80 | 81 | it('filter by package version with RegExp', async () => { 82 | const pkg = { 83 | dependencies: { 84 | 'ncu-test-v2': '1.0.0', 85 | 'ncu-test-return-version': '1.0.1', 86 | 'fp-and-or': '0.1.0', 87 | }, 88 | } 89 | 90 | const upgraded = await ncu({ 91 | packageData: pkg, 92 | filterVersion: /^1/, 93 | }) 94 | 95 | upgraded!.should.have.property('ncu-test-v2') 96 | upgraded!.should.have.property('ncu-test-return-version') 97 | upgraded!.should.not.have.property('fp-and-or') 98 | }) 99 | 100 | it('filter by package version with RegExp string', async () => { 101 | const pkg = { 102 | dependencies: { 103 | 'ncu-test-v2': '1.0.0', 104 | 'ncu-test-return-version': '1.0.1', 105 | 'fp-and-or': '0.1.0', 106 | }, 107 | } 108 | 109 | const upgraded = await ncu({ 110 | packageData: pkg, 111 | filterVersion: '/^1/', 112 | }) 113 | 114 | upgraded!.should.have.property('ncu-test-v2') 115 | upgraded!.should.have.property('ncu-test-return-version') 116 | upgraded!.should.not.have.property('fp-and-or') 117 | }) 118 | }) 119 | 120 | describe('cli', () => { 121 | it('allow multiple --filterVersion options', async () => { 122 | const stub = stubVersions('99.9.9', { spawn: true }) 123 | const pkgData = { 124 | dependencies: { 125 | 'ncu-test-v2': '1.0.0', 126 | 'ncu-test-10': '1.0.9', 127 | }, 128 | } 129 | 130 | const { stdout } = await spawn( 131 | 'node', 132 | [bin, '--jsonUpgraded', '--verbose', '--stdin', '--filterVersion', '1.0.0', '--filterVersion', '1.0.9'], 133 | { stdin: JSON.stringify(pkgData) }, 134 | ) 135 | const upgraded = JSON.parse(stdout) 136 | upgraded.should.have.property('ncu-test-v2') 137 | upgraded.should.have.property('ncu-test-10') 138 | stub.restore() 139 | }) 140 | }) 141 | }) 142 | 143 | describe('rejectVersion', () => { 144 | describe('cli', () => { 145 | it('allow multiple --rejectVersion options', async () => { 146 | const stub = stubVersions('99.9.9', { spawn: true }) 147 | const pkgData = { 148 | dependencies: { 149 | 'ncu-test-v2': '1.0.0', 150 | 'ncu-test-10': '1.0.9', 151 | }, 152 | } 153 | 154 | const { stdout } = await spawn( 155 | 'node', 156 | [bin, '--jsonUpgraded', '--verbose', '--stdin', '--rejectVersion', '1.0.0', '--rejectVersion', '1.0.9'], 157 | { stdin: JSON.stringify(pkgData) }, 158 | ) 159 | const upgraded = JSON.parse(stdout) 160 | upgraded.should.not.have.property('ncu-test-v2') 161 | upgraded.should.not.have.property('ncu-test-10') 162 | stub.restore() 163 | }) 164 | }) 165 | }) 166 | --------------------------------------------------------------------------------