Skip to content

Commit 57277bf

Browse files
authored
Implement "output.hashCharacters" option to define character set for file hashes (#5371)
* Add documentation * Add new hashing functions and update hashes The hashes changed due to how they are now encoded * Add new hashing functions in JavaScript * Implement new output.hashCharacters option
1 parent 63a91a6 commit 57277bf

File tree

476 files changed

+852
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

476 files changed

+852
-440
lines changed

browser/src/wasm.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-unresolved
2-
export { parse, xxhashBase64Url } from '../../wasm/bindings_wasm.js';
2+
export { parse, xxhashBase64Url, xxhashBase36, xxhashBase16 } from '../../wasm/bindings_wasm.js';
33

44
// eslint-disable-next-line import/no-unresolved
55
import { parse } from '../../wasm/bindings_wasm.js';

cli/help.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Basic options:
4848
--generatedCode.objectShorthand Use shorthand properties in generated code
4949
--no-generatedCode.reservedNamesAsProps Always quote reserved names as props
5050
--generatedCode.symbols Use symbols in generated code
51+
--hashCharacters <name> Use the specified character set for file hashes
5152
--no-hoistTransitiveImports Do not hoist transitive imports into entry chunks
5253
--no-indent Don't indent result
5354
--inlineDynamicImports Create single bundle when using dynamic imports

docs/command-line-interface/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default {
9090
externalImportAttributes,
9191
footer,
9292
generatedCode,
93+
hashCharacters,
9394
hoistTransitiveImports,
9495
inlineDynamicImports,
9596
interop,
@@ -398,6 +399,7 @@ Many options have command line equivalents. In those cases, any arguments passed
398399
--generatedCode.objectShorthand Use shorthand properties in generated code
399400
--no-generatedCode.reservedNamesAsProps Always quote reserved names as props
400401
--generatedCode.symbols Use symbols in generated code
402+
--hashCharacters <name> Use the specified character set for file hashes
401403
--no-hoistTransitiveImports Do not hoist transitive imports into entry chunks
402404
--no-indent Don't indent result
403405
--inlineDynamicImports Create single bundle when using dynamic imports

docs/configuration-options/index.md

+18-4
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ The pattern to use for naming custom emitted assets to include in the build outp
539539
540540
- `[extname]`: The file extension of the asset including a leading dot, e.g. `.css`.
541541
- `[ext]`: The file extension without a leading dot, e.g. `css`.
542-
- `[hash]`: A hash based on the content of the asset. You can also set a specific hash length via e.g. `[hash:10]`.
542+
- `[hash]`: A hash based on the content of the asset. You can also set a specific hash length via e.g. `[hash:10]`. By default, it will create a base-64 hash. If you need a reduced character sets, see [`output.hashCharacters`](#output-hashcharacters)
543543
- `[name]`: The file name of the asset excluding any extension.
544544
545545
Forward slashes `/` can be used to place files in sub-directories. When using a function, `assetInfo` is a reduced version of the one in [`generateBundle`](../plugin-development/index.md#generatebundle) without the `fileName`. See also [`output.chunkFileNames`](#output-chunkfilenames), [`output.entryFileNames`](#output-entryfilenames).
@@ -585,7 +585,7 @@ See also [`output.intro/output.outro`](#output-intro-output-outro).
585585
The pattern to use for naming shared chunks created when code-splitting, or a function that is called per chunk to return such a pattern. Patterns support the following placeholders:
586586
587587
- `[format]`: The rendering format defined in the output options, e.g. `es` or `cjs`.
588-
- `[hash]`: A hash based only on the content of the final generated chunk, including transformations in [`renderChunk`](../plugin-development/index.md#renderchunk) and any referenced file hashes. You can also set a specific hash length via e.g. `[hash:10]`.
588+
- `[hash]`: A hash based only on the content of the final generated chunk, including transformations in [`renderChunk`](../plugin-development/index.md#renderchunk) and any referenced file hashes. You can also set a specific hash length via e.g. `[hash:10]`. By default, it will create a base-64 hash. If you need a reduced character sets, see [`output.hashCharacters`](#output-hashcharacters)
589589
- `[name]`: The name of the chunk. This can be explicitly set via the [`output.manualChunks`](#output-manualchunks) option or when the chunk is created by a plugin via [`this.emitFile`](../plugin-development/index.md#this-emitfile). Otherwise, it will be derived from the chunk contents.
590590
591591
Forward slashes `/` can be used to place files in sub-directories. When using a function, `chunkInfo` is a reduced version of the one in [`generateBundle`](../plugin-development/index.md#generatebundle) without properties that depend on file names and no information about the rendered modules as rendering only happens after file names have been generated. You can however access a list of included `moduleIds`. See also [`output.assetFileNames`](#output-assetfilenames), [`output.entryFileNames`](#output-entryfilenames).
@@ -661,7 +661,7 @@ Promise.resolve()
661661
The pattern to use for chunks created from entry points, or a function that is called per entry chunk to return such a pattern. Patterns support the following placeholders:
662662
663663
- `[format]`: The rendering format defined in the output options, e.g. `es` or `cjs`.
664-
- `[hash]`: A hash based only on the content of the final generated entry chunk, including transformations in [`renderChunk`](../plugin-development/index.md#renderchunk) and any referenced file hashes. You can also set a specific hash length via e.g. `[hash:10]`.
664+
- `[hash]`: A hash based only on the content of the final generated entry chunk, including transformations in [`renderChunk`](../plugin-development/index.md#renderchunk) and any referenced file hashes. You can also set a specific hash length via e.g. `[hash:10]`. By default, it will create a base-64 hash. If you need a reduced character sets, see [`output.hashCharacters`](#output-hashcharacters)
665665
- `[name]`: The file name (without extension) of the entry point, unless the object form of input was used to define a different name.
666666
667667
Forward slashes `/` can be used to place files in sub-directories. When using a function, `chunkInfo` is a reduced version of the one in [`generateBundle`](../plugin-development/index.md#generatebundle) without properties that depend on file names and no information about the rendered modules as rendering only happens after file names have been generated. You can however access a list of included `moduleIds`. See also [`output.assetFileNames`](#output-assetfilenames), [`output.chunkFileNames`](#output-chunkfilenames).
@@ -863,6 +863,20 @@ const foo = 42;
863863
exports.foo = foo;
864864
```
865865
866+
### output.hashCharacters
867+
868+
| | |
869+
| -------: | :------------------------------ |
870+
| Type: | `"base64" \| "base32" \| "hex"` |
871+
| CLI: | `--hashCharacters <name>` |
872+
| Default: | `"base64"` |
873+
874+
This determines the character set that Rollup is allowed to use in file hashes.
875+
876+
- the default `"base64"` will use url-safe base-64 hashes with potential characters `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`.
877+
- `"base36"` will only use lower-case letters and numbers `abcdefghijklmnopqrstuvwxyz0123456789`.
878+
- `"hex"` will create hexadecimal hashes with characters `abcdef0123456789`.
879+
866880
### output.hoistTransitiveImports
867881
868882
| | |
@@ -1480,7 +1494,7 @@ The location of the generated bundle. If this is an absolute path, all the `sour
14801494
The pattern to use for sourcemaps, or a function that is called per sourcemap to return such a pattern. Patterns support the following placeholders:
14811495

14821496
- `[format]`: The rendering format defined in the output options, e.g. `es` or `cjs`.
1483-
- `[hash]`: A hash based only on the content of the final generated sourcemap. You can also set a specific hash length via e.g. `[hash:10]`.
1497+
- `[hash]`: A hash based only on the content of the final generated sourcemap. You can also set a specific hash length via e.g. `[hash:10]`. By default, it will create a base-64 hash. If you need a reduced character sets, see [`output.hashCharacters`](#output-hashcharacters)
14841498
- `[chunkhash]`: The same hash as the one used for the corresponding generated chunk (if any).
14851499
- `[name]`: The file name (without extension) of the entry point, unless the object form of input was used to define a different name.
14861500

docs/javascript-api/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ const outputOptions = {
165165
externalImportAttributes,
166166
footer,
167167
generatedCode,
168+
hashCharacters,
168169
hoistTransitiveImports,
169170
inlineDynamicImports,
170171
interop,

docs/repl/stores/options.ts

+6
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ export const useOptions = defineStore('options2', () => {
242242
name: 'output.globals',
243243
required: () => true
244244
});
245+
const optionOutputHashCharacters = getSelect({
246+
defaultValue: 'base64',
247+
name: 'output.hashCharacters',
248+
options: () => ['base64', 'base36', 'hex']
249+
});
245250
const optionOutputHoistTransitiveImports = getBoolean({
246251
available: alwaysTrue,
247252
defaultValue: true,
@@ -432,6 +437,7 @@ export const useOptions = defineStore('options2', () => {
432437
optionOutputGeneratedCodeReservedNamesAsProperties,
433438
optionOutputGeneratedCodeSymbols,
434439
optionOutputGlobals,
440+
optionOutputHashCharacters,
435441
optionOutputHoistTransitiveImports,
436442
optionOutputIndent,
437443
optionOutputInlineDynamicImports,

native.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
export function parse(code: string, allowReturnOutsideFunction: boolean): Buffer
77
export function parseAsync(code: string, allowReturnOutsideFunction: boolean, signal?: AbortSignal | undefined | null): Promise<Buffer>
88
export function xxhashBase64Url(input: Uint8Array): string
9+
export function xxhashBase36(input: Uint8Array): string
10+
export function xxhashBase16(input: Uint8Array): string

native.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ const requireWithFriendlyError = id => {
9393
}
9494
};
9595

96-
const { parse, parseAsync, xxhashBase64Url } = requireWithFriendlyError(
96+
const { parse, parseAsync, xxhashBase64Url, xxhashBase36, xxhashBase16 } = requireWithFriendlyError(
9797
existsSync(join(__dirname, localName)) ? localName : `@rollup/rollup-${packageBase}`
9898
);
9999

100100
module.exports.parse = parse;
101101
module.exports.parseAsync = parseAsync;
102102
module.exports.xxhashBase64Url = xxhashBase64Url;
103+
module.exports.xxhashBase36 = xxhashBase36;
104+
module.exports.xxhashBase16 = xxhashBase16;

native.wasm.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
const { parse, xxhashBase64Url } = require('./wasm-node/bindings_wasm.js');
1+
const {
2+
parse,
3+
xxhashBase64Url,
4+
xxhashBase36,
5+
xxhashBase16
6+
} = require('./wasm-node/bindings_wasm.js');
27

38
exports.parse = parse;
49
exports.parseAsync = async (code, allowReturnOutsideFunction, _signal) =>
510
parse(code, allowReturnOutsideFunction);
611
exports.xxhashBase64Url = xxhashBase64Url;
12+
exports.xxhashBase36 = xxhashBase36;
13+
exports.xxhashBase16 = xxhashBase16;

rust/Cargo.lock

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/bindings_napi/src/lib.rs

+10
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,13 @@ pub fn parse_async(
5252
pub fn xxhash_base64_url(input: Uint8Array) -> String {
5353
xxhash::xxhash_base64_url(&input)
5454
}
55+
56+
#[napi]
57+
pub fn xxhash_base36(input: Uint8Array) -> String {
58+
xxhash::xxhash_base36(&input)
59+
}
60+
61+
#[napi]
62+
pub fn xxhash_base16(input: Uint8Array) -> String {
63+
xxhash::xxhash_base16(&input)
64+
}

rust/bindings_wasm/src/lib.rs

+10
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ pub fn parse(code: String, allow_return_outside_function: bool) -> Vec<u8> {
1111
pub fn xxhash_base64_url(input: Uint8Array) -> String {
1212
xxhash::xxhash_base64_url(&input.to_vec())
1313
}
14+
15+
#[wasm_bindgen(js_name=xxhashBase36)]
16+
pub fn xxhash_base36(input: Uint8Array) -> String {
17+
xxhash::xxhash_base36(&input.to_vec())
18+
}
19+
20+
#[wasm_bindgen(js_name=xxhashBase16)]
21+
pub fn xxhash_base16(input: Uint8Array) -> String {
22+
xxhash::xxhash_base16(&input.to_vec())
23+
}

rust/xxhash/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9-
base64 = '0.21.7'
9+
base-encode = "0.3.1"
1010
xxhash-rust = { version = "0.8.8", features = ["xxh3"] }

rust/xxhash/src/lib.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
use base64::{engine::general_purpose, Engine as _};
1+
use base_encode::to_string;
22
use xxhash_rust::xxh3::xxh3_128;
33

4+
const CHARACTERS_BASE64: &[u8; 64] =
5+
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
6+
7+
const CHARACTERS_BASE36: &[u8; 36] = b"abcdefghijklmnopqrstuvwxyz0123456789";
8+
9+
const CHARACTERS_BASE16: &[u8; 16] = b"abcdef0123456789";
10+
411
pub fn xxhash_base64_url(input: &[u8]) -> String {
5-
let hash = xxh3_128(input).to_le_bytes();
6-
general_purpose::URL_SAFE_NO_PAD.encode(hash)
12+
to_string(&xxh3_128(input).to_le_bytes(), 64, CHARACTERS_BASE64).unwrap()
13+
}
14+
15+
pub fn xxhash_base36(input: &[u8]) -> String {
16+
to_string(&xxh3_128(input).to_le_bytes(), 36, CHARACTERS_BASE36).unwrap()
17+
}
18+
19+
pub fn xxhash_base16(input: &[u8]) -> String {
20+
to_string(&xxh3_128(input).to_le_bytes(), 16, CHARACTERS_BASE16).unwrap()
721
}

src/Chunk.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import getIndentString from './utils/getIndentString';
3737
import { getNewArray, getOrCreate } from './utils/getOrCreate';
3838
import { getStaticDependencies } from './utils/getStaticDependencies';
3939
import type { HashPlaceholderGenerator } from './utils/hashPlaceholders';
40-
import { replacePlaceholders } from './utils/hashPlaceholders';
40+
import { DEFAULT_HASH_SIZE, replacePlaceholders } from './utils/hashPlaceholders';
4141
import { makeLegal } from './utils/identifierHelpers';
4242
import {
4343
defaultInteropHelpersByInteropType,
@@ -533,7 +533,8 @@ export default class Chunk {
533533
{
534534
format: () => format,
535535
hash: size =>
536-
hashPlaceholder || (hashPlaceholder = this.getPlaceholder(patternName, size)),
536+
hashPlaceholder ||
537+
(hashPlaceholder = this.getPlaceholder(patternName, size || DEFAULT_HASH_SIZE)),
537538
name: () => this.getChunkName()
538539
}
539540
);
@@ -566,7 +567,8 @@ export default class Chunk {
566567
chunkhash: () => this.getPreliminaryFileName().hashPlaceholder || '',
567568
format: () => format,
568569
hash: size =>
569-
hashPlaceholder || (hashPlaceholder = this.getPlaceholder(patternName, size)),
570+
hashPlaceholder ||
571+
(hashPlaceholder = this.getPlaceholder(patternName, size || DEFAULT_HASH_SIZE)),
570572
name: () => this.getChunkName()
571573
}
572574
);

src/rollup/types.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ type AddonFunction = (chunk: RenderedChunk) => string | Promise<string>;
683683

684684
type OutputPluginOption = MaybePromise<OutputPlugin | NullValue | false | OutputPluginOption[]>;
685685

686+
type HashCharacters = 'base64' | 'base36' | 'hex';
687+
686688
export interface OutputOptions {
687689
amd?: AmdOptions;
688690
assetFileNames?: string | ((chunkInfo: PreRenderedAsset) => string);
@@ -708,6 +710,7 @@ export interface OutputOptions {
708710
freeze?: boolean;
709711
generatedCode?: GeneratedCodePreset | GeneratedCodeOptions;
710712
globals?: GlobalsOption;
713+
hashCharacters?: HashCharacters;
711714
hoistTransitiveImports?: boolean;
712715
indent?: string | boolean;
713716
inlineDynamicImports?: boolean;
@@ -758,6 +761,7 @@ export interface NormalizedOutputOptions {
758761
freeze: boolean;
759762
generatedCode: NormalizedGeneratedCodeOptions;
760763
globals: GlobalsOption;
764+
hashCharacters: HashCharacters;
761765
hoistTransitiveImports: boolean;
762766
indent: true | string;
763767
inlineDynamicImports: boolean;

src/utils/FileEmitter.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import type {
1111
OutputChunk
1212
} from '../rollup/types';
1313
import { BuildPhase } from './buildPhase';
14-
import { getXxhash } from './crypto';
14+
import type { GetHash } from './crypto';
15+
import { getHash64, hasherByType } from './crypto';
1516
import { getOrCreate } from './getOrCreate';
16-
import { defaultHashSize } from './hashPlaceholders';
17+
import { DEFAULT_HASH_SIZE } from './hashPlaceholders';
1718
import { LOGLEVEL_WARN } from './logging';
1819
import {
1920
error,
@@ -50,7 +51,7 @@ function generateAssetFileName(
5051
{
5152
ext: () => extname(emittedName).slice(1),
5253
extname: () => extname(emittedName),
53-
hash: size => sourceHash.slice(0, Math.max(0, size || defaultHashSize)),
54+
hash: size => sourceHash.slice(0, Math.max(0, size || DEFAULT_HASH_SIZE)),
5455
name: () =>
5556
emittedName.slice(0, Math.max(0, emittedName.length - extname(emittedName).length))
5657
}
@@ -155,6 +156,7 @@ interface FileEmitterOutput {
155156
bundle: OutputBundleWithPlaceholders;
156157
fileNamesBySource: Map<string, string>;
157158
outputOptions: NormalizedOutputOptions;
159+
getHash: GetHash;
158160
}
159161

160162
export class FileEmitter {
@@ -254,9 +256,11 @@ export class FileEmitter {
254256
bundle: OutputBundleWithPlaceholders,
255257
outputOptions: NormalizedOutputOptions
256258
): void => {
259+
const getHash = hasherByType[outputOptions.hashCharacters];
257260
const output = (this.output = {
258261
bundle,
259262
fileNamesBySource: new Map<string, string>(),
263+
getHash,
260264
outputOptions
261265
});
262266
for (const emittedFile of this.filesByReferenceId.values()) {
@@ -270,7 +274,7 @@ export class FileEmitter {
270274
if (consumedFile.fileName) {
271275
this.finalizeAdditionalAsset(consumedFile, consumedFile.source, output);
272276
} else {
273-
const sourceHash = getXxhash(consumedFile.source);
277+
const sourceHash = getHash(consumedFile.source);
274278
getOrCreate(consumedAssetsByHash, sourceHash, () => []).push(consumedFile);
275279
}
276280
} else if (consumedFile.type === 'prebuilt-chunk') {
@@ -290,7 +294,7 @@ export class FileEmitter {
290294
let referenceId = idBase;
291295

292296
do {
293-
referenceId = getXxhash(referenceId).slice(0, 8).replaceAll('-', '$');
297+
referenceId = getHash64(referenceId).slice(0, 8).replaceAll('-', '$');
294298
} while (
295299
this.filesByReferenceId.has(referenceId) ||
296300
this.outputFileEmitters.some(({ filesByReferenceId }) => filesByReferenceId.has(referenceId))
@@ -439,13 +443,13 @@ export class FileEmitter {
439443
private finalizeAdditionalAsset(
440444
consumedFile: Readonly<ConsumedAsset>,
441445
source: string | Uint8Array,
442-
{ bundle, fileNamesBySource, outputOptions }: FileEmitterOutput
446+
{ bundle, fileNamesBySource, getHash, outputOptions }: FileEmitterOutput
443447
): void {
444448
let { fileName, needsCodeReference, referenceId } = consumedFile;
445449

446450
// Deduplicate assets if an explicit fileName is not provided
447451
if (!fileName) {
448-
const sourceHash = getXxhash(source);
452+
const sourceHash = getHash(source);
449453
fileName = fileNamesBySource.get(sourceHash);
450454
if (!fileName) {
451455
fileName = generateAssetFileName(

src/utils/crypto.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { xxhashBase64Url } from '../../native';
1+
import { xxhashBase16, xxhashBase36, xxhashBase64Url } from '../../native';
2+
import type { HashCharacters } from '../rollup/types';
23

34
let textEncoder: TextEncoder;
4-
export function getXxhash(input: string | Uint8Array) {
5-
let buffer: Uint8Array;
5+
6+
export type GetHash = (input: string | Uint8Array) => string;
7+
8+
export const getHash64: GetHash = input => xxhashBase64Url(ensureBuffer(input));
9+
export const getHash36: GetHash = input => xxhashBase36(ensureBuffer(input));
10+
export const getHash16: GetHash = input => xxhashBase16(ensureBuffer(input));
11+
12+
export const hasherByType: Record<HashCharacters, GetHash> = {
13+
base36: getHash36,
14+
base64: getHash64,
15+
hex: getHash16
16+
};
17+
18+
function ensureBuffer(input: string | Uint8Array): Uint8Array {
619
if (typeof input === 'string') {
720
if (typeof Buffer === 'undefined') {
821
textEncoder ??= new TextEncoder();
9-
buffer = textEncoder.encode(input);
10-
} else {
11-
buffer = Buffer.from(input);
22+
return textEncoder.encode(input);
1223
}
13-
} else {
14-
buffer = input;
24+
return Buffer.from(input);
1525
}
16-
return xxhashBase64Url(buffer);
26+
return input;
1727
}

0 commit comments

Comments
 (0)