Skip to content

Commit ecc00c0

Browse files
Support inline hash digest/length in path placeholders and CSS localIdentName (#21259)
* Support inline digest/length in CSS module localIdentName hash placeholders Normalize css-loader / loader-utils style `[hash:<digest>:<length>]` placeholders in the native CSS generator: fold the digest and length into the local ident hash options and map `[hash]`/`[fullhash]` to the local ident hash (css-loader semantics) so suffixed placeholders like `[fullhash:6]` and `[hash:base64:6]` work. * Support inline digest in hash path placeholders Extend TemplatedPathPlugin.interpolate to accept `[hash:<digest>:<length>]` (e.g. `[contenthash:base64:8]`) for filename/path templates, re-encoding the stored hash into the requested digest. The source hash is already truncated to output.hashDigestLength, so the result derives from those bytes. * Revert CSS localIdentName-specific hash handling Drop the css-loader-specific localIdentName hash remapping; inline digest and length are handled generically in TemplatedPathPlugin.interpolate. * Support inline hash digest and length in CSS localIdentName Make the localIdent hash-detection regexes suffix-tolerant so [fullhash:N] and [contenthash:...] trigger hash computation, and pass the output digest into interpolate so an inline digest on [hash]/[contenthash] re-encodes correctly. * Centralize placeholder-kind detection in TemplatedPathPlugin Export getPresentKinds as the single source of truth for which [kind] placeholders a template references, and route CssGenerator, RuntimePlugin and SourceMapDevToolPlugin through it instead of their own hand-rolled regexes so they can't drift from interpolate's grammar. * Throw on an unknown inline hash digest in path templates Re-encoding silently kept the source encoding for a typo'd digest; validate the requested digest and throw a clear error instead, matching how interpolate already rejects unimplemented path variables. * Re-encode [fullhash]/[chunkhash] inline digests from the full hash Inline-digest path re-encoded the hashDigestLength-truncated hash, capping entropy below the requested length. Pass the untruncated compilation/chunk hash (which already exists) so [fullhash:<digest>] and [chunkhash:<digest>] carry full entropy and match an encode-then-slice result. * Reject inline digest on [contenthash] when realContentHash is enabled RealContentHashPlugin recomputes content hashes in output.hashDigest and would silently overwrite an inline-digest [contenthash] with a hex hash. Throw a clear error for that combination instead of emitting the wrong encoding. * Support inline digest on [fullhash] in CSS localIdentName The local ident hash uses localIdentHashDigest, not output.hashDigest, so an inline [fullhash:<digest>] re-encoded from the wrong source. Pass the untruncated local ident hash and its own digest (fullHashDigest) so interpolate re-encodes it correctly with full entropy. * Extract digestNonNumericOnly content-hash helper The contentHash producers all did nonNumericOnlyHash(hash.digest(d), n); wrap that composition in one helper and route JavascriptModulesPlugin, CssModulesPlugin, HtmlModulesPlugin and getLocalIdent through it. AssetGenerator keeps the inline form since it also returns the untruncated digest. * Resolve CSS localIdentName [hash] to the local ident hash, not the module hash Fixes issue point 2: the module-context block in interpolate repurposed [hash] to the module hash, diverging from css-loader. Add a data.hashAsFullHash flag that getLocalIdent sets so [hash] stays the local ident hash (like [fullhash]); [modulehash] still yields the module hash. Closes #21141 (point 2). * Fix test types and update CSS cache snapshots * Support base64url hash digest on Node < 14.18
1 parent 9c64a63 commit ecc00c0

24 files changed

Lines changed: 830 additions & 146 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Support inline hash digest and length in CSS module `localIdentName` placeholders.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
CSS `localIdentName` `[hash]` now resolves to the local ident hash (matching css-loader); use `[modulehash]` for the module hash.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Support an inline digest in hash path placeholders, e.g. `[contenthash:base64:8]`.

lib/Compilation.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ const { isSourceEqual } = require("./util/source");
358358
* @typedef {object} PathData
359359
* @property {ChunkGraph=} chunkGraph
360360
* @property {string=} hash
361+
* @property {string=} fullHash untruncated compilation hash, for re-encoding `[fullhash:<digest>]`
362+
* @property {string=} fullHashDigest digest `fullHash` is encoded in (defaults to `hashDigest`)
361363
* @property {HashWithLengthFunction=} hashWithLength
362364
* @property {(Chunk | ChunkPathData)=} chunk
363365
* @property {(Module | ModulePathData)=} module
@@ -368,6 +370,9 @@ const { isSourceEqual } = require("./util/source");
368370
* @property {string=} contentHashType
369371
* @property {string=} contentHash
370372
* @property {HashWithLengthFunction=} contentHashWithLength
373+
* @property {string=} hashDigest digest the stored hashes are encoded in (for `[hash:<digest>]`)
374+
* @property {boolean=} realContentHash whether `optimization.realContentHash` recomputes content hashes (rejects an inline digest on `[contenthash]`)
375+
* @property {boolean=} hashAsFullHash treat `[hash]` as `[fullhash]` rather than the module hash (CSS local idents)
371376
* @property {boolean=} noChunkHash
372377
* @property {string=} url
373378
* @property {string=} local

lib/RuntimePlugin.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"use strict";
77

88
const RuntimeGlobals = require("./RuntimeGlobals");
9+
const { getPresentKinds } = require("./TemplatedPathPlugin");
910
const RuntimeRequirementsDependency = require("./dependencies/RuntimeRequirementsDependency");
1011
const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
1112
const AsyncModuleRuntimeModule = require("./runtime/AsyncModuleRuntimeModule");
@@ -114,7 +115,14 @@ const TREE_DEPENDENCIES = {
114115
[RuntimeGlobals.shareScopeMap]: [RuntimeGlobals.hasOwnProperty]
115116
};
116117

117-
const FULLHASH_REGEXP = /\[(?:full)?hash(?::\d+)?\]/;
118+
/**
119+
* @param {string} template path template
120+
* @returns {boolean} true when it references the compilation `[fullhash]`/`[hash]`
121+
*/
122+
const usesFullHash = (template) => {
123+
const kinds = getPresentKinds(template);
124+
return kinds.has("fullhash") || kinds.has("hash");
125+
};
118126

119127
const PLUGIN_NAME = "RuntimePlugin";
120128

@@ -280,10 +288,7 @@ class RuntimePlugin {
280288
} else {
281289
const module = new PublicPathRuntimeModule(publicPath);
282290

283-
if (
284-
typeof publicPath !== "string" ||
285-
FULLHASH_REGEXP.test(publicPath)
286-
) {
291+
if (typeof publicPath !== "string" || usesFullHash(publicPath)) {
287292
module.fullHash = true;
288293
}
289294

@@ -330,7 +335,7 @@ class RuntimePlugin {
330335
.tap(PLUGIN_NAME, (chunk, set, { chunkGraph }) => {
331336
if (
332337
typeof compilation.outputOptions.chunkFilename === "string" &&
333-
FULLHASH_REGEXP.test(compilation.outputOptions.chunkFilename)
338+
usesFullHash(compilation.outputOptions.chunkFilename)
334339
) {
335340
set.add(RuntimeGlobals.getFullHash);
336341
}
@@ -361,7 +366,7 @@ class RuntimePlugin {
361366
.tap(PLUGIN_NAME, (chunk, set, { chunkGraph }) => {
362367
if (
363368
typeof compilation.outputOptions.cssChunkFilename === "string" &&
364-
FULLHASH_REGEXP.test(compilation.outputOptions.cssChunkFilename)
369+
usesFullHash(compilation.outputOptions.cssChunkFilename)
365370
) {
366371
set.add(RuntimeGlobals.getFullHash);
367372
}
@@ -390,11 +395,7 @@ class RuntimePlugin {
390395
compilation.hooks.runtimeRequirementInTree
391396
.for(RuntimeGlobals.getChunkUpdateScriptFilename)
392397
.tap(PLUGIN_NAME, (chunk, set) => {
393-
if (
394-
FULLHASH_REGEXP.test(
395-
compilation.outputOptions.hotUpdateChunkFilename
396-
)
397-
) {
398+
if (usesFullHash(compilation.outputOptions.hotUpdateChunkFilename)) {
398399
set.add(RuntimeGlobals.getFullHash);
399400
}
400401
compilation.addRuntimeModule(
@@ -412,11 +413,7 @@ class RuntimePlugin {
412413
compilation.hooks.runtimeRequirementInTree
413414
.for(RuntimeGlobals.getUpdateManifestFilename)
414415
.tap(PLUGIN_NAME, (chunk, set) => {
415-
if (
416-
FULLHASH_REGEXP.test(
417-
compilation.outputOptions.hotUpdateMainFilename
418-
)
419-
) {
416+
if (usesFullHash(compilation.outputOptions.hotUpdateMainFilename)) {
420417
set.add(RuntimeGlobals.getFullHash);
421418
}
422419
compilation.addRuntimeModule(

lib/SourceMapDevToolPlugin.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Compilation = require("./Compilation");
1111
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
1212
const ProgressPlugin = require("./ProgressPlugin");
1313
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
14+
const { getPresentKinds } = require("./TemplatedPathPlugin");
1415
const createHash = require("./util/createHash");
1516
const { dirname, relative } = require("./util/fs");
1617
const generateDebugId = require("./util/generateDebugId");
@@ -46,7 +47,6 @@ const { makePathsAbsolute } = require("./util/identifier");
4647
*/
4748

4849
const METACHARACTERS_REGEXP = /[-[\]\\/{}()*+?.^$|]/g;
49-
const CONTENT_HASH_DETECT_REGEXP = /\[contenthash(?::\w+)?\]/;
5050
const CSS_AND_JS_MODULE_EXTENSIONS_REGEXP = /\.((c|m)?js|css)($|\?)/i;
5151
const CSS_EXTENSION_DETECT_REGEXP = /\.css(?:$|\?)/i;
5252
const MAP_URL_COMMENT_REGEXP = /\[map\]/g;
@@ -681,10 +681,8 @@ class SourceMapDevToolPlugin {
681681
}
682682

683683
const usesContentHash =
684-
sourceMapFilename &&
685-
CONTENT_HASH_DETECT_REGEXP.test(sourceMapFilename);
686-
687-
resetRegexpState(CONTENT_HASH_DETECT_REGEXP);
684+
typeof sourceMapFilename === "string" &&
685+
getPresentKinds(sourceMapFilename).has("contenthash");
688686

689687
let outputFile = file;
690688
// If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`

0 commit comments

Comments
 (0)