Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only set asset names when finalizing #4919

Merged
merged 3 commits into from Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 78 additions & 30 deletions src/utils/FileEmitter.ts
Expand Up @@ -21,6 +21,7 @@ import {
errorInvalidRollupPhaseForChunkEmission,
errorNoAssetSourceSet
} from './error';
import { getOrCreate } from './getOrCreate';
import { defaultHashSize } from './hashPlaceholders';
import type { OutputBundleWithPlaceholders } from './outputBundle';
import { FILE_PLACEHOLDER, lowercaseBundleKeys } from './outputBundle';
Expand Down Expand Up @@ -74,13 +75,15 @@ interface ConsumedChunk {
fileName: string | undefined;
module: null | Module;
name: string;
referenceId: string;
type: 'chunk';
}

interface ConsumedAsset {
fileName: string | undefined;
name: string | undefined;
needsCodeReference: boolean;
referenceId: string;
source: string | Uint8Array | undefined;
type: 'asset';
}
Expand Down Expand Up @@ -231,11 +234,11 @@ export class FileEmitter {
}
const source = getValidSource(requestedSource, consumedFile, referenceId);
if (this.output) {
this.finalizeAsset(consumedFile, source, referenceId, this.output);
this.finalizeAdditionalAsset(consumedFile, source, this.output);
} else {
consumedFile.source = source;
for (const emitter of this.outputFileEmitters) {
emitter.finalizeAsset(consumedFile, source, referenceId, emitter.output!);
emitter.finalizeAdditionalAsset(consumedFile, source, emitter.output!);
}
}
};
Expand All @@ -258,11 +261,20 @@ export class FileEmitter {
reserveFileNameInBundle(emittedFile.fileName, output, this.options.onwarn);
}
}
for (const [referenceId, consumedFile] of this.filesByReferenceId) {
const consumedAssetsByHash = new Map<string, ConsumedAsset[]>();
for (const consumedFile of this.filesByReferenceId.values()) {
if (consumedFile.type === 'asset' && consumedFile.source !== undefined) {
this.finalizeAsset(consumedFile, consumedFile.source, referenceId, output);
if (consumedFile.fileName) {
this.finalizeAdditionalAsset(consumedFile, consumedFile.source, output);
} else {
const sourceHash = getSourceHash(consumedFile.source);
getOrCreate(consumedAssetsByHash, sourceHash, () => []).push(consumedFile);
}
}
}
for (const [sourceHash, consumedFiles] of consumedAssetsByHash) {
this.finalizeAssetsWithSameSource(consumedFiles, sourceHash, output);
}
};

private addOutputFileEmitter(outputFileEmitter: FileEmitter) {
Expand All @@ -278,6 +290,7 @@ export class FileEmitter {
this.filesByReferenceId.has(referenceId) ||
this.outputFileEmitters.some(({ filesByReferenceId }) => filesByReferenceId.has(referenceId))
);
file.referenceId = referenceId;
this.filesByReferenceId.set(referenceId, file);
for (const { filesByReferenceId } of this.outputFileEmitters) {
filesByReferenceId.set(referenceId, file);
Expand All @@ -294,6 +307,7 @@ export class FileEmitter {
fileName: emittedAsset.fileName,
name: emittedAsset.name,
needsCodeReference: !!emittedAsset.needsCodeReference,
referenceId: '',
source,
type: 'asset'
};
Expand All @@ -302,26 +316,25 @@ export class FileEmitter {
emittedAsset.fileName || emittedAsset.name || String(this.nextIdBase++)
);
if (this.output) {
this.emitAssetWithReferenceId(consumedAsset, referenceId, this.output);
this.emitAssetWithReferenceId(consumedAsset, this.output);
} else {
for (const fileEmitter of this.outputFileEmitters) {
fileEmitter.emitAssetWithReferenceId(consumedAsset, referenceId, fileEmitter.output!);
fileEmitter.emitAssetWithReferenceId(consumedAsset, fileEmitter.output!);
}
}
return referenceId;
}

private emitAssetWithReferenceId(
consumedAsset: Readonly<ConsumedAsset>,
referenceId: string,
output: FileEmitterOutput
) {
const { fileName, source } = consumedAsset;
if (fileName) {
reserveFileNameInBundle(fileName, output, this.options.onwarn);
}
if (source !== undefined) {
this.finalizeAsset(consumedAsset, source, referenceId, output);
this.finalizeAdditionalAsset(consumedAsset, source, output);
}
}

Expand All @@ -340,6 +353,7 @@ export class FileEmitter {
fileName: emittedChunk.fileName,
module: null,
name: emittedChunk.name || emittedChunk.id,
referenceId: '',
type: 'chunk'
};
this.graph.moduleLoader
Expand All @@ -353,48 +367,82 @@ export class FileEmitter {
return this.assignReferenceId(consumedChunk, emittedChunk.id);
}

private finalizeAsset(
private finalizeAdditionalAsset(
consumedFile: Readonly<ConsumedAsset>,
source: string | Uint8Array,
referenceId: string,
{ bundle, fileNamesBySource, outputOptions }: FileEmitterOutput
): void {
let fileName = consumedFile.fileName;
let { fileName, needsCodeReference, referenceId } = consumedFile;

// Deduplicate assets if an explicit fileName is not provided
if (!fileName) {
const sourceHash = getSourceHash(source);
fileName = fileNamesBySource.get(sourceHash);
const newFileName = generateAssetFileName(
consumedFile.name,
if (!fileName) {
fileName = generateAssetFileName(
consumedFile.name,
source,
sourceHash,
outputOptions,
bundle
);
fileNamesBySource.set(sourceHash, fileName);
}
}

// We must not modify the original assets to avoid interaction between outputs
const assetWithFileName = { ...consumedFile, fileName, source };
this.filesByReferenceId.set(referenceId, assetWithFileName);

const existingAsset = bundle[fileName];
if (existingAsset?.type === 'asset') {
existingAsset.needsCodeReference &&= needsCodeReference;
} else {
bundle[fileName] = {
fileName,
name: consumedFile.name,
needsCodeReference,
source,
type: 'asset'
};
}
}

private finalizeAssetsWithSameSource(
consumedFiles: ReadonlyArray<ConsumedAsset>,
sourceHash: string,
{ bundle, fileNamesBySource, outputOptions }: FileEmitterOutput
): void {
let fileName = '';
let usedConsumedFile: ConsumedAsset;
let needsCodeReference = true;
for (const consumedFile of consumedFiles) {
needsCodeReference &&= consumedFile.needsCodeReference;
const assetFileName = generateAssetFileName(
consumedFile.name,
consumedFile.source!,
sourceHash,
outputOptions,
bundle
);
// make sure file name deterministic in parallel emits, always use the shorter and smaller file name
if (
!fileName ||
fileName.length > newFileName.length ||
(fileName.length === newFileName.length && fileName > newFileName)
) {
if (fileName) {
delete bundle[fileName];
}
fileName = newFileName;
fileNamesBySource.set(sourceHash, fileName);
if (!fileName || assetFileName < fileName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fileName sort strategy is a bit different from #4912. The previous code always uses the shortest fileName while this one doesn't; I think it's better to keep the same strategy, otherwise this would be a breaking change.

fileName = assetFileName;
usedConsumedFile = consumedFile;
}
}
fileNamesBySource.set(sourceHash, fileName);

// We must not modify the original assets to avoid interaction between outputs
const assetWithFileName = { ...consumedFile, fileName, source };
this.filesByReferenceId.set(referenceId, assetWithFileName);
for (const consumedFile of consumedFiles) {
// We must not modify the original assets to avoid interaction between outputs
const assetWithFileName = { ...consumedFile, fileName };
this.filesByReferenceId.set(consumedFile.referenceId, assetWithFileName);
}

bundle[fileName] = {
fileName,
name: consumedFile.name,
needsCodeReference: consumedFile.needsCodeReference,
source,
name: usedConsumedFile!.name,
needsCodeReference,
source: usedConsumedFile!.source!,
type: 'asset'
};
}
Expand Down
74 changes: 64 additions & 10 deletions test/chunking-form/samples/emit-file/deduplicate-assets/_config.js
@@ -1,40 +1,59 @@
const assert = require('node:assert');
let string1Id,
string2Id,
stringSameSourceId,
stringSameAsBufferId,
otherStringId,
bufferId,
bufferSameSourceId,
sameBufferAsStringId,
otherBufferId;

module.exports = {
description: 'deduplicates asset that have the same source',
options: {
input: ['main.js'],
plugins: {
buildStart() {
// emit 'string' source in a random order
this.emitFile({ type: 'asset', name: 'stringSameSource.txt', source: 'string' });
this.emitFile({ type: 'asset', name: 'string2.txt', source: 'string' });
this.emitFile({ type: 'asset', name: 'string1.txt', source: 'string' });
this.emitFile({
stringSameSourceId = this.emitFile({
type: 'asset',
name: 'sameStringAsBuffer.txt',
name: 'stringSameSource.txt',
source: 'string'
});
string2Id = this.emitFile({ type: 'asset', name: 'string2.txt', source: 'string' });
string1Id = this.emitFile({ type: 'asset', name: 'string1.txt', source: 'string' });
stringSameAsBufferId = this.emitFile({
type: 'asset',
name: 'stringSameAsBuffer.txt',
source: Buffer.from('string') // Test cross Buffer/string deduplication
});

// Different string source
this.emitFile({ type: 'asset', name: 'otherString.txt', source: 'otherString' });
otherStringId = this.emitFile({
type: 'asset',
name: 'otherString.txt',
source: 'otherString'
});

const bufferSource = () => Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
this.emitFile({
bufferId = this.emitFile({
type: 'asset',
name: 'buffer.txt',
source: bufferSource()
});
this.emitFile({
bufferSameSourceId = this.emitFile({
type: 'asset',
name: 'bufferSameSource.txt',
source: bufferSource()
});
this.emitFile({
sameBufferAsStringId = this.emitFile({
type: 'asset',
name: 'sameBufferAsString.txt',
source: bufferSource().toString() // Test cross Buffer/string deduplication
});
// Different buffer source
this.emitFile({
otherBufferId = this.emitFile({
type: 'asset',
name: 'otherBuffer.txt',
source: Buffer.from('otherBuffer')
Expand All @@ -48,6 +67,41 @@ module.exports = {
source: bufferSource()
});
return null;
},
generateBundle() {
assert.strictEqual(this.getFileName(string1Id), 'assets/string1-473287f8.txt', 'string1');
assert.strictEqual(this.getFileName(string2Id), 'assets/string1-473287f8.txt', 'string2');
assert.strictEqual(
this.getFileName(stringSameSourceId),
'assets/string1-473287f8.txt',
'stringSameSource'
);
assert.strictEqual(
this.getFileName(stringSameAsBufferId),
'assets/string1-473287f8.txt',
'stringSameAsBuffer'
);
assert.strictEqual(
this.getFileName(otherStringId),
'assets/otherString-e296c1ca.txt',
'otherString'
);
assert.strictEqual(this.getFileName(bufferId), 'assets/buffer-d0ca8c2a.txt', 'buffer');
assert.strictEqual(
this.getFileName(bufferSameSourceId),
'assets/buffer-d0ca8c2a.txt',
'bufferSameSource'
);
assert.strictEqual(
this.getFileName(sameBufferAsStringId),
'assets/buffer-d0ca8c2a.txt',
'sameBufferAsString'
);
assert.strictEqual(
this.getFileName(otherBufferId),
'assets/otherBuffer-e8d9b528.txt',
'otherBuffer'
);
}
}
}
Expand Down