UNPKG

rollup-plugin-license

Version:

Rollup plugin to add license banner to the final bundle and output third party licenses

1,395 lines (1,308 loc) 39.4 kB
/** * 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 _ = 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 fdir = require("fdir"); 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 <EMAIL> (URL) (where email and url are optional). * * @param {string|object} person The person identity. * @constructor */ constructor(person) { let result = person; if (_.isString(result)) { const o = {}; let current = "name"; for (let i = 0, size = result.length; i < size; ++i) { const character = result.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]); } }); result = o; } this.name = result.name || null; this.email = result.email || null; this.url = result.url || null; } /** * Serialize the person to a string with the following format: * NAME <EMAIL> (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. * @param {boolean} self If the package is the "self" package. * @constructor */ constructor(pkg, self) { this.self = self || false; 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<string|number>} 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>|Object} schema The validation schema. * @param {Array<string|number>} path The path being validated. * @returns {Array<Object>} 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>|Object} schema The validation schema. * @param {Array<string|number>} current The current path being validated. * @returns {Array<Object>} 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>|Object} schema The validation schema. * @param {Array<string|number>} current The path being validated. * @return {Array<Object>} 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>|Object} schema The schema to use for validation. * @param {string} current The path being validated. * @return {Array<Object>} 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<string>} current The current path context of given object, useful to validate against subobject. * @return {Array<Object>} 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<string>} current The current path context of given object, useful to validate against subobject. * @return {Array<Object>} 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(), includeSelf: 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|Array<string>} names Potential filenames. * @returns {string|null} File content, or `null` if file does not exist. */ function readFile(dir, names) { const inputs = _.castArray(names); // eslint-disable-next-line new-cap const finder = new fdir.fdir(); for (let i = 0; i < inputs.length; ++i) { const input = inputs[i]; const absolutePath = path.join(dir, input); const relativeToDir = path.relative(dir, absolutePath); const findings = finder .withRelativePaths() .withSymlinks() .withMaxDepth(input.split(path.sep).length) .filter(pathsMatch(relativeToDir)) .crawl(dir) .sync(); const firstPath = findings[0]; if (firstPath) { const file = path.join(dir, firstPath); return fs.readFileSync(file, "utf-8"); } } return null; } /** * Returns a predicate function that returns `true` if the given path matches the target path. * * @param {string} target Target path. * @returns {function(*): boolean} Predicate function. */ function pathsMatch(target) { const targetRegExp = generatePattern(target); return (p) => targetRegExp.test(p); } /** * Generate a pattern where all regexp special characters are escaped. * @param {string} input Input. * @returns {string} Escaped input. */ function escapeRegExp(input) { return input.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } const FILE_FORBIDDEN_CHARACTERS = [ "#", "%", "&", "*", ":", "<", ">", "?", "/", path.sep, "{", "|", "}", ].map((c) => escapeRegExp(c)); const FILE_SUFFIX_PTN = `[^${FILE_FORBIDDEN_CHARACTERS.join("")}]`; /** * Generate filename pattern for the given input: the generated regexp will match any file * starting with `input` (case insensitively). * * @param {string} input Input. * @returns {RegExp} Generated pattern. */ function generatePattern(input) { return new RegExp(`^${input}(${FILE_SUFFIX_PTN})*$`, "i"); } /** * 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<string, 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 = new Map(); this._debug = this._options.debug || false; // eslint-disable-next-line import/no-dynamic-require, global-require this._pkg = require(path.join(this._cwd, "package.json")); // 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 = new Map(); } /** * 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) { var _this$_options$thirdP; if (id.startsWith("\0")) { // eslint-disable-next-line no-param-reassign 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 includeSelf = !!( (_this$_options$thirdP = this._options.thirdParty) !== null && _this$_options$thirdP !== void 0 && _this$_options$thirdP.includeSelf ); const scannedDirs = new Set(); this.debug(`iterative over directory tree, starting with: ${dir}`); while (dir) { const isSelf = dir === this._cwd; if (isSelf && !includeSelf) { // No need to scan "self" if it's not explicitly allowed. break; } // Try the cache. if (this._cache.has(dir)) { pkg = this._cache.get(dir); if (pkg) { this.debug(`found package.json in cache (package: ${pkg.name})`); this.addDependency(pkg, isSelf); } break; } scannedDirs.add(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, version } = pkgJson; const isValidPackageName = name && packageNameRegex.test(name); if ((isValidPackageName && version) || hasLicense) { // We found it! pkg = pkgJson; const licenseText = readFile(dir, ["license", "licence"]); if (licenseText) { pkg.licenseText = licenseText; } const noticeText = readFile(dir, "notice"); if (noticeText) { pkg.noticeText = noticeText; } // Add the new dependency to the set of third-party dependencies. this.addDependency(pkg, isSelf); // We can stop now. break; } } if (isSelf) { // If "self" has been scanned, no need to go up in the directory tree. break; } // Go up in the directory tree. dir = path.resolve(path.join(dir, "..")); if (!dir || scannedDirs.has(dir)) { break; } this.debug(`going up in the directory tree: ${dir}`); } // Update the cache scannedDirs.forEach((scannedDir) => { this._cache.set(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; 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. * @param {boolean} self If the package is the "self" package. * @return {void} */ addDependency(pkg, self) { var _this$_options$thirdP2; 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$thirdP2 = this._options.thirdParty) !== null && _this$_options$thirdP2 !== void 0 && _this$_options$thirdP2.multipleVersions ? `${name}@${version}` : name; if (!this._dependencies.has(key)) { this._dependencies.set(key, new Dependency(pkg, self)); } } /** * Scan third-party dependencies, and: * - Warn for license violations. * - Generate summary. * * @return {void} */ scanThirdParties() { const { thirdParty } = this._options; if (!thirdParty) { return; } const includePrivate = thirdParty.includePrivate || false; const includeSelf = thirdParty.includeSelf || false; const outputDependencies = [...this._dependencies.values()].filter( (dependency) => { if (dependency.self && includeSelf) { return true; } if (!dependency.private) { return true; } return includePrivate; }, ); if (_.isFunction(thirdParty)) { thirdParty(outputDependencies); return; } const { allow, output } = thirdParty; if (allow) { this._scanLicenseViolations(outputDependencies, allow); } 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; 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 = [...this._dependencies.values()]; 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<Dependency>} 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 {Dependency} 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) { if (dependency.self) { // Do not validate license for the "self" package. // It's likely this package will use a private/proprietary license, and we only want to detect // violations for third party dependencies. return; } 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<Object>} 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<Object>} 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. fs.mkdirSync(path.parse(file).dir, { recursive: true, }); 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;