Repository: rollup/rollup
File: src/utils/FileEmitter.ts (TypeScript)

Code before:
		.update(source)
		.digest('hex')
		.slice(0, Math.max(0, size || defaultHashSize));
}

function pregenerateAssetFileName(
	name: string | undefined,
	source: string | Uint8Array,
	outputOptions: NormalizedOutputOptions
): { fileName: string; sourceHash: string } {
	let sourceHash: string | undefined;
	const emittedName = outputOptions.sanitizeFileName(name || 'asset');
	const fileName = renderNamePattern(
		typeof outputOptions.assetFileNames === 'function'
			? outputOptions.assetFileNames({ name, source, type: 'asset' })
			: outputOptions.assetFileNames,
		'output.assetFileNames',
		{
			ext: () => extname(emittedName).slice(1),
			extname: () => extname(emittedName),
			hash: size => (sourceHash = getSourceHash(source, size)),
			name: () =>
				emittedName.slice(0, Math.max(0, emittedName.length - extname(emittedName).length))
		}
	);
	sourceHash ??= getSourceHash(source);
	return { fileName, sourceHash };
}

function reserveFileNameInBundle(
	fileName: string,
	{ bundle }: FileEmitterOutput,
	warn: WarningHandler
) {
	if (bundle[lowercaseBundleKeys].has(fileName.toLowerCase())) {
		warn(errorFileNameConflict(fileName));
	} else {
		bundle[fileName] = FILE_PLACEHOLDER;
	}
}

interface ConsumedChunk {
	fileName: string | undefined;
	module: null | Module;
	name: string;
	type: 'chunk';
}

interface ConsumedAsset {
	fileName: string | undefined;
	name: string | undefined;

Review comment (bug):
I thought: Why not just `const sourceHash = getSourceHash(source)` at the top? But then I noticed that we allow specifying a hash size. Now this raises a problem, though an extremely edge case one: If for some reason with a dynamic asset file name function two assets with the same source are given file names with different hash length, they are not recognized as identical.
Going even deeper, why would we even be calling the `assetFileNames` function twice in such a situation? What we actually want is that we first calculate the hash, then compare the hash, and only if it does not match we call the function for the name. For this to work reliably, the first hash needs to have the maximum hash length (e.g. by passing `Infinity` as the size, which means we do not slice). Then during the actual file name generation we can slice the hash to the required size. What do you think?

Code after:
}

function generateAssetFileName(
	name: string | undefined,
	source: string | Uint8Array,
	sourceHash: string,
	outputOptions: NormalizedOutputOptions,
	bundle: OutputBundleWithPlaceholders
): string {
	const emittedName = outputOptions.sanitizeFileName(name || 'asset');
	return makeUnique(
		renderNamePattern(
			typeof outputOptions.assetFileNames === 'function'
				? outputOptions.assetFileNames({ name, source, type: 'asset' })
				: outputOptions.assetFileNames,
			'output.assetFileNames',
			{
				ext: () => extname(emittedName).slice(1),
				extname: () => extname(emittedName),
				hash: size => sourceHash.slice(0, Math.max(0, size || defaultHashSize)),
				name: () =>
					emittedName.slice(0, Math.max(0, emittedName.length - extname(emittedName).length))
			}
		),
		bundle
	);
}

function reserveFileNameInBundle(
	fileName: string,
	{ bundle }: FileEmitterOutput,
	warn: WarningHandler
) {
	if (bundle[lowercaseBundleKeys].has(fileName.toLowerCase())) {
		warn(errorFileNameConflict(fileName));
	} else {
		bundle[fileName] = FILE_PLACEHOLDER;
	}
}

interface ConsumedChunk {
	fileName: string | undefined;
	module: null | Module;
	name: string;
	type: 'chunk';
}

interface ConsumedAsset {
	fileName: string | undefined;
	name: string | undefined;
	source: string | Uint8Array | undefined;