BannerPlugin.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { ConcatSource } = require("webpack-sources");
  7. const Compilation = require("./Compilation");
  8. const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
  9. const Template = require("./Template");
  10. /** @import { Source } from "webpack-sources" */
  11. /**
  12. * @import {
  13. * BannerPluginArgument,
  14. * BannerPluginOptions
  15. * } from "../declarations/plugins/BannerPlugin"
  16. */
  17. /** @import { PathDataChunk } from "./Compilation" */
  18. /** @import Compiler from "./Compiler" */
  19. /** @import Chunk from "./Chunk" */
  20. /** @typedef {(data: { hash?: string, chunk: Chunk, filename: string }) => string} BannerFunction */
  21. /**
  22. * Wraps banner text in a JavaScript block comment, preserving multi-line
  23. * formatting and escaping accidental comment terminators.
  24. * @param {string} str string to wrap
  25. * @returns {string} wrapped string
  26. */
  27. const wrapComment = (str) => {
  28. if (!str.includes("\n")) {
  29. return Template.toComment(str);
  30. }
  31. return `/*!\n * ${str
  32. .replace(/\*\//g, "* /")
  33. .split("\n")
  34. .join("\n * ")
  35. .replace(/\s+\n/g, "\n")
  36. .trimEnd()}\n */`;
  37. };
  38. const PLUGIN_NAME = "BannerPlugin";
  39. /**
  40. * Prepends or appends banner text to emitted assets that match the configured
  41. * file filters.
  42. */
  43. class BannerPlugin {
  44. /**
  45. * Normalizes banner options and compiles the configured banner source into a
  46. * function that can render per-asset banner text.
  47. * @param {BannerPluginArgument} options options object
  48. */
  49. constructor(options) {
  50. if (typeof options === "string" || typeof options === "function") {
  51. options = {
  52. banner: options
  53. };
  54. }
  55. /** @type {BannerPluginOptions} */
  56. this.options = options;
  57. const bannerOption = options.banner;
  58. if (typeof bannerOption === "function") {
  59. const getBanner = bannerOption;
  60. /** @type {BannerFunction} */
  61. this.banner = this.options.raw
  62. ? getBanner
  63. : /** @type {BannerFunction} */ (data) => wrapComment(getBanner(data));
  64. } else {
  65. const banner = this.options.raw
  66. ? bannerOption
  67. : wrapComment(bannerOption);
  68. /** @type {BannerFunction} */
  69. this.banner = () => banner;
  70. }
  71. }
  72. /**
  73. * Validates the configured options and injects rendered banner comments into
  74. * matching compilation assets at the configured process-assets stage.
  75. * @param {Compiler} compiler the compiler instance
  76. * @returns {void}
  77. */
  78. apply(compiler) {
  79. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  80. compiler.validate(
  81. () => require("../schemas/plugins/BannerPlugin.json"),
  82. this.options,
  83. {
  84. name: "Banner Plugin",
  85. baseDataPath: "options"
  86. },
  87. (options) => require("../schemas/plugins/BannerPlugin.check")(options)
  88. );
  89. });
  90. const options = this.options;
  91. const banner = this.banner;
  92. const matchObject = ModuleFilenameHelpers.matchObject.bind(
  93. undefined,
  94. options
  95. );
  96. /** @type {WeakMap<Source, { source: ConcatSource, comment: string }>} */
  97. const cache = new WeakMap();
  98. const stage =
  99. this.options.stage || Compilation.PROCESS_ASSETS_STAGE_ADDITIONS;
  100. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  101. compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, () => {
  102. for (const chunk of compilation.chunks) {
  103. if (options.entryOnly && !chunk.canBeInitial()) {
  104. continue;
  105. }
  106. for (const file of chunk.files) {
  107. if (!matchObject(file)) {
  108. continue;
  109. }
  110. /** @type {PathDataChunk} */
  111. const data = { chunk, filename: file };
  112. const comment = compilation.getPath(
  113. /** @type {string | import("./TemplatedPathPlugin").TemplatePathFn<PathDataChunk>} */
  114. (banner),
  115. data
  116. );
  117. compilation.updateAsset(file, (old) => {
  118. const cached = cache.get(old);
  119. if (!cached || cached.comment !== comment) {
  120. const source = options.footer
  121. ? new ConcatSource(old, "\n", comment)
  122. : new ConcatSource(comment, "\n", old);
  123. cache.set(old, { source, comment });
  124. return source;
  125. }
  126. return cached.source;
  127. });
  128. }
  129. }
  130. });
  131. });
  132. }
  133. }
  134. module.exports = BannerPlugin;