├── .eslintrc ├── .github └── workflows │ ├── npm-publish.yml │ └── npm-test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── SECURITY.md ├── jest-runner-eslint.config.js ├── jest.eslint.config.js ├── jest.test.config.js ├── package-lock.json ├── package.json └── src ├── LicenseError.js ├── __mocks__ ├── module-with-licence │ └── LICENCE └── module-with-license │ └── LICENSE ├── defaultOutputTemplate.ejs ├── defaultOutputWriter.js ├── index.js ├── licenseUtils.js ├── licenseUtils.spec.js ├── optionsUtils.js └── optionsUtils.spec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017 4 | }, 5 | "env": { 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": ["eslint:recommended", "plugin:jest/recommended", "prettier"], 10 | "plugins": ["jest", "prettier"], 11 | "rules": { 12 | "prettier/prettier": "error" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to the npm registry when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish to npm 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node when a branch or a pull request is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - run: npm ci 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | /*.config.js 3 | /src/*.spec.js 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # license-checker-webpack-plugin 2 | 3 | Webpack plugin that verifies licenses of all external dependencies in a compilation, and outputs all that information to a file. 4 | 5 | ## Installation 6 | 7 | ### npm 8 | 9 | ``` 10 | npm install license-checker-webpack-plugin --save-dev 11 | ``` 12 | 13 | ### yarn 14 | 15 | ``` 16 | yarn add license-checker-webpack-plugin --dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | Require the plugin into your Webpack configuration, and pass it to the `plugins` array. 22 | 23 | ```js 24 | const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin"); 25 | 26 | module.exports = { 27 | // ... 28 | plugins: [new LicenseCheckerWebpackPlugin({ outputFilename: "ThirdPartyNotices.txt" })] 29 | }; 30 | ``` 31 | 32 | ## Options 33 | 34 | | Property | Type | Default | Description | 35 | | ---------------- | ---------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | 36 | | `allow` | `string` | `"(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)"` | SPDX expression with allowed licenses. | 37 | | `ignore` | `array` | `[]` | Array of dependencies to ignore, in the format `["@"]`. For example, `["assignment@^2.0.0"]`. | 38 | | `override` | `object` | `{}` | Object of dependencies to override, in the format `{"@": { ... }}`. For example, `{"assignment@^2.0.0": { licenseName: "MIT" }}`. | 39 | | `emitError` | `boolean` | `false` | Whether to emit errors instead of warnings. | 40 | | `outputWriter` | `string` or `function` | See [`defaultOutputWriter`](./src/defaultOutputWriter.js). | Path to a `.ejs` template, or function that will generate the contents of the third-party notices file. | 41 | | `outputFilename` | `string` | `"ThirdPartyNotices.txt"` | Name of the third-party notices file with all licensing information. | 42 | 43 | The data that gets passed to the `outputWriter` function looks like this: 44 | 45 | ```json 46 | [ 47 | { 48 | "name": "react", 49 | "version": "16.3.2", 50 | "repository": "git+https://github.com/facebook/react.git", 51 | "licenseName": "MIT", 52 | "licenseText": "MIT License\n\nCopyright (c) 2013-present, Facebook, Inc. [...]" 53 | }, 54 | { 55 | "name": "webpack", 56 | "version": "4.8.3", 57 | "author": "Tobias Koppers @sokra", 58 | "repository": "git+https://github.com/webpack/webpack.git", 59 | "licenseName": "MIT", 60 | "licenseText": "Copyright JS Foundation and other contributors [...]" 61 | }, 62 | { 63 | "name": "whatwg-fetch", 64 | "version": "2.0.4", 65 | "repository": "git+https://github.com/github/fetch.git", 66 | "licenseName": "MIT", 67 | "licenseText": "Copyright (c) 2014-2016 GitHub, Inc. [...]" 68 | } 69 | ] 70 | ``` 71 | 72 | Here's an example `webpack.config.js` file that uses all options: 73 | 74 | ```js 75 | const path = require("path"); 76 | const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin"); 77 | const template = require("lodash.template"); 78 | 79 | module.exports = { 80 | // ... 81 | plugins: [ 82 | new LicenseCheckerWebpackPlugin({ 83 | allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)", 84 | ignore: ["@microsoft/*"], 85 | override: { 86 | "assignment@2.0.0": { licenseName: "MIT" }, 87 | "intersection-observer@0.5.0": { licenseName: "MIT" }, 88 | "querystring-es3@0.2.1": { licenseName: "MIT" } 89 | }, 90 | emitError: true, 91 | outputWriter: path.resolve(__dirname, "customTemplate.ejs"), 92 | outputFilename: "ThirdPartyNotices.txt" 93 | }) 94 | ] 95 | }; 96 | ``` 97 | 98 | ## Contributing 99 | 100 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . 101 | 102 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 103 | 104 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 105 | 106 | ## Licensing 107 | 108 | All files on this repository are subject to the MIT license. Please read the `LICENSE` file at the root of the project. 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /jest-runner-eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cliOptions: { 3 | format: "pretty" 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /jest.eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runner: "jest-runner-eslint", 3 | displayName: "eslint", 4 | modulePathIgnorePatterns: ["dist"], 5 | testMatch: ["/**/*.js"] 6 | }; 7 | -------------------------------------------------------------------------------- /jest.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: "test", 3 | testEnvironment: "node", 4 | moduleDirectories: ["src", "node_modules"] 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "license-checker-webpack-plugin", 3 | "description": "Verifies licenses of all external dependencies in a compilation, and outputs them to a file.", 4 | "version": "0.2.1", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Daniel Perez Alvarez", 8 | "email": "unindented@gmail.com", 9 | "url": "http://unindented.org/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Microsoft/license-checker-webpack-plugin.git" 14 | }, 15 | "keywords": [ 16 | "license", 17 | "webpack" 18 | ], 19 | "main": "src/index.js", 20 | "scripts": { 21 | "test": "jest --projects jest.*.config.js" 22 | }, 23 | "peerDependencies": { 24 | "webpack": "^4.4.0 || ^5.4.0" 25 | }, 26 | "dependencies": { 27 | "glob": "^7.1.6", 28 | "lodash.template": "^4.5.0", 29 | "minimatch": "^3.0.4", 30 | "semver": "^6.3.0", 31 | "spdx-expression-validate": "^2.0.0", 32 | "spdx-satisfies": "^5.0.0", 33 | "superstruct": "^0.10.12", 34 | "webpack-sources": "^1.4.3", 35 | "wrap-ansi": "^6.1.0" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^6.5.1", 39 | "eslint-config-prettier": "^6.4.0", 40 | "eslint-formatter-pretty": "^2.1.1", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "eslint-plugin-jest": "^22.20.0", 43 | "jest": "^24.9.0", 44 | "jest-runner-eslint": "^0.7.5", 45 | "prettier": "^1.18.2", 46 | "webpack": "^5.4.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LicenseError.js: -------------------------------------------------------------------------------- 1 | const WebpackError = require("webpack/lib/WebpackError"); 2 | 3 | class LicenseError extends WebpackError { 4 | constructor(message) { 5 | super(`License: ${message}`); 6 | this.name = "LicenseError"; 7 | 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | 12 | module.exports = LicenseError; 13 | -------------------------------------------------------------------------------- /src/__mocks__/module-with-licence/LICENCE: -------------------------------------------------------------------------------- 1 | LICENCE TEXT -------------------------------------------------------------------------------- /src/__mocks__/module-with-license/LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE TEXT -------------------------------------------------------------------------------- /src/defaultOutputTemplate.ejs: -------------------------------------------------------------------------------- 1 | THIRD PARTY SOFTWARE NOTICES AND INFORMATION 2 | Do Not Translate or Localize 3 | 4 | <% dependencies.forEach(dep => { %> 5 | -------------------------------------------------------------------------------- 6 | <%= dep.name %> v<%= dep.version %><%= dep.author ? ` - ${dep.author}` : '' %><%= dep.repository ? `\n${dep.repository}` : '' %> 7 | -------------------------------------------------------------------------------- 8 | 9 | <%= dep.licenseText || dep.licenseName %> 10 | <% }) %> 11 | -------------------------------------------------------------------------------- /src/defaultOutputWriter.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | const defaultLicenseWriter = resolve(__dirname, "./defaultOutputTemplate.ejs"); 4 | 5 | module.exports = defaultLicenseWriter; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const { RawSource } = webpack.sources || require("webpack-sources"); 3 | const { 4 | getLicenseInformationForCompilation, 5 | getLicenseViolations, 6 | getSortedLicenseInformation, 7 | ignoreLicenses, 8 | overrideLicenses, 9 | writeLicenseInformation 10 | } = require("./licenseUtils"); 11 | const { getOptions } = require("./optionsUtils"); 12 | 13 | async function buildAsset(compilation, options) { 14 | const { filter, allow, ignore, override, emitError, outputFilename, outputWriter } = options; 15 | 16 | let licenseInformation = getLicenseInformationForCompilation(compilation, filter); 17 | licenseInformation = ignoreLicenses(licenseInformation, ignore); 18 | licenseInformation = overrideLicenses(licenseInformation, override); 19 | 20 | const licenseViolations = getLicenseViolations(licenseInformation, allow); 21 | if (emitError) { 22 | compilation.errors.push(...licenseViolations); 23 | } else { 24 | compilation.warnings.push(...licenseViolations); 25 | } 26 | 27 | const sortedLicenseInformation = getSortedLicenseInformation(licenseInformation); 28 | return [ 29 | outputFilename, 30 | new RawSource(await writeLicenseInformation(outputWriter, sortedLicenseInformation)) 31 | ]; 32 | } 33 | 34 | class LicenseCheckerWebpackPlugin { 35 | constructor(options) { 36 | this.options = getOptions(options); 37 | } 38 | 39 | apply(compiler) { 40 | const { name } = this.constructor; 41 | if (webpack.version.startsWith("4.")) { 42 | compiler.hooks.emit.tapPromise(name, async compilation => { 43 | const [filename, source] = await buildAsset(compilation, this.options); 44 | compilation.assets[filename] = source; 45 | }); 46 | } else { 47 | compiler.hooks.thisCompilation.tap(name, compilation => { 48 | const hook = { 49 | name, 50 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL 51 | }; 52 | compilation.hooks.processAssets.tapPromise(hook, async () => { 53 | const [filename, source] = await buildAsset(compilation, this.options); 54 | compilation.emitAsset(filename, source); 55 | }); 56 | }); 57 | } 58 | } 59 | } 60 | 61 | module.exports = LicenseCheckerWebpackPlugin; 62 | -------------------------------------------------------------------------------- /src/licenseUtils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { resolve } = require("path"); 3 | const glob = require("glob"); 4 | const template = require("lodash.template"); 5 | const satisfiesGlob = require("minimatch"); 6 | const { satisfies: isSatisfiedVersion } = require("semver"); 7 | const isValidLicense = require("spdx-expression-validate"); 8 | const isSatisfiedLicense = require("spdx-satisfies"); 9 | const wrap = require("wrap-ansi"); 10 | const LicenseError = require("./LicenseError"); 11 | 12 | const licenseGlob = "LICEN@(C|S)E*"; 13 | const licenseWrap = 80; 14 | 15 | const getLicenseContents = dependencyPath => { 16 | const [licenseFilename] = glob.sync(licenseGlob, { 17 | cwd: dependencyPath, 18 | nocase: true, 19 | nodir: true 20 | }); 21 | const licensePath = licenseFilename && resolve(dependencyPath, licenseFilename); 22 | return licensePath && wrap(readFileSync(licensePath).toString(), licenseWrap); 23 | }; 24 | 25 | const getLicenseName = pkg => { 26 | // Valid license formats are an SPDX string expression 27 | // https://www.npmjs.com/package/spdx 28 | if (typeof pkg.license === "string") { 29 | return pkg.license; 30 | } 31 | // Check for deprecated licence formats 32 | // https://docs.npmjs.com/files/package.json#license 33 | if (typeof pkg.license === "object") { 34 | return pkg.license.type || ""; 35 | } 36 | if (pkg.licenses && Array.isArray(pkg.licenses)) { 37 | const licenseName = pkg.licenses.map(license => license.type).join(" OR "); 38 | // Return the list of licenses as a composite license expression 39 | return pkg.licenses.length > 1 ? `(${licenseName})` : licenseName; 40 | } 41 | // Fall-back to an empty string and let the validation handle the error. 42 | return ""; 43 | }; 44 | 45 | const getLicenseInformationForDependency = dependencyPath => { 46 | const pkg = require(`${dependencyPath}/package.json`); 47 | return { 48 | name: pkg.name, 49 | version: pkg.version, 50 | author: (pkg.author && pkg.author.name) || pkg.author, 51 | repository: (pkg.repository && pkg.repository.url) || pkg.repository, 52 | licenseName: getLicenseName(pkg), 53 | licenseText: getLicenseContents(dependencyPath) 54 | }; 55 | }; 56 | 57 | const getLicenseInformationForCompilation = (compilation, filter) => { 58 | const fileDependencies = Array.from(compilation.fileDependencies); 59 | return fileDependencies.reduce((memo, dependencyPath) => { 60 | const match = dependencyPath.match(filter); 61 | if (match) { 62 | const [, rootPath, dependencyName] = match; 63 | memo[dependencyName] = getLicenseInformationForDependency(rootPath); 64 | } 65 | return memo; 66 | }, {}); 67 | }; 68 | 69 | const getLicenseViolations = (licenseInformation, allow) => { 70 | return Object.keys(licenseInformation).reduce((memo, name) => { 71 | const { version, licenseName } = licenseInformation[name]; 72 | if (!licenseName || licenseName === "UNLICENSED") { 73 | memo.push(new LicenseError(`${name}@${version} is unlicensed`)); 74 | } else if (!isValidLicense(licenseName) || !isSatisfiedLicense(licenseName, allow)) { 75 | memo.push(new LicenseError(`${name}@${version} has disallowed license ${licenseName}`)); 76 | } 77 | return memo; 78 | }, []); 79 | }; 80 | 81 | const getSortedLicenseInformation = licenseInformation => { 82 | const licenses = Object.values(licenseInformation); 83 | licenses.sort(({ name: nameA }, { name: nameB }) => (nameA < nameB ? -1 : nameA > nameB ? 1 : 0)); 84 | return licenses; 85 | }; 86 | 87 | const parseVersionExpression = expr => expr.split(/(?!^)@/); 88 | 89 | const ignoreLicenses = (licenseInformation, ignore) => { 90 | return Object.keys(licenseInformation).reduce( 91 | (memo, dependencyName) => 92 | ignore.reduce((memo, ignoredExpression) => { 93 | const { version: dependencyVersion } = licenseInformation[dependencyName]; 94 | const [ignoredName, ignoredVersionRange] = parseVersionExpression(ignoredExpression); 95 | const matchesName = satisfiesGlob(dependencyName, ignoredName); 96 | const matchesVersion = 97 | !ignoredVersionRange || isSatisfiedVersion(dependencyVersion, ignoredVersionRange); 98 | if (matchesName && matchesVersion) { 99 | delete memo[dependencyName]; 100 | } 101 | return memo; 102 | }, memo), 103 | JSON.parse(JSON.stringify(licenseInformation)) 104 | ); 105 | }; 106 | 107 | const overrideLicenses = (licenseInformation, override) => { 108 | return Object.keys(licenseInformation).reduce( 109 | (memo, dependencyName) => 110 | Object.keys(override).reduce((memo, overriddenKey) => { 111 | const { version: dependencyVersion } = licenseInformation[dependencyName]; 112 | const [overriddenName, overriddenVersionRange] = parseVersionExpression(overriddenKey); 113 | const matchesName = dependencyName === overriddenName; 114 | const matchesVersion = 115 | !overriddenVersionRange || isSatisfiedVersion(dependencyVersion, overriddenVersionRange); 116 | if (matchesName && matchesVersion) { 117 | Object.assign(memo[dependencyName], override[overriddenKey]); 118 | } 119 | return memo; 120 | }, memo), 121 | JSON.parse(JSON.stringify(licenseInformation)) 122 | ); 123 | }; 124 | 125 | const writeLicenseInformation = (outputWriter, dependencies) => { 126 | if (typeof outputWriter === "string") { 127 | outputWriter = template(readFileSync(outputWriter)); 128 | } 129 | return outputWriter({ dependencies }); 130 | }; 131 | 132 | module.exports = { 133 | getLicenseName, 134 | getLicenseContents, 135 | getLicenseInformationForCompilation, 136 | getLicenseViolations, 137 | getSortedLicenseInformation, 138 | ignoreLicenses, 139 | overrideLicenses, 140 | writeLicenseInformation 141 | }; 142 | -------------------------------------------------------------------------------- /src/licenseUtils.spec.js: -------------------------------------------------------------------------------- 1 | const { getLicenseName, getLicenseContents } = require("./licenseUtils"); 2 | 3 | describe("getLicenseName", () => { 4 | let pkg; 5 | let licenseName; 6 | 7 | describe("with valid license format", () => { 8 | beforeEach(() => { 9 | pkg = { license: "MIT" }; 10 | licenseName = getLicenseName(pkg); 11 | }); 12 | 13 | it(`should return "MIT" license name`, () => { 14 | expect(licenseName).toBe("MIT"); 15 | }); 16 | }); 17 | 18 | describe("with deprecated license format (as object)", () => { 19 | beforeEach(() => { 20 | pkg = { license: { type: "MIT" } }; 21 | licenseName = getLicenseName(pkg); 22 | }); 23 | 24 | it(`should return "MIT" license name`, () => { 25 | expect(licenseName).toBe("MIT"); 26 | }); 27 | }); 28 | 29 | describe("with deprecated license format (as array)", () => { 30 | describe("with only one license", () => { 31 | beforeEach(() => { 32 | pkg = { licenses: [{ type: "MIT" }] }; 33 | licenseName = getLicenseName(pkg); 34 | }); 35 | 36 | it(`should return "MIT" license name`, () => { 37 | expect(licenseName).toBe("MIT"); 38 | }); 39 | }); 40 | 41 | describe("with multiple licenses", () => { 42 | beforeEach(() => { 43 | pkg = { licenses: [{ type: "MIT" }, { type: "BSD-3-Clause" }] }; 44 | licenseName = getLicenseName(pkg); 45 | }); 46 | 47 | it("should return composite license names as OR", () => { 48 | expect(licenseName).toBe("(MIT OR BSD-3-Clause)"); 49 | }); 50 | }); 51 | }); 52 | 53 | describe("with unknown license value of format", () => { 54 | beforeEach(() => { 55 | pkg = { foo: "bar" }; 56 | licenseName = getLicenseName(pkg); 57 | }); 58 | 59 | it("should return empty string", () => { 60 | expect(licenseName).toBe(""); 61 | }); 62 | }); 63 | }); 64 | 65 | describe("getLicenseContents", () => { 66 | let licenseContent; 67 | let depPath; 68 | 69 | describe("with licen(s)e file", () => { 70 | beforeEach(() => { 71 | depPath = process.cwd() + "/src/__mocks__/module-with-license/"; 72 | licenseContent = getLicenseContents(depPath); 73 | }); 74 | 75 | it("should return text content", () => { 76 | expect(licenseContent).not.toBe(undefined); 77 | }); 78 | }); 79 | 80 | describe("with licen(c)e file", () => { 81 | beforeEach(() => { 82 | depPath = process.cwd() + "/src/__mocks__/module-with-licence/"; 83 | licenseContent = getLicenseContents(depPath); 84 | }); 85 | 86 | it("should return text content", () => { 87 | expect(licenseContent).not.toBe(undefined); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/optionsUtils.js: -------------------------------------------------------------------------------- 1 | const { 2 | array, 3 | assert, 4 | boolean, 5 | instance, 6 | object, 7 | partial, 8 | record, 9 | string, 10 | union 11 | } = require("superstruct"); 12 | 13 | const defaultOutputWriter = require("./defaultOutputWriter"); 14 | 15 | const optionsSchema = object({ 16 | filter: instance(RegExp), 17 | allow: string(), 18 | ignore: array(string()), 19 | override: record( 20 | string(), 21 | partial({ 22 | name: string(), 23 | version: string(), 24 | repository: string(), 25 | licenseName: string(), 26 | licenseText: string() 27 | }) 28 | ), 29 | emitError: boolean(), 30 | outputWriter: union([string(), instance(Function)]), 31 | outputFilename: string() 32 | }); 33 | 34 | const defaultOptions = { 35 | filter: /(^.*[/\\]node_modules[/\\]((?:@[^/\\]+[/\\])?(?:[^/\\]+)))/, 36 | allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)", 37 | ignore: [], 38 | override: {}, 39 | emitError: false, 40 | outputWriter: defaultOutputWriter, 41 | outputFilename: "ThirdPartyNotice.txt" 42 | }; 43 | 44 | const getOptions = options => { 45 | const finalOptions = Object.assign({}, defaultOptions, options); 46 | 47 | assert(finalOptions, optionsSchema); 48 | 49 | return finalOptions; 50 | }; 51 | 52 | module.exports = { 53 | getOptions 54 | }; 55 | -------------------------------------------------------------------------------- /src/optionsUtils.spec.js: -------------------------------------------------------------------------------- 1 | const { getOptions } = require("./optionsUtils"); 2 | 3 | const defaults = { 4 | allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)", 5 | emitError: false, 6 | filter: expect.any(RegExp), 7 | ignore: [], 8 | outputFilename: "ThirdPartyNotice.txt", 9 | outputWriter: expect.any(String), 10 | override: {} 11 | }; 12 | 13 | describe("getOptions", () => { 14 | it("should provide defaults when undefined", () => { 15 | expect(getOptions()).toStrictEqual(defaults); 16 | }); 17 | 18 | it("should provide defaults when empty", () => { 19 | expect(getOptions({})).toStrictEqual(defaults); 20 | }); 21 | 22 | it("should throw for unknown property", () => { 23 | expect(() => getOptions({ bad: 1 })).toThrowErrorMatchingInlineSnapshot( 24 | `"Expected a value of type \`never\` for \`bad\` but received \`1\`."` 25 | ); 26 | }); 27 | 28 | it("should accept 'ignore' property", () => { 29 | const options = { ignore: ["assignment@^2.0.0"] }; 30 | expect(getOptions(options)).toStrictEqual(Object.assign({}, defaults, options)); 31 | }); 32 | 33 | it("should accept 'outputWriter' property as function", () => { 34 | const options = { outputWriter: () => "some output" }; 35 | expect(getOptions(options)).toStrictEqual(Object.assign({}, defaults, options)); 36 | }); 37 | 38 | it("should accept 'override' property", () => { 39 | const options = { override: { "assignment@^2.0.0": { licenseName: "MIT" } } }; 40 | expect(getOptions(options)).toStrictEqual(Object.assign({}, defaults, options)); 41 | }); 42 | 43 | it("should throw for unknown 'override' value property", () => { 44 | const options = { override: { "assignment@^2.0.0": { licenceName: "MIT" } } }; 45 | expect(() => getOptions(options)).toThrowErrorMatchingInlineSnapshot( 46 | `"Expected a value of type \`never\` for \`override.assignment@^2.0.0.licenceName\` but received \`\\"MIT\\"\`."` 47 | ); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------