Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create
licenses
-command for PNPM
Introduces a new command `licenses`-command which allows to list the licenses of the packages refs pnpm#2825
- Loading branch information
Weyert de Boer
committed
Oct 30, 2022
1 parent
f50fd10
commit 8ae8bf9
Showing
28 changed files
with
3,774 additions
and
1,395 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# @pnpm/licenses | ||
|
||
> Check for the licensse of packages | ||
<!--@shields('npm')--> | ||
[![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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('../../jest.config.js'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<LicenseInfo> { | ||
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, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { licensesDepsOfProjects } from './licensesDepsOfProjects' | ||
export { LicensePackage } from './licenses' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> | ||
include?: IncludedDependencies | ||
lockfileDir: string | ||
manifest: ProjectManifest | ||
match?: (dependencyName: string) => boolean | ||
prefix: string | ||
registries: Registries | ||
wantedLockfile: Lockfile | null | ||
} | ||
): Promise<LicensePackage[]> { | ||
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 | ||
} |
Oops, something went wrong.