diff --git a/lib/Compilation.js b/lib/Compilation.js index 93144f27f..f12484946 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -2710,12 +2710,16 @@ Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_* let filenameTemplate; /** @type {string} */ let file; + /** @type {AssetInfo} */ + let assetInfo; let inTry = true; const errorAndCallback = err => { const filename = file || - (typeof filenameTemplate === "string" + (typeof file === "string" + ? file + : typeof filenameTemplate === "string" ? filenameTemplate : ""); @@ -2725,13 +2729,18 @@ Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_* }; try { - filenameTemplate = fileManifest.filenameTemplate; - const pathAndInfo = this.getPathWithInfo( - filenameTemplate, - fileManifest.pathOptions - ); - file = pathAndInfo.path; - const assetInfo = pathAndInfo.info; + if ("filename" in fileManifest) { + file = fileManifest.filename; + assetInfo = fileManifest.info; + } else { + filenameTemplate = fileManifest.filenameTemplate; + const pathAndInfo = this.getPathWithInfo( + filenameTemplate, + fileManifest.pathOptions + ); + file = pathAndInfo.path; + assetInfo = pathAndInfo.info; + } if (err) { return errorAndCallback(err); diff --git a/lib/HotModuleReplacementPlugin.js b/lib/HotModuleReplacementPlugin.js index 00afc0983..f82f3d870 100644 --- a/lib/HotModuleReplacementPlugin.js +++ b/lib/HotModuleReplacementPlugin.js @@ -28,6 +28,7 @@ const { find } = require("./util/SetHelpers"); const { compareModulesById } = require("./util/comparators"); /** @typedef {import("./Chunk")} Chunk */ +/** @typedef {import("./Compilation").AssetInfo} AssetInfo */ /** @typedef {import("./Compiler")} Compiler */ /** @typedef {import("./Module")} Module */ @@ -454,13 +455,22 @@ class HotModuleReplacementPlugin { chunkGraph }); for (const entry of renderManifest) { - const { - path: filename, - info: assetInfo - } = compilation.getPathWithInfo( - entry.filenameTemplate, - entry.pathOptions - ); + /** @type {string} */ + let filename; + /** @type {AssetInfo} */ + let assetInfo; + if ("filename" in entry) { + filename = entry.filename; + assetInfo = entry.info; + } else { + ({ + path: filename, + info: assetInfo + } = compilation.getPathWithInfo( + entry.filenameTemplate, + entry.pathOptions + )); + } const source = entry.render(); compilation.additionalChunkAssets.push(filename); compilation.emitAsset(filename, source, { diff --git a/lib/Template.js b/lib/Template.js index 4bd62d150..b446fa139 100644 --- a/lib/Template.js +++ b/lib/Template.js @@ -53,8 +53,10 @@ const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g; * @property {ChunkGraph} chunkGraph */ +/** @typedef {RenderManifestEntryTemplated | RenderManifestEntryStatic} RenderManifestEntry */ + /** - * @typedef {Object} RenderManifestEntry + * @typedef {Object} RenderManifestEntryTemplated * @property {function(): Source} render * @property {string | function(PathData, AssetInfo=): string} filenameTemplate * @property {PathData=} pathOptions @@ -63,6 +65,16 @@ const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g; * @property {boolean=} auxiliary */ +/** + * @typedef {Object} RenderManifestEntryStatic + * @property {function(): Source} render + * @property {string} filename + * @property {AssetInfo} info + * @property {string} identifier + * @property {string=} hash + * @property {boolean=} auxiliary + */ + /** * @typedef {Object} HasId * @property {number | string} id diff --git a/lib/TemplatedPathPlugin.js b/lib/TemplatedPathPlugin.js index 0e32fefd2..141a16c8d 100644 --- a/lib/TemplatedPathPlugin.js +++ b/lib/TemplatedPathPlugin.js @@ -225,7 +225,7 @@ const replacePathVariables = (path, data, assetInfo) => { module instanceof Module ? chunkGraph.getModuleId(module) : module.id ) ); - const hashReplacer = hashLength( + const moduleHashReplacer = hashLength( replacer( module instanceof Module ? chunkGraph.getRenderedModuleHash(module) @@ -234,9 +234,19 @@ const replacePathVariables = (path, data, assetInfo) => { "hashWithLength" in module ? module.hashWithLength : undefined, assetInfo ); + const contentHashReplacer = hashLength( + replacer(data.contentHash), + undefined, + assetInfo + ); replacements.set("id", idReplacer); - replacements.set("hash", hashReplacer); + replacements.set("modulehash", moduleHashReplacer); + replacements.set("contenthash", contentHashReplacer); + replacements.set( + "hash", + data.contentHash ? contentHashReplacer : moduleHashReplacer + ); // Legacy replacements.set( "moduleid", @@ -246,14 +256,6 @@ const replacePathVariables = (path, data, assetInfo) => { "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_ID" ) ); - replacements.set( - "modulehash", - deprecated( - hashReplacer, - "[modulehash] is now [hash]", - "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_HASH" - ) - ); } // Other things diff --git a/lib/asset/AssetGenerator.js b/lib/asset/AssetGenerator.js index 945ea0d00..35a2a72a9 100644 --- a/lib/asset/AssetGenerator.js +++ b/lib/asset/AssetGenerator.js @@ -10,6 +10,7 @@ const path = require("path"); const { RawSource } = require("webpack-sources"); const Generator = require("../Generator"); const RuntimeGlobals = require("../RuntimeGlobals"); +const createHash = require("../util/createHash"); /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../../declarations/plugins/AssetModulesPluginGenerator").AssetModulesPluginGeneratorOptions} AssetModulesPluginGeneratorOptions */ @@ -51,9 +52,8 @@ class AssetGenerator extends Generator { default: { runtimeRequirements.add(RuntimeGlobals.module); + const originalSource = module.originalSource(); if (module.buildInfo.dataUrl) { - const originalSource = module.originalSource(); - let encodedSource; if (typeof this.dataUrlOptions === "function") { encodedSource = this.dataUrlOptions.call( @@ -108,21 +108,39 @@ class AssetGenerator extends Generator { )};` ); } else { - const filename = module.nameForCondition(); const assetModuleFilename = this.filename || runtimeTemplate.outputOptions.assetModuleFilename; - const url = this.compilation.getAssetPath(assetModuleFilename, { + const hash = createHash(runtimeTemplate.outputOptions.hashFunction); + if (runtimeTemplate.outputOptions.hashSalt) { + hash.update(runtimeTemplate.outputOptions.hashSalt); + } + hash.update(originalSource.buffer()); + const fullHash = /** @type {string} */ (hash.digest( + runtimeTemplate.outputOptions.hashDigest + )); + const contentHash = fullHash.slice( + 0, + runtimeTemplate.outputOptions.hashDigestLength + ); + module.buildInfo.fullContentHash = fullHash; + const { + path: filename, + info + } = this.compilation.getAssetPathWithInfo(assetModuleFilename, { module, - filename, - chunkGraph + filename: module.nameForCondition(), + chunkGraph, + contentHash }); + module.buildInfo.filename = filename; + module.buildInfo.assetInfo = info; runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p return new RawSource( `${RuntimeGlobals.module}.exports = ${ RuntimeGlobals.publicPath - } + ${JSON.stringify(url)};` + } + ${JSON.stringify(filename)};` ); } } diff --git a/lib/asset/AssetModulesPlugin.js b/lib/asset/AssetModulesPlugin.js index d8e036402..ad43fd8a7 100644 --- a/lib/asset/AssetModulesPlugin.js +++ b/lib/asset/AssetModulesPlugin.js @@ -148,9 +148,7 @@ class AssetModulesPlugin { compilation.hooks.renderManifest.tap(plugin, (result, options) => { const { chunkGraph } = compilation; - const { chunk, runtimeTemplate, codeGenerationResults } = options; - - const { outputOptions } = runtimeTemplate; + const { chunk, codeGenerationResults } = options; const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType( chunk, @@ -159,23 +157,14 @@ class AssetModulesPlugin { ); if (modules) { for (const module of modules) { - const filename = module.nameForCondition(); - const filenameTemplate = - module.buildInfo.assetFilename || - outputOptions.assetModuleFilename; - result.push({ render: () => codeGenerationResults.get(module).sources.get(type), - filenameTemplate, - pathOptions: { - module, - filename, - chunkGraph - }, + filename: module.buildInfo.filename, + info: module.buildInfo.assetInfo, auxiliary: true, identifier: `assetModule${chunkGraph.getModuleId(module)}`, - hash: chunkGraph.getModuleHash(module) + hash: module.buildInfo.fullContentHash }); } } diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index d4e7f4ba6..74908931c 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -135,10 +135,10 @@ exports[`StatsTestCases should print correct stats for asset 1`] = ` Time: X ms Built at: 1970-04-20 12:42:42 Asset Size +89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [name: (main)] bundle.js 10.9 KiB [emitted] [name: main] -d896afc62bcc5d86ee78.png 14.6 KiB [emitted] [immutable] [name: (main)] static/file.html 12 bytes [emitted] [name: (main)] -Entrypoint main = bundle.js (d896afc62bcc5d86ee78.png static/file.html) +Entrypoint main = bundle.js (89a353e9c515885abd8e.png static/file.html) ./index.js 150 bytes [built] ./images/file.png 42 bytes (javascript) 14.6 KiB (asset) [built] ./images/file.svg 915 bytes [built] diff --git a/test/configCases/asset-modules/_images/file_copy.png b/test/configCases/asset-modules/_images/file_copy.png new file mode 100644 index 000000000..fb53b9ded Binary files /dev/null and b/test/configCases/asset-modules/_images/file_copy.png differ diff --git a/test/configCases/asset-modules/real-content-hash/index.js b/test/configCases/asset-modules/real-content-hash/index.js new file mode 100644 index 000000000..8fe6312b3 --- /dev/null +++ b/test/configCases/asset-modules/real-content-hash/index.js @@ -0,0 +1,6 @@ +import a from "../_images/file.png"; +import b from "../_images/file_copy.png"; + +it("should use a real content hash for assets", () => { + expect(a).toBe(b); +}); diff --git a/test/configCases/asset-modules/real-content-hash/webpack.config.js b/test/configCases/asset-modules/real-content-hash/webpack.config.js new file mode 100644 index 000000000..cf5511ab5 --- /dev/null +++ b/test/configCases/asset-modules/real-content-hash/webpack.config.js @@ -0,0 +1,19 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + mode: "development", + output: { + publicPath: "assets/", + assetModuleFilename: "file[ext]" + }, + module: { + rules: [ + { + test: /\.png$/, + type: "asset/resource" + } + ] + }, + experiments: { + asset: true + } +}; diff --git a/types.d.ts b/types.d.ts index dfbc68a3f..8deaa2a06 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1006,7 +1006,10 @@ declare class Compilation { needAdditionalSeal: SyncBailHook<[], boolean>; afterSeal: AsyncSeriesHook<[]>; renderManifest: SyncWaterfallHook< - [RenderManifestEntry[], RenderManifestOptions] + [ + (RenderManifestEntryTemplated | RenderManifestEntryStatic)[], + RenderManifestOptions + ] >; fullHash: SyncHook<[Hash], void>; chunkHash: SyncHook<[Chunk, Hash, ChunkHashContext], void>; @@ -1203,7 +1206,9 @@ declare class Compilation { getAsset(name: string): Readonly; clearAssets(): void; createModuleAssets(): void; - getRenderManifest(options: RenderManifestOptions): RenderManifestEntry[]; + getRenderManifest( + options: RenderManifestOptions + ): (RenderManifestEntryTemplated | RenderManifestEntryStatic)[]; createChunkAssets(callback: (err?: WebpackError) => void): void; getPath( filename: string | ((arg0: PathData, arg1: AssetInfo) => string), @@ -5849,7 +5854,15 @@ declare interface RenderContextObject { */ codeGenerationResults: Map; } -declare interface RenderManifestEntry { +declare interface RenderManifestEntryStatic { + render: () => Source; + filename: string; + info: AssetInfo; + identifier: string; + hash?: string; + auxiliary?: boolean; +} +declare interface RenderManifestEntryTemplated { render: () => Source; filenameTemplate: string | ((arg0: PathData, arg1: AssetInfo) => string); pathOptions?: PathData;