From 51dc643f4227716c27456bf5c23be37e72d4137a Mon Sep 17 00:00:00 2001 From: Mickael Jeanroy Date: Tue, 1 Jun 2021 00:59:46 +0200 Subject: [PATCH] release: release version --- dist/index.d.ts | 333 ++++++++++++ dist/index.js | 1374 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 1708 insertions(+), 1 deletion(-) create mode 100644 dist/index.d.ts create mode 100644 dist/index.js diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 00000000..305449e1 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,333 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016-2020 Mickael Jeanroy + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type {Plugin} from 'rollup'; + +type FilePath = string; +type FileEncoding = string; +type FactoryFn = () => T; +type Factory = T | FactoryFn; + +/** + * A person, as described in NPM documentation. + * + * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#people-fields-author-contributors + */ +export interface Person { + /** + * Person Name. + */ + readonly name: string; + + /** + * Person Email. + */ + readonly email: string | null; + + /** + * Person URL. + */ + readonly url: string | null; + + /** + * Turns the person into a formatted string + * @returns formatted person info + */ + text: () => string; +} + +/** + * @see {@link https://github.com/mjeanroy/rollup-plugin-license#comment-style} + */ +export type CommentStyle = 'regular' | 'ignored' | 'slash'; + +/** + * Banner content descriptor. + */ +interface BannerContentOptions { + /** + * File to get banner content from. + */ + file: FilePath; + + /** + * File encoding. + * @default utf-8 + */ + encoding?: FileEncoding; +} + +/** + * Banner content, can be: + * - A raw string, evaluated as a (lodash) template. + * - A file description, the content being read and evaluated as a (lodash) template. + */ +type BannerContent = string | BannerContentOptions; + +/** + * Data injected during banner "rendering" (i.e evaluated as template + * model). + */ +interface BannerContentData { + [key: string]: any; +} + +/** + * Banner Options. + */ +interface BannerOptions { + content: Factory; + commentStyle?: CommentStyle; + data?: Factory; +} + +export type Banner = string | BannerOptions; + +/** + * Dependency Repository Description. + */ +interface DependencyRepository { + /** + * Repository URL. + */ + readonly url: string; + + /** + * Repository Type (git, svn, etc.). + */ + readonly type: string; +} + +/** + * Dependency information is derived from the package.json file + */ +export interface Dependency { + /** + * Dependency Name. + */ + readonly name: string | null; + + /** + * Dependency Maintainers list. + */ + readonly maintainers: string[]; + + /** + * Dependency Version. + */ + readonly version: string | null; + + /** + * Dependency Description. + */ + readonly description: string | null; + + /** + * Dependency Repository Location. + */ + readonly repository: string | DependencyRepository | null; + + /** + * Repository Public Homepage. + */ + readonly homepage: string | null; + + /** + * If dependency is private. + */ + readonly private: boolean; + + /** + * SPDX License short ID. + */ + readonly license: string | null; + + /** + * Full License file text. + */ + readonly licenseText: string | null; + + /** + * Author information. + */ + readonly author: Person | null; + + /** + * Dependency Contributes list. + */ + readonly contributors: Person[]; + + /** + * Turns the dependency into a formatted string + * @returns formatted dependency license info + */ + text: () => string; +} + +/** + * SPDX Licence Identifier. + */ +type SpdxId = string; + +/** + * Function checking dependency license validity. + */ +type ThirdPartyDependencyValidatorFn = (Dependency: Dependency) => boolean; + +type ThirdPartyValidator = SpdxId | ThirdPartyDependencyValidatorFn; + +interface ThirdPartyAllowOptions { + /** + * Testing if the license if valid + */ + test: ThirdPartyValidator; + + /** + * Fail if a dependency does not specify any licenses + * @default false + */ + failOnUnlicensed?: boolean; + + /** + * Fail if a dependency specify a license that does not match given requirement + * @default false + */ + failOnViolation?: boolean; +} + +/** + * Output generator: may write a file to disk, or something else as long as it is a + * synchronous operation. + */ +type ThirdPartyOutputGeneratorFn = (dependencies: Dependency[]) => void; + +/** + * Template as a raw string. + */ +type ThirdPartyOutputTemplate = string; + +/** + * Template function. + */ +type ThirdPartyOutputTemplateFn = (dependencies: Dependency[]) => void; + +/** + * Third Party output options object. + */ +interface ThirdPartyOutputOptions { + /** + * Name of file to write licenses to + */ + file: FilePath; + + /** + * @default utf-8 + */ + encoding?: FileEncoding; + + /** + * Template function that can be defined to customize report output. + * + * @example + * template(dependencies) { + * return dependencies.map((dependency) => ( + * `${dependency.name}:${dependency.version} -- ${dependency.license}`).join('\n') + * ); + * }, + * + * // Lodash template that can be defined to customize report output + * template: ` + * <% _.forEach(dependencies, function (dependency) { %> + * <%= dependency.name %>:<%= dependency.version%> -- <%= dependency.license %> + * <% }) %> + * ` + */ + template?: ThirdPartyOutputTemplate | ThirdPartyOutputTemplateFn; +} + +type ThirdPartyOutput = FilePath | ThirdPartyOutputGeneratorFn | ThirdPartyOutputOptions; + +interface ThirdPartyOptions { + /** + * Output for third party report. + */ + output: ThirdPartyOutput | ThirdPartyOutput[]; + + /** + * If private dependencies should be checked (`private: true` in package.json) + * @default false + */ + includePrivate?: boolean; + + /** + * Ensures that dependencies does not violate any license restriction. + * + * For example, suppose you want to limit dependencies with MIT or Apache-2.0 + * licenses, simply define the restriction: + * + * @example + * { + * allow: '(MIT OR Apache-2.0)' + * } + * + * allow(dependency) { + * return dependency.license === 'MIT'; + * } + */ + allow?: ThirdPartyValidator | ThirdPartyAllowOptions; +} + +export type ThirdParty = ThirdPartyOutputGeneratorFn | ThirdPartyOptions; + +export interface Options { + sourcemap?: boolean | string; + + /** + * Debug mode + * @default false + */ + debug?: boolean; + + /** + * Current Working Directory + * @default process.cwd() + */ + cwd?: string; + + /** + * License banner to place at the top of your bundle + */ + banner?: Factory; + + /** + * For third party dependencies. + * Creates a file containing a summary of all dependencies can be generated + * automatically + */ + thirdParty?: ThirdParty; +} + +declare function rollupPluginLicense(options: Options): Plugin; + +export default rollupPluginLicense; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 00000000..ad7ef3a3 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,1374 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016-2020 Mickael Jeanroy + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +"use strict"; + +var _ = require("lodash"); +var fs = require("fs"); +var path = require("path"); +var mkdirp = require("mkdirp"); +var moment = require("moment"); +var MagicString = require("magic-string"); +var glob = require("glob"); +var packageNameRegex = require("package-name-regex"); +var commenting = require("commenting"); +var spdxExpressionValidate = require("spdx-expression-validate"); +var spdxSatisfies = require("spdx-satisfies"); + +function _interopDefaultLegacy(e) { + return e && typeof e === "object" && "default" in e ? e : { default: e }; +} + +var ___default = /*#__PURE__*/ _interopDefaultLegacy(_); +var fs__default = /*#__PURE__*/ _interopDefaultLegacy(fs); +var path__default = /*#__PURE__*/ _interopDefaultLegacy(path); +var mkdirp__default = /*#__PURE__*/ _interopDefaultLegacy(mkdirp); +var moment__default = /*#__PURE__*/ _interopDefaultLegacy(moment); +var MagicString__default = /*#__PURE__*/ _interopDefaultLegacy(MagicString); +var glob__default = /*#__PURE__*/ _interopDefaultLegacy(glob); +var packageNameRegex__default = + /*#__PURE__*/ _interopDefaultLegacy(packageNameRegex); +var commenting__default = /*#__PURE__*/ _interopDefaultLegacy(commenting); +var spdxExpressionValidate__default = /*#__PURE__*/ _interopDefaultLegacy( + spdxExpressionValidate +); +var spdxSatisfies__default = /*#__PURE__*/ _interopDefaultLegacy(spdxSatisfies); + +const EOL = "\n"; + +/** + * Person, defined by: + * - A name. + * - An email (optional). + * - An URL (optional). + */ + +class Person { + /** + * Create the person. + * + * If parameter is a string, it will be automatically parsed according to + * this format: NAME (URL) (where email and url are optional). + * + * @param {string|object} person The person identity. + * @constructor + */ + constructor(person) { + if (___default["default"].isString(person)) { + const o = {}; + let current = "name"; + + for (let i = 0, size = person.length; i < size; ++i) { + const character = person.charAt(i); + + if (character === "<") { + current = "email"; + } else if (character === "(") { + current = "url"; + } else if (character !== ")" && character !== ">") { + o[current] = (o[current] || "") + character; + } + } + + ___default["default"].forEach(["name", "email", "url"], (prop) => { + if (___default["default"].has(o, prop)) { + o[prop] = ___default["default"].trim(o[prop]); + } + }); + + person = o; + } + + this.name = person.name || null; + this.email = person.email || null; + this.url = person.url || null; + } + /** + * Serialize the person to a string with the following format: + * NAME (URL) + * + * @param {string} prefix Optional prefix prepended to the output string. + * @param {string} suffix Optional suffix appended to the output string. + * @return {string} The person as a string. + */ + + text() { + let text = `${this.name}`; + + if (this.email) { + text += ` <${this.email}>`; + } + + if (this.url) { + text += ` (${this.url})`; + } + + return text; + } +} + +/** + * Dependency structure. + */ + +class Dependency { + /** + * Create new dependency from package description. + * + * @param {Object} pkg Package description. + * @constructor + */ + constructor(pkg) { + this.name = pkg.name || null; + this.maintainers = pkg.maintainers || []; + this.version = pkg.version || null; + this.description = pkg.description || null; + this.repository = pkg.repository || null; + this.homepage = pkg.homepage || null; + this.private = pkg.private || false; + this.license = pkg.license || null; + this.licenseText = pkg.licenseText || null; // Parse the author field to get an object. + + this.author = pkg.author ? new Person(pkg.author) : null; // Parse the contributor array. + + this.contributors = ___default["default"].map( + ___default["default"].castArray(pkg.contributors || []), + (contributor) => new Person(contributor) + ); // The `licenses` field is deprecated but may be used in some packages. + // Map it to a standard license field. + + if (!this.license && pkg.licenses) { + // Map it to a valid license field. + // See: https://docs.npmjs.com/files/package.json#license + this.license = `(${___default["default"] + .chain(pkg.licenses) + .map((license) => license.type || license) + .join(" OR ") + .value()})`; + } + } + /** + * Serialize dependency as a string. + * + * @return {string} The dependency correctly formatted. + */ + + text() { + const lines = []; + lines.push(`Name: ${this.name}`); + lines.push(`Version: ${this.version}`); + lines.push(`License: ${this.license}`); + lines.push(`Private: ${this.private}`); + + if (this.description) { + lines.push(`Description: ${this.description || false}`); + } + + if (this.repository) { + lines.push(`Repository: ${this.repository.url}`); + } + + if (this.homepage) { + lines.push(`Homepage: ${this.homepage}`); + } + + if (this.author) { + lines.push(`Author: ${this.author.text()}`); + } + + if (!___default["default"].isEmpty(this.contributors)) { + lines.push(`Contributors:`); + + const allContributors = ___default["default"] + .chain(this.contributors) + .map((contributor) => contributor.text()) + .map((line) => ` ${line}`) + .value(); + + lines.push(...allContributors); + } + + if (this.licenseText) { + lines.push("License Copyright:"); + lines.push("==="); + lines.push(""); + lines.push(this.licenseText); + } + + return lines.join(EOL); + } +} + +/** + * Generate block comment from given text content. + * + * @param {string} text Text content. + * @param {Object} commentStyle The comment style setting. + * @return {string} Block comment. + */ + +function generateBlockComment(text, commentStyle) { + const options = { + extension: ".js", + }; + + if (commentStyle) { + options.style = new commenting__default["default"].Style( + commentStyle.body, + commentStyle.start, + commentStyle.end + ); + } + + return commenting__default["default"](text.trim(), options); +} + +/** + * The plugin name. + * @type {string} + */ +const PLUGIN_NAME = "rollup-plugin-license"; + +/** + * Check if given value is a `string`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is a string, `false` otherwise. + */ + +function isString(value) { + return ___default["default"].isString(value); +} +/** + * Check if given value is a `boolean`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is a boolean, `false` otherwise. + */ + +function isBoolean(value) { + return ___default["default"].isBoolean(value); +} +/** + * Check if given value is a `function`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is a function, `false` otherwise. + */ + +function isFunction(value) { + return ___default["default"].isFunction(value); +} +/** + * Check if given value is a `number`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is a number, `false` otherwise. + */ + +function isNumber(value) { + return ___default["default"].isNumber(value); +} +/** + * Check if given value is `null` or `undefined`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is `null` or `undefined`, `false` otherwise. + */ + +function isNil(value) { + return ___default["default"].isNil(value); +} +/** + * Check if given value is an `array`. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is an array, `false` otherwise. + */ + +function isArray(value) { + return ___default["default"].isArray(value); +} +/** + * Check if given value is an plain object. + * + * @param {*} value The value to check. + * @return {boolean} `true` if `value` is a plain object, `false` otherwise. + */ + +function isObject(value) { + return ( + ___default["default"].isObject(value) && + !isArray(value) && + !isFunction(value) && + !isNil(value) && + !isString(value) && + !isNumber(value) + ); +} + +const validators = { + string() { + return { + type: "object.type.string", + message: "must be a string", + schema: null, + test: isString, + }; + }, + + boolean() { + return { + type: "object.type.boolean", + message: "must be a boolean", + schema: null, + test: isBoolean, + }; + }, + + func() { + return { + type: "object.type.func", + message: "must be a function", + schema: null, + test: isFunction, + }; + }, + + object(schema) { + return { + type: "object.type.object", + message: "must be an object", + schema, + test: isObject, + }; + }, + + array(schema) { + return { + type: "object.type.array", + message: "must be an array", + schema, + test: isArray, + }; + }, + + any() { + return { + type: "object.any", + message: null, + schema: null, + test: () => true, + }; + }, +}; + +/** + * Format given array of path to a human readable path. + * + * @param {Array} paths List of paths. + * @return {string} The full path. + */ + +function formatPath(paths) { + let str = ""; + + ___default["default"].forEach(paths, (p) => { + if (___default["default"].isNumber(p)) { + str += `[${p}]`; + } else if (!str) { + str += p; + } else { + str += `.${p}`; + } + }); + + return str; +} + +/** + * Validate value against given schema. + * It is assumed that `value` will not be `null` or `undefined`. + * + * @param {*} value The value being validated. + * @param {Array|Object} schema The validation schema. + * @param {Array} path The path being validated. + * @returns {Array} Found errors. + */ + +function doItemValidation(value, schema, path) { + const validators = ___default["default"].castArray(schema); + + const matchedValidators = ___default["default"].filter( + validators, + (validator) => validator.test(value) + ); // No one matched, we can stop here and return an error with a proper message. + + if (___default["default"].isEmpty(matchedValidators)) { + return [ + { + path, + message: ___default["default"] + .map( + validators, + (validator) => `"${formatPath(path)}" ${validator.message}` + ) + .join(" OR "), + }, + ]; + } // Run "sub-validators" + + return ___default["default"] + .chain(matchedValidators) + .filter((validator) => validator.schema) + .map((validator) => validate(value, validator.schema, path)) + .flatten() + .value(); +} +/** + * Validate object against given schema. + * Note that `null` or `undefined` is allowed and do not produce an error. + * + * @param {Object} obj The object to validate. + * @param {Array|Object} schema The validation schema. + * @param {Array} current The current path being validated. + * @returns {Array} Found errors. + */ + +function validateObject(obj, schema, current) { + const errors = []; + + ___default["default"].forEach(obj, (value, k) => { + if (___default["default"].isNil(value)) { + return; + } + + const path = [...current, k]; + + if (!___default["default"].has(schema, k)) { + errors.push({ + type: "object.allowUnknown", + path, + }); + } else { + errors.push(...doItemValidation(value, schema[k], path)); + } + }); + + return errors; +} +/** + * Validate element of an array. + * + * Instead of "classic" object validation, `null` and `undefined` will produce + * an error here. + * + * @param {*} item The item to validate. + * @param {number} idx The index of item in original array. + * @param {Array|Object} schema The validation schema. + * @param {Array} current The path being validated. + * @return {Array} Found errors. + */ + +function validateArrayItem(item, idx, schema, current) { + const path = [...current, idx]; + + if (___default["default"].isUndefined(item)) { + return [ + { + path, + message: `"${formatPath(path)}" is undefined.`, + }, + ]; + } + + if (___default["default"].isNull(item)) { + return [ + { + path, + message: `"${formatPath(path)}" is null.`, + }, + ]; + } + + return doItemValidation(item, schema, path); +} +/** + * Validate all elements of given array against given schema (or array of schemas). + * + * @param {Array<*>} array Array of elements to validate. + * @param {Array|Object} schema The schema to use for validation. + * @param {string} current The path being validated. + * @return {Array} Found errors. + */ + +function validateArray(array, schema, current) { + return ___default["default"] + .chain(array) + .map((item, idx) => validateArrayItem(item, idx, schema, current)) + .flatten() + .value(); +} +/** + * Validate given object against given schema. + * + * Note that the very first version used `@hapi/joi` but this package does not support node < 8 in its latest version. + * Since I don't want to depends on deprecated and non maintained packages, and I want to keep compatibility with + * Node 6, I re-implemented the small part I needed here. + * + * Once node 6 will not be supported (probably with rollup >= 2), it will be time to drop this in favor of `@hapi/joi` + * for example. + * + * @param {Object} obj Object to validate. + * @param {Object} schema The schema against the given object will be validated. + * @param {Array} current The current path context of given object, useful to validate against subobject. + * @return {Array} Found errors. + */ + +function validate(obj, schema, current = []) { + return ___default["default"].isArray(obj) + ? validateArray(obj, schema, current) + : validateObject(obj, schema, current); +} +/** + * Validate given object against given schema. + * + * @param {Object} obj Object to validate. + * @param {Object} schema The schema against the given object will be validated. + * @param {Array} current The current path context of given object, useful to validate against subobject. + * @return {Array} Found errors. + */ + +function validateSchema(obj, schema, current) { + return validate(obj, schema, current); +} + +/** + * The option object schema. + * @type {Object} + */ + +const SCHEMA = { + sourcemap: [validators.string(), validators.boolean()], + debug: validators.boolean(), + cwd: validators.string(), + banner: [ + validators.func(), + validators.string(), + validators.object({ + commentStyle: validators.string(), + data: validators.any(), + content: [ + validators.func(), + validators.string(), + validators.object({ + file: validators.string(), + encoding: validators.string(), + }), + ], + }), + ], + thirdParty: [ + validators.func(), + validators.object({ + includePrivate: validators.boolean(), + allow: [ + validators.string(), + validators.func(), + validators.object({ + test: [validators.string(), validators.func()], + failOnUnlicensed: validators.boolean(), + failOnViolation: validators.boolean(), + }), + ], + output: [ + validators.func(), + validators.string(), + validators.object({ + file: validators.string(), + encoding: validators.string(), + template: [validators.string(), validators.func()], + }), + validators.array([ + validators.func(), + validators.string(), + validators.object({ + file: validators.string(), + encoding: validators.string(), + template: [validators.string(), validators.func()], + }), + ]), + ], + }), + ], +}; +/** + * Print warning message to the console. + * + * @param {string} msg Message to log. + * @return {void} + */ + +function warn(msg) { + console.warn(`[${PLUGIN_NAME}] -- ${msg}`); +} +/** + * Validate given option object. + * + * @param {Object} options Option object. + * @return {Array} An array of all errors. + */ + +function doValidation(options) { + return validateSchema(options, SCHEMA); +} +/** + * Validate option object according to pre-defined schema. + * + * @param {Object} options Option object. + * @return {void} + */ + +function validateOptions(options) { + const errors = doValidation(options); + + if (___default["default"].isEmpty(errors)) { + return; + } + + const messages = []; + + ___default["default"].forEach(errors, (e) => { + if (e.type === "object.allowUnknown") { + warn( + `Unknown property: "${formatPath( + e.path + )}", allowed options are: ${___default["default"] + .keys(SCHEMA) + .join(", ")}.` + ); + } else { + messages.push(e.message); + } + }); + + if (!___default["default"].isEmpty(messages)) { + throw new Error( + `[${PLUGIN_NAME}] -- Error during validation of option object: ${messages.join( + " ; " + )}` + ); + } +} +/** + * Normalize and validate option object. + * + * @param {Object} options Option object to validate. + * @return {Object} New normalized options. + */ + +function licensePluginOptions(options) { + validateOptions(options); + return options; +} + +/** + * Normalize license name: + * - Returns `UNLICENSED` for nil parameter. + * - Trim license value. + * + * @param {string} license The license name. + * @return {string} The normalized license name. + */ + +function normalizeLicense(license) { + if (!license) { + return "UNLICENSED"; + } + + return license.trim(); +} +/** + * Check if given license name is the `UNLICENSED` value. + * + * @param {string} license The license to check. + * @return {boolean} `true` if `license` is the UNLICENSED one, `false` otherwise. + */ + +function checkUnlicensed(license) { + return license.toUpperCase() === "UNLICENSED"; +} +/** + * Check if dependency is unlicensed, or not. + * + * @param {Object} dependency The dependency. + * @return {boolean} `true` if dependency does not have any license, `false` otherwise. + */ + +function isUnlicensed(dependency) { + const license = normalizeLicense(dependency.license); + return checkUnlicensed(license); +} +/** + * Check if license dependency is valid according to given SPDX validator pattern. + * + * @param {Object} dependency The dependency. + * @param {string} allow The validator as a SPDX pattern. + * @return {boolean} `true` if dependency license is valid, `false` otherwise. + */ + +function isValid(dependency, allow) { + const license = normalizeLicense(dependency.license); + + if (checkUnlicensed(license)) { + return false; + } + + return ( + spdxExpressionValidate__default["default"](license) && + spdxSatisfies__default["default"](license, allow) + ); +} + +const licenseValidator = { + isUnlicensed, + isValid, +}; + +/** + * Pre-Defined comment style: + * + * - `regular` stands for "classic" block comment. + * - `ignored` stands for block comment starting with standard prefix ignored by minifier. + * - `slash` stands for "inline" style (i.e `//`). + * - `none` stands for no comment style at all. + * + * @type {Object} + */ + +const COMMENT_STYLES = { + regular: { + start: "/**", + body: " *", + end: " */", + }, + ignored: { + start: "/*!", + body: " *", + end: " */", + }, + slash: { + start: "//", + body: "//", + end: "//", + }, + none: null, +}; +/** + * Compute the comment style to use for given text: + * - If text starts with a block comment, nothing is done (i.e use `none`). + * - Otherwise, use the `regular` style. + * + * @param {string} text The text to comment. + * @return {string} The comment style name. + */ + +function computeDefaultCommentStyle(text) { + const trimmedText = text.trim(); + const start = trimmedText.slice(0, 3); + const startWithComment = start === "/**" || start === "/*!"; + return startWithComment ? "none" : "regular"; +} +/** + * Rollup Plugin. + * @class + */ + +class LicensePlugin { + /** + * Initialize plugin. + * + * @param {Object} options Plugin options. + */ + constructor(options = {}) { + // Plugin name, used by rollup. + this.name = PLUGIN_NAME; // Initialize main options. + + this._options = options; + this._cwd = this._options.cwd || process.cwd(); + this._dependencies = {}; + this._pkg = require(path__default["default"].join( + this._cwd, + "package.json" + )); + this._debug = this._options.debug || false; // SourceMap can now be disable/enable on the plugin. + + this._sourcemap = this._options.sourcemap !== false; // This is a cache storing a directory path to associated package. + // This is an improvement to avoid looking for package information for + // already scanned directory. + + this._cache = {}; + } + /** + * Enable source map. + * + * @return {void} + */ + + disableSourceMap() { + this._sourcemap = false; + } + /** + * Hook triggered by `rollup` to load code from given path file. + * + * This hook is used here to analyze a JavaScript file to extract + * associated `package.json` file and store the main information about + * it (license, author, etc.). + * + * This method is used to analyse all the files added to the final bundle + * to extract license informations. + * + * @param {string} id Module identifier. + * @return {void} + */ + + scanDependency(id) { + if (id.startsWith("\0")) { + id = id.replace(/^\0/, ""); + this.debug(`scanning internal module ${id}`); + } + + this.debug(`scanning ${id}`); // Look for the `package.json` file + + let dir = path__default["default"].parse(id).dir; + let pkg = null; + const scannedDirs = []; + + while (dir && dir !== this._cwd) { + // Try the cache. + if (___default["default"].has(this._cache, dir)) { + pkg = this._cache[dir]; + + if (pkg) { + this.debug(`found package.json in cache (package: ${pkg.name})`); + this.addDependency(pkg); + } + + break; + } + + scannedDirs.push(dir); + const pkgPath = path__default["default"].join(dir, "package.json"); + const exists = fs__default["default"].existsSync(pkgPath); + + if (exists) { + this.debug(`found package.json at: ${pkgPath}, read it`); // Read `package.json` file + + const pkgJson = JSON.parse( + fs__default["default"].readFileSync(pkgPath, "utf-8") + ); // We are probably in a package.json specifying the type of package (module, cjs). + // Nevertheless, if the package name is not defined, we must not use this `package.json` descriptor. + + const license = pkgJson.license || pkgJson.licenses; + const hasLicense = license && license.length > 0; + const name = pkgJson.name; + const isValidPackageName = + name && packageNameRegex__default["default"].test(name); + + if (isValidPackageName || hasLicense) { + // We found it! + pkg = pkgJson; // Read license file, if it exists. + + const licenseFile = glob__default["default"].sync( + path__default["default"].join(dir, "LICENSE*") + )[0]; + + if (licenseFile) { + pkg.licenseText = fs__default["default"].readFileSync( + licenseFile, + "utf-8" + ); + } // Add the new dependency to the set of third-party dependencies. + + this.addDependency(pkg); // We can stop now. + + break; + } + } // Go up in the directory tree. + + dir = path__default["default"].normalize( + path__default["default"].join(dir, "..") + ); + } // Update the cache + + ___default["default"].forEach(scannedDirs, (scannedDir) => { + this._cache[scannedDir] = pkg; + }); + } + /** + * Hook triggered by `rollup` to load code from given path file. + * + * @param {Object} dependencies List of modules included in the final bundle. + * @return {void} + */ + + scanDependencies(dependencies) { + this.debug(`Scanning: ${dependencies}`); + + ___default["default"].forEach(dependencies, (dependency) => { + this.scanDependency(dependency); + }); + } + /** + * Hook triggered by `rollup` to transform the final generated bundle. + * This hook is used here to prepend the license banner to the final bundle. + * + * @param {string} code The bundle content. + * @param {boolean} sourcemap If sourcemap must be generated. + * @return {Object} The result containing the code and, optionnally, the source map + * if it has been enabled (using `enableSourceMap` method). + */ + + prependBanner(code, sourcemap) { + // Create a magicString: do not manipulate the string directly since it + // will be used to generate the sourcemap. + const magicString = new MagicString__default["default"](code); + const banner = this._options.banner; + + const content = this._readBanner(banner); + + if (content) { + magicString.prepend(EOL); + magicString.prepend(this._generateBanner(content, banner)); + } + + const result = { + code: magicString.toString(), + }; + + if (this._sourcemap !== false && sourcemap !== false) { + result.map = magicString.generateMap({ + hires: true, + }); + } + + return result; + } + /** + * Add new dependency to the bundle descriptor. + * + * @param {Object} pkg Dependency package information. + * @return {void} + */ + + addDependency(pkg) { + const name = pkg.name; + + if (!name) { + this.warn("Trying to add dependency without any name, skipping it."); + } else if (!___default["default"].has(this._dependencies, name)) { + this._dependencies[name] = new Dependency(pkg); + } + } + /** + * Scan third-party dependencies, and: + * - Warn for license violations. + * - Generate summary. + * + * @param {boolean} includePrivate Flag that can be used to include / exclude private dependencies. + * @return {void} + */ + + scanThirdParties() { + const thirdParty = this._options.thirdParty; + + if (!thirdParty) { + return; + } + + const includePrivate = thirdParty.includePrivate || false; + + const outputDependencies = ___default["default"] + .chain(this._dependencies) + .values() + .filter((dependency) => includePrivate || !dependency.private) + .value(); + + if (___default["default"].isFunction(thirdParty)) { + thirdParty(outputDependencies); + return; + } + + const allow = thirdParty.allow; + + if (allow) { + this._scanLicenseViolations(outputDependencies, allow); + } + + const output = thirdParty.output; + + if (output) { + this._exportThirdParties(outputDependencies, output); + } + } + /** + * Log debug message if debug mode is enabled. + * + * @param {string} msg Log message. + * @return {void} + */ + + debug(msg) { + if (this._debug) { + console.debug(`[${this.name}] -- ${msg}`); + } + } + /** + * Log warn message. + * + * @param {string} msg Log message. + * @return {void} + */ + + warn(msg) { + console.warn(`[${this.name}] -- ${msg}`); + } + /** + * Read banner from given options and returns it. + * + * @param {Object|string} banner Banner as a raw string, or banner options. + * @return {string|null} The banner template. + * @private + */ + + _readBanner(banner) { + if (___default["default"].isNil(banner)) { + return null; + } // Banner can be defined as a simple inline string. + + if (___default["default"].isString(banner)) { + this.debug("prepend banner from raw string"); + return banner; + } // Extract banner content. + + const content = ___default["default"].result(banner, "content"); // Content can be an inline string. + + if (___default["default"].isString(content)) { + this.debug("prepend banner from content raw string"); + return content; + } // Otherwise, file must be defined (if not, that's an error). + + if (!___default["default"].has(content, "file")) { + throw new Error( + `[${this.name}] -- Cannot find banner content, please specify an inline content, or a path to a file` + ); + } + + const file = content.file; + const encoding = content.encoding || "utf-8"; + this.debug(`prepend banner from file: ${file}`); + this.debug(`use encoding: ${encoding}`); + const filePath = path__default["default"].resolve(file); + const exists = fs__default["default"].existsSync(filePath); // Fail fast if file does not exist. + + if (!exists) { + throw new Error( + `[${this.name}] -- Template file ${filePath} does not exist, or cannot be read` + ); + } + + return fs__default["default"].readFileSync(filePath, encoding); + } + /** + * Generate banner output from given raw string and given options. + * + * Banner output will be a JavaScript comment block, comment style may be customized using + * the `commentStyle` option. + * + * @param {string} content Banner content, as a raw string. + * @param {Object} banner Banner options. + * @return {string} The banner output. + * @private + */ + + _generateBanner(content, banner) { + // Create the template function with lodash. + const tmpl = ___default["default"].template(content); // Generate the banner. + + const pkg = this._pkg; + + const dependencies = ___default["default"].values(this._dependencies); + + const data = banner.data + ? ___default["default"].result(banner, "data") + : {}; + const text = tmpl({ + _: ___default["default"], + moment: moment__default["default"], + pkg, + dependencies, + data, + }); // Compute comment style to use. + + const style = ___default["default"].has(banner, "commentStyle") + ? banner.commentStyle + : computeDefaultCommentStyle(text); // Ensure given style name is valid. + + if (!___default["default"].has(COMMENT_STYLES, style)) { + throw new Error( + `Unknown comment style ${style}, please use one of: ${___default[ + "default" + ].keys(COMMENT_STYLES)}` + ); + } + + this.debug(`generate banner using comment style: ${style}`); + return COMMENT_STYLES[style] + ? generateBlockComment(text, COMMENT_STYLES[style]) + : text; + } + /** + * Scan for dependency violations and print a warning if some violations are found. + * + * @param {Array} outputDependencies The dependencies to scan. + * @param {string} allow The allowed licenses as a SPDX pattern. + * @return {void} + */ + + _scanLicenseViolations(outputDependencies, allow) { + ___default["default"].forEach(outputDependencies, (dependency) => { + this._scanLicenseViolation(dependency, allow); + }); + } + /** + * Scan dependency for a dependency violation. + * + * @param {Object} dependency The dependency to scan. + * @param {string|function|object} allow The allowed licenses as a SPDX pattern, or a validator function. + * @return {void} + */ + + _scanLicenseViolation(dependency, allow) { + const testFn = + ___default["default"].isString(allow) || + ___default["default"].isFunction(allow) + ? allow + : allow.test; + const isValid = ___default["default"].isFunction(testFn) + ? testFn(dependency) + : licenseValidator.isValid(dependency, testFn); + + if (!isValid) { + const failOnUnlicensed = allow.failOnUnlicensed === true; + const failOnViolation = allow.failOnViolation === true; + + this._handleInvalidLicense(dependency, failOnUnlicensed, failOnViolation); + } + } + /** + * Handle invalid dependency: + * - Print a warning for unlicensed dependency. + * - Print a warning for dependency violation. + * + * @param {Object} dependency The dependency to scan. + * @param {boolean} failOnUnlicensed `true` to fail on unlicensed dependency, `false` otherwise. + * @param {boolean} failOnViolation `true` to fail on license violation, `false` otherwise. + * @return {void} + */ + + _handleInvalidLicense(dependency, failOnUnlicensed, failOnViolation) { + if (licenseValidator.isUnlicensed(dependency)) { + this._handleUnlicensedDependency(dependency, failOnUnlicensed); + } else { + this._handleLicenseViolation(dependency, failOnViolation); + } + } + /** + * Handle unlicensed dependency: print a warning to the console to alert for the dependency + * that should be fixed. + * + * @param {Object} dependency The dependency. + * @param {boolean} fail `true` to fail instead of emitting a simple warning. + * @return {void} + */ + + _handleUnlicensedDependency(dependency, fail) { + const message = `Dependency "${dependency.name}" does not specify any license.`; + + if (!fail) { + this.warn(message); + } else { + throw new Error(message); + } + } + /** + * Handle license violation: print a warning to the console to alert about the violation. + * + * @param {Object} dependency The dependency. + * @param {boolean} fail `true` to fail instead of emitting a simple warning. + * @return {void} + */ + + _handleLicenseViolation(dependency, fail) { + const message = + `Dependency "${dependency.name}" has a license (${dependency.license}) which is not compatible with ` + + `requirement, looks like a license violation to fix.`; + + if (!fail) { + this.warn(message); + } else { + throw new Error(message); + } + } + /** + * Export scanned third party dependencies to a destination output (a function, a + * file written to disk, etc.). + * + * @param {Array} outputDependencies The dependencies to include in the output. + * @param {Object|function|string|Array} outputs The output (or the array of output) destination. + * @return {void} + */ + + _exportThirdParties(outputDependencies, outputs) { + ___default["default"].forEach( + ___default["default"].castArray(outputs), + (output) => { + this._exportThirdPartiesToOutput(outputDependencies, output); + } + ); + } + /** + * Export scanned third party dependencies to a destination output (a function, a + * file written to disk, etc.). + * + * @param {Array} outputDependencies The dependencies to include in the output. + * @param {Array} output The output destination. + * @return {void} + */ + + _exportThirdPartiesToOutput(outputDependencies, output) { + if (___default["default"].isFunction(output)) { + output(outputDependencies); + return; + } // Default is to export to given file. + // Allow custom formatting of output using given template option. + + const template = ___default["default"].isString(output.template) + ? (dependencies) => + ___default["default"].template(output.template)({ + dependencies, + _: ___default["default"], + moment: moment__default["default"], + }) + : output.template; + + const defaultTemplate = (dependencies) => + ___default["default"].isEmpty(dependencies) + ? "No third parties dependencies" + : ___default["default"] + .map(dependencies, (d) => d.text()) + .join(`${EOL}${EOL}---${EOL}${EOL}`); + + const text = ___default["default"].isFunction(template) + ? template(outputDependencies) + : defaultTemplate(outputDependencies); + + const isOutputFile = ___default["default"].isString(output); + + const file = isOutputFile ? output : output.file; + const encoding = isOutputFile ? "utf-8" : output.encoding || "utf-8"; + this.debug(`exporting third-party summary to ${file}`); + this.debug(`use encoding: ${encoding}`); // Create directory if it does not already exist. + + mkdirp__default["default"].sync(path__default["default"].parse(file).dir); + fs__default["default"].writeFileSync(file, (text || "").trim(), { + encoding, + }); + } +} +/** + * Create new `rollup-plugin-license` instance with given + * options. + * + * @param {Object} options Option object. + * @return {LicensePlugin} The new instance. + */ + +function licensePlugin(options) { + return new LicensePlugin(licensePluginOptions(options)); +} + +/** + * Create rollup plugin compatible with rollup >= 1.0.0 + * + * @param {Object} options Plugin options. + * @return {Object} Plugin instance. + */ + +function rollupPluginLicense(options = {}) { + const plugin = licensePlugin(options); + return { + /** + * Name of the plugin, used automatically by rollup. + * @type {string} + */ + name: plugin.name, + + /** + * Function called by rollup when the final bundle is generated: it is used + * to prepend the banner file on the generated bundle. + * + * @param {string} code Bundle content. + * @param {Object} chunk The chunk being generated. + * @param {Object} outputOptions The options for the generated output. + * @return {void} + */ + renderChunk(code, chunk, outputOptions = {}) { + plugin.scanDependencies( + ___default["default"] + .chain(chunk.modules) + .toPairs() + .reject((mod) => mod[1].isAsset) + .filter((mod) => mod[1].renderedLength > 0) + .map((mod) => mod[0]) + .value() + ); + return plugin.prependBanner(code, outputOptions.sourcemap !== false); + }, + + /** + * Function called by rollup when the final bundle will be written on disk: it + * is used to generate a file containing a summary of all third-party dependencies + * with license information. + * + * @return {void} + */ + generateBundle() { + plugin.scanThirdParties(); + }, + }; +} + +module.exports = rollupPluginLicense; diff --git a/package.json b/package.json index 9aedad3b..bbff6553 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup-plugin-license", - "version": "2.3.0", + "version": "2.4.0", "description": "Rollup plugin to add license banner to the final bundle and output third party licenses", "main": "dist/index.js", "types": "dist/index.d.ts",