ManifestPlugin.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Haijie Xie @hai-x
  4. */
  5. "use strict";
  6. const { RawSource } = require("webpack-sources");
  7. const Compilation = require("./Compilation");
  8. const HotUpdateChunk = require("./HotUpdateChunk");
  9. /** @import Compiler from "./Compiler" */
  10. /** @import Chunk, { ChunkName, ChunkId } from "./Chunk" */
  11. /** @import { AssetInfo } from "./Compilation" */
  12. /**
  13. * @import {
  14. * ManifestPluginOptions,
  15. * ManifestObject,
  16. * ManifestEntrypoint,
  17. * ManifestItem
  18. * } from "../declarations/plugins/ManifestPlugin"
  19. */
  20. /** @typedef {(item: ManifestItem) => boolean} Filter */
  21. /** @typedef {(manifest: ManifestObject) => ManifestObject} Generate */
  22. /** @typedef {(manifest: ManifestObject) => string} Serialize */
  23. const PLUGIN_NAME = "ManifestPlugin";
  24. /**
  25. * Returns extname.
  26. * @param {string} filename filename
  27. * @returns {string} extname
  28. */
  29. const extname = (filename) => {
  30. const replaced = filename.replace(/\?.*/, "");
  31. const split = replaced.split(".");
  32. const last = split.pop();
  33. if (!last) return "";
  34. return last && /^(?:gz|br|map)$/i.test(last)
  35. ? `${split.pop()}.${last}`
  36. : last;
  37. };
  38. const DEFAULT_PREFIX = "[publicpath]";
  39. const DEFAULT_FILENAME = "manifest.json";
  40. class ManifestPlugin {
  41. /**
  42. * Creates an instance of ManifestPlugin.
  43. * @param {ManifestPluginOptions} options options
  44. */
  45. constructor(options = {}) {
  46. /** @type {ManifestPluginOptions} */
  47. this.options = options;
  48. }
  49. /**
  50. * Applies the plugin by registering its hooks on the compiler.
  51. * @param {Compiler} compiler the compiler instance
  52. * @returns {void}
  53. */
  54. apply(compiler) {
  55. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  56. compiler.validate(
  57. () => require("../schemas/plugins/ManifestPlugin.json"),
  58. this.options,
  59. {
  60. name: "ManifestPlugin",
  61. baseDataPath: "options"
  62. },
  63. (options) => require("../schemas/plugins/ManifestPlugin.check")(options)
  64. );
  65. });
  66. const entrypoints =
  67. this.options.entrypoints !== undefined ? this.options.entrypoints : true;
  68. const serialize =
  69. this.options.serialize ||
  70. ((manifest) => JSON.stringify(manifest, null, 2));
  71. compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
  72. compilation.hooks.processAssets.tap(
  73. {
  74. name: PLUGIN_NAME,
  75. stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
  76. },
  77. () => {
  78. const hashDigestLength = compilation.outputOptions.hashDigestLength;
  79. const publicPath = compilation.getPath(
  80. compilation.outputOptions.publicPath
  81. );
  82. /**
  83. * Creates a hash reg exp.
  84. * @param {string | string[]} value value
  85. * @returns {RegExp} regexp to remove hash
  86. */
  87. const createHashRegExp = (value) =>
  88. new RegExp(
  89. `(?:\\.${Array.isArray(value) ? `(${value.join("|")})` : value})(?=\\.)`,
  90. "gi"
  91. );
  92. /**
  93. * Removes the provided name from the manifest plugin.
  94. * @param {string} name name
  95. * @param {AssetInfo | null} info asset info
  96. * @returns {string} hash removed name
  97. */
  98. const removeHash = (name, info) => {
  99. // Handles hashes that match configured `hashDigestLength`
  100. // i.e. index.XXXX.html -> index.html (html-webpack-plugin)
  101. if (hashDigestLength <= 0) return name;
  102. const reg = createHashRegExp(`[a-f0-9]{${hashDigestLength},32}`);
  103. return name.replace(reg, "");
  104. };
  105. /**
  106. * Returns chunk name or chunk id.
  107. * @param {Chunk} chunk chunk
  108. * @returns {ChunkName | ChunkId} chunk name or chunk id
  109. */
  110. const getName = (chunk) => {
  111. if (chunk.name) return chunk.name;
  112. return chunk.id;
  113. };
  114. /** @type {ManifestObject} */
  115. let manifest = {};
  116. if (entrypoints) {
  117. /** @type {ManifestObject["entrypoints"]} */
  118. const entrypoints = {};
  119. for (const [name, entrypoint] of compilation.entrypoints) {
  120. /** @type {string[]} */
  121. const imports = [];
  122. for (const chunk of entrypoint.chunks) {
  123. for (const file of chunk.files) {
  124. const name = getName(chunk);
  125. imports.push(name ? `${name}.${extname(file)}` : file);
  126. }
  127. }
  128. /** @type {ManifestEntrypoint} */
  129. const item = { imports };
  130. const parents = entrypoint
  131. .getParents()
  132. .map((item) => /** @type {string} */ (item.name));
  133. if (parents.length > 0) {
  134. item.parents = parents;
  135. }
  136. entrypoints[name] = item;
  137. }
  138. manifest.entrypoints = entrypoints;
  139. }
  140. /** @type {ManifestObject["assets"]} */
  141. const assets = {};
  142. /** @type {Set<string>} */
  143. const added = new Set();
  144. /**
  145. * Processes the provided file.
  146. * @param {string} file file
  147. * @param {string=} usedName usedName
  148. * @returns {void}
  149. */
  150. const handleFile = (file, usedName) => {
  151. if (added.has(file)) return;
  152. added.add(file);
  153. const asset = compilation.getAsset(file);
  154. if (!asset) return;
  155. const sourceFilename = asset.info.sourceFilename;
  156. const name =
  157. usedName ||
  158. sourceFilename ||
  159. // Fallback for unofficial plugins, just remove hash from filename
  160. removeHash(file, asset.info);
  161. const prefix = (this.options.prefix || DEFAULT_PREFIX).replace(
  162. /\[publicpath\]/gi,
  163. () => (publicPath === "auto" ? "/" : publicPath)
  164. );
  165. /** @type {ManifestItem} */
  166. const item = { file: prefix + file };
  167. if (sourceFilename) {
  168. item.src = sourceFilename;
  169. }
  170. if (this.options.filter) {
  171. const needKeep = this.options.filter(item);
  172. if (!needKeep) {
  173. return;
  174. }
  175. }
  176. assets[name] = item;
  177. };
  178. for (const chunk of compilation.chunks) {
  179. if (chunk instanceof HotUpdateChunk) continue;
  180. for (const auxiliaryFile of chunk.auxiliaryFiles) {
  181. handleFile(auxiliaryFile);
  182. }
  183. const name = getName(chunk);
  184. for (const file of chunk.files) {
  185. handleFile(file, name ? `${name}.${extname(file)}` : file);
  186. }
  187. }
  188. for (const asset of compilation.getAssets()) {
  189. if (asset.info.hotModuleReplacement) {
  190. continue;
  191. }
  192. handleFile(asset.name);
  193. }
  194. manifest.assets = assets;
  195. if (this.options.generate) {
  196. manifest = this.options.generate(manifest);
  197. }
  198. compilation.emitAsset(
  199. this.options.filename || DEFAULT_FILENAME,
  200. new RawSource(serialize(manifest)),
  201. { manifest: true }
  202. );
  203. }
  204. );
  205. });
  206. }
  207. }
  208. module.exports = ManifestPlugin;