diff --git a/packages/licenses/README.md b/packages/licenses/README.md new file mode 100644 index 00000000000..6947e55b733 --- /dev/null +++ b/packages/licenses/README.md @@ -0,0 +1,17 @@ +# @pnpm/licenses + +> Check for the licensse of packages + + +[![npm version](https://img.shields.io/npm/v/@pnpm/licenses.svg)](https://www.npmjs.com/package/@pnpm/licenses) + + +## Installation + +```sh +pnpm add @pnpm/licenses +``` + +## License + +[MIT](LICENSE) diff --git a/packages/licenses/jest.config.js b/packages/licenses/jest.config.js new file mode 100644 index 00000000000..58141f076dc --- /dev/null +++ b/packages/licenses/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.js'); diff --git a/packages/licenses/package.json b/packages/licenses/package.json new file mode 100644 index 00000000000..3c9b3892e64 --- /dev/null +++ b/packages/licenses/package.json @@ -0,0 +1,66 @@ +{ + "name": "@pnpm/licenses", + "version": "11.0.2", + "description": "Check for licenses packages", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "engines": { + "node": ">=14.6" + }, + "files": [ + "lib", + "!*.map" + ], + "scripts": { + "test": "pnpm run compile && pnpm run _test", + "lint": "eslint src/**/*.ts test/**/*.ts", + "prepublishOnly": "pnpm run compile", + "registry-mock": "registry-mock", + "test:jest": "jest", + "_test": "jest", + "test:e2e": "registry-mock prepare && run-p -r registry-mock test:jest", + "compile": "tsc --build && pnpm run lint --fix" + }, + "repository": "https://github.com/pnpm/pnpm/blob/main/packages/licenses", + "keywords": [ + "pnpm7", + "pnpm", + "outdated" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "homepage": "https://github.com/pnpm/pnpm/blob/main/packages/licenses#readme", + "peerDependencies": { + "@pnpm/logger": "^5.0.0" + }, + "dependencies": { + "@pnpm/client": "workspace:*", + "@pnpm/constants": "workspace:*", + "@pnpm/error": "workspace:*", + "@pnpm/lockfile-file": "workspace:*", + "@pnpm/lockfile-utils": "workspace:*", + "@pnpm/manifest-utils": "workspace:*", + "@pnpm/matcher": "workspace:*", + "@pnpm/modules-yaml": "workspace:*", + "@pnpm/npm-resolver": "workspace:*", + "@pnpm/pick-registry-for-package": "workspace:*", + "@pnpm/read-package-json": "workspace:*", + "@pnpm/types": "workspace:*", + "dependency-path": "workspace:*", + "p-limit": "^3.1.0", + "ramda": "npm:@pnpm/ramda@0.28.1", + "semver": "^7.3.8" + }, + "devDependencies": { + "@pnpm/licenses": "workspace:*", + "@types/ramda": "0.28.15", + "@types/semver": "7.3.12", + "npm-run-all": "^4.1.5" + }, + "funding": "https://opencollective.com/pnpm", + "exports": { + ".": "./lib/index.js" + } +} diff --git a/packages/licenses/src/getPkgInfo.ts b/packages/licenses/src/getPkgInfo.ts new file mode 100644 index 00000000000..2ced37a99f0 --- /dev/null +++ b/packages/licenses/src/getPkgInfo.ts @@ -0,0 +1,131 @@ +import path from 'node:path' +import fs from 'node:fs/promises' +import { readPackageJson } from '@pnpm/read-package-json' +import pLimit from 'p-limit' +import { PackageManifest } from '@pnpm/types' + +const limitPkgReads = pLimit(4) + +export async function readPkg (pkgPath: string) { + return limitPkgReads(async () => readPackageJson(pkgPath)) +} + +/** + * @const + * List of typical names for license files + */ +const LICENSE_FILES = ['./LICENSE', './LICENCE'] + +export type LicenseInfo = { + name: string + licenseFile?: string +} + +/** + * Coerce the given value to a string or a null value + * @param field the string to be converted + * @returns string | null + */ +function coerceToString (field: unknown): string | null { + const string = String(field) + return typeof field === 'string' || field === string ? string : null +} + +function parseLicenseManifestField (field: unknown) { + if (Array.isArray(field)) { + const licenses = field + const licenseTypes = licenses.reduce((listOfLicenseTypes, license) => { + const type = coerceToString(license.type) + if (type) { + listOfLicenseTypes.push(type) + } + return listOfLicenseTypes + }, []) + + if (licenseTypes.length > 1) { + const combinedLicenseTypes = licenseTypes.join(' OR ') as string + return `(${combinedLicenseTypes})` + } + + return licenseTypes[0] ?? null + } else { + return (field as { type: string })?.type ?? coerceToString(field) + } +} + +/** + * + * @param {*} packageInfo + * @returns + */ +async function parseLicense (packageInfo: { + manifest: PackageManifest + path: string +}): Promise { + const license = parseLicenseManifestField(packageInfo.manifest.license) + + // check if we discovered a license, if not attempt to parse the LICENSE file + if ( + (!license || /see license/i.test(license)) + ) { + for (const filename of LICENSE_FILES) { + try { + const licensePath = path.join(packageInfo.path, filename) + const licenseContents = await fs.readFile(licensePath) + return { + name: 'Unknown', + licenseFile: licenseContents.toString('utf-8'), + } + } catch (err) { + // NOOP + } + } + } + + return { name: license ?? 'Unknown' } +} + +export async function getPkgInfo ( + pkg: { + alias: string + name: string + version: string + prefix: string + } +) { + let manifest + let packageModulePath + let licenseInfo: LicenseInfo + try { + packageModulePath = path.join(pkg.prefix, 'node_modules', pkg.name) + const packageManifestPath = path.join(packageModulePath, 'package.json') + manifest = await readPkg(packageManifestPath) + + licenseInfo = await parseLicense({ manifest, path: packageModulePath }) + + manifest.license = licenseInfo.name + } catch (err: any) { // eslint-disable-line + // This will probably never happen + throw new Error(`Failed to fetch manifest data for ${pkg.name}`) + } + + return { + packageManifest: manifest, + packageInfo: { + alias: pkg.alias, + from: pkg.name, + path: packageModulePath, + version: pkg.version, + description: manifest.description, + license: licenseInfo.name, + licenseContents: licenseInfo.licenseFile, + author: (manifest.author && ( + typeof manifest.author === 'string' ? manifest.author : (manifest.author as { name: string }).name + )) ?? undefined, + homepage: manifest.homepage, + repository: (manifest.repository && ( + typeof manifest.repository === 'string' ? manifest.repository : manifest.repository.url + )) ?? undefined, + }, + } +} diff --git a/packages/licenses/src/index.ts b/packages/licenses/src/index.ts new file mode 100644 index 00000000000..dce82639f34 --- /dev/null +++ b/packages/licenses/src/index.ts @@ -0,0 +1,2 @@ +export { licensesDepsOfProjects } from './licensesDepsOfProjects' +export { LicensePackage } from './licenses' diff --git a/packages/licenses/src/licenses.ts b/packages/licenses/src/licenses.ts new file mode 100644 index 00000000000..daa8f209caa --- /dev/null +++ b/packages/licenses/src/licenses.ts @@ -0,0 +1,125 @@ +import { WANTED_LOCKFILE } from '@pnpm/constants' +import { PnpmError } from '@pnpm/error' +import { + getLockfileImporterId, + Lockfile, +} from '@pnpm/lockfile-file' +import { nameVerFromPkgSnapshot } from '@pnpm/lockfile-utils' +import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils' +import { + DependenciesField, + DEPENDENCIES_FIELDS, + IncludedDependencies, + PackageManifest, + ProjectManifest, + Registries, +} from '@pnpm/types' +import * as dp from 'dependency-path' +import { getPkgInfo } from './getPkgInfo' + +export interface LicensePackage { + alias: string + belongsTo: DependenciesField + version: string + packageManifest?: PackageManifest + packageName: string + license: string + licenseContents?: string + author?: string + packageDirectory?: string +} + +export async function licences ( + opts: { + compatible?: boolean + currentLockfile: Lockfile | null + ignoreDependencies?: Set + include?: IncludedDependencies + lockfileDir: string + manifest: ProjectManifest + match?: (dependencyName: string) => boolean + prefix: string + registries: Registries + wantedLockfile: Lockfile | null + } +): Promise { + if (packageHasNoDeps(opts.manifest)) return [] + if (opts.wantedLockfile == null) { + throw new PnpmError('LICENSES_NO_LOCKFILE', `No lockfile in directory "${opts.lockfileDir}". Run \`pnpm install\` to generate one.`) + } + const allDeps = getAllDependenciesFromManifest(opts.manifest) + const importerId = getLockfileImporterId(opts.lockfileDir, opts.prefix) + const licenses: LicensePackage[] = [] + + await Promise.all( + DEPENDENCIES_FIELDS.map(async (depType) => { + if ( + opts.include?.[depType] === false || + (opts.wantedLockfile!.importers[importerId][depType] == null) + ) return + + let pkgs = Object.keys(opts.wantedLockfile!.importers[importerId][depType]!) + if (opts.match != null) { + pkgs = pkgs.filter((pkgName) => opts.match!(pkgName)) + } + + await Promise.all( + pkgs.map(async (alias) => { + if (!allDeps[alias]) return + const ref = opts.wantedLockfile!.importers[importerId][depType]![alias] + if ( + ref.startsWith('file:') || // ignoring linked packages. (For backward compatibility) + opts.ignoreDependencies?.has(alias) + ) { + return + } + + const relativeDepPath = dp.refToRelative(ref, alias) + + // ignoring linked packages + if (relativeDepPath === null) return + + const pkgSnapshot = opts.wantedLockfile!.packages?.[relativeDepPath] + if (pkgSnapshot == null) { + throw new Error(`Invalid ${WANTED_LOCKFILE} file. ${relativeDepPath} not found in packages field`) + } + + const { name: packageName, version: packageVersion } = nameVerFromPkgSnapshot(relativeDepPath, pkgSnapshot) + const name = dp.parse(relativeDepPath).name ?? packageName + + // Fetch the most recent package by the give name + const { packageManifest, packageInfo } = await getPkgInfo({ + alias, + name, + version: packageVersion, + prefix: opts.prefix, + }) + + licenses.push({ + alias, + belongsTo: depType, + version: packageVersion, + packageManifest, + packageName, + license: packageInfo.license, + licenseContents: packageInfo.licenseContents, + author: packageInfo.author, + packageDirectory: packageInfo.path, + }) + }) + ) + }) + ) + + return licenses.sort((pkg1, pkg2) => pkg1.packageName.localeCompare(pkg2.packageName)) +} + +function packageHasNoDeps (manifest: ProjectManifest) { + return ((manifest.dependencies == null) || isEmpty(manifest.dependencies)) && + ((manifest.devDependencies == null) || isEmpty(manifest.devDependencies)) && + ((manifest.optionalDependencies == null) || isEmpty(manifest.optionalDependencies)) +} + +function isEmpty (obj: object) { + return Object.keys(obj).length === 0 +} 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/licenses/tsconfig.json b/packages/licenses/tsconfig.json new file mode 100644 index 00000000000..e1cb1e7801e --- /dev/null +++ b/packages/licenses/tsconfig.json @@ -0,0 +1,52 @@ +{ + "extends": "@pnpm/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts", + "../../typings/**/*.d.ts" + ], + "references": [ + { + "path": "../client" + }, + { + "path": "../constants" + }, + { + "path": "../dependency-path" + }, + { + "path": "../error" + }, + { + "path": "../lockfile-file" + }, + { + "path": "../lockfile-utils" + }, + { + "path": "../manifest-utils" + }, + { + "path": "../matcher" + }, + { + "path": "../modules-yaml" + }, + { + "path": "../npm-resolver" + }, + { + "path": "../pick-registry-for-package" + }, + { + "path": "../read-package-json" + }, + { + "path": "../types" + } + ] +} diff --git a/packages/licenses/tsconfig.lint.json b/packages/licenses/tsconfig.lint.json new file mode 100644 index 00000000000..0dc5add6b7b --- /dev/null +++ b/packages/licenses/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../typings/**/*.d.ts" + ] +} diff --git a/packages/plugin-commands-licenses/README.md b/packages/plugin-commands-licenses/README.md new file mode 100644 index 00000000000..ad0091e3d9a --- /dev/null +++ b/packages/plugin-commands-licenses/README.md @@ -0,0 +1,15 @@ +# @pnpm/plugin-commands-licenses + +> The licenses command of pnpm + +[![npm version](https://img.shields.io/npm/v/@pnpm/plugin-commands-licenses.svg)](https://www.npmjs.com/package/@pnpm/plugin-commands-licenses) + +## Installation + +```sh +pnpm add @pnpm/plugin-commands-licenses +``` + +## License + +MIT diff --git a/packages/plugin-commands-licenses/jest.config.js b/packages/plugin-commands-licenses/jest.config.js new file mode 100644 index 00000000000..f697d831691 --- /dev/null +++ b/packages/plugin-commands-licenses/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.js') diff --git a/packages/plugin-commands-licenses/package.json b/packages/plugin-commands-licenses/package.json new file mode 100644 index 00000000000..a5b96690d78 --- /dev/null +++ b/packages/plugin-commands-licenses/package.json @@ -0,0 +1,73 @@ +{ + "name": "@pnpm/plugin-commands-licenses", + "version": "7.0.3", + "description": "The licenses command of pnpm", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib", + "!*.map" + ], + "engines": { + "node": ">=14.6" + }, + "scripts": { + "lint": "eslint src/**/*.ts test/**/*.ts", + "registry-mock": "registry-mock", + "test:jest": "jest", + "test:e2e": "registry-mock prepare && run-p -r registry-mock test:jest", + "_test": "jest", + "test": "pnpm run compile && pnpm run _test", + "prepublishOnly": "pnpm run compile", + "compile": "tsc --build && pnpm run lint --fix" + }, + "repository": "https://github.com/pnpm/pnpm/blob/main/packages/plugin-commands-licenses", + "keywords": [ + "pnpm7", + "pnpm", + "outdated" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "homepage": "https://github.com/pnpm/pnpm/blob/main/packages/plugin-commands-licenses#readme", + "devDependencies": { + "@pnpm/constants": "workspace:*", + "@pnpm/filter-workspace-packages": "workspace:*", + "@pnpm/plugin-commands-installation": "workspace:*", + "@pnpm/plugin-commands-licenses": "workspace:*", + "@pnpm/prepare": "workspace:*", + "@pnpm/registry-mock": "3.1.0", + "@types/ramda": "0.28.15", + "@types/wrap-ansi": "^3.0.0", + "@types/zkochan__table": "npm:@types/table@6.0.0" + }, + "dependencies": { + "@pnpm/cli-utils": "workspace:*", + "@pnpm/colorize-semver-diff": "^1.0.1", + "@pnpm/command": "workspace:*", + "@pnpm/common-cli-options-help": "workspace:*", + "@pnpm/config": "workspace:*", + "@pnpm/default-resolver": "workspace:*", + "@pnpm/error": "workspace:*", + "@pnpm/licenses": "workspace:*", + "@pnpm/lockfile-file": "workspace:*", + "@pnpm/matcher": "workspace:*", + "@pnpm/modules-yaml": "workspace:*", + "@pnpm/semver-diff": "^1.1.0", + "@pnpm/store-path": "workspace:*", + "@pnpm/types": "workspace:*", + "@zkochan/table": "^1.0.0", + "chalk": "^4.1.2", + "lru-cache": "^7.14.0", + "ramda": "npm:@pnpm/ramda@0.28.1", + "render-help": "^1.0.2", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "funding": "https://opencollective.com/pnpm", + "exports": { + ".": "./lib/index.js" + } +} diff --git a/packages/plugin-commands-licenses/src/index.ts b/packages/plugin-commands-licenses/src/index.ts new file mode 100644 index 00000000000..9b5ab8ecd4c --- /dev/null +++ b/packages/plugin-commands-licenses/src/index.ts @@ -0,0 +1,3 @@ +import * as licenses from './licenses' + +export { licenses } diff --git a/packages/plugin-commands-licenses/src/licenses.ts b/packages/plugin-commands-licenses/src/licenses.ts new file mode 100644 index 00000000000..e125eab01fb --- /dev/null +++ b/packages/plugin-commands-licenses/src/licenses.ts @@ -0,0 +1,196 @@ +import { + docsUrl, + readDepNameCompletions, + readProjectManifestOnly, + TABLE_OPTIONS, +} from '@pnpm/cli-utils' +// import colorizeSemverDiff from '@pnpm/colorize-semver-diff' +import { CompletionFunc } from '@pnpm/command' +import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help' +import { Config, types as allTypes } from '@pnpm/config' +import { + licensesDepsOfProjects, + LicensePackage, +} from '@pnpm/licenses' +import { table } from '@zkochan/table' +import chalk from 'chalk' +import pick from 'ramda/src/pick' +// import sortWith from 'ramda/src/sortWith' +import renderHelp from 'render-help' +import stripAnsi from 'strip-ansi' +import wrapAnsi from 'wrap-ansi' +import { renderLicences } from './outputRenderer' +import { licensesRecursive } from './recursive' + +export function rcOptionsTypes () { + return { + ...pick([ + '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', +} + +export const commandNames = ['licenses'] + +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). + +Examples: +pnpm licenses +pnpm licenses --long +pnpm licenses gulp-* @babel/core`, + descriptionLists: [ + { + title: 'Options', + + list: [ + { + description: 'Print only versions that satisfy specs in package.json', + name: '--compatible', + }, + { + 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', + }, + { + description: 'Check for outdated dependencies in every package found in subdirectories \ +or in every workspace package, when executed inside a workspace. \ +For options that may be used with `-r`, see "pnpm help recursive"', + name: '--recursive', + shortAlias: '-r', + }, + { + description: 'Show information in JSON format', + name: '--json', + }, + { + description: 'Prints the outdated packages in a list. Good for small consoles', + name: '--no-table', + }, + { + description: 'Check only "dependencies" and "optionalDependencies"', + name: '--prod', + shortAlias: '-P', + }, + { + description: 'Check only "devDependencies"', + name: '--dev', + shortAlias: '-D', + }, + { + description: 'Don\'t check "optionalDependencies"', + name: '--no-optional', + }, + OPTIONS.globalDir, + ...UNIVERSAL_OPTIONS, + ], + }, + FILTERING, + ], + url: docsUrl('licenses'), + usages: ['pnpm licenses [ ...]'], + }) +} + +export const completion: CompletionFunc = async (cliOpts) => { + return readDepNameCompletions(cliOpts.dir as string) +} + +export type LicensesCommandOptions = { + compatible?: boolean + long?: boolean + recursive?: boolean + table?: boolean +} & Pick & Partial> + +export async function handler ( + opts: LicensesCommandOptions, + params: string[] = [] +) { + const include = { + dependencies: opts.production !== false, + devDependencies: opts.dev !== false, + optionalDependencies: opts.optional !== false, + } + if (opts.recursive && (opts.selectedProjectsGraph != null)) { + const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package) + return licensesRecursive(pkgs, params, { ...opts, include }) + } + const manifest = await readProjectManifestOnly(opts.dir, opts) + const packages = [ + { + dir: opts.dir, + manifest, + }, + ] + const [licensePackages] = await licensesDepsOfProjects(packages, params, { + ...opts, + fullMetadata: opts.long, + ignoreDependencies: new Set(manifest?.pnpm?.updateConfig?.ignoreDependencies ?? []), + include, + retry: { + factor: opts.fetchRetryFactor, + maxTimeout: opts.fetchRetryMaxtimeout, + minTimeout: opts.fetchRetryMintimeout, + retries: opts.fetchRetries, + }, + timeout: opts.fetchTimeout, + }) + + if (licensePackages.length === 0) return { output: '', exitCode: 0 } + + return renderLicences(licensePackages, opts) +} diff --git a/packages/plugin-commands-licenses/src/outputRenderer.ts b/packages/plugin-commands-licenses/src/outputRenderer.ts new file mode 100644 index 00000000000..7112a3ee40a --- /dev/null +++ b/packages/plugin-commands-licenses/src/outputRenderer.ts @@ -0,0 +1,191 @@ +import { TABLE_OPTIONS } from '@pnpm/cli-utils' +import { LicensePackage } from '@pnpm/licenses' +import chalk from 'chalk' +import stripAnsi from 'strip-ansi' +import { LicensesInWorkspace } from './recursive' +import { table } from '@zkochan/table' +import { groupBy, sortWith } from 'ramda' + +/** + * + * @param licensePackages + * @returns + */ +function sortLicensesPackages (licensePackages: readonly LicensePackage[]) { + return sortWith( + [ + (o1: LicensePackage, o2: LicensePackage) => o1.license.localeCompare(o2.license), + ], + licensePackages + ) +} + +export function getCellWidth (data: string[][], columnNumber: number, maxWidth: number) { + const maxCellWidth = data.reduce((cellWidth, row) => { + 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) +} + +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 + } +} + +export function renderPackageLicense ({ license }: LicensePackage) { + const output = license ?? 'unknown' + return output as string +} + +export function renderDetails ({ packageManifest, author }: LicensePackage) { + if (packageManifest == null) return '' + const outputs = [] + if (author) { + outputs.push(author) + } + if (packageManifest.homepage) { + outputs.push(chalk.underline(packageManifest.homepage)) + } + return outputs.join('\n') +} + +/** + * + * @param licensesMap + * @param opts + * @returns + */ +export function renderLicences (licensesMap: LicensePackage[], opts: { long?: boolean, json?: boolean }) { + if (opts.json) { + return { output: renderLicensesJson(licensesMap), exitCode: 1 } + } + + return { output: renderLicensesTable(licensesMap, opts), exitCode: 1 } +} + +export function renderLicencesInWorkspace (licensesMap: Record, opts: { long?: boolean, json?: boolean }) { + if (opts.json) { + return { output: renderLicencesJsonInWorkspace(licensesMap, opts), exitCode: 1 } + } + + return { output: renderLicensesTableInWorkspace(licensesMap, opts), exitCode: 1 } +} + +function renderLicensesJson (licensePackages: readonly LicensePackage[]) { + const data = [ + ...licensePackages + .map((licensePkg) => { + return { + name: licensePkg.packageName, + path: licensePkg.packageDirectory, + license: licensePkg.license, + licenseContents: licensePkg.licenseContents, + vendorName: licensePkg.author, + vendorUrl: licensePkg.packageManifest?.homepage, + } as LicensePackageJson + }), + ].flat() + + // Group the package by license const byGrade = R.groupBy(function(student) { + const groupByLicense = groupBy((item: LicensePackageJson) => item.license) + const groupedByLicense = groupByLicense(data) + + return JSON.stringify(groupedByLicense, null, 2) +} + +export type LicensePackageJson = { + name: string + license: string + vendorName: string + vendorUrl: string + path: string +} + +function renderLicencesJsonInWorkspace (licensesMap: Record, opts: { long?: boolean, json?: boolean }) { + const data = [ + ...Object.values(licensesMap) + .map((licensePkg) => { + return { + name: licensePkg.packageName, + path: licensePkg.packageDirectory, + license: licensePkg.license, + licenseContents: licensePkg.licenseContents, + vendorName: licensePkg.author, + vendorUrl: licensePkg.packageManifest?.homepage, + } as LicensePackageJson + }), + ].flat() + + // Group the package by license const byGrade = R.groupBy(function(student) { + const groupByLicense = groupBy((item: LicensePackageJson) => item.license) + const groupedByLicense = groupByLicense(data) + + return JSON.stringify(groupedByLicense, null, 2) +} + +function renderLicensesTableInWorkspace (licensesMap: Record, opts: { long?: boolean }) { + const columnNames = [ + 'Package', + 'License', + ] + + const columnFns = [ + renderPackageName, + renderPackageLicense, + ] + + if (opts.long) { + 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]) + + const data = [ + columnNames, + ...sortLicensesPackages(Object.values(licensesMap)) + .map((licensePkg) => columnFns.map((fn) => fn(licensePkg))), + ] + return table(data, { + ...TABLE_OPTIONS, + columns: { + ...TABLE_OPTIONS.columns, + }, + }) +} + +function renderLicensesTable (licensePackages: readonly LicensePackage[], opts: { long?: boolean }) { + const columnNames = [ + 'Package', + 'License', + ] + + const columnFns = [ + renderPackageName, + renderPackageLicense, + ] + + if (opts.long) { + 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]) + + return table([ + columnNames, + ...sortLicensesPackages(licensePackages) + .map((outdatedPkg) => columnFns.map((fn) => fn(outdatedPkg))), + ], 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 new file mode 100644 index 00000000000..3b3c2dc233b --- /dev/null +++ b/packages/plugin-commands-licenses/test/index.ts @@ -0,0 +1,335 @@ +/// +import { promises as fs } from 'fs' +import path from 'path' +import { WANTED_LOCKFILE } from '@pnpm/constants' +import { PnpmError } from '@pnpm/error' +import { licenses } from '@pnpm/plugin-commands-licenses' +import { prepare, tempDir } from '@pnpm/prepare' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import stripAnsi from 'strip-ansi' + +const fixtures = path.join(__dirname, '../../../fixtures') +const hasOutdatedDepsFixture = path.join(fixtures, 'has-outdated-deps') +const has2OutdatedDepsFixture = path.join(fixtures, 'has-2-outdated-deps') +const hasOutdatedDepsFixtureAndExternalLockfile = path.join(fixtures, 'has-outdated-deps-and-external-shrinkwrap', 'pkg') +const hasNotOutdatedDepsFixture = path.join(fixtures, 'has-not-outdated-deps') +const hasMajorOutdatedDepsFixture = path.join(fixtures, 'has-major-outdated-deps') +const hasNoLockfileFixture = path.join(fixtures, 'has-no-lockfile') +const withPnpmUpdateIgnore = path.join(fixtures, 'with-pnpm-update-ignore') + +const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}` + +const OUTDATED_OPTIONS = { + cacheDir: 'cache', + fetchRetries: 1, + fetchRetryFactor: 1, + fetchRetryMaxtimeout: 60, + fetchRetryMintimeout: 10, + global: false, + networkConcurrency: 16, + offline: false, + rawConfig: { registry: REGISTRY_URL }, + registries: { default: REGISTRY_URL }, + strictSsl: false, + tag: 'latest', + userAgent: '', + userConfig: {}, +} + +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 │ +└─────────────┴─────────┴────────┘ +`) +}) 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 new file mode 100644 index 00000000000..4803cb9f3e8 --- /dev/null +++ b/packages/plugin-commands-licenses/tsconfig.json @@ -0,0 +1,61 @@ +{ + "extends": "@pnpm/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts", + "../../typings/**/*.d.ts" + ], + "references": [ + { + "path": "../../privatePackages/prepare" + }, + { + "path": "../cli-utils" + }, + { + "path": "../command" + }, + { + "path": "../common-cli-options-help" + }, + { + "path": "../config" + }, + { + "path": "../constants" + }, + { + "path": "../default-resolver" + }, + { + "path": "../error" + }, + { + "path": "../filter-workspace-packages" + }, + { + "path": "../licenses" + }, + { + "path": "../lockfile-file" + }, + { + "path": "../matcher" + }, + { + "path": "../modules-yaml" + }, + { + "path": "../plugin-commands-installation" + }, + { + "path": "../store-path" + }, + { + "path": "../types" + } + ] +} diff --git a/packages/plugin-commands-licenses/tsconfig.lint.json b/packages/plugin-commands-licenses/tsconfig.lint.json new file mode 100644 index 00000000000..0dc5add6b7b --- /dev/null +++ b/packages/plugin-commands-licenses/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../typings/**/*.d.ts" + ] +} diff --git a/packages/pnpm/package.json b/packages/pnpm/package.json index ebf2fef0755..c7e7ba4354b 100644 --- a/packages/pnpm/package.json +++ b/packages/pnpm/package.json @@ -45,6 +45,7 @@ "@pnpm/plugin-commands-env": "workspace:*", "@pnpm/plugin-commands-init": "workspace:*", "@pnpm/plugin-commands-installation": "workspace:*", + "@pnpm/plugin-commands-licenses": "workspace:*", "@pnpm/plugin-commands-listing": "workspace:*", "@pnpm/plugin-commands-outdated": "workspace:*", "@pnpm/plugin-commands-patching": "workspace:*", diff --git a/packages/pnpm/src/cmd/help.ts b/packages/pnpm/src/cmd/help.ts index e7baf7abea7..e91686d786e 100644 --- a/packages/pnpm/src/cmd/help.ts +++ b/packages/pnpm/src/cmd/help.ts @@ -89,6 +89,10 @@ function getHelpText () { description: 'Check for outdated packages', name: 'outdated', }, + { + description: 'Check licenses in consumed packages', + name: 'licenses', + }, ], }, { diff --git a/packages/pnpm/src/cmd/index.ts b/packages/pnpm/src/cmd/index.ts index 720f7d07206..f56eab94494 100644 --- a/packages/pnpm/src/cmd/index.ts +++ b/packages/pnpm/src/cmd/index.ts @@ -6,6 +6,7 @@ import { env } from '@pnpm/plugin-commands-env' import { deploy } from '@pnpm/plugin-commands-deploy' import { add, fetch, install, link, prune, remove, unlink, update, importCommand } from '@pnpm/plugin-commands-installation' import { list, ll, why } from '@pnpm/plugin-commands-listing' +import { licenses } from '@pnpm/plugin-commands-licenses' import { outdated } from '@pnpm/plugin-commands-outdated' import { pack, publish } from '@pnpm/plugin-commands-publishing' import { patch, patchCommit } from '@pnpm/plugin-commands-patching' @@ -112,6 +113,7 @@ const commands: CommandDefinition[] = [ link, list, ll, + licenses, outdated, pack, patch, diff --git a/packages/pnpm/tsconfig.json b/packages/pnpm/tsconfig.json index 399cf79e9b1..839b222b97d 100644 --- a/packages/pnpm/tsconfig.json +++ b/packages/pnpm/tsconfig.json @@ -90,6 +90,9 @@ { "path": "../plugin-commands-installation" }, + { + "path": "../plugin-commands-licenses" + }, { "path": "../plugin-commands-listing" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2013e6b7f3..5849f427e99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1807,6 +1807,73 @@ importers: specifier: 4.0.0-rc.27 version: 4.0.0-rc.27_typanion@3.12.0 + packages/licenses: + dependencies: + '@pnpm/client': + specifier: workspace:* + version: link:../client + '@pnpm/constants': + specifier: workspace:* + version: link:../constants + '@pnpm/error': + specifier: workspace:* + version: link:../error + '@pnpm/lockfile-file': + specifier: workspace:* + version: link:../lockfile-file + '@pnpm/lockfile-utils': + specifier: workspace:* + version: link:../lockfile-utils + '@pnpm/logger': + specifier: ^5.0.0 + version: 5.0.0 + '@pnpm/manifest-utils': + specifier: workspace:* + version: link:../manifest-utils + '@pnpm/matcher': + specifier: workspace:* + version: link:../matcher + '@pnpm/modules-yaml': + specifier: workspace:* + version: link:../modules-yaml + '@pnpm/npm-resolver': + specifier: workspace:* + version: link:../npm-resolver + '@pnpm/pick-registry-for-package': + specifier: workspace:* + version: link:../pick-registry-for-package + '@pnpm/read-package-json': + specifier: workspace:* + version: link:../read-package-json + '@pnpm/types': + specifier: workspace:* + version: link:../types + dependency-path: + specifier: workspace:* + version: link:../dependency-path + p-limit: + specifier: ^3.1.0 + version: 3.1.0 + ramda: + specifier: npm:@pnpm/ramda@0.28.1 + version: /@pnpm/ramda/0.28.1 + semver: + specifier: ^7.3.8 + version: 7.3.8 + devDependencies: + '@pnpm/licenses': + specifier: workspace:* + version: 'link:' + '@types/ramda': + specifier: 0.28.15 + version: 0.28.15 + '@types/semver': + specifier: 7.3.12 + version: 7.3.12 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + packages/lifecycle: dependencies: '@pnpm/core-loggers': @@ -3483,6 +3550,100 @@ importers: specifier: ^4.2.0 version: 4.2.0 + packages/plugin-commands-licenses: + dependencies: + '@pnpm/cli-utils': + specifier: workspace:* + version: link:../cli-utils + '@pnpm/colorize-semver-diff': + specifier: ^1.0.1 + version: 1.0.1 + '@pnpm/command': + specifier: workspace:* + version: link:../command + '@pnpm/common-cli-options-help': + specifier: workspace:* + version: link:../common-cli-options-help + '@pnpm/config': + specifier: workspace:* + version: link:../config + '@pnpm/default-resolver': + specifier: workspace:* + version: link:../default-resolver + '@pnpm/error': + specifier: workspace:* + version: link:../error + '@pnpm/licenses': + specifier: workspace:* + version: link:../licenses + '@pnpm/lockfile-file': + specifier: workspace:* + version: link:../lockfile-file + '@pnpm/matcher': + specifier: workspace:* + version: link:../matcher + '@pnpm/modules-yaml': + specifier: workspace:* + version: link:../modules-yaml + '@pnpm/semver-diff': + specifier: ^1.1.0 + version: 1.1.0 + '@pnpm/store-path': + specifier: workspace:* + version: link:../store-path + '@pnpm/types': + specifier: workspace:* + version: link:../types + '@zkochan/table': + specifier: ^1.0.0 + version: 1.0.0 + chalk: + specifier: ^4.1.2 + version: 4.1.2 + lru-cache: + specifier: ^7.14.0 + version: 7.14.0 + ramda: + specifier: npm:@pnpm/ramda@0.28.1 + version: /@pnpm/ramda/0.28.1 + render-help: + specifier: ^1.0.2 + version: 1.0.2 + strip-ansi: + specifier: ^6.0.1 + version: 6.0.1 + wrap-ansi: + specifier: ^7.0.0 + version: 7.0.0 + devDependencies: + '@pnpm/constants': + specifier: workspace:* + version: link:../constants + '@pnpm/filter-workspace-packages': + specifier: workspace:* + version: link:../filter-workspace-packages + '@pnpm/plugin-commands-installation': + specifier: workspace:* + version: link:../plugin-commands-installation + '@pnpm/plugin-commands-licenses': + specifier: workspace:* + version: 'link:' + '@pnpm/prepare': + specifier: workspace:* + version: link:../../privatePackages/prepare + '@pnpm/registry-mock': + specifier: 3.1.0 + version: 3.1.0_typanion@3.12.0 + '@types/ramda': + specifier: 0.28.15 + version: 0.28.15 + '@types/wrap-ansi': + specifier: ^3.0.0 + version: 3.0.0 + '@types/zkochan__table': + specifier: npm:@types/table@6.0.0 + version: /@types/table/6.0.0 + packages/plugin-commands-listing: dependencies: '@pnpm/cli-utils': @@ -4350,6 +4511,9 @@ importers: '@pnpm/plugin-commands-installation': specifier: workspace:* version: link:../plugin-commands-installation + '@pnpm/plugin-commands-licenses': + specifier: workspace:* + version: link:../plugin-commands-licenses '@pnpm/plugin-commands-listing': specifier: workspace:* version: link:../plugin-commands-listing @@ -8261,6 +8425,10 @@ packages: /@types/semver/6.2.3: resolution: {integrity: sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==} + /@types/semver/7.3.12: + resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==} + dev: true + /@types/semver/7.3.13: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} @@ -17213,6 +17381,7 @@ time: /@types/ramda/0.28.15: '2022-07-09T08:02:32.716Z' /@types/retry/0.12.2: '2022-04-26T19:32:23.281Z' /@types/rimraf/3.0.2: '2021-08-18T21:02:03.570Z' + /@types/semver/7.3.12: '2022-08-11T21:32:18.856Z' /@types/semver/7.3.13: '2022-10-26T20:03:07.384Z' /@types/signal-exit/3.0.1: '2021-07-06T17:09:42.542Z' /@types/sinon/10.0.13: '2022-07-20T05:32:19.345Z'