diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 00000000..77aa32f7 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,1324 @@ +/** + * 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"; + +function _interopDefault(ex) { + return ex && typeof ex === "object" && "default" in ex ? ex["default"] : ex; +} + +var _ = _interopDefault(require("lodash")); +var fs = _interopDefault(require("fs")); +var path = _interopDefault(require("path")); +var mkdirp = _interopDefault(require("mkdirp")); +var moment = _interopDefault(require("moment")); +var MagicString = _interopDefault(require("magic-string")); +var glob = _interopDefault(require("glob")); +var packageNameRegex = _interopDefault(require("package-name-regex")); +var commenting = _interopDefault(require("commenting")); +var spdxExpressionValidate = _interopDefault( + require("spdx-expression-validate") +); +var spdxSatisfies = _interopDefault(require("spdx-satisfies")); + +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; + } + } + + _.forEach(["name", "email", "url"], (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) + * + * @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 = _.map( + _.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 = `(${_.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 (!_.isEmpty(this.contributors)) { + lines.push(`Contributors:`); + + const allContributors = _.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.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 _.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 _.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 = ""; + + _.forEach(paths, (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 = _.filter(validators, (validator) => + validator.test(value) + ); // No one matched, we can stop here and return an error with a proper message. + + if (_.isEmpty(matchedValidators)) { + return [ + { + path, + message: _.map( + validators, + (validator) => `"${formatPath(path)}" ${validator.message}` + ).join(" OR "), + }, + ]; + } // Run "sub-validators" + + return _.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 = []; + + _.forEach(obj, (value, k) => { + if (_.isNil(value)) { + 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 (_.isUndefined(item)) { + return [ + { + path, + message: `"${formatPath(path)}" is undefined.`, + }, + ]; + } + + if (_.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 _.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 _.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 (_.isEmpty(errors)) { + return; + } + + const messages = []; + + _.forEach(errors, (e) => { + if (e.type === "object.allowUnknown") { + warn( + `Unknown property: "${formatPath( + e.path + )}", allowed options are: ${_.keys(SCHEMA).join(", ")}.` + ); + } else { + messages.push(e.message); + } + }); + + if (!_.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(license) && spdxSatisfies(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.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.parse(id).dir; + let pkg = null; + const scannedDirs = []; + + while (dir && dir !== this._cwd) { + // 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); + 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 isValidPackageName = name && packageNameRegex.test(name); + + if (isValidPackageName || hasLicense) { + // We found it! + pkg = pkgJson; // Read license file, if it exists. + + const licenseFile = glob.sync(path.join(dir, "LICENSE*"))[0]; + + if (licenseFile) { + pkg.licenseText = fs.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.normalize(path.join(dir, "..")); + } // Update the cache + + _.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}`); + + _.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(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 (!_.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 = _.chain(this._dependencies) + .values() + .filter((dependency) => includePrivate || !dependency.private) + .value(); + + if (_.isFunction(thirdParty)) { + return thirdParty(outputDependencies); + } + + 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 (_.isNil(banner)) { + 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 = _.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) { + _.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 = + _.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) { + _.forEach(_.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 (_.isFunction(output)) { + return output(outputDependencies); + } // 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) => + _.isEmpty(dependencies) + ? "No third parties dependencies" + : _.map(dependencies, (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.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 = {}) { + plugin.scanDependencies( + _.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 1cda68ca..e5329d0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup-plugin-license", - "version": "2.1.0", + "version": "2.2.0", "description": "Rollup plugin to add license banner to the final bundle and output third party licenses", "main": "dist/index.js", "scripts": {