From 8ea2668399bcd52f312b3e13364c3e232e50fdf6 Mon Sep 17 00:00:00 2001 From: Mickael Jeanroy Date: Sun, 10 Mar 2024 15:00:02 +0000 Subject: [PATCH] release: release version --- dist/index.d.ts | 345 ++++++++++++ dist/index.js | 1332 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 1678 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..1743a6a3 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,345 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 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' | 'none'; + +/** + * 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; + + /** + * Full notice file text. + */ + readonly noticeText: 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; + + /** + * Track each dependency version as a different dependency. + * Particularly useful when a dependency changed its licensing between versions. + * Default is `false` far backward compatibility. + */ + multipleVersions?: boolean; +} + +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..fc0c77e3 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,1332 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016-2023 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 fs = require("fs"); +var path = require("path"); +var mkdirp = require("mkdirp"); +var _ = require("lodash"); +var moment = require("moment"); +var MagicString = require("magic-string"); +var packageNameRegex = require("package-name-regex"); +var commenting = require("commenting"); +var spdxExpressionValidate = require("spdx-expression-validate"); +var spdxSatisfies = require("spdx-satisfies"); +var glob = require("glob"); + +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 (_.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; + } + } + ["name", "email", "url"].forEach((prop) => { + if (_.has(o, prop)) { + o[prop] = _.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) + * + * @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; + this.noticeText = pkg.noticeText || null; + + // Parse the author field to get an object. + this.author = pkg.author ? new Person(pkg.author) : null; + + // Parse the contributor array. + this.contributors = _.castArray(pkg.contributors || []).map( + (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 = `(${pkg.licenses.map((license) => license.type || license).join(" OR ")})`; + } + } + + /** + * 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 (this.contributors.length > 0) { + lines.push(`Contributors:`); + lines.push( + ...this.contributors.map((contributor) => ` ${contributor.text()}`), + ); + } + if (this.licenseText) { + lines.push("License Copyright:"); + lines.push("==="); + lines.push(""); + lines.push(this.licenseText); + lines.push(""); + } + if (this.noticeText) { + lines.push("Notice:"); + lines.push("==="); + lines.push(""); + lines.push(this.noticeText); + lines.push(""); + } + return lines.join(EOL).trim(); + } +} + +/** + * 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.Style( + commentStyle.body, + commentStyle.start, + commentStyle.end, + ); + } + return commenting(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 _.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 _.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 _.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 _.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 value == null; +} + +/** + * 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 Array.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 ( + _.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 = ""; + paths.forEach((p) => { + if (_.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 = _.castArray(schema); + const matchedValidators = validators.filter((validator) => + validator.test(value), + ); + + // No one matched, we can stop here and return an error with a proper message. + if (matchedValidators.length === 0) { + return [ + { + path, + message: validators + .map((validator) => `"${formatPath(path)}" ${validator.message}`) + .join(" OR "), + }, + ]; + } + const outputs = []; + for (let i = 0; i < matchedValidators.length; ++i) { + const validator = matchedValidators[i]; + if (validator.schema) { + outputs.push(...validate(value, validator.schema, path)); + } + } + return outputs; +} + +/** + * 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 = []; + if (!obj) { + return errors; + } + Object.keys(obj).forEach((k) => { + const value = obj[k]; + if (value == null) { + return; + } + const path = [...current, k]; + if (!_.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 (typeof item === "undefined") { + return [ + { + path, + message: `"${formatPath(path)}" is undefined.`, + }, + ]; + } + if (item === null) { + 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) { + const outputs = []; + for (let idx = 0; idx < array.length; ++idx) { + outputs.push(...validateArrayItem(array[idx], idx, schema, current)); + } + return outputs; +} + +/** + * 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 Array.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(), + multipleVersions: 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 (errors.length === 0) { + return; + } + const messages = []; + errors.forEach((e) => { + if (e.type === "object.allowUnknown") { + warn( + `Unknown property: "${formatPath(e.path)}", allowed options are: ${_.keys(SCHEMA).join(", ")}.`, + ); + } else { + messages.push(e.message); + } + }); + if (messages.length > 0) { + 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(license) && spdxSatisfies(license, allow); +} +const licenseValidator = { + isUnlicensed, + isValid, +}; + +/** + * Find file and returns its content if file exists. + * + * @param {string} dir File directory. + * @param {string} cwd Working directory. + * @param {string|Array} names Potential filenames. + * @returns {string|null} File content, or `null` if file does not exist. + */ +function readFile(dir, cwd, names) { + const inputs = _.castArray(names); + for (let i = 0; i < inputs.length; ++i) { + const input = generatePattern(inputs[i]); + const absolutePath = path.join(dir, input); + const relativeToCwd = path.relative(cwd, absolutePath); + const findings = glob.sync(relativeToCwd, { + cwd, + }); + for (let j = 0; j < findings.length; ++j) { + const file = path.join(cwd, findings[j]); + if (isFile(file)) { + return fs.readFileSync(file, "utf-8"); + } + } + } + return null; +} + +/** + * Check that given file exists, and is a real file. + * + * @param {string} file File path. + * @returns {boolean} `true` if `file` is a file, `false` otherwise. + */ +function isFile(file) { + return !!fs.existsSync(file) && !!fs.lstatSync(file).isFile(); +} + +/** + * Generate glob pattern for given input. + * + * @param {string} input Given input. + * @returns {string} Glob pattern. + */ +function generatePattern(input) { + let pattern = ""; + for (let i = 0; i < input.length; ++i) { + const c = input[i]; + const up = c.toUpperCase(); + const low = c.toLowerCase(); + pattern += up !== low ? `[${low}${up}]` : low; + } + return pattern + "*"; +} + +/** + * 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.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}`); + } + if (id.indexOf("virtual:") === 0) { + this.debug(`skipping virtual module: ${id}`); + return; + } + this.debug(`scanning ${id}`); + + // Look for the `package.json` file + let dir = path.resolve(path.parse(id).dir); + let pkg = null; + const scannedDirs = []; + this.debug(`iterative over directory tree, starting with: ${dir}`); + while (dir && dir !== this._cwd && !scannedDirs.includes(dir)) { + // Try the cache. + if (_.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); + this.debug(`looking for package.json file in: ${dir}`); + const pkgPath = path.join(dir, "package.json"); + const exists = fs.existsSync(pkgPath); + if (exists) { + this.debug(`found package.json at: ${pkgPath}, read it`); + + // Read `package.json` file + const pkgJson = JSON.parse(fs.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 version = pkgJson.version; + const isValidPackageName = name && packageNameRegex.test(name); + if ((isValidPackageName && version) || hasLicense) { + // We found it! + pkg = pkgJson; + + // Read license & notice files, if it exists. + const cwd = this._cwd || process.cwd(); + const licenseText = readFile(dir, cwd, ["license", "licence"]); + if (licenseText) { + pkg.licenseText = licenseText; + } + const noticeText = readFile(dir, cwd, "notice"); + if (noticeText) { + pkg.noticeText = noticeText; + } + + // 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.resolve(path.join(dir, "..")); + this.debug(`going up in the directory tree: ${dir}`); + } + + // Update the cache + scannedDirs.forEach((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}`); + dependencies.forEach((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(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) { + var _this$_options$thirdP; + const name = pkg.name || ""; + if (!name) { + this.warn("Trying to add dependency without any name, skipping it."); + return; + } + const version = pkg.version || ""; + const key = + (_this$_options$thirdP = this._options.thirdParty) !== null && + _this$_options$thirdP !== void 0 && + _this$_options$thirdP.multipleVersions + ? `${name}@${version}` + : name; + if (!_.has(this._dependencies, key)) { + this._dependencies[key] = new Dependency(pkg); + } + } + + /** + * Scan third-party dependencies, and: + * - Warn for license violations. + * - Generate summary. + * + * @return {void} + */ + scanThirdParties() { + const thirdParty = this._options.thirdParty; + if (!thirdParty) { + return; + } + const includePrivate = thirdParty.includePrivate || false; + const outputDependencies = Object.values(this._dependencies).filter( + (dependency) => includePrivate || !dependency.private, + ); + if (_.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 (banner == null) { + return null; + } + + // Banner can be defined as a simple inline string. + if (_.isString(banner)) { + this.debug("prepend banner from raw string"); + return banner; + } + + // Extract banner content. + const content = _.result(banner, "content"); + + // Content can be an inline string. + if (_.isString(content)) { + this.debug("prepend banner from content raw string"); + return content; + } + + // Otherwise, file must be defined (if not, that's an error). + if (!_.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.resolve(file); + const exists = fs.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.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 = _.template(content); + + // Generate the banner. + const pkg = this._pkg; + const dependencies = Object.values(this._dependencies); + const data = banner.data ? _.result(banner, "data") : {}; + const text = tmpl({ + _, + moment, + pkg, + dependencies, + data, + }); + + // Compute comment style to use. + const style = _.has(banner, "commentStyle") + ? banner.commentStyle + : computeDefaultCommentStyle(text); + + // Ensure given style name is valid. + if (!_.has(COMMENT_STYLES, style)) { + throw new Error( + `Unknown comment style ${style}, please use one of: ${_.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) { + outputDependencies.forEach((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 = + _.isString(allow) || _.isFunction(allow) ? allow : allow.test; + const isValid = _.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) { + _.castArray(outputs).forEach((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 (_.isFunction(output)) { + output(outputDependencies); + return; + } + + // Default is to export to given file. + + // Allow custom formatting of output using given template option. + const template = _.isString(output.template) + ? (dependencies) => + _.template(output.template)({ + dependencies, + _, + moment, + }) + : output.template; + const defaultTemplate = (dependencies) => + dependencies.length === 0 + ? "No third parties dependencies" + : dependencies.map((d) => d.text()).join(`${EOL}${EOL}---${EOL}${EOL}`); + const text = _.isFunction(template) + ? template(outputDependencies) + : defaultTemplate(outputDependencies); + const isOutputFile = _.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.mkdirp.sync(path.parse(file).dir); + fs.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 = {}) { + const dependencies = []; + if (chunk.modules) { + Object.keys(chunk.modules).forEach((id) => { + const mod = chunk.modules[id]; + if (mod && !mod.isAsset && mod.renderedLength > 0) { + dependencies.push(id); + } + }); + } + plugin.scanDependencies(dependencies); + 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 b7f086ea..2c3e04a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup-plugin-license", - "version": "3.3.0", + "version": "3.3.1", "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",