webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
219 lines (192 loc) • 6.07 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { ConcatSource } = require("webpack-sources");
const Compilation = require("../Compilation");
const ModuleFilenameHelpers = require("../devtool/ModuleFilenameHelpers");
const Template = require("../template/Template");
/** @import { Source } from "webpack-sources" */
/** @import { PathDataChunk } from "../Compilation" */
/** @import Compiler from "../Compiler" */
/** @import Chunk from "../graph/Chunk" */
/** @import { MatcherFn } from "../devtool/ModuleFilenameHelpers" */
/**
* The banner as function, it will be wrapped in a comment.
* @tsType import('./BannerPlugin').BannerFunction
* @typedef {(data: { hash?: string, chunk: Chunk, filename: string }) => string} BannerFunction
*/
/**
* @inline
* @minLength 1
* @typedef {string} NonEmptyString
*/
/**
* A glob pattern.
* @inline
* @minLength 1
* @typedef {string} RuleGlobPattern
*/
/**
* @minItems 1
* @inline
* @typedef {RuleGlobPattern[]} RuleGlobList
*/
/**
* Glob patterns matched against the file name, `!` in front of a pattern excludes it, and a relative pattern matches at any depth.
* @since 5.112.0
* @inline
* @typedef {object} RuleGlob
* @property {RuleGlobList | NonEmptyString} glob Glob patterns to match.
*/
/**
* Filtering rule as regex or string.
* @typedef {RegExp | NonEmptyString | RuleGlob | import("../devtool/ModuleFilenameHelpers").MatcherFn} Rule
*/
/**
* A rule condition.
* @inline
* @typedef {Rule} RuleCondition
*/
/**
* Filtering rules.
* @typedef {RuleCondition[] | Rule} Rules
*/
/**
* @inline
* @title BannerPluginOptions
* @typedef {object} BannerPluginOptions
* @property {string | BannerFunction} banner Specifies the banner.
* @property {boolean=} entryOnly If true, the banner will only be added to the entry chunks.
* @property {Rules=} exclude Exclude all modules matching any of these conditions.
* @property {boolean=} footer If true, banner will be placed at the end of the output.
* @property {Rules=} include Include all modules matching any of these conditions.
* @property {boolean=} raw If true, banner will not be wrapped in a comment.
* @property {number=} stage Specifies the stage when add a banner.
* @property {Rules=} test Include all modules that pass test assertion.
*/
/**
* The banner as string, it will be wrapped in a comment.
* @inline
* @typedef {NonEmptyString} BannerString
*/
/**
* @schema plugins/BannerPlugin
* @typedef {BannerString | BannerPluginOptions | BannerFunction} BannerPluginArgument
*/
/**
* Wraps banner text in a JavaScript block comment, preserving multi-line
* formatting and escaping accidental comment terminators.
* @param {string} str string to wrap
* @returns {string} wrapped string
*/
const wrapComment = (str) => {
if (!str.includes("\n")) {
return Template.toComment(str);
}
return `/*!\n * ${str
.replace(/\*\//g, "* /")
.split("\n")
.join("\n * ")
.replace(/\s+\n/g, "\n")
.trimEnd()}\n */`;
};
const PLUGIN_NAME = "BannerPlugin";
/**
* Prepends or appends banner text to emitted assets that match the configured
* file filters.
*/
class BannerPlugin {
/**
* Normalizes banner options and compiles the configured banner source into a
* function that can render per-asset banner text.
* @param {BannerPluginArgument} options options object
*/
constructor(options) {
if (typeof options === "string" || typeof options === "function") {
options = {
banner: options
};
}
/** @type {BannerPluginOptions} */
this.options = options;
const bannerOption = options.banner;
if (typeof bannerOption === "function") {
const getBanner = bannerOption;
/** @type {BannerFunction} */
this.banner = this.options.raw
? getBanner
: /** @type {BannerFunction} */ (data) => wrapComment(getBanner(data));
} else {
const banner = this.options.raw
? bannerOption
: wrapComment(bannerOption);
/** @type {BannerFunction} */
this.banner = () => banner;
}
}
/**
* Validates the configured options and injects rendered banner comments into
* matching compilation assets at the configured process-assets stage.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.validate.tap(PLUGIN_NAME, () => {
compiler.validate(
() => require("../../schemas/plugins/BannerPlugin.json"),
this.options,
{
name: "Banner Plugin",
baseDataPath: "options"
},
(options) =>
require("../../schemas/plugins/BannerPlugin.check")(options)
);
});
const options = this.options;
const banner = this.banner;
const matchObject = ModuleFilenameHelpers.matchObject.bind(
undefined,
options
);
/** @type {WeakMap<Source, { source: ConcatSource, comment: string }>} */
const cache = new WeakMap();
const stage =
this.options.stage || Compilation.PROCESS_ASSETS_STAGE_ADDITIONS;
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, () => {
for (const chunk of compilation.chunks) {
if (options.entryOnly && !chunk.canBeInitial()) {
continue;
}
for (const file of chunk.files) {
if (!matchObject(file)) {
continue;
}
/** @type {PathDataChunk} */
const data = { chunk, filename: file };
const comment = compilation.getPath(
/** @type {string | import("../template/TemplatedPathPlugin").TemplatePathFn<PathDataChunk>} */
(banner),
data
);
compilation.updateAsset(file, (old) => {
const cached = cache.get(old);
if (!cached || cached.comment !== comment) {
const source = options.footer
? new ConcatSource(old, "\n", comment)
: new ConcatSource(comment, "\n", old);
cache.set(old, { source, comment });
return source;
}
return cached.source;
});
}
}
});
});
}
}
module.exports = BannerPlugin;