UNPKG

36.4 kBJavaScriptView Raw
1/**
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2016-2022 Mickael Jeanroy
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 *
24 */
25
26"use strict";
27
28var _ = require("lodash");
29var fs = require("fs");
30var path = require("path");
31var mkdirp = require("mkdirp");
32var moment = require("moment");
33var MagicString = require("magic-string");
34var glob = require("glob");
35var packageNameRegex = require("package-name-regex");
36var commenting = require("commenting");
37var spdxExpressionValidate = require("spdx-expression-validate");
38var spdxSatisfies = require("spdx-satisfies");
39
40const EOL = "\n";
41
42/**
43 * Person, defined by:
44 * - A name.
45 * - An email (optional).
46 * - An URL (optional).
47 */
48class Person {
49 /**
50 * Create the person.
51 *
52 * If parameter is a string, it will be automatically parsed according to
53 * this format: NAME <EMAIL> (URL) (where email and url are optional).
54 *
55 * @param {string|object} person The person identity.
56 * @constructor
57 */
58 constructor(person) {
59 if (_.isString(person)) {
60 const o = {};
61 let current = "name";
62 for (let i = 0, size = person.length; i < size; ++i) {
63 const character = person.charAt(i);
64 if (character === "<") {
65 current = "email";
66 } else if (character === "(") {
67 current = "url";
68 } else if (character !== ")" && character !== ">") {
69 o[current] = (o[current] || "") + character;
70 }
71 }
72 _.forEach(["name", "email", "url"], (prop) => {
73 if (_.has(o, prop)) {
74 o[prop] = _.trim(o[prop]);
75 }
76 });
77 person = o;
78 }
79 this.name = person.name || null;
80 this.email = person.email || null;
81 this.url = person.url || null;
82 }
83
84 /**
85 * Serialize the person to a string with the following format:
86 * NAME <EMAIL> (URL)
87 *
88 * @return {string} The person as a string.
89 */
90 text() {
91 let text = `${this.name}`;
92 if (this.email) {
93 text += ` <${this.email}>`;
94 }
95 if (this.url) {
96 text += ` (${this.url})`;
97 }
98 return text;
99 }
100}
101
102/**
103 * Dependency structure.
104 */
105class Dependency {
106 /**
107 * Create new dependency from package description.
108 *
109 * @param {Object} pkg Package description.
110 * @constructor
111 */
112 constructor(pkg) {
113 this.name = pkg.name || null;
114 this.maintainers = pkg.maintainers || [];
115 this.version = pkg.version || null;
116 this.description = pkg.description || null;
117 this.repository = pkg.repository || null;
118 this.homepage = pkg.homepage || null;
119 this.private = pkg.private || false;
120 this.license = pkg.license || null;
121 this.licenseText = pkg.licenseText || null;
122
123 // Parse the author field to get an object.
124 this.author = pkg.author ? new Person(pkg.author) : null;
125
126 // Parse the contributor array.
127 this.contributors = _.map(
128 _.castArray(pkg.contributors || []),
129 (contributor) => new Person(contributor),
130 );
131
132 // The `licenses` field is deprecated but may be used in some packages.
133 // Map it to a standard license field.
134 if (!this.license && pkg.licenses) {
135 // Map it to a valid license field.
136 // See: https://docs.npmjs.com/files/package.json#license
137 this.license = `(${_.chain(pkg.licenses)
138 .map((license) => license.type || license)
139 .join(" OR ")
140 .value()})`;
141 }
142 }
143
144 /**
145 * Serialize dependency as a string.
146 *
147 * @return {string} The dependency correctly formatted.
148 */
149 text() {
150 const lines = [];
151 lines.push(`Name: ${this.name}`);
152 lines.push(`Version: ${this.version}`);
153 lines.push(`License: ${this.license}`);
154 lines.push(`Private: ${this.private}`);
155 if (this.description) {
156 lines.push(`Description: ${this.description || false}`);
157 }
158 if (this.repository) {
159 lines.push(`Repository: ${this.repository.url}`);
160 }
161 if (this.homepage) {
162 lines.push(`Homepage: ${this.homepage}`);
163 }
164 if (this.author) {
165 lines.push(`Author: ${this.author.text()}`);
166 }
167 if (!_.isEmpty(this.contributors)) {
168 lines.push(`Contributors:`);
169 const allContributors = _.chain(this.contributors)
170 .map((contributor) => contributor.text())
171 .map((line) => ` ${line}`)
172 .value();
173 lines.push(...allContributors);
174 }
175 if (this.licenseText) {
176 lines.push("License Copyright:");
177 lines.push("===");
178 lines.push("");
179 lines.push(this.licenseText);
180 }
181 return lines.join(EOL);
182 }
183}
184
185/**
186 * Generate block comment from given text content.
187 *
188 * @param {string} text Text content.
189 * @param {Object} commentStyle The comment style setting.
190 * @return {string} Block comment.
191 */
192function generateBlockComment(text, commentStyle) {
193 const options = {
194 extension: ".js",
195 };
196 if (commentStyle) {
197 options.style = new commenting.Style(
198 commentStyle.body,
199 commentStyle.start,
200 commentStyle.end,
201 );
202 }
203 return commenting(text.trim(), options);
204}
205
206/**
207 * The plugin name.
208 * @type {string}
209 */
210const PLUGIN_NAME = "rollup-plugin-license";
211
212/**
213 * Check if given value is a `string`.
214 *
215 * @param {*} value The value to check.
216 * @return {boolean} `true` if `value` is a string, `false` otherwise.
217 */
218function isString(value) {
219 return _.isString(value);
220}
221
222/**
223 * Check if given value is a `boolean`.
224 *
225 * @param {*} value The value to check.
226 * @return {boolean} `true` if `value` is a boolean, `false` otherwise.
227 */
228function isBoolean(value) {
229 return _.isBoolean(value);
230}
231
232/**
233 * Check if given value is a `function`.
234 *
235 * @param {*} value The value to check.
236 * @return {boolean} `true` if `value` is a function, `false` otherwise.
237 */
238function isFunction(value) {
239 return _.isFunction(value);
240}
241
242/**
243 * Check if given value is a `number`.
244 *
245 * @param {*} value The value to check.
246 * @return {boolean} `true` if `value` is a number, `false` otherwise.
247 */
248function isNumber(value) {
249 return _.isNumber(value);
250}
251
252/**
253 * Check if given value is `null` or `undefined`.
254 *
255 * @param {*} value The value to check.
256 * @return {boolean} `true` if `value` is `null` or `undefined`, `false` otherwise.
257 */
258function isNil(value) {
259 return _.isNil(value);
260}
261
262/**
263 * Check if given value is an `array`.
264 *
265 * @param {*} value The value to check.
266 * @return {boolean} `true` if `value` is an array, `false` otherwise.
267 */
268function isArray(value) {
269 return _.isArray(value);
270}
271
272/**
273 * Check if given value is an plain object.
274 *
275 * @param {*} value The value to check.
276 * @return {boolean} `true` if `value` is a plain object, `false` otherwise.
277 */
278function isObject(value) {
279 return (
280 _.isObject(value) &&
281 !isArray(value) &&
282 !isFunction(value) &&
283 !isNil(value) &&
284 !isString(value) &&
285 !isNumber(value)
286 );
287}
288const validators = {
289 string() {
290 return {
291 type: "object.type.string",
292 message: "must be a string",
293 schema: null,
294 test: isString,
295 };
296 },
297 boolean() {
298 return {
299 type: "object.type.boolean",
300 message: "must be a boolean",
301 schema: null,
302 test: isBoolean,
303 };
304 },
305 func() {
306 return {
307 type: "object.type.func",
308 message: "must be a function",
309 schema: null,
310 test: isFunction,
311 };
312 },
313 object(schema) {
314 return {
315 type: "object.type.object",
316 message: "must be an object",
317 schema,
318 test: isObject,
319 };
320 },
321 array(schema) {
322 return {
323 type: "object.type.array",
324 message: "must be an array",
325 schema,
326 test: isArray,
327 };
328 },
329 any() {
330 return {
331 type: "object.any",
332 message: null,
333 schema: null,
334 test: () => true,
335 };
336 },
337};
338
339/**
340 * Format given array of path to a human readable path.
341 *
342 * @param {Array<string|number>} paths List of paths.
343 * @return {string} The full path.
344 */
345function formatPath(paths) {
346 let str = "";
347 _.forEach(paths, (p) => {
348 if (_.isNumber(p)) {
349 str += `[${p}]`;
350 } else if (!str) {
351 str += p;
352 } else {
353 str += `.${p}`;
354 }
355 });
356 return str;
357}
358
359/**
360 * Validate value against given schema.
361 * It is assumed that `value` will not be `null` or `undefined`.
362 *
363 * @param {*} value The value being validated.
364 * @param {Array<Object>|Object} schema The validation schema.
365 * @param {Array<string|number>} path The path being validated.
366 * @returns {Array<Object>} Found errors.
367 */
368function doItemValidation(value, schema, path) {
369 const validators = _.castArray(schema);
370 const matchedValidators = _.filter(validators, (validator) =>
371 validator.test(value),
372 );
373
374 // No one matched, we can stop here and return an error with a proper message.
375 if (_.isEmpty(matchedValidators)) {
376 return [
377 {
378 path,
379 message: _.map(
380 validators,
381 (validator) => `"${formatPath(path)}" ${validator.message}`,
382 ).join(" OR "),
383 },
384 ];
385 }
386
387 // Run "sub-validators"
388 return _.chain(matchedValidators)
389 .filter((validator) => validator.schema)
390 .map((validator) => validate(value, validator.schema, path))
391 .flatten()
392 .value();
393}
394
395/**
396 * Validate object against given schema.
397 * Note that `null` or `undefined` is allowed and do not produce an error.
398 *
399 * @param {Object} obj The object to validate.
400 * @param {Array<Object>|Object} schema The validation schema.
401 * @param {Array<string|number>} current The current path being validated.
402 * @returns {Array<Object>} Found errors.
403 */
404function validateObject(obj, schema, current) {
405 const errors = [];
406 _.forEach(obj, (value, k) => {
407 if (_.isNil(value)) {
408 return;
409 }
410 const path = [...current, k];
411 if (!_.has(schema, k)) {
412 errors.push({
413 type: "object.allowUnknown",
414 path,
415 });
416 } else {
417 errors.push(...doItemValidation(value, schema[k], path));
418 }
419 });
420 return errors;
421}
422
423/**
424 * Validate element of an array.
425 *
426 * Instead of "classic" object validation, `null` and `undefined` will produce
427 * an error here.
428 *
429 * @param {*} item The item to validate.
430 * @param {number} idx The index of item in original array.
431 * @param {Array<Object>|Object} schema The validation schema.
432 * @param {Array<string|number>} current The path being validated.
433 * @return {Array<Object>} Found errors.
434 */
435function validateArrayItem(item, idx, schema, current) {
436 const path = [...current, idx];
437 if (_.isUndefined(item)) {
438 return [
439 {
440 path,
441 message: `"${formatPath(path)}" is undefined.`,
442 },
443 ];
444 }
445 if (_.isNull(item)) {
446 return [
447 {
448 path,
449 message: `"${formatPath(path)}" is null.`,
450 },
451 ];
452 }
453 return doItemValidation(item, schema, path);
454}
455
456/**
457 * Validate all elements of given array against given schema (or array of schemas).
458 *
459 * @param {Array<*>} array Array of elements to validate.
460 * @param {Array<Object>|Object} schema The schema to use for validation.
461 * @param {string} current The path being validated.
462 * @return {Array<Object>} Found errors.
463 */
464function validateArray(array, schema, current) {
465 return _.chain(array)
466 .map((item, idx) => validateArrayItem(item, idx, schema, current))
467 .flatten()
468 .value();
469}
470
471/**
472 * Validate given object against given schema.
473 *
474 * Note that the very first version used `@hapi/joi` but this package does not support node < 8 in its latest version.
475 * Since I don't want to depends on deprecated and non maintained packages, and I want to keep compatibility with
476 * Node 6, I re-implemented the small part I needed here.
477 *
478 * Once node 6 will not be supported (probably with rollup >= 2), it will be time to drop this in favor of `@hapi/joi`
479 * for example.
480 *
481 * @param {Object} obj Object to validate.
482 * @param {Object} schema The schema against the given object will be validated.
483 * @param {Array<string>} current The current path context of given object, useful to validate against subobject.
484 * @return {Array<Object>} Found errors.
485 */
486function validate(obj, schema, current = []) {
487 return _.isArray(obj)
488 ? validateArray(obj, schema, current)
489 : validateObject(obj, schema, current);
490}
491
492/**
493 * Validate given object against given schema.
494 *
495 * @param {Object} obj Object to validate.
496 * @param {Object} schema The schema against the given object will be validated.
497 * @param {Array<string>} current The current path context of given object, useful to validate against subobject.
498 * @return {Array<Object>} Found errors.
499 */
500function validateSchema(obj, schema, current) {
501 return validate(obj, schema, current);
502}
503
504/**
505 * The option object schema.
506 * @type {Object}
507 */
508const SCHEMA = {
509 sourcemap: [validators.string(), validators.boolean()],
510 debug: validators.boolean(),
511 cwd: validators.string(),
512 banner: [
513 validators.func(),
514 validators.string(),
515 validators.object({
516 commentStyle: validators.string(),
517 data: validators.any(),
518 content: [
519 validators.func(),
520 validators.string(),
521 validators.object({
522 file: validators.string(),
523 encoding: validators.string(),
524 }),
525 ],
526 }),
527 ],
528 thirdParty: [
529 validators.func(),
530 validators.object({
531 includePrivate: validators.boolean(),
532 allow: [
533 validators.string(),
534 validators.func(),
535 validators.object({
536 test: [validators.string(), validators.func()],
537 failOnUnlicensed: validators.boolean(),
538 failOnViolation: validators.boolean(),
539 }),
540 ],
541 output: [
542 validators.func(),
543 validators.string(),
544 validators.object({
545 file: validators.string(),
546 encoding: validators.string(),
547 template: [validators.string(), validators.func()],
548 }),
549 validators.array([
550 validators.func(),
551 validators.string(),
552 validators.object({
553 file: validators.string(),
554 encoding: validators.string(),
555 template: [validators.string(), validators.func()],
556 }),
557 ]),
558 ],
559 }),
560 ],
561};
562
563/**
564 * Print warning message to the console.
565 *
566 * @param {string} msg Message to log.
567 * @return {void}
568 */
569function warn(msg) {
570 console.warn(`[${PLUGIN_NAME}] -- ${msg}`);
571}
572
573/**
574 * Validate given option object.
575 *
576 * @param {Object} options Option object.
577 * @return {Array} An array of all errors.
578 */
579function doValidation(options) {
580 return validateSchema(options, SCHEMA);
581}
582
583/**
584 * Validate option object according to pre-defined schema.
585 *
586 * @param {Object} options Option object.
587 * @return {void}
588 */
589function validateOptions(options) {
590 const errors = doValidation(options);
591 if (_.isEmpty(errors)) {
592 return;
593 }
594 const messages = [];
595 _.forEach(errors, (e) => {
596 if (e.type === "object.allowUnknown") {
597 warn(
598 `Unknown property: "${formatPath(
599 e.path,
600 )}", allowed options are: ${_.keys(SCHEMA).join(", ")}.`,
601 );
602 } else {
603 messages.push(e.message);
604 }
605 });
606 if (!_.isEmpty(messages)) {
607 throw new Error(
608 `[${PLUGIN_NAME}] -- Error during validation of option object: ${messages.join(
609 " ; ",
610 )}`,
611 );
612 }
613}
614
615/**
616 * Normalize and validate option object.
617 *
618 * @param {Object} options Option object to validate.
619 * @return {Object} New normalized options.
620 */
621function licensePluginOptions(options) {
622 validateOptions(options);
623 return options;
624}
625
626/**
627 * Normalize license name:
628 * - Returns `UNLICENSED` for nil parameter.
629 * - Trim license value.
630 *
631 * @param {string} license The license name.
632 * @return {string} The normalized license name.
633 */
634function normalizeLicense(license) {
635 if (!license) {
636 return "UNLICENSED";
637 }
638 return license.trim();
639}
640
641/**
642 * Check if given license name is the `UNLICENSED` value.
643 *
644 * @param {string} license The license to check.
645 * @return {boolean} `true` if `license` is the UNLICENSED one, `false` otherwise.
646 */
647function checkUnlicensed(license) {
648 return license.toUpperCase() === "UNLICENSED";
649}
650
651/**
652 * Check if dependency is unlicensed, or not.
653 *
654 * @param {Object} dependency The dependency.
655 * @return {boolean} `true` if dependency does not have any license, `false` otherwise.
656 */
657function isUnlicensed(dependency) {
658 const license = normalizeLicense(dependency.license);
659 return checkUnlicensed(license);
660}
661
662/**
663 * Check if license dependency is valid according to given SPDX validator pattern.
664 *
665 * @param {Object} dependency The dependency.
666 * @param {string} allow The validator as a SPDX pattern.
667 * @return {boolean} `true` if dependency license is valid, `false` otherwise.
668 */
669function isValid(dependency, allow) {
670 const license = normalizeLicense(dependency.license);
671 if (checkUnlicensed(license)) {
672 return false;
673 }
674 return spdxExpressionValidate(license) && spdxSatisfies(license, allow);
675}
676const licenseValidator = {
677 isUnlicensed,
678 isValid,
679};
680
681/**
682 * Pre-Defined comment style:
683 *
684 * - `regular` stands for "classic" block comment.
685 * - `ignored` stands for block comment starting with standard prefix ignored by minifier.
686 * - `slash` stands for "inline" style (i.e `//`).
687 * - `none` stands for no comment style at all.
688 *
689 * @type {Object<string, Object>}
690 */
691const COMMENT_STYLES = {
692 regular: {
693 start: "/**",
694 body: " *",
695 end: " */",
696 },
697 ignored: {
698 start: "/*!",
699 body: " *",
700 end: " */",
701 },
702 slash: {
703 start: "//",
704 body: "//",
705 end: "//",
706 },
707 none: null,
708};
709
710/**
711 * Compute the comment style to use for given text:
712 * - If text starts with a block comment, nothing is done (i.e use `none`).
713 * - Otherwise, use the `regular` style.
714 *
715 * @param {string} text The text to comment.
716 * @return {string} The comment style name.
717 */
718function computeDefaultCommentStyle(text) {
719 const trimmedText = text.trim();
720 const start = trimmedText.slice(0, 3);
721 const startWithComment = start === "/**" || start === "/*!";
722 return startWithComment ? "none" : "regular";
723}
724
725/**
726 * Rollup Plugin.
727 * @class
728 */
729class LicensePlugin {
730 /**
731 * Initialize plugin.
732 *
733 * @param {Object} options Plugin options.
734 */
735 constructor(options = {}) {
736 // Plugin name, used by rollup.
737 this.name = PLUGIN_NAME;
738
739 // Initialize main options.
740 this._options = options;
741 this._cwd = this._options.cwd || process.cwd();
742 this._dependencies = {};
743 this._pkg = require(path.join(this._cwd, "package.json"));
744 this._debug = this._options.debug || false;
745
746 // SourceMap can now be disable/enable on the plugin.
747 this._sourcemap = this._options.sourcemap !== false;
748
749 // This is a cache storing a directory path to associated package.
750 // This is an improvement to avoid looking for package information for
751 // already scanned directory.
752 this._cache = {};
753 }
754
755 /**
756 * Enable source map.
757 *
758 * @return {void}
759 */
760 disableSourceMap() {
761 this._sourcemap = false;
762 }
763
764 /**
765 * Hook triggered by `rollup` to load code from given path file.
766 *
767 * This hook is used here to analyze a JavaScript file to extract
768 * associated `package.json` file and store the main information about
769 * it (license, author, etc.).
770 *
771 * This method is used to analyse all the files added to the final bundle
772 * to extract license informations.
773 *
774 * @param {string} id Module identifier.
775 * @return {void}
776 */
777 scanDependency(id) {
778 if (id.startsWith("\0")) {
779 id = id.replace(/^\0/, "");
780 this.debug(`scanning internal module ${id}`);
781 }
782 if (id.indexOf("virtual:") === 0) {
783 this.debug(`skipping virtual module: ${id}`);
784 return;
785 }
786 this.debug(`scanning ${id}`);
787
788 // Look for the `package.json` file
789 let dir = path.resolve(path.parse(id).dir);
790 let pkg = null;
791 const scannedDirs = [];
792 this.debug(`iterative over directory tree, starting with: ${dir}`);
793 while (dir && dir !== this._cwd && !scannedDirs.includes(dir)) {
794 // Try the cache.
795 if (_.has(this._cache, dir)) {
796 pkg = this._cache[dir];
797 if (pkg) {
798 this.debug(`found package.json in cache (package: ${pkg.name})`);
799 this.addDependency(pkg);
800 }
801 break;
802 }
803 scannedDirs.push(dir);
804 this.debug(`looking for package.json file in: ${dir}`);
805 const pkgPath = path.join(dir, "package.json");
806 const exists = fs.existsSync(pkgPath);
807 if (exists) {
808 this.debug(`found package.json at: ${pkgPath}, read it`);
809
810 // Read `package.json` file
811 const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
812
813 // We are probably in a package.json specifying the type of package (module, cjs).
814 // Nevertheless, if the package name is not defined, we must not use this `package.json` descriptor.
815 const license = pkgJson.license || pkgJson.licenses;
816 const hasLicense = license && license.length > 0;
817 const name = pkgJson.name;
818 const version = pkgJson.version;
819 const isValidPackageName = name && packageNameRegex.test(name);
820 if ((isValidPackageName && version) || hasLicense) {
821 // We found it!
822 pkg = pkgJson;
823
824 // Read license file, if it exists.
825 const cwd = this._cwd || process.cwd();
826 const absolutePath = path.join(
827 dir,
828 "[lL][iI][cC][eE][nN][cCsS][eE]*",
829 );
830 const relativeToCwd = path.relative(cwd, absolutePath);
831 const licenseFile = this._findGlob(relativeToCwd, cwd).find(
832 (file) => fs.existsSync(file) && fs.lstatSync(file).isFile(),
833 );
834 if (licenseFile) {
835 pkg.licenseText = fs.readFileSync(licenseFile, "utf-8");
836 }
837
838 // Add the new dependency to the set of third-party dependencies.
839 this.addDependency(pkg);
840
841 // We can stop now.
842 break;
843 }
844 }
845
846 // Go up in the directory tree.
847 dir = path.resolve(path.join(dir, ".."));
848 this.debug(`going up in the directory tree: ${dir}`);
849 }
850
851 // Update the cache
852 _.forEach(scannedDirs, (scannedDir) => {
853 this._cache[scannedDir] = pkg;
854 });
855 }
856
857 /**
858 * Hook triggered by `rollup` to load code from given path file.
859 *
860 * @param {Object} dependencies List of modules included in the final bundle.
861 * @return {void}
862 */
863 scanDependencies(dependencies) {
864 this.debug(`Scanning: ${dependencies}`);
865 _.forEach(dependencies, (dependency) => {
866 this.scanDependency(dependency);
867 });
868 }
869
870 /**
871 * Hook triggered by `rollup` to transform the final generated bundle.
872 * This hook is used here to prepend the license banner to the final bundle.
873 *
874 * @param {string} code The bundle content.
875 * @param {boolean} sourcemap If sourcemap must be generated.
876 * @return {Object} The result containing the code and, optionnally, the source map
877 * if it has been enabled (using `enableSourceMap` method).
878 */
879 prependBanner(code, sourcemap) {
880 // Create a magicString: do not manipulate the string directly since it
881 // will be used to generate the sourcemap.
882 const magicString = new MagicString(code);
883 const banner = this._options.banner;
884 const content = this._readBanner(banner);
885 if (content) {
886 magicString.prepend(EOL);
887 magicString.prepend(this._generateBanner(content, banner));
888 }
889 const result = {
890 code: magicString.toString(),
891 };
892 if (this._sourcemap !== false && sourcemap !== false) {
893 result.map = magicString.generateMap({
894 hires: true,
895 });
896 }
897 return result;
898 }
899
900 /**
901 * Add new dependency to the bundle descriptor.
902 *
903 * @param {Object} pkg Dependency package information.
904 * @return {void}
905 */
906 addDependency(pkg) {
907 var _this$_options$thirdP;
908 const name = pkg.name || "";
909 if (!name) {
910 this.warn("Trying to add dependency without any name, skipping it.");
911 return;
912 }
913 const version = pkg.version || "";
914 const key =
915 (_this$_options$thirdP = this._options.thirdParty) !== null &&
916 _this$_options$thirdP !== void 0 &&
917 _this$_options$thirdP.multipleVersions
918 ? `${name}@${version}`
919 : name;
920 if (!_.has(this._dependencies, key)) {
921 this._dependencies[key] = new Dependency(pkg);
922 }
923 }
924
925 /**
926 * Scan third-party dependencies, and:
927 * - Warn for license violations.
928 * - Generate summary.
929 *
930 * @return {void}
931 */
932 scanThirdParties() {
933 const thirdParty = this._options.thirdParty;
934 if (!thirdParty) {
935 return;
936 }
937 const includePrivate = thirdParty.includePrivate || false;
938 const outputDependencies = _.chain(this._dependencies)
939 .values()
940 .filter((dependency) => includePrivate || !dependency.private)
941 .value();
942 if (_.isFunction(thirdParty)) {
943 thirdParty(outputDependencies);
944 return;
945 }
946 const allow = thirdParty.allow;
947 if (allow) {
948 this._scanLicenseViolations(outputDependencies, allow);
949 }
950 const output = thirdParty.output;
951 if (output) {
952 this._exportThirdParties(outputDependencies, output);
953 }
954 }
955
956 /**
957 * Log debug message if debug mode is enabled.
958 *
959 * @param {string} msg Log message.
960 * @return {void}
961 */
962 debug(msg) {
963 if (this._debug) {
964 console.debug(`[${this.name}] -- ${msg}`);
965 }
966 }
967
968 /**
969 * Log warn message.
970 *
971 * @param {string} msg Log message.
972 * @return {void}
973 */
974 warn(msg) {
975 console.warn(`[${this.name}] -- ${msg}`);
976 }
977
978 /**
979 * Find given file, matching given pattern.
980 *
981 * @param {string} pattern Pattern to look for.
982 * @param {string} cwd Working directory.
983 * @returns {*} All match.
984 * @private
985 */
986 _findGlob(pattern, cwd) {
987 return glob.sync(pattern, {
988 cwd,
989 });
990 }
991
992 /**
993 * Read banner from given options and returns it.
994 *
995 * @param {Object|string} banner Banner as a raw string, or banner options.
996 * @return {string|null} The banner template.
997 * @private
998 */
999 _readBanner(banner) {
1000 if (_.isNil(banner)) {
1001 return null;
1002 }
1003
1004 // Banner can be defined as a simple inline string.
1005 if (_.isString(banner)) {
1006 this.debug("prepend banner from raw string");
1007 return banner;
1008 }
1009
1010 // Extract banner content.
1011 const content = _.result(banner, "content");
1012
1013 // Content can be an inline string.
1014 if (_.isString(content)) {
1015 this.debug("prepend banner from content raw string");
1016 return content;
1017 }
1018
1019 // Otherwise, file must be defined (if not, that's an error).
1020 if (!_.has(content, "file")) {
1021 throw new Error(
1022 `[${this.name}] -- Cannot find banner content, please specify an inline content, or a path to a file`,
1023 );
1024 }
1025 const file = content.file;
1026 const encoding = content.encoding || "utf-8";
1027 this.debug(`prepend banner from file: ${file}`);
1028 this.debug(`use encoding: ${encoding}`);
1029 const filePath = path.resolve(file);
1030 const exists = fs.existsSync(filePath);
1031
1032 // Fail fast if file does not exist.
1033 if (!exists) {
1034 throw new Error(
1035 `[${this.name}] -- Template file ${filePath} does not exist, or cannot be read`,
1036 );
1037 }
1038 return fs.readFileSync(filePath, encoding);
1039 }
1040
1041 /**
1042 * Generate banner output from given raw string and given options.
1043 *
1044 * Banner output will be a JavaScript comment block, comment style may be customized using
1045 * the `commentStyle` option.
1046 *
1047 * @param {string} content Banner content, as a raw string.
1048 * @param {Object} banner Banner options.
1049 * @return {string} The banner output.
1050 * @private
1051 */
1052 _generateBanner(content, banner) {
1053 // Create the template function with lodash.
1054 const tmpl = _.template(content);
1055
1056 // Generate the banner.
1057 const pkg = this._pkg;
1058 const dependencies = _.values(this._dependencies);
1059 const data = banner.data ? _.result(banner, "data") : {};
1060 const text = tmpl({
1061 _,
1062 moment,
1063 pkg,
1064 dependencies,
1065 data,
1066 });
1067
1068 // Compute comment style to use.
1069 const style = _.has(banner, "commentStyle")
1070 ? banner.commentStyle
1071 : computeDefaultCommentStyle(text);
1072
1073 // Ensure given style name is valid.
1074 if (!_.has(COMMENT_STYLES, style)) {
1075 throw new Error(
1076 `Unknown comment style ${style}, please use one of: ${_.keys(
1077 COMMENT_STYLES,
1078 )}`,
1079 );
1080 }
1081 this.debug(`generate banner using comment style: ${style}`);
1082 return COMMENT_STYLES[style]
1083 ? generateBlockComment(text, COMMENT_STYLES[style])
1084 : text;
1085 }
1086
1087 /**
1088 * Scan for dependency violations and print a warning if some violations are found.
1089 *
1090 * @param {Array<Object>} outputDependencies The dependencies to scan.
1091 * @param {string} allow The allowed licenses as a SPDX pattern.
1092 * @return {void}
1093 */
1094 _scanLicenseViolations(outputDependencies, allow) {
1095 _.forEach(outputDependencies, (dependency) => {
1096 this._scanLicenseViolation(dependency, allow);
1097 });
1098 }
1099
1100 /**
1101 * Scan dependency for a dependency violation.
1102 *
1103 * @param {Object} dependency The dependency to scan.
1104 * @param {string|function|object} allow The allowed licenses as a SPDX pattern, or a validator function.
1105 * @return {void}
1106 */
1107 _scanLicenseViolation(dependency, allow) {
1108 const testFn =
1109 _.isString(allow) || _.isFunction(allow) ? allow : allow.test;
1110 const isValid = _.isFunction(testFn)
1111 ? testFn(dependency)
1112 : licenseValidator.isValid(dependency, testFn);
1113 if (!isValid) {
1114 const failOnUnlicensed = allow.failOnUnlicensed === true;
1115 const failOnViolation = allow.failOnViolation === true;
1116 this._handleInvalidLicense(dependency, failOnUnlicensed, failOnViolation);
1117 }
1118 }
1119
1120 /**
1121 * Handle invalid dependency:
1122 * - Print a warning for unlicensed dependency.
1123 * - Print a warning for dependency violation.
1124 *
1125 * @param {Object} dependency The dependency to scan.
1126 * @param {boolean} failOnUnlicensed `true` to fail on unlicensed dependency, `false` otherwise.
1127 * @param {boolean} failOnViolation `true` to fail on license violation, `false` otherwise.
1128 * @return {void}
1129 */
1130 _handleInvalidLicense(dependency, failOnUnlicensed, failOnViolation) {
1131 if (licenseValidator.isUnlicensed(dependency)) {
1132 this._handleUnlicensedDependency(dependency, failOnUnlicensed);
1133 } else {
1134 this._handleLicenseViolation(dependency, failOnViolation);
1135 }
1136 }
1137
1138 /**
1139 * Handle unlicensed dependency: print a warning to the console to alert for the dependency
1140 * that should be fixed.
1141 *
1142 * @param {Object} dependency The dependency.
1143 * @param {boolean} fail `true` to fail instead of emitting a simple warning.
1144 * @return {void}
1145 */
1146 _handleUnlicensedDependency(dependency, fail) {
1147 const message = `Dependency "${dependency.name}" does not specify any license.`;
1148 if (!fail) {
1149 this.warn(message);
1150 } else {
1151 throw new Error(message);
1152 }
1153 }
1154
1155 /**
1156 * Handle license violation: print a warning to the console to alert about the violation.
1157 *
1158 * @param {Object} dependency The dependency.
1159 * @param {boolean} fail `true` to fail instead of emitting a simple warning.
1160 * @return {void}
1161 */
1162 _handleLicenseViolation(dependency, fail) {
1163 const message =
1164 `Dependency "${dependency.name}" has a license (${dependency.license}) which is not compatible with ` +
1165 `requirement, looks like a license violation to fix.`;
1166 if (!fail) {
1167 this.warn(message);
1168 } else {
1169 throw new Error(message);
1170 }
1171 }
1172
1173 /**
1174 * Export scanned third party dependencies to a destination output (a function, a
1175 * file written to disk, etc.).
1176 *
1177 * @param {Array<Object>} outputDependencies The dependencies to include in the output.
1178 * @param {Object|function|string|Array} outputs The output (or the array of output) destination.
1179 * @return {void}
1180 */
1181 _exportThirdParties(outputDependencies, outputs) {
1182 _.forEach(_.castArray(outputs), (output) => {
1183 this._exportThirdPartiesToOutput(outputDependencies, output);
1184 });
1185 }
1186
1187 /**
1188 * Export scanned third party dependencies to a destination output (a function, a
1189 * file written to disk, etc.).
1190 *
1191 * @param {Array<Object>} outputDependencies The dependencies to include in the output.
1192 * @param {Array} output The output destination.
1193 * @return {void}
1194 */
1195 _exportThirdPartiesToOutput(outputDependencies, output) {
1196 if (_.isFunction(output)) {
1197 output(outputDependencies);
1198 return;
1199 }
1200
1201 // Default is to export to given file.
1202
1203 // Allow custom formatting of output using given template option.
1204 const template = _.isString(output.template)
1205 ? (dependencies) =>
1206 _.template(output.template)({
1207 dependencies,
1208 _,
1209 moment,
1210 })
1211 : output.template;
1212 const defaultTemplate = (dependencies) =>
1213 _.isEmpty(dependencies)
1214 ? "No third parties dependencies"
1215 : _.map(dependencies, (d) => d.text()).join(
1216 `${EOL}${EOL}---${EOL}${EOL}`,
1217 );
1218 const text = _.isFunction(template)
1219 ? template(outputDependencies)
1220 : defaultTemplate(outputDependencies);
1221 const isOutputFile = _.isString(output);
1222 const file = isOutputFile ? output : output.file;
1223 const encoding = isOutputFile ? "utf-8" : output.encoding || "utf-8";
1224 this.debug(`exporting third-party summary to ${file}`);
1225 this.debug(`use encoding: ${encoding}`);
1226
1227 // Create directory if it does not already exist.
1228 mkdirp.mkdirp.sync(path.parse(file).dir);
1229 fs.writeFileSync(file, (text || "").trim(), {
1230 encoding,
1231 });
1232 }
1233}
1234
1235/**
1236 * Create new `rollup-plugin-license` instance with given
1237 * options.
1238 *
1239 * @param {Object} options Option object.
1240 * @return {LicensePlugin} The new instance.
1241 */
1242function licensePlugin(options) {
1243 return new LicensePlugin(licensePluginOptions(options));
1244}
1245
1246/**
1247 * Create rollup plugin compatible with rollup >= 1.0.0
1248 *
1249 * @param {Object} options Plugin options.
1250 * @return {Object} Plugin instance.
1251 */
1252function rollupPluginLicense(options = {}) {
1253 const plugin = licensePlugin(options);
1254 return {
1255 /**
1256 * Name of the plugin, used automatically by rollup.
1257 * @type {string}
1258 */
1259 name: plugin.name,
1260 /**
1261 * Function called by rollup when the final bundle is generated: it is used
1262 * to prepend the banner file on the generated bundle.
1263 *
1264 * @param {string} code Bundle content.
1265 * @param {Object} chunk The chunk being generated.
1266 * @param {Object} outputOptions The options for the generated output.
1267 * @return {void}
1268 */
1269 renderChunk(code, chunk, outputOptions = {}) {
1270 plugin.scanDependencies(
1271 _.chain(chunk.modules)
1272 .toPairs()
1273 .reject((mod) => mod[1].isAsset)
1274 .filter((mod) => mod[1].renderedLength > 0)
1275 .map((mod) => mod[0])
1276 .value(),
1277 );
1278 return plugin.prependBanner(code, outputOptions.sourcemap !== false);
1279 },
1280 /**
1281 * Function called by rollup when the final bundle will be written on disk: it
1282 * is used to generate a file containing a summary of all third-party dependencies
1283 * with license information.
1284 *
1285 * @return {void}
1286 */
1287 generateBundle() {
1288 plugin.scanThirdParties();
1289 },
1290 };
1291}
1292
1293module.exports = rollupPluginLicense;