From adb9999ab864282167de67176aade57c7f29c9aa Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 30 Oct 2022 22:18:52 +0000 Subject: [PATCH] feat: create `licenses`-command for PNPM Introduces a new command `licenses`-command which allows to list the licenses of the packages refs #2825 --- .../licenses/src/licensesDepsOfProjects.ts | 67 +++ packages/licenses/test/getManifest.spec.ts | 56 ++ packages/licenses/test/outdated.spec.ts | 532 ++++++++++++++++++ .../plugin-commands-licenses/package.json | 2 +- .../plugin-commands-licenses/src/licenses.ts | 187 +++--- .../src/outputRenderer.ts | 113 ++-- .../plugin-commands-licenses/src/recursive.ts | 58 ++ .../plugin-commands-licenses/test/index.ts | 299 ++++++++++ .../test/recursive.ts | 310 ++++++++++ .../test/utils/index.ts | 52 ++ .../plugin-commands-licenses/tsconfig.json | 5 +- 11 files changed, 1528 insertions(+), 153 deletions(-) create mode 100644 packages/licenses/src/licensesDepsOfProjects.ts create mode 100644 packages/licenses/test/getManifest.spec.ts create mode 100644 packages/licenses/test/outdated.spec.ts create mode 100644 packages/plugin-commands-licenses/src/recursive.ts create mode 100644 packages/plugin-commands-licenses/test/recursive.ts create mode 100644 packages/plugin-commands-licenses/test/utils/index.ts diff --git a/packages/licenses/src/licensesDepsOfProjects.ts b/packages/licenses/src/licensesDepsOfProjects.ts new file mode 100644 index 00000000000..3e309cdf3b0 --- /dev/null +++ b/packages/licenses/src/licensesDepsOfProjects.ts @@ -0,0 +1,67 @@ +import path from 'path' +import { + readCurrentLockfile, + readWantedLockfile, +} from '@pnpm/lockfile-file' +import { createMatcher } from '@pnpm/matcher' +import { readModulesManifest } from '@pnpm/modules-yaml' +import { + IncludedDependencies, + ProjectManifest, + Registries, +} from '@pnpm/types' +import unnest from 'ramda/src/unnest' +import { licences, LicensePackage } from './licenses' +import { ClientOptions } from '@pnpm/client' + +interface GetManifestOpts { + dir: string + lockfileDir: string + virtualStoreDir: string + rawConfig: object + registries: Registries +} + +export type ManifestGetterOptions = Omit +& GetManifestOpts +& { fullMetadata: boolean, rawConfig: Record } + +export async function licensesDepsOfProjects ( + pkgs: Array<{ dir: string, manifest: ProjectManifest }>, + args: string[], + opts: Omit & { + compatible?: boolean + ignoreDependencies?: Set + include: IncludedDependencies + } & Partial> +): Promise { + if (!opts.lockfileDir) { + return unnest(await Promise.all( + pkgs.map(async (pkg) => { + return licensesDepsOfProjects([pkg], args, { ...opts, lockfileDir: pkg.dir }) + } + ) + )) + } + + const lockfileDir = opts.lockfileDir ?? opts.dir + const modules = await readModulesManifest(path.join(lockfileDir, 'node_modules')) + const virtualStoreDir = modules?.virtualStoreDir ?? path.join(lockfileDir, 'node_modules/.pnpm') + const currentLockfile = await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false }) + const wantedLockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }) ?? currentLockfile + return Promise.all(pkgs.map(async ({ dir, manifest }) => { + const match = (args.length > 0) && createMatcher(args) || undefined + return licences({ + compatible: opts.compatible, + currentLockfile, + ignoreDependencies: opts.ignoreDependencies, + include: opts.include, + lockfileDir, + manifest, + match, + prefix: dir, + registries: opts.registries, + wantedLockfile, + }) + })) +} diff --git a/packages/licenses/test/getManifest.spec.ts b/packages/licenses/test/getManifest.spec.ts new file mode 100644 index 00000000000..b97eaeb43e9 --- /dev/null +++ b/packages/licenses/test/getManifest.spec.ts @@ -0,0 +1,56 @@ +import { ResolveFunction } from '@pnpm/client' +import { getManifest } from '../lib/createManifestGetter' + +test('getManifest()', async () => { + const opts = { + dir: '', + lockfileDir: '', + rawConfig: {}, + registries: { + '@scope': 'https://pnpm.io/', + default: 'https://registry.npmjs.org/', + }, + } + + const resolve: ResolveFunction = async function (wantedPackage, opts) { + expect(opts.registry).toEqual('https://registry.npmjs.org/') + return { + id: 'foo/1.0.0', + latest: '1.0.0', + manifest: { + name: 'foo', + version: '1.0.0', + }, + resolution: { + type: 'tarball', + }, + resolvedVia: 'npm-registry', + } + } + + expect(await getManifest(resolve, opts, 'foo', 'latest')).toStrictEqual({ + name: 'foo', + version: '1.0.0', + }) + + const resolve2: ResolveFunction = async function (wantedPackage, opts) { + expect(opts.registry).toEqual('https://pnpm.io/') + return { + id: 'foo/2.0.0', + latest: '2.0.0', + manifest: { + name: 'foo', + version: '2.0.0', + }, + resolution: { + type: 'tarball', + }, + resolvedVia: 'npm-registry', + } + } + + expect(await getManifest(resolve2, opts, '@scope/foo', 'latest')).toStrictEqual({ + name: 'foo', + version: '2.0.0', + }) +}) diff --git a/packages/licenses/test/outdated.spec.ts b/packages/licenses/test/outdated.spec.ts new file mode 100644 index 00000000000..f0aeb6bc5fa --- /dev/null +++ b/packages/licenses/test/outdated.spec.ts @@ -0,0 +1,532 @@ +import { outdated } from '../lib/outdated' + +async function getLatestManifest (packageName: string) { + return ({ + 'deprecated-pkg': { + deprecated: 'This package is deprecated', + name: 'deprecated-pkg', + version: '1.0.0', + }, + 'is-negative': { + name: 'is-negative', + version: '2.1.0', + }, + 'is-positive': { + name: 'is-positive', + version: '3.1.0', + }, + 'pkg-with-1-dep': { + name: 'pkg-with-1-dep', + version: '1.0.0', + }, + })[packageName] ?? null +} + +test('outdated()', async () => { + const outdatedPkgs = await outdated({ + currentLockfile: { + importers: { + '.': { + dependencies: { + 'from-github': 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b4', + }, + devDependencies: { + 'is-negative': '1.0.0', + 'is-positive': '1.0.0', + }, + optionalDependencies: { + 'linked-1': 'link:../linked-1', + 'linked-2': 'file:../linked-2', + }, + specifiers: { + 'from-github': 'github:blabla/from-github#d5f8d5500f7faf593d32e134c1b0043ff69151b4', + 'is-negative': '^2.1.0', + 'is-positive': '^1.0.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-negative/2.1.0': { + dev: true, + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/is-positive/1.0.0': { + dev: true, + resolution: { + integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==', + }, + }, + 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b4': { + name: 'from-github', + version: '1.1.0', + + dev: false, + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + }, + }, + getLatestManifest, + lockfileDir: 'project', + manifest: { + name: 'wanted-shrinkwrap', + version: '1.0.0', + dependencies: { + 'from-github': 'github:blabla/from-github#d5f8d5500f7faf593d32e134c1b0043ff69151b4', + 'from-github-2': 'github:blabla/from-github-2#d5f8d5500f7faf593d32e134c1b0043ff69151b4', + }, + devDependencies: { + 'is-negative': '^2.1.0', + 'is-positive': '^3.1.0', + }, + }, + prefix: 'project', + wantedLockfile: { + importers: { + '.': { + dependencies: { + 'from-github': 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + 'from-github-2': 'github.com/blabla/from-github-2/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + devDependencies: { + 'is-negative': '1.1.0', + 'is-positive': '3.1.0', + }, + optionalDependencies: { + 'linked-1': 'link:../linked-1', + 'linked-2': 'file:../linked-2', + }, + specifiers: { + 'from-github': 'github:blabla/from-github#d5f8d5500f7faf593d32e134c1b0043ff69151b4', + 'from-github-2': 'github:blabla/from-github-2#d5f8d5500f7faf593d32e134c1b0043ff69151b4', + 'is-negative': '^2.1.0', + 'is-positive': '^3.1.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-negative/1.1.0': { + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/is-positive/3.1.0': { + resolution: { + integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==', + }, + }, + 'github.com/blabla/from-github-2/d5f8d5500f7faf593d32e134c1b0043ff69151b3': { + name: 'from-github-2', + version: '1.0.0', + + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github-2/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b3': { + name: 'from-github', + version: '1.0.0', + + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + }, + }, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'from-github', + belongsTo: 'dependencies', + current: 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b4', + latestManifest: undefined, + packageName: 'from-github', + wanted: 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + { + alias: 'from-github-2', + belongsTo: 'dependencies', + current: undefined, + latestManifest: undefined, + packageName: 'from-github-2', + wanted: 'github.com/blabla/from-github-2/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + { + alias: 'is-negative', + belongsTo: 'devDependencies', + current: '1.0.0', + latestManifest: { + name: 'is-negative', + version: '2.1.0', + }, + packageName: 'is-negative', + wanted: '1.1.0', + }, + { + alias: 'is-positive', + belongsTo: 'devDependencies', + current: '1.0.0', + latestManifest: { + name: 'is-positive', + version: '3.1.0', + }, + packageName: 'is-positive', + wanted: '3.1.0', + }, + ]) +}) + +test('outdated() should return deprecated package even if its current version is latest', async () => { + const lockfile = { + importers: { + '.': { + dependencies: { + 'deprecated-pkg': '1.0.0', + }, + specifiers: { + 'deprecated-pkg': '^1.0.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/deprecated-pkg/1.0.0': { + dev: false, + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + }, + } + const outdatedPkgs = await outdated({ + currentLockfile: lockfile, + getLatestManifest, + lockfileDir: 'project', + manifest: { + name: 'wanted-shrinkwrap', + version: '1.0.0', + + dependencies: { + 'deprecated-pkg': '1.0.0', + }, + }, + prefix: 'project', + wantedLockfile: lockfile, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'deprecated-pkg', + belongsTo: 'dependencies', + current: '1.0.0', + latestManifest: { + deprecated: 'This package is deprecated', + name: 'deprecated-pkg', + version: '1.0.0', + }, + packageName: 'deprecated-pkg', + wanted: '1.0.0', + }, + ]) +}) + +test('using a matcher', async () => { + const outdatedPkgs = await outdated({ + currentLockfile: { + importers: { + '.': { + dependencies: { + 'from-github': 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b4', + 'is-negative': '1.0.0', + 'is-positive': '1.0.0', + 'linked-1': 'link:../linked-1', + 'linked-2': 'file:../linked-2', + }, + specifiers: { + 'is-negative': '^2.1.0', + 'is-positive': '^1.0.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-negative/2.1.0': { + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/is-positive/1.0.0': { + resolution: { + integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==', + }, + }, + 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b4': { + name: 'from-github', + version: '1.1.0', + + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + }, + }, + getLatestManifest, + lockfileDir: 'wanted-shrinkwrap', + manifest: { + name: 'wanted-shrinkwrap', + version: '1.0.0', + + dependencies: { + 'is-negative': '^2.1.0', + 'is-positive': '^3.1.0', + }, + }, + match: (dependencyName) => dependencyName === 'is-negative', + prefix: 'wanted-shrinkwrap', + wantedLockfile: { + importers: { + '.': { + dependencies: { + 'from-github': 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + 'from-github-2': 'github.com/blabla/from-github-2/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + 'is-negative': '1.1.0', + 'is-positive': '3.1.0', + 'linked-1': 'link:../linked-1', + 'linked-2': 'file:../linked-2', + }, + specifiers: { + 'is-negative': '^2.1.0', + 'is-positive': '^3.1.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-negative/1.1.0': { + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/is-positive/3.1.0': { + resolution: { + integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==', + }, + }, + 'github.com/blabla/from-github-2/d5f8d5500f7faf593d32e134c1b0043ff69151b3': { + name: 'from-github-2', + version: '1.0.0', + + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github-2/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + 'github.com/blabla/from-github/d5f8d5500f7faf593d32e134c1b0043ff69151b3': { + name: 'from-github', + version: '1.0.0', + + resolution: { + tarball: 'https://codeload.github.com/blabla/from-github/tar.gz/d5f8d5500f7faf593d32e134c1b0043ff69151b3', + }, + }, + }, + }, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'is-negative', + belongsTo: 'dependencies', + current: '1.0.0', + latestManifest: { + name: 'is-negative', + version: '2.1.0', + }, + packageName: 'is-negative', + wanted: '1.1.0', + }, + ]) +}) + +test('outdated() aliased dependency', async () => { + const outdatedPkgs = await outdated({ + currentLockfile: { + importers: { + '.': { + dependencies: { + positive: '/is-positive/1.0.0', + }, + specifiers: { + positive: 'npm:is-positive@^1.0.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-positive/1.0.0': { + resolution: { + integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==', + }, + }, + }, + }, + getLatestManifest, + lockfileDir: 'project', + manifest: { + name: 'wanted-shrinkwrap', + version: '1.0.0', + + dependencies: { + positive: 'npm:is-positive@^3.1.0', + }, + }, + prefix: 'project', + wantedLockfile: { + importers: { + '.': { + dependencies: { + positive: '/is-positive/3.1.0', + }, + specifiers: { + positive: 'npm:is-positive@^3.1.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/is-positive/3.1.0': { + resolution: { + integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==', + }, + }, + }, + }, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'positive', + belongsTo: 'dependencies', + current: '1.0.0', + latestManifest: { + name: 'is-positive', + version: '3.1.0', + }, + packageName: 'is-positive', + wanted: '3.1.0', + }, + ]) +}) + +test('a dependency is not outdated if it is newer than the latest version', async () => { + const lockfile = { + importers: { + '.': { + dependencies: { + foo: '1.0.0', + foo2: '2.0.0-0', + foo3: '2.0.0', + }, + specifiers: { + foo: '^1.0.0', + foo2: '2.0.0-0', + foo3: '2.0.0', + }, + }, + }, + lockfileVersion: 5, + packages: { + '/foo/1.0.0': { + dev: false, + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/foo2/2.0.0-0': { + dev: false, + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + '/foo3/2.0.0': { + dev: false, + resolution: { + integrity: 'sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc=', + }, + }, + }, + } + const outdatedPkgs = await outdated({ + currentLockfile: lockfile, + getLatestManifest: async (packageName) => { + switch (packageName) { + case 'foo': + return { + name: 'foo', + version: '0.1.0', + } + case 'foo2': + return { + name: 'foo2', + version: '1.0.0', + } + case 'foo3': + return { + name: 'foo3', + version: '2.0.0', + } + } + return null + }, + lockfileDir: 'project', + manifest: { + name: 'pkg', + version: '1.0.0', + + dependencies: { + foo: '^1.0.0', + foo2: '2.0.0-0', + foo3: '2.0.0', + }, + }, + prefix: 'project', + wantedLockfile: lockfile, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([]) +}) + +test('outdated() should [] when there is no dependency', async () => { + const outdatedPkgs = await outdated({ + currentLockfile: null, + getLatestManifest: async () => { + return null + }, + lockfileDir: 'project', + manifest: { + name: 'pkg', + version: '1.0.0', + }, + prefix: 'project', + wantedLockfile: null, + registries: { + default: 'https://registry.npmjs.org/', + }, + }) + expect(outdatedPkgs).toStrictEqual([]) +}) diff --git a/packages/plugin-commands-licenses/package.json b/packages/plugin-commands-licenses/package.json index ddcdd9e0223..112604f3fb7 100644 --- a/packages/plugin-commands-licenses/package.json +++ b/packages/plugin-commands-licenses/package.json @@ -16,7 +16,7 @@ "registry-mock": "registry-mock", "test:jest": "jest", "test:e2e": "registry-mock prepare && run-p -r registry-mock test:jest", - "_test": "jest -u", + "_test": "jest", "test": "pnpm run compile && pnpm run _test", "prepublishOnly": "pnpm run compile", "compile": "tsc --build && pnpm run lint --fix" diff --git a/packages/plugin-commands-licenses/src/licenses.ts b/packages/plugin-commands-licenses/src/licenses.ts index 2dbb2ec235f..ef3ea6f4a67 100644 --- a/packages/plugin-commands-licenses/src/licenses.ts +++ b/packages/plugin-commands-licenses/src/licenses.ts @@ -2,55 +2,55 @@ import { docsUrl, readDepNameCompletions, readProjectManifestOnly, -} from '@pnpm/cli-utils' -import { CompletionFunc } from '@pnpm/command' -import { WANTED_LOCKFILE } from '@pnpm/constants' -import { readWantedLockfile } from '@pnpm/lockfile-file' +} from "@pnpm/cli-utils"; +import { CompletionFunc } from "@pnpm/command"; +import { WANTED_LOCKFILE } from "@pnpm/constants"; +import { readWantedLockfile } from "@pnpm/lockfile-file"; import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS, -} from '@pnpm/common-cli-options-help' -import { Config, types as allTypes } from '@pnpm/config' -import { PnpmError } from '@pnpm/error' -import { licences } from '@pnpm/licenses' -import pick from 'ramda/src/pick' -import renderHelp from 'render-help' -import { renderLicences } from './outputRenderer' +} from "@pnpm/common-cli-options-help"; +import { Config, types as allTypes } from "@pnpm/config"; +import { PnpmError } from "@pnpm/error"; +import { licences } from "@pnpm/licenses"; +import pick from "ramda/src/pick"; +import renderHelp from "render-help"; +import { renderLicences } from "./outputRenderer"; -export function rcOptionsTypes () { +export function rcOptionsTypes() { return { ...pick( [ - 'depth', - 'dev', - 'global-dir', - 'global', - 'json', - 'long', - 'optional', - 'production', + "depth", + "dev", + "global-dir", + "global", + "json", + "long", + "optional", + "production", ], allTypes ), compatible: Boolean, table: Boolean, - } + }; } export const cliOptionsTypes = () => ({ ...rcOptionsTypes(), recursive: Boolean, -}) +}); export const shorthands = { - D: '--dev', - P: '--production', -} + D: "--dev", + P: "--production", +}; -export const commandNames = ['licenses'] +export const commandNames = ["licenses"]; -export function help () { +export function help() { return renderHelp({ description: `Check for licenses packages. The check can be limited to a subset of the installed packages by providing arguments (patterns are supported). @@ -60,37 +60,37 @@ pnpm licenses --long pnpm licenses gulp-* @babel/core`, descriptionLists: [ { - title: 'Options', + title: "Options", list: [ { description: - 'By default, details about the outdated packages (such as a link to the repo) are not displayed. \ -To display the details, pass this option.', - name: '--long', + "By default, details about the outdated packages (such as a link to the repo) are not displayed. \ +To display the details, pass this option.", + name: "--long", }, { - description: 'Show information in JSON format', - name: '--json', + description: "Show information in JSON format", + name: "--json", }, { description: - 'Prints the outdated packages in a list. Good for small consoles', - name: '--no-table', + "Prints the outdated packages in a list. Good for small consoles", + name: "--no-table", }, { description: 'Check only "dependencies" and "optionalDependencies"', - name: '--prod', - shortAlias: '-P', + name: "--prod", + shortAlias: "-P", }, { description: 'Check only "devDependencies"', - name: '--dev', - shortAlias: '-D', + name: "--dev", + shortAlias: "-D", }, { description: 'Don\'t check "optionalDependencies"', - name: '--no-optional', + name: "--no-optional", }, OPTIONS.globalDir, ...UNIVERSAL_OPTIONS, @@ -98,85 +98,90 @@ To display the details, pass this option.', }, FILTERING, ], - url: docsUrl('licenses'), - usages: ['pnpm licenses [ ...]'], - }) + url: docsUrl("licenses"), + usages: ["pnpm licenses [ ...]"], + }); } export const completion: CompletionFunc = async (cliOpts) => { - return readDepNameCompletions(cliOpts.dir as string) -} + return readDepNameCompletions(cliOpts.dir as string); +}; export type LicensesCommandOptions = { - compatible?: boolean - long?: boolean - recursive?: boolean - json?: boolean + compatible?: boolean; + long?: boolean; + recursive?: boolean; + json?: boolean; } & Pick< -Config, -| 'allProjects' -| 'ca' -| 'cacheDir' -| 'cert' -| 'dev' -| 'dir' -| 'engineStrict' -| 'fetchRetries' -| 'fetchRetryFactor' -| 'fetchRetryMaxtimeout' -| 'fetchRetryMintimeout' -| 'fetchTimeout' -| 'global' -| 'httpProxy' -| 'httpsProxy' -| 'key' -| 'localAddress' -| 'lockfileDir' -| 'networkConcurrency' -| 'noProxy' -| 'offline' -| 'optional' -| 'production' -| 'rawConfig' -| 'registries' -| 'selectedProjectsGraph' -| 'strictSsl' -| 'tag' -| 'userAgent' -| 'virtualStoreDir' -| 'modulesDir' + Config, + | "allProjects" + | "ca" + | "cacheDir" + | "cert" + | "dev" + | "dir" + | "engineStrict" + | "fetchRetries" + | "fetchRetryFactor" + | "fetchRetryMaxtimeout" + | "fetchRetryMintimeout" + | "fetchTimeout" + | "global" + | "httpProxy" + | "httpsProxy" + | "key" + | "localAddress" + | "lockfileDir" + | "networkConcurrency" + | "noProxy" + | "offline" + | "optional" + | "production" + | "rawConfig" + | "registries" + | "selectedProjectsGraph" + | "strictSsl" + | "tag" + | "userAgent" + | "virtualStoreDir" + | "modulesDir" > & -Partial> + Partial>; -export async function handler ( +export async function handler( opts: LicensesCommandOptions, params: string[] = [] ) { - const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, { ignoreIncompatible: true }) + const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, { + ignoreIncompatible: true, + }); if (lockfile == null) { - throw new PnpmError('LICENSES_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found: Cannot check a project without a lockfile`) + throw new PnpmError( + "LICENSES_NO_LOCKFILE", + `No ${WANTED_LOCKFILE} found: Cannot check a project without a lockfile` + ); } const include = { dependencies: opts.production !== false, devDependencies: opts.dev !== false, optionalDependencies: opts.optional !== false, - } + }; - const manifest = await readProjectManifestOnly(opts.dir, opts) + const manifest = await readProjectManifestOnly(opts.dir, opts); const licensePackages = await licences({ include, lockfileDir: opts.dir, prefix: opts.dir, - virtualStoreDir: opts.virtualStoreDir ?? '.', + virtualStoreDir: opts.virtualStoreDir ?? ".", modulesDir: opts.modulesDir, registries: opts.registries, wantedLockfile: lockfile, manifest, - }) + }); - if (licensePackages.length === 0) return { output: '', exitCode: 0 } + if (licensePackages.length === 0) return { output: "", exitCode: 0 }; - return renderLicences(licensePackages, opts) + return renderLicences(licensePackages, opts); } diff --git a/packages/plugin-commands-licenses/src/outputRenderer.ts b/packages/plugin-commands-licenses/src/outputRenderer.ts index be2b05b6b01..f96eac1350a 100644 --- a/packages/plugin-commands-licenses/src/outputRenderer.ts +++ b/packages/plugin-commands-licenses/src/outputRenderer.ts @@ -1,80 +1,80 @@ -import { TABLE_OPTIONS } from '@pnpm/cli-utils' -import { LicensePackage } from '@pnpm/licenses' -import chalk from 'chalk' -import stripAnsi from 'strip-ansi' -import { table } from '@zkochan/table' -import { groupBy, sortWith } from 'ramda' +import { TABLE_OPTIONS } from "@pnpm/cli-utils"; +import { LicensePackage } from "@pnpm/licenses"; +import chalk from "chalk"; +import stripAnsi from "strip-ansi"; +import { table } from "@zkochan/table"; +import { groupBy, sortWith } from "ramda"; /** * * @param licensePackages * @returns */ -function sortLicensesPackages (licensePackages: readonly LicensePackage[]) { +function sortLicensesPackages(licensePackages: readonly LicensePackage[]) { return sortWith( [ (o1: LicensePackage, o2: LicensePackage) => o1.license.localeCompare(o2.license), ], licensePackages - ) + ); } -export function getCellWidth ( +export function getCellWidth( data: string[][], columnNumber: number, maxWidth: number ) { const maxCellWidth = data.reduce((cellWidth, row) => { - const cellLines = stripAnsi(row[columnNumber])?.split('\n') ?? [] + const cellLines = stripAnsi(row[columnNumber])?.split("\n") ?? []; const currentCellWidth = cellLines.reduce((lineWidth, line) => { - return Math.max(lineWidth, line.length) - }, 0) - return Math.max(cellWidth, currentCellWidth) - }, 0) - return Math.min(maxWidth, maxCellWidth) + return Math.max(lineWidth, line.length); + }, 0); + return Math.max(cellWidth, currentCellWidth); + }, 0); + return Math.min(maxWidth, maxCellWidth); } -export function renderPackageName ({ belongsTo, packageName }: LicensePackage) { +export function renderPackageName({ belongsTo, packageName }: LicensePackage) { switch (belongsTo) { - case 'devDependencies': - return `${packageName} ${chalk.dim('(dev)')}` - case 'optionalDependencies': - return `${packageName} ${chalk.dim('(optional)')}` - default: - return packageName as string + case "devDependencies": + return `${packageName} ${chalk.dim("(dev)")}`; + case "optionalDependencies": + return `${packageName} ${chalk.dim("(optional)")}`; + default: + return packageName as string; } } -export function renderPackageLicense ({ license }: LicensePackage) { - const output = license ?? 'Unknown' - return output as string +export function renderPackageLicense({ license }: LicensePackage) { + const output = license ?? "Unknown"; + return output as string; } -export function renderDetails (licensePackage: LicensePackage) { - const { packageManifest, author } = licensePackage - if (packageManifest == null) return '' - const outputs = [] +export function renderDetails(licensePackage: LicensePackage) { + const { packageManifest, author } = licensePackage; + if (packageManifest == null) return ""; + const outputs = []; if (author) { - outputs.push(author) + outputs.push(author); } if (packageManifest.homepage) { - outputs.push(chalk.underline(packageManifest.homepage)) + outputs.push(chalk.underline(packageManifest.homepage)); } - return outputs.join('\n') + return outputs.join("\n"); } -export function renderLicences ( +export function renderLicences( licensesMap: LicensePackage[], - opts: { long?: boolean, json?: boolean } + opts: { long?: boolean; json?: boolean } ) { if (opts.json) { - return { output: renderLicensesJson(licensesMap), exitCode: 0 } + return { output: renderLicensesJson(licensesMap), exitCode: 0 }; } - return { output: renderLicensesTable(licensesMap, opts), exitCode: 0 } + return { output: renderLicensesTable(licensesMap, opts), exitCode: 0 }; } -function renderLicensesJson (licensePackages: readonly LicensePackage[]) { +function renderLicensesJson(licensePackages: readonly LicensePackage[]) { const data = [ ...licensePackages.map((licensePkg) => { return { @@ -85,50 +85,49 @@ function renderLicensesJson (licensePackages: readonly LicensePackage[]) { licenseContents: licensePkg.licenseContents, vendorName: licensePkg.author, vendorUrl: licensePkg.packageManifest?.homepage, - } as LicensePackageJson + } as LicensePackageJson; }), - ].flat() + ].flat(); // Group the package by license - const groupByLicense = groupBy((item: LicensePackageJson) => item.license) - const groupedByLicense = groupByLicense(data) + const groupByLicense = groupBy((item: LicensePackageJson) => item.license); + const groupedByLicense = groupByLicense(data); - return JSON.stringify(groupedByLicense, null, 2) + return JSON.stringify(groupedByLicense, null, 2); } export interface LicensePackageJson { - name: string - license: string - vendorName: string - vendorUrl: string - path: string + name: string; + license: string; + vendorName: string; + vendorUrl: string; + path: string; } -function renderLicensesTable ( +function renderLicensesTable( licensePackages: readonly LicensePackage[], opts: { long?: boolean } ) { - const columnNames = ['Package', 'License'] + const columnNames = ["Package", "License"]; - const columnFns = [renderPackageName, renderPackageLicense] + const columnFns = [renderPackageName, renderPackageLicense]; if (opts.long) { - columnNames.push('Details') - columnFns.push(renderDetails) + columnNames.push("Details"); + columnFns.push(renderDetails); } // Avoid the overhead of allocating a new array caused by calling `array.map()` for (let i = 0; i < columnNames.length; i++) - columnNames[i] = chalk.blueBright(columnNames[i]) + columnNames[i] = chalk.blueBright(columnNames[i]); return table( [ columnNames, ...sortLicensesPackages(licensePackages).map((licensePkg) => { - return columnFns.map((fn) => fn(licensePkg)) - } - ), + return columnFns.map((fn) => fn(licensePkg)); + }), ], TABLE_OPTIONS - ) + ); } diff --git a/packages/plugin-commands-licenses/src/recursive.ts b/packages/plugin-commands-licenses/src/recursive.ts new file mode 100644 index 00000000000..6942827d811 --- /dev/null +++ b/packages/plugin-commands-licenses/src/recursive.ts @@ -0,0 +1,58 @@ +import { + licensesDepsOfProjects, + LicensePackage, +} from '@pnpm/licenses' +import { + DependenciesField, + IncludedDependencies, + ProjectManifest, +} from '@pnpm/types' + +import isEmpty from 'ramda/src/isEmpty' +import { renderLicencesInWorkspace } from './outputRenderer' +import { + LicensesCommandOptions, +} from './licenses' + +export interface LicensesInWorkspace extends LicensePackage { + belongsTo: DependenciesField + current?: string + dependentPkgs: Array<{ location: string, manifest: ProjectManifest }> + latest?: string + packageName: string +} + +export async function licensesRecursive ( + pkgs: Array<{ dir: string, manifest: ProjectManifest }>, + params: string[], + opts: LicensesCommandOptions & { include: IncludedDependencies } +) { + const licensesMap = {} as Record + const rootManifest = pkgs.find(({ dir }) => dir === opts.lockfileDir ?? opts.dir) + const LicensePackagesByProject = await licensesDepsOfProjects(pkgs, params, { + ...opts, + fullMetadata: opts.long, + ignoreDependencies: new Set(rootManifest?.manifest?.pnpm?.updateConfig?.ignoreDependencies ?? []), + retry: { + factor: opts.fetchRetryFactor, + maxTimeout: opts.fetchRetryMaxtimeout, + minTimeout: opts.fetchRetryMintimeout, + retries: opts.fetchRetries, + }, + timeout: opts.fetchTimeout, + }) + for (let i = 0; i < LicensePackagesByProject.length; i++) { + const { dir, manifest } = pkgs[i] + LicensePackagesByProject[i].forEach((licensePkg: LicensePackage) => { + const key = JSON.stringify([licensePkg.packageName, licensePkg.version, licensePkg.belongsTo]) + if (!licensesMap[key]) { + licensesMap[key] = { ...licensePkg, dependentPkgs: [] } + } + licensesMap[key].dependentPkgs.push({ location: dir, manifest }) + }) + } + + if (isEmpty(licensesMap)) return { output: '', exitCode: 0 } + + return renderLicencesInWorkspace(licensesMap, opts) +} diff --git a/packages/plugin-commands-licenses/test/index.ts b/packages/plugin-commands-licenses/test/index.ts index ce229633fca..a178bd2d2fc 100644 --- a/packages/plugin-commands-licenses/test/index.ts +++ b/packages/plugin-commands-licenses/test/index.ts @@ -24,6 +24,7 @@ const LICENSES_OPTIONS = { userConfig: {}, } +<<<<<<< HEAD jest.mock('@pnpm/read-package-json', () => ({ readPackageJson: async (pkgPath: string) => { // mock the readPackageJson-call used in getPkgInfo to ensure @@ -82,4 +83,302 @@ test('pnpm licenses: show details', async () => { ).rejects.toThrowErrorMatchingInlineSnapshot( '"No pnpm-lock.yaml found: Cannot check a project without a lockfile"' ) +======= +test('pnpm outdated: show details', async () => { + tempDir() + + await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true }) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + long: true, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌──────────────────────┬─────────┬────────────┬─────────────────────────────────────────────┐ +│ Package │ Current │ Latest │ Details │ +├──────────────────────┼─────────┼────────────┼─────────────────────────────────────────────┤ +│ @pnpm.e2e/deprecated │ 1.0.0 │ Deprecated │ This package is deprecated. Lorem ipsum │ +│ │ │ │ dolor sit amet, consectetur adipiscing │ +│ │ │ │ elit. │ +│ │ │ │ https://foo.bar/qar │ +├──────────────────────┼─────────┼────────────┼─────────────────────────────────────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ https://github.com/kevva/is-negative#readme │ +├──────────────────────┼─────────┼────────────┼─────────────────────────────────────────────┤ +│ is-positive (dev) │ 1.0.0 │ 3.1.0 │ https://github.com/kevva/is-positive#readme │ +└──────────────────────┴─────────┴────────────┴─────────────────────────────────────────────┘ +`) +}) + +test('pnpm outdated: show details (using the public registry to verify that full metadata is being requested)', async () => { + tempDir() + + await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true }) + await fs.copyFile(path.join(has2OutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + await fs.copyFile(path.join(has2OutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + long: true, + rawConfig: { registry: 'https://registry.npmjs.org/' }, + registries: { default: 'https://registry.npmjs.org/' }, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌───────────────────┬─────────┬────────┬─────────────────────────────────────────────┐ +│ Package │ Current │ Latest │ Details │ +├───────────────────┼─────────┼────────┼─────────────────────────────────────────────┤ +│ is-negative │ 1.0.1 │ 2.1.0 │ https://github.com/kevva/is-negative#readme │ +├───────────────────┼─────────┼────────┼─────────────────────────────────────────────┤ +│ is-positive (dev) │ 1.0.0 │ 3.1.0 │ https://github.com/kevva/is-positive#readme │ +└───────────────────┴─────────┴────────┴─────────────────────────────────────────────┘ +`) +}) + +test('pnpm outdated: showing only prod or dev dependencies', async () => { + tempDir() + + await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true }) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + production: false, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌───────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├───────────────────┼─────────┼────────┤ +│ is-positive (dev) │ 1.0.0 │ 3.1.0 │ +└───────────────────┴─────────┴────────┘ +`) + } + + { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dev: false, + dir: process.cwd(), + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌──────────────────────┬─────────┬────────────┐ +│ Package │ Current │ Latest │ +├──────────────────────┼─────────┼────────────┤ +│ @pnpm.e2e/deprecated │ 1.0.0 │ Deprecated │ +├──────────────────────┼─────────┼────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ +└──────────────────────┴─────────┴────────────┘ +`) + } +}) + +test('pnpm outdated: no table', async () => { + tempDir() + + await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true }) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + table: false, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`@pnpm.e2e/deprecated +1.0.0 => Deprecated + +is-negative +1.0.0 => 2.1.0 + +is-positive (dev) +1.0.0 => 3.1.0 +`) + } + + { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + long: true, + table: false, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`@pnpm.e2e/deprecated +1.0.0 => Deprecated +This package is deprecated. Lorem ipsum +dolor sit amet, consectetur adipiscing +elit. +https://foo.bar/qar + +is-negative +1.0.0 => 2.1.0 +https://github.com/kevva/is-negative#readme + +is-positive (dev) +1.0.0 => 3.1.0 +https://github.com/kevva/is-positive#readme +`) + } +}) + +test('pnpm outdated: only current lockfile is available', async () => { + tempDir() + + await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true }) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌──────────────────────┬─────────┬────────────┐ +│ Package │ Current │ Latest │ +├──────────────────────┼─────────┼────────────┤ +│ @pnpm.e2e/deprecated │ 1.0.0 │ Deprecated │ +├──────────────────────┼─────────┼────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ +├──────────────────────┼─────────┼────────────┤ +│ is-positive (dev) │ 1.0.0 │ 3.1.0 │ +└──────────────────────┴─────────┴────────────┘ +`) +}) + +test('pnpm outdated: only wanted lockfile is available', async () => { + tempDir() + + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'pnpm-lock.yaml'), path.resolve('pnpm-lock.yaml')) + await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌──────────────────────┬────────────────────────┬────────────┐ +│ Package │ Current │ Latest │ +├──────────────────────┼────────────────────────┼────────────┤ +│ @pnpm.e2e/deprecated │ missing (wanted 1.0.0) │ Deprecated │ +├──────────────────────┼────────────────────────┼────────────┤ +│ is-negative │ missing (wanted 2.1.0) │ 2.1.0 │ +├──────────────────────┼────────────────────────┼────────────┤ +│ is-positive (dev) │ missing (wanted 3.1.0) │ 3.1.0 │ +└──────────────────────┴────────────────────────┴────────────┘ +`) +}) + +test('pnpm outdated does not print anything when all is good', async () => { + process.chdir(hasNotOutdatedDepsFixture) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + + expect(output).toBe('') + expect(exitCode).toBe(0) +}) + +test('pnpm outdated with external lockfile', async () => { + process.chdir(hasOutdatedDepsFixtureAndExternalLockfile) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + lockfileDir: path.resolve('..'), + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌─────────────┬──────────────────────┬────────┐ +│ Package │ Current │ Latest │ +├─────────────┼──────────────────────┼────────┤ +│ is-positive │ 1.0.0 (wanted 3.1.0) │ 3.1.0 │ +├─────────────┼──────────────────────┼────────┤ +│ is-negative │ 1.0.0 (wanted 1.1.0) │ 2.1.0 │ +└─────────────┴──────────────────────┴────────┘ +`) +}) + +test(`pnpm outdated should fail when there is no ${WANTED_LOCKFILE} file in the root of the project`, async () => { + process.chdir(hasNoLockfileFixture) + + let err!: PnpmError + try { + await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + } catch (_err: any) { // eslint-disable-line + err = _err + } + expect(err.code).toBe('ERR_PNPM_OUTDATED_NO_LOCKFILE') +}) + +test('pnpm outdated should return empty when there is no lockfile and no dependencies', async () => { + prepare(undefined) + + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + + expect(output).toBe('') + expect(exitCode).toBe(0) +}) + +test('pnpm outdated: print only compatible versions', async () => { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + compatible: true, + dir: hasMajorOutdatedDepsFixture, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌─────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├─────────────┼─────────┼────────┤ +│ is-negative │ 1.0.0 │ 1.0.1 │ +└─────────────┴─────────┴────────┘ +`) +}) + +test('ignore packages in package.json > pnpm.updateConfig.ignoreDependencies in outdated command', async () => { + const { output, exitCode } = await licenses.handler({ + ...OUTDATED_OPTIONS, + dir: withPnpmUpdateIgnore, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌─────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├─────────────┼─────────┼────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ +└─────────────┴─────────┴────────┘ +`) +>>>>>>> c63abf1bf (feat: create `licenses`-command for PNPM) }) diff --git a/packages/plugin-commands-licenses/test/recursive.ts b/packages/plugin-commands-licenses/test/recursive.ts new file mode 100644 index 00000000000..841ab18ca1b --- /dev/null +++ b/packages/plugin-commands-licenses/test/recursive.ts @@ -0,0 +1,310 @@ +import { readProjects } from '@pnpm/filter-workspace-packages' +import { install } from '@pnpm/plugin-commands-installation' +import { outdated } from '@pnpm/plugin-commands-outdated' +import { preparePackages } from '@pnpm/prepare' +import stripAnsi from 'strip-ansi' +import { DEFAULT_OPTS } from './utils' + +test('pnpm recursive outdated', async () => { + preparePackages([ + { + name: 'project-1', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + }, + }, + { + name: 'project-2', + version: '1.0.0', + + dependencies: { + 'is-negative': '1.0.0', + 'is-positive': '2.0.0', + }, + }, + { + name: 'project-3', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + }, + devDependencies: { + 'is-negative': '1.0.0', + }, + }, + ]) + + const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), []) + await install.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + workspaceDir: process.cwd(), + }) + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌───────────────────┬─────────┬────────┬──────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ project-2 │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-negative (dev) │ 1.0.0 │ 2.1.0 │ project-3 │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 1.0.0 │ 3.1.0 │ project-1, project-3 │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 2.0.0 │ 3.1.0 │ project-2 │ +└───────────────────┴─────────┴────────┴──────────────────────┘ +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + production: false, + recursive: true, + selectedProjectsGraph, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌───────────────────┬─────────┬────────┬────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├───────────────────┼─────────┼────────┼────────────┤ +│ is-negative (dev) │ 1.0.0 │ 2.1.0 │ project-3 │ +└───────────────────┴─────────┴────────┴────────────┘ +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + long: true, + recursive: true, + selectedProjectsGraph, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌───────────────────┬─────────┬────────┬──────────────────────┬─────────────────────────────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ Details │ +├───────────────────┼─────────┼────────┼──────────────────────┼─────────────────────────────────────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ project-2 │ https://github.com/kevva/is-negative#readme │ +├───────────────────┼─────────┼────────┼──────────────────────┼─────────────────────────────────────────────┤ +│ is-negative (dev) │ 1.0.0 │ 2.1.0 │ project-3 │ https://github.com/kevva/is-negative#readme │ +├───────────────────┼─────────┼────────┼──────────────────────┼─────────────────────────────────────────────┤ +│ is-positive │ 1.0.0 │ 3.1.0 │ project-1, project-3 │ https://github.com/kevva/is-positive#readme │ +├───────────────────┼─────────┼────────┼──────────────────────┼─────────────────────────────────────────────┤ +│ is-positive │ 2.0.0 │ 3.1.0 │ project-2 │ https://github.com/kevva/is-positive#readme │ +└───────────────────┴─────────┴────────┴──────────────────────┴─────────────────────────────────────────────┘ +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + table: false, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +is-negative +1.0.0 => 2.1.0 +Dependent: project-2 + +is-negative (dev) +1.0.0 => 2.1.0 +Dependent: project-3 + +is-positive +1.0.0 => 3.1.0 +Dependents: project-1, project-3 + +is-positive +2.0.0 => 3.1.0 +Dependent: project-2 +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + long: true, + recursive: true, + selectedProjectsGraph, + table: false, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +is-negative +1.0.0 => 2.1.0 +Dependent: project-2 +https://github.com/kevva/is-negative#readme + +is-negative (dev) +1.0.0 => 2.1.0 +Dependent: project-3 +https://github.com/kevva/is-negative#readme + +is-positive +1.0.0 => 3.1.0 +Dependents: project-1, project-3 +https://github.com/kevva/is-positive#readme + +is-positive +2.0.0 => 3.1.0 +Dependent: project-2 +https://github.com/kevva/is-positive#readme +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + }, ['is-positive']) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌─────────────┬─────────┬────────┬──────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├─────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 1.0.0 │ 3.1.0 │ project-1, project-3 │ +├─────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 2.0.0 │ 3.1.0 │ project-2 │ +└─────────────┴─────────┴────────┴──────────────────────┘ +`) + } +}) + +test('pnpm recursive outdated in workspace with shared lockfile', async () => { + preparePackages([ + { + name: 'project-1', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + }, + }, + { + name: 'project-2', + version: '1.0.0', + + dependencies: { + 'is-negative': '1.0.0', + }, + }, + { + name: 'project-3', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + }, + devDependencies: { + 'is-negative': '1.0.0', + }, + }, + ]) + + const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), []) + await install.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + workspaceDir: process.cwd(), + }) + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌───────────────────┬─────────┬────────┬──────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ project-2 │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-negative (dev) │ 1.0.0 │ 2.1.0 │ project-3 │ +├───────────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 1.0.0 │ 3.1.0 │ project-1, project-3 │ +└───────────────────┴─────────┴────────┴──────────────────────┘ +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + production: false, + recursive: true, + selectedProjectsGraph, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌───────────────────┬─────────┬────────┬────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├───────────────────┼─────────┼────────┼────────────┤ +│ is-negative (dev) │ 1.0.0 │ 2.1.0 │ project-3 │ +└───────────────────┴─────────┴────────┴────────────┘ +`) + } + + { + const { output, exitCode } = await outdated.handler({ + ...DEFAULT_OPTS, + allProjects, + dir: process.cwd(), + recursive: true, + selectedProjectsGraph, + }, ['is-positive']) + + expect(exitCode).toBe(1) + expect(stripAnsi(output as unknown as string)).toBe(`\ +┌─────────────┬─────────┬────────┬──────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├─────────────┼─────────┼────────┼──────────────────────┤ +│ is-positive │ 1.0.0 │ 3.1.0 │ project-1, project-3 │ +└─────────────┴─────────┴────────┴──────────────────────┘ +`) + } +}) diff --git a/packages/plugin-commands-licenses/test/utils/index.ts b/packages/plugin-commands-licenses/test/utils/index.ts new file mode 100644 index 00000000000..d2bc5519c36 --- /dev/null +++ b/packages/plugin-commands-licenses/test/utils/index.ts @@ -0,0 +1,52 @@ +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' + +const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}` + +export const DEFAULT_OPTS = { + argv: { + original: [], + }, + bail: false, + bin: 'node_modules/.bin', + ca: undefined, + cacheDir: '../cache', + cert: undefined, + extraEnv: {}, + cliOptions: {}, + fetchRetries: 2, + fetchRetryFactor: 90, + fetchRetryMaxtimeout: 90, + fetchRetryMintimeout: 10, + filter: [] as string[], + global: false, + httpsProxy: undefined, + include: { + dependencies: true, + devDependencies: true, + optionalDependencies: true, + }, + key: undefined, + linkWorkspacePackages: true, + localAddress: undefined, + lock: false, + lockStaleDuration: 90, + networkConcurrency: 16, + offline: false, + pending: false, + pnpmfile: './.pnpmfile.cjs', + pnpmHomeDir: '', + proxy: undefined, + rawConfig: { registry: REGISTRY }, + rawLocalConfig: {}, + registries: { default: REGISTRY }, + registry: REGISTRY, + sort: true, + storeDir: '../store', + strictSsl: false, + tag: 'latest', + userAgent: 'pnpm', + userConfig: {}, + useRunningStoreServer: false, + useStoreServer: false, + workspaceConcurrency: 4, +} diff --git a/packages/plugin-commands-licenses/tsconfig.json b/packages/plugin-commands-licenses/tsconfig.json index 3f028dff19a..d4cffed4ea1 100644 --- a/packages/plugin-commands-licenses/tsconfig.json +++ b/packages/plugin-commands-licenses/tsconfig.json @@ -4,10 +4,7 @@ "outDir": "lib", "rootDir": "src" }, - "include": [ - "src/**/*.ts", - "../../typings/**/*.d.ts" - ], + "include": ["src/**/*.ts", "../../typings/**/*.d.ts"], "references": [ { "path": "../../privatePackages/prepare"