diff --git a/README.md b/README.md index d649af73e..24e3445fc 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,9 @@ ncu "/^(?!react-).*$/" # windows --packageData Package file data (you can also use stdin). --packageFile Package file(s) location. (default: ./package.json) --p, --packageManager npm, yarn, pnpm, staticRegistry (default: npm). - Run "ncu --help --packageManager" for details. +-p, --packageManager npm, yarn, pnpm, deno, staticRegistry (default: + npm). Run "ncu --help --packageManager" for + details. --peer Check peer dependencies of installed packages and filter updates to compatible versions. Run "ncu --help --peer" for details. diff --git a/package.json b/package.json index a3fe21279..be3da6267 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "prepare": "husky install", "prepublishOnly": "npm run build", "prettier": "prettier .", - "test": "mocha test test/package-managers/npm test/package-managers/yarn test/package-managers/staticRegistry", + "test": "mocha test test/package-managers/*", "test:cov": "npm run c8 npm run test", "ncu": "node build/src/bin/cli.js" }, diff --git a/src/cli-options.ts b/src/cli-options.ts index d3ab7c44e..ce426c58f 100755 --- a/src/cli-options.ts +++ b/src/cli-options.ts @@ -517,9 +517,9 @@ const cliOptions: CLIOption[] = [ long: 'packageManager', short: 'p', arg: 's', - description: 'npm, yarn, pnpm, staticRegistry (default: npm).', + description: 'npm, yarn, pnpm, deno, staticRegistry (default: npm).', help: extendedHelpPackageManager, - type: `'npm' | 'yarn' | 'pnpm' | 'staticRegistry'`, + type: `'npm' | 'yarn' | 'pnpm' | 'deno' | 'staticRegistry'`, }, { long: 'peer', diff --git a/src/lib/determinePackageManager.ts b/src/lib/determinePackageManager.ts index 3a88cab44..fa35a1a2a 100644 --- a/src/lib/determinePackageManager.ts +++ b/src/lib/determinePackageManager.ts @@ -9,6 +9,7 @@ const packageManagerLockfileMap: Index = { 'package-lock': 'npm', yarn: 'yarn', 'pnpm-lock': 'pnpm', + deno: 'deno', } /** diff --git a/src/lib/findLockfile.ts b/src/lib/findLockfile.ts index 666832f00..429533c23 100644 --- a/src/lib/findLockfile.ts +++ b/src/lib/findLockfile.ts @@ -29,6 +29,8 @@ export default async function findLockfile( return { directoryPath: currentPath, filename: 'yarn.lock' } } else if (files.includes('pnpm-lock.yaml')) { return { directoryPath: currentPath, filename: 'pnpm-lock.yaml' } + } else if (files.includes('deno.json')) { + return { directoryPath: currentPath, filename: 'deno.json' } } const pathParent = path.resolve(currentPath, '..') diff --git a/src/lib/findPackage.ts b/src/lib/findPackage.ts index 33b14c65b..f2b17c628 100644 --- a/src/lib/findPackage.ts +++ b/src/lib/findPackage.ts @@ -42,7 +42,9 @@ async function findPackage(options: Options) { ) } - return fs.readFile(pkgFile!, 'utf-8') + return fs.readFile(pkgFile!, 'utf-8').catch(e => { + programError(options, chalk.red(e)) + }) } print(options, 'Running in local mode', 'verbose') diff --git a/src/lib/getCurrentDependencies.ts b/src/lib/getCurrentDependencies.ts index 7107c0976..5b57892f1 100644 --- a/src/lib/getCurrentDependencies.ts +++ b/src/lib/getCurrentDependencies.ts @@ -6,6 +6,7 @@ import { VersionSpec } from '../types/VersionSpec' import filterAndReject from './filterAndReject' import filterObject from './filterObject' import { keyValueBy } from './keyValueBy' +import resolveDepSections from './resolveDepSections' /** Returns true if spec1 is greater than spec2, ignoring invalid version ranges. */ const isGreaterThanSafe = (spec1: VersionSpec, spec2: VersionSpec) => @@ -32,21 +33,7 @@ const parsePackageManager = (pkgData: PackageFile) => { * @returns Promised {packageName: version} collection */ function getCurrentDependencies(pkgData: PackageFile = {}, options: Options = {}) { - const depOptions = options.dep - ? typeof options.dep === 'string' - ? options.dep.split(',') - : options.dep - : ['prod', 'dev', 'optional'] - - // map the dependency section option to a full dependency section name - const depSections = depOptions.map( - short => - (short === 'prod' - ? 'dependencies' - : short === 'packageManager' - ? short - : short + 'Dependencies') as keyof PackageFile, - ) + const depSections = resolveDepSections(options.dep) // get all dependencies from the selected sections // if a dependency appears in more than one section, take the lowest version number diff --git a/src/lib/getPackageManager.ts b/src/lib/getPackageManager.ts index 98ca28bb8..ced354a7d 100644 --- a/src/lib/getPackageManager.ts +++ b/src/lib/getPackageManager.ts @@ -12,7 +12,7 @@ import { PackageManager } from '../types/PackageManager' */ function getPackageManager(name: Maybe): PackageManager { // default to npm - if (!name) { + if (!name || name === 'deno') { return packageManagers.npm } diff --git a/src/lib/initOptions.ts b/src/lib/initOptions.ts index c5755caf9..cf9214d40 100644 --- a/src/lib/initOptions.ts +++ b/src/lib/initOptions.ts @@ -1,6 +1,6 @@ import isEqual from 'lodash/isEqual' import propertyOf from 'lodash/propertyOf' -import cliOptions from '../cli-options' +import cliOptions, { cliOptionsMap } from '../cli-options' import { print } from '../lib/logging' import { FilterPattern } from '../types/FilterPattern' import { Options } from '../types/Options' @@ -156,14 +156,14 @@ async function initOptions(runOptions: RunOptions, { cli }: { cli?: boolean } = const packageManager = await determinePackageManager(options) - // print 'Using yarn/pnpm/etc' when autodetected - if (!options.packageManager && packageManager !== 'npm') { - print(options, `Using ${packageManager}`) - } - const resolvedOptions: Options = { ...options, - ...(options.deep ? { packageFile: '**/package.json' } : null), + ...(options.deep + ? { packageFile: '**/package.json' } + : packageManager === 'deno' && !options.packageFile + ? { packageFile: 'deno.json' } + : null), + ...(packageManager === 'deno' && options.dep !== cliOptionsMap.dep.default ? { dep: ['imports'] } : null), ...(options.format && options.format.length > 0 ? { format: options.format } : null), filter: args || filter, // add shortcut for any keys that start with 'json' @@ -180,6 +180,12 @@ async function initOptions(runOptions: RunOptions, { cli }: { cli?: boolean } = } resolvedOptions.cacher = await cacher(resolvedOptions) + // print 'Using yarn/pnpm/etc' when autodetected + // use resolved options so that options.json is set + if (!options.packageManager && packageManager !== 'npm') { + print(resolvedOptions, `Using ${packageManager}`) + } + return resolvedOptions } diff --git a/src/lib/resolveDepSections.ts b/src/lib/resolveDepSections.ts new file mode 100644 index 000000000..6076918a2 --- /dev/null +++ b/src/lib/resolveDepSections.ts @@ -0,0 +1,24 @@ +import { cliOptionsMap } from '../cli-options' +import { Index } from '../types/IndexType' +import { PackageFile } from '../types/PackageFile' + +// dependency section aliases that will be resolved to the full name +const depAliases: Index = { + dev: 'devDependencies', + peer: 'peerDependencies', + prod: 'dependencies', + optional: 'optionalDependencies', +} + +/** Gets a list of dependency sections based on options.dep. */ +const resolveDepSections = (dep?: string | string[]): (keyof PackageFile)[] => { + // parse dep string and set default + const depOptions: string[] = dep ? (typeof dep === 'string' ? dep.split(',') : dep) : cliOptionsMap.dep.default + + // map the dependency section option to a full dependency section name + const depSections = depOptions.map(name => depAliases[name] || name) + + return depSections +} + +export default resolveDepSections diff --git a/src/lib/runLocal.ts b/src/lib/runLocal.ts index 5427d5d39..fa1f0541a 100644 --- a/src/lib/runLocal.ts +++ b/src/lib/runLocal.ts @@ -20,6 +20,7 @@ import getPeerDependencies from './getPeerDependencies' import keyValueBy from './keyValueBy' import { print, printIgnoredUpdates, printJson, printUpgrades, toDependencyTable } from './logging' import programError from './programError' +import resolveDepSections from './resolveDepSections' import upgradePackageData from './upgradePackageData' import upgradePackageDefinitions from './upgradePackageDefinitions' import { getDependencyGroups } from './version-util' @@ -255,7 +256,7 @@ async function runLocal( const output = options.jsonAll ? (jph.parse(newPkgData) as PackageFile) : options.jsonDeps - ? pick(jph.parse(newPkgData) as PackageFile, 'dependencies', 'devDependencies', 'optionalDependencies') + ? pick(jph.parse(newPkgData) as PackageFile, resolveDepSections(options.dep)) : chosenUpgraded // will be overwritten with the result of fs.writeFile so that the return promise waits for the package file to be written diff --git a/src/lib/upgradePackageData.ts b/src/lib/upgradePackageData.ts index 914090c08..ba084a186 100644 --- a/src/lib/upgradePackageData.ts +++ b/src/lib/upgradePackageData.ts @@ -2,6 +2,7 @@ import { Index } from '../types/IndexType' import { Options } from '../types/Options' import { PackageFile } from '../types/PackageFile' import { VersionSpec } from '../types/VersionSpec' +import resolveDepSections from './resolveDepSections' /** * @returns String safe for use in `new RegExp()` @@ -25,21 +26,7 @@ async function upgradePackageData( upgraded: Index, options: Options, ) { - const depOptions = options.dep - ? typeof options.dep === 'string' - ? options.dep.split(',') - : options.dep - : ['prod', 'dev', 'optional'] - - // map the dependency section option to a full dependency section name - const depSections = depOptions.map( - short => - (short === 'prod' - ? 'dependencies' - : short === 'packageManager' - ? short - : short + 'Dependencies') as keyof PackageFile, - ) + const depSections = resolveDepSections(options.dep) // iterate through each dependency section const sectionRegExp = new RegExp(`"(${depSections.join(`|`)})"s*:[^}]*`, 'g') @@ -54,7 +41,7 @@ async function upgradePackageData( return section }) - if (depOptions.includes('packageManager')) { + if (depSections.includes('packageManager')) { const pkg = JSON.parse(pkgData) as PackageFile if (pkg.packageManager) { const [name] = pkg.packageManager.split('@') diff --git a/src/types/PackageFile.ts b/src/types/PackageFile.ts index 0d1e6b29e..326bae83e 100644 --- a/src/types/PackageFile.ts +++ b/src/types/PackageFile.ts @@ -6,6 +6,8 @@ import { VersionSpec } from './VersionSpec' export interface PackageFile { dependencies?: Index devDependencies?: Index + // deno only + imports?: Index engines?: Index name?: string // https://nodejs.org/api/packages.html#packagemanager diff --git a/src/types/PackageManagerName.ts b/src/types/PackageManagerName.ts index 20fad76b7..cdd7e5401 100644 --- a/src/types/PackageManagerName.ts +++ b/src/types/PackageManagerName.ts @@ -1,2 +1,2 @@ /** A valid package manager. */ -export type PackageManagerName = 'npm' | 'yarn' | 'pnpm' | 'staticRegistry' +export type PackageManagerName = 'npm' | 'yarn' | 'pnpm' | 'deno' | 'staticRegistry' diff --git a/src/types/RunOptions.ts b/src/types/RunOptions.ts index 9dd03d4ae..e87a21d9e 100644 --- a/src/types/RunOptions.ts +++ b/src/types/RunOptions.ts @@ -99,8 +99,8 @@ export interface RunOptions { /** Package file(s) location. (default: ./package.json) */ packageFile?: string - /** npm, yarn, pnpm, staticRegistry (default: npm). Run "ncu --help --packageManager" for details. */ - packageManager?: 'npm' | 'yarn' | 'pnpm' | 'staticRegistry' + /** npm, yarn, pnpm, deno, staticRegistry (default: npm). Run "ncu --help --packageManager" for details. */ + packageManager?: 'npm' | 'yarn' | 'pnpm' | 'deno' | 'staticRegistry' /** Check peer dependencies of installed packages and filter updates to compatible versions. Run "ncu --help --peer" for details. */ peer?: boolean diff --git a/test/package-managers/deno/index.test.ts b/test/package-managers/deno/index.test.ts new file mode 100644 index 000000000..429ae68fc --- /dev/null +++ b/test/package-managers/deno/index.test.ts @@ -0,0 +1,60 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import chaiString from 'chai-string' +import fs from 'fs/promises' +import jph from 'json-parse-helpfulerror' +import os from 'os' +import path from 'path' +import spawn from 'spawn-please' + +chai.should() +chai.use(chaiAsPromised) +chai.use(chaiString) + +process.env.NCU_TESTS = 'true' + +const bin = path.join(__dirname, '../../../build/src/bin/cli.js') + +describe('deno', async function () { + it('handle import map', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) + const pkgFile = path.join(tempDir, 'deno.json') + const pkg = { + imports: { + 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', + }, + } + await fs.writeFile(pkgFile, JSON.stringify(pkg), 'utf-8') + try { + const pkgData = await spawn( + 'node', + [bin, '--jsonUpgraded', '--verbose', '--packageManager', 'deno', '--packageFile', pkgFile], + undefined, + ) + const pkg = jph.parse(pkgData) + pkg.should.have.property('ncu-test-v2') + } finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) + + it('auto detect deno.json', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-')) + const pkgFile = path.join(tempDir, 'deno.json') + const pkg = { + imports: { + 'ncu-test-v2': 'npm:ncu-test-v2@1.0.0', + }, + } + await fs.writeFile(pkgFile, JSON.stringify(pkg), 'utf-8') + try { + const pkgData = await spawn('node', [bin, '--jsonUpgraded', '--verbose'], undefined, { + cwd: tempDir, + }) + const pkg = jph.parse(pkgData) + pkg.should.have.property('ncu-test-v2') + } finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) +}) diff --git a/test/package-managers/yarn/index.test.ts b/test/package-managers/yarn/index.test.ts index 2d58f60d8..b594f448f 100644 --- a/test/package-managers/yarn/index.test.ts +++ b/test/package-managers/yarn/index.test.ts @@ -94,35 +94,35 @@ describe('yarn', function () { should.equal(authToken, null) }) }) -}) -describe('getPathToLookForLocalYarnrc', () => { - it('returns the correct path when using Yarn workspaces', async () => { - /** Mock for filesystem calls. */ - function readdirMock(path: string): Promise { - switch (path) { - case '/home/test-repo/packages/package-a': - case 'C:\\home\\test-repo\\packages\\package-a': - return Promise.resolve(['index.ts']) - case '/home/test-repo/packages': - case 'C:\\home\\test-repo\\packages': - return Promise.resolve([]) - case '/home/test-repo': - case 'C:\\home\\test-repo': - return Promise.resolve(['yarn.lock']) + describe('getPathToLookForLocalYarnrc', () => { + it('returns the correct path when using Yarn workspaces', async () => { + /** Mock for filesystem calls. */ + function readdirMock(path: string): Promise { + switch (path) { + case '/home/test-repo/packages/package-a': + case 'C:\\home\\test-repo\\packages\\package-a': + return Promise.resolve(['index.ts']) + case '/home/test-repo/packages': + case 'C:\\home\\test-repo\\packages': + return Promise.resolve([]) + case '/home/test-repo': + case 'C:\\home\\test-repo': + return Promise.resolve(['yarn.lock']) + } + + throw new Error(`Mock cannot handle path: ${path}.`) } - throw new Error(`Mock cannot handle path: ${path}.`) - } - - const yarnrcPath = await getPathToLookForYarnrc( - { - cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', - }, - readdirMock, - ) + const yarnrcPath = await getPathToLookForYarnrc( + { + cwd: isWindows ? 'C:\\home\\test-repo\\packages\\package-a' : '/home/test-repo/packages/package-a', + }, + readdirMock, + ) - should.exist(yarnrcPath) - yarnrcPath!.should.equal(isWindows ? 'C:\\home\\test-repo\\.yarnrc.yml' : '/home/test-repo/.yarnrc.yml') + should.exist(yarnrcPath) + yarnrcPath!.should.equal(isWindows ? 'C:\\home\\test-repo\\.yarnrc.yml' : '/home/test-repo/.yarnrc.yml') + }) }) })