UNPKG

js-green-licenses

Version:
445 lines 17.7 kB
"use strict"; // Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LicenseChecker = exports.GitHubRepository = void 0; const events_1 = require("events"); const fs = __importStar(require("fs")); const npm_package_arg_1 = __importDefault(require("npm-package-arg")); const package_json_1 = __importDefault(require("package-json")); const path = __importStar(require("path")); const spdx_correct_1 = __importDefault(require("spdx-correct")); const spdx_satisfies_1 = __importDefault(require("spdx-satisfies")); const util_1 = require("util"); const semver_1 = __importDefault(require("semver")); const config = __importStar(require("./config")); const github_1 = require("./github"); const package_json_file_1 = require("./package-json-file"); var github_2 = require("./github"); Object.defineProperty(exports, "GitHubRepository", { enumerable: true, get: function () { return github_2.GitHubRepository; } }); const fsAccess = (0, util_1.promisify)(fs.access); const fsReadDir = (0, util_1.promisify)(fs.readdir); const fsReadFile = (0, util_1.promisify)(fs.readFile); // Valid license IDs defined in https://spdx.org/licenses/ must be used whenever // possible. When adding new licenses, please consult the relevant documents. const DEFAULT_GREEN_LICENSES = [ '0BSD', 'AFL-2.1', 'AFL-3.0', 'APSL-2.0', 'Apache-1.1', 'Apache-2.0', 'Artistic-1.0', 'Artistic-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSL-1.0', 'CC-BY-1.0', 'CC-BY-2.0', 'CC-BY-2.5', 'CC-BY-3.0', 'CC-BY-4.0', 'CC0-1.0', 'CDDL-1.0', 'CDDL-1.1', 'CPL-1.0', 'EPL-1.0', 'FTL', 'IPL-1.0', 'ISC', 'LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0', 'LPL-1.02', 'MIT', 'MPL-1.0', 'MPL-1.1', 'MPL-2.0', 'MS-PL', 'NCSA', 'OpenSSL', 'PHP-3.0', 'Ruby', 'Unlicense', 'W3C', 'Xnet', 'ZPL-2.0', 'Zend-2.0', 'Zlib', 'libtiff', ]; class LicenseChecker extends events_1.EventEmitter { constructor({ dev = false, verbose = false } = {}) { super(); // Cache for packageName@version's that are already processed. this.processedPackages = new Set(); // Cache for packageName@version's that failed for fetching. this.failedPackages = new Set(); // Local packages, for monorepo this.localPackages = new Set(); this.config = {}; // Licenses in this expression must be valid license IDs defined in // https://spdx.org/licenses/. this.greenLicenseExpr = ''; // List of license names that are not SPDX-conforming IDs but are allowed. this.allowlistedLicenses = []; this.opts = { dev, verbose }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event, listener) { return super.on(event, listener); } // eslint-disable-next-line @typescript-eslint/no-explicit-any emit(event, ...args) { return super.emit(event, ...args); } init(cfg) { this.config = cfg || {}; const greenLicenses = this.config.greenLicenses || DEFAULT_GREEN_LICENSES; const validGreenLicenses = []; const invalidGreenLicenses = []; for (const license of greenLicenses) { const corrected = this.correctLicenseName(license); if (corrected) { validGreenLicenses.push(corrected); } else { invalidGreenLicenses.push(license); } } this.greenLicenseExpr = `(${validGreenLicenses.join(' OR ')})`; this.allowlistedLicenses = invalidGreenLicenses; this.processedPackages.clear(); this.failedPackages.clear(); } getLicense(pkgJson) { // Some package.json files have incorrect license fields, and old packages // may have legacy licence field format. See // https://docs.npmjs.com/files/package.json#license for details. The code // below is a little complicated to deal with those cases. const license = pkgJson.license || pkgJson.licenses; if (!license) { if (pkgJson.private) { return 'private'; } return null; } if (typeof license === 'string') { return license; } if (Array.isArray(license)) { if (license.length === 0) { return null; } const types = license.map(x => x.type).filter(x => !!x); return types.length === 1 ? types[0] : `(${types.join(' OR ')})`; } return license.type || null; } correctLicenseName(license) { // NPM specific value. if (license === 'UNLICENSED' || license === 'UNLICENCED') { console.warn(`Unlicensed package, specified license: ${license}`); return 'UNLICENSED'; } const corrected = (0, spdx_correct_1.default)(license); if (this.opts.verbose && corrected && corrected !== license) { console.warn(`Correcting ${license} to ${corrected}`); } return corrected; } isPackageAllowlisted(packageName) { return (!!this.config.packageAllowlist && this.config.packageAllowlist.some(p => p === packageName)); } isGreenLicense(license) { if (!license) return false; const correctedName = this.correctLicenseName(license); // `license` is not a valid or correctable SPDX id. Check the allowlist. if (!correctedName) { return this.allowlistedLicenses.some(l => l === license); } try { return (0, spdx_satisfies_1.default)(correctedName, this.greenLicenseExpr); } catch (e) { const err = e; // Most likely because license is not recognized. Just return false. if (this.opts.verbose) { console.error(err.message || err); } return false; } } async getPackageJson(packageName, versionSpec, localDirectory) { // If this has a relative URL, and is a local package, find the package json from the // indicated directory if (versionSpec.startsWith('file:') && localDirectory) { const relativePath = versionSpec.slice('file:'.length); const packageJsonPath = path.join(localDirectory, relativePath, 'package.json'); this.emit('package.json', packageJsonPath); const contents = await fsReadFile(packageJsonPath, 'utf8'); return JSON.parse(contents); } return (0, package_json_1.default)(packageName, { version: versionSpec, fullMetadata: true, }); } async checkLicenses(packageName, versionSpec, localDirectory, ...parents) { const spec = `${packageName}@${versionSpec}`; if (this.failedPackages.has(spec)) return; // remove tilde/caret to check for an exact version, ^0.5.0-rc.0 becomes 0.5.0-rc.0 const version = versionSpec.replace(/^[^~]/, ''); // if the dependency is a local package then skip verification at this step. will be checked independently if (this.localPackages.has(`${packageName}@${version}`)) return; try { const json = await this.getPackageJson(packageName, versionSpec, localDirectory); await this.checkPackageJson(json, packageName, localDirectory, ...parents); } catch (e) { const err = e; this.failedPackages.add(spec); this.emit('error', { err, packageName, versionSpec, parentPackages: parents, }); } } async checkLicensesForDeps(deps, localDirectory, ...parents) { if (!deps) return; for (const pkg of Object.keys(deps)) { const depVersion = deps[pkg]; await this.checkLicenses(pkg, depVersion, localDirectory, ...parents); } } async checkPackageJson(json, packageName, localDirectory, ...parents) { packageName = (packageName || json.name || 'undefined'); const isAllowlisted = this.isPackageAllowlisted(packageName); if (isAllowlisted) { json.version = semver_1.default.valid(json.version) ? json.version : '0.0.0'; } else { (0, package_json_file_1.ensurePackageJson)(json); } if (json.name !== packageName) { console.warn(`Package name mismatch. Expected ${packageName}, but got ${json.name}`); } const pkgVersion = json.version; const packageAndVersion = `${packageName}@${pkgVersion}`; if (this.processedPackages.has(packageAndVersion)) return; this.processedPackages.add(packageAndVersion); if (this.isPackageAllowlisted(packageName)) { console.log(`${packageName} is allowlisted.`); } else { const license = this.getLicense(json); if (!this.isGreenLicense(license)) { this.emit('non-green-license', { packageName, version: pkgVersion || 'undefined', licenseName: license, parentPackages: parents, }); } } await this.checkLicensesForDeps(json.dependencies, localDirectory, ...parents, packageAndVersion); if (this.opts.dev) { await this.checkLicensesForDeps(json.devDependencies, localDirectory, ...parents, packageAndVersion); } } async checkPackageJsonContent(content, localDirectory) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let json = null; try { json = JSON.parse(content); await this.checkPackageJson(json, json.name, localDirectory); } catch (e) { const err = e; const packageName = (json === null || json === void 0 ? void 0 : json.name) || '(unknown package)'; const versionSpec = (json === null || json === void 0 ? void 0 : json.version) || '(unknown version)'; this.emit('error', { err, packageName, versionSpec, parentPackages: [], }); } } async getLocalPackageJsonFiles(directory) { const packageJsons = []; const addPackageJson = async (dir) => { try { const pj = path.join(dir, 'package.json'); await fsAccess(pj); packageJsons.push(pj); } catch (_a) { // package.json doesn't exist. Ignore. } }; // Find the top-level package.json first. await addPackageJson(directory); // Find `packages/<name>/package.json` files in case this is a monorepo. try { const packages = path.join(directory, 'packages'); const subpackages = await fsReadDir(packages); // This is a monorepo. Find package.json from each directory under // `packages`. for (const dir of subpackages) { await addPackageJson(path.join(packages, dir)); } } catch (_a) { // The `packages` directory doesn't exist. Not a monorepo. Return just the // top-level package.json. } return packageJsons; } async checkLocalDirectory(directory) { this.init(await config.getLocalConfig(directory)); const packageJsons = await this.getLocalPackageJsonFiles(directory); if (packageJsons.length === 0) { console.log('No package.json files have been found.'); } for (const pj of packageJsons) { const content = await fsReadFile(pj, 'utf8'); const json = JSON.parse(content); if (json && json.name && json.version) { this.localPackages.add(`${json.name}@${json.version}`); } } for (const pj of packageJsons) { this.emit('package.json', pj); const content = await fsReadFile(pj, 'utf8'); await this.checkPackageJsonContent(content, path.dirname(pj)); } this.emit('end'); } async checkRemotePackage(pkg) { // For checking remote packages, use config file in the current directory. this.init(await config.getLocalConfig(process.cwd())); const pkgArgs = (0, npm_package_arg_1.default)(pkg); const pkgType = pkgArgs.type; if (!['tag', 'version', 'range'].some(type => type === pkgType)) { throw new Error(`Unsupported package spec: ${pkg}`); } if (!pkgArgs.name || !pkgArgs.fetchSpec) { throw new Error(`Invalid package spec: ${pkg}`); } await this.checkLicenses(pkgArgs.name, pkgArgs.fetchSpec, null); this.emit('end'); } /** @param prPath Must be in a form of <owner>/<repo>/pull/<id>. */ prPathToGitHubRepoAndId(prPath) { const regexp = /^([^/]+)\/([^/]+)\/pull\/(\d+)$/; const matched = regexp.exec(prPath); if (!matched) { throw new Error(`Invalid github pull request path: ${prPath}. ` + 'Must be in the form <owner>/<repo>/pull/<id>.'); } const [, owner, repoName, prId] = matched; return { repo: new github_1.GitHubRepository(owner, repoName), prId: Number(prId) }; } async checkGitHubPR(repo, mergeCommitSha) { this.init(await config.getGitHubConfig(repo, mergeCommitSha)); const packageJsons = await repo.getPackageJsonFiles(mergeCommitSha); if (packageJsons.length === 0) { console.log('No package.json files have been found.'); } for (const pj of packageJsons) { this.emit('package.json', pj.filePath); await this.checkPackageJsonContent(pj.content, null); } this.emit('end'); } /** set default event handlers for CLI output. */ setDefaultHandlers(options = {}) { let nonGreenCount = 0; let errorCount = 0; this.on('non-green-license', ({ packageName, version, licenseName, parentPackages }) => { nonGreenCount++; const licenseDisplay = licenseName || '(no license)'; const packageAndVersion = `${packageName}@${version}`; console.log(`${licenseDisplay}: ${packageAndVersion}`); console.log(` ${[...parentPackages, packageAndVersion].join(' -> ')}`); console.log(); }) .on('package.json', filePath => { console.log(`Checking ${filePath}...`); console.log(); }) .on('error', ({ err, packageName, versionSpec, parentPackages }) => { errorCount++; const packageAndVersion = `${packageName}@${versionSpec}`; console.log(`Error while checking ${packageAndVersion}:`); console.log(` ${[...parentPackages, packageAndVersion].join(' -> ')}`); console.log(); console.log(`${(0, util_1.inspect)(err)}`); console.log(); }) .on('end', () => { if (nonGreenCount > 0 || errorCount > 0) { if (options.setExitCode) { process.exitCode = 1; } if (nonGreenCount > 0) { console.log(`${nonGreenCount} non-green licenses found.`); } if (errorCount > 0) { console.log(`${errorCount} errors found.`); } } else { console.log('All green!'); } }); } } exports.LicenseChecker = LicenseChecker; //# sourceMappingURL=checker.js.map