get-manifest-entries-from-compilation.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. "use strict";
  2. /*
  3. Copyright 2018 Google LLC
  4. Use of this source code is governed by an MIT-style
  5. license that can be found in the LICENSE file or at
  6. https://opensource.org/licenses/MIT.
  7. */
  8. const {
  9. matchPart
  10. } = require('webpack').ModuleFilenameHelpers;
  11. const transformManifest = require('workbox-build/build/lib/transform-manifest');
  12. const getAssetHash = require('./get-asset-hash');
  13. const resolveWebpackURL = require('./resolve-webpack-url');
  14. /**
  15. * For a given asset, checks whether at least one of the conditions matches.
  16. *
  17. * @param {Asset} asset The webpack asset in question. This will be passed
  18. * to any functions that are listed as conditions.
  19. * @param {Compilation} compilation The webpack compilation. This will be passed
  20. * to any functions that are listed as conditions.
  21. * @param {Array<string|RegExp|Function>} conditions
  22. * @return {boolean} Whether or not at least one condition matches.
  23. * @private
  24. */
  25. function checkConditions(asset, compilation, conditions = []) {
  26. for (const condition of conditions) {
  27. if (typeof condition === 'function') {
  28. if (condition({
  29. asset,
  30. compilation
  31. })) {
  32. return true;
  33. }
  34. } else {
  35. if (matchPart(asset.name, condition)) {
  36. return true;
  37. }
  38. }
  39. } // We'll only get here if none of the conditions applied.
  40. return false;
  41. }
  42. /**
  43. * Creates a mapping of an asset name to an Set of zero or more chunk names
  44. * that the asset is associated with.
  45. *
  46. * Those chunk names come from a combination of the `chunkName` property on the
  47. * asset, as well as the `stats.namedChunkGroups` property. That is the only
  48. * way to find out if an asset has an implicit descendent relationship with a
  49. * chunk, if it was, e.g., created by `SplitChunksPlugin`.
  50. *
  51. * See https://github.com/GoogleChrome/workbox/issues/1859
  52. * See https://github.com/webpack/webpack/issues/7073
  53. *
  54. * @param {Object} stats The webpack compilation stats.
  55. * @return {object<string, Set<string>>}
  56. * @private
  57. */
  58. function assetToChunkNameMapping(stats) {
  59. const mapping = {};
  60. for (const asset of stats.assets) {
  61. mapping[asset.name] = new Set(asset.chunkNames);
  62. }
  63. for (const [chunkName, {
  64. assets
  65. }] of Object.entries(stats.namedChunkGroups)) {
  66. for (const assetName of assets) {
  67. // See https://github.com/GoogleChrome/workbox/issues/2194
  68. if (mapping[assetName]) {
  69. mapping[assetName].add(chunkName);
  70. }
  71. }
  72. }
  73. return mapping;
  74. }
  75. /**
  76. * Filters the set of assets out, based on the configuration options provided:
  77. * - chunks and excludeChunks, for chunkName-based criteria.
  78. * - include and exclude, for more general criteria.
  79. *
  80. * @param {Compilation} compilation The webpack compilation.
  81. * @param {Object} config The validated configuration, obtained from the plugin.
  82. * @return {Set<Asset>} The assets that should be included in the manifest,
  83. * based on the criteria provided.
  84. * @private
  85. */
  86. function filterAssets(compilation, config) {
  87. const filteredAssets = new Set(); // See https://webpack.js.org/configuration/stats/#stats
  88. // We only need assets and chunkGroups here.
  89. const stats = compilation.getStats().toJson({
  90. assets: true,
  91. chunkGroups: true
  92. });
  93. const assetNameToChunkNames = assetToChunkNameMapping(stats); // See https://github.com/GoogleChrome/workbox/issues/1287
  94. if (Array.isArray(config.chunks)) {
  95. for (const chunk of config.chunks) {
  96. if (!(chunk in stats.namedChunkGroups)) {
  97. compilation.warnings.push(`The chunk '${chunk}' was provided in ` + `your Workbox chunks config, but was not found in the compilation.`);
  98. }
  99. }
  100. } // See https://webpack.js.org/api/stats/#asset-objects
  101. for (const asset of stats.assets) {
  102. // chunkName based filtering is funky because:
  103. // - Each asset might belong to one or more chunkNames.
  104. // - If *any* of those chunk names match our config.excludeChunks,
  105. // then we skip that asset.
  106. // - If the config.chunks is defined *and* there's no match
  107. // between at least one of the chunkNames and one entry, then
  108. // we skip that assets as well.
  109. const isExcludedChunk = Array.isArray(config.excludeChunks) && config.excludeChunks.some(chunkName => {
  110. return assetNameToChunkNames[asset.name].has(chunkName);
  111. });
  112. if (isExcludedChunk) {
  113. continue;
  114. }
  115. const isIncludedChunk = !Array.isArray(config.chunks) || config.chunks.some(chunkName => {
  116. return assetNameToChunkNames[asset.name].has(chunkName);
  117. });
  118. if (!isIncludedChunk) {
  119. continue;
  120. } // Next, check asset-level checks via includes/excludes:
  121. const isExcluded = checkConditions(asset, compilation, config.exclude);
  122. if (isExcluded) {
  123. continue;
  124. } // Treat an empty config.includes as an implicit inclusion.
  125. const isIncluded = !Array.isArray(config.include) || checkConditions(asset, compilation, config.include);
  126. if (!isIncluded) {
  127. continue;
  128. } // If we've gotten this far, then add the asset.
  129. filteredAssets.add(asset);
  130. }
  131. return filteredAssets;
  132. }
  133. module.exports = async (compilation, config) => {
  134. const filteredAssets = filterAssets(compilation, config);
  135. const {
  136. publicPath
  137. } = compilation.options.output;
  138. const fileDetails = [];
  139. for (const asset of filteredAssets) {
  140. // Not sure why this would be false, but checking just in case, since
  141. // our original list of assets comes from compilation.getStats().toJson(),
  142. // not from compilation.assets.
  143. if (asset.name in compilation.assets) {
  144. // This matches the format expected by transformManifest().
  145. fileDetails.push({
  146. file: resolveWebpackURL(publicPath, asset.name),
  147. hash: getAssetHash(compilation.assets[asset.name]),
  148. size: asset.size || 0
  149. });
  150. } else {
  151. compilation.warnings.push(`Could not precache ${asset.name}, as it's ` + `missing from compilation.assets. Please open a bug against Workbox ` + `with details about your webpack config.`);
  152. }
  153. } // We also get back `size` and `count`, and it would be nice to log that
  154. // somewhere, but... webpack doesn't offer info-level logs?
  155. // https://github.com/webpack/webpack/issues/3996
  156. const {
  157. manifestEntries,
  158. warnings
  159. } = await transformManifest({
  160. fileDetails,
  161. additionalManifestEntries: config.additionalManifestEntries,
  162. dontCacheBustURLsMatching: config.dontCacheBustURLsMatching,
  163. manifestTransforms: config.manifestTransforms,
  164. maximumFileSizeToCacheInBytes: config.maximumFileSizeToCacheInBytes,
  165. modifyURLPrefix: config.modifyURLPrefix,
  166. transformParam: compilation
  167. });
  168. compilation.warnings = compilation.warnings.concat(warnings || []); // Ensure that the entries are properly sorted by URL.
  169. const sortedEntries = manifestEntries.sort((a, b) => a.url === b.url ? 0 : a.url > b.url ? 1 : -1);
  170. return sortedEntries;
  171. };