ManifestPlugin.js 6.4 KB

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