BannerPlugin.js 4.2 KB

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