ManifestPlugin.js 6.8 KB

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