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 all commits
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
106 changes: 79 additions & 27 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,86 @@ 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)
assetFileName.length < fileName.length ||
(assetFileName.length === fileName.length && assetFileName < fileName)
) {
if (fileName) {
delete bundle[fileName];
}
fileName = newFileName;
fileNamesBySource.set(sourceHash, fileName);
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
@@ -0,0 +1,98 @@
const assert = require('node:assert');

module.exports = {
description:
'emits unreferenced assets if needsCodeReference is true if they are also emitted without that flag',
options: {
output: {
assetFileNames: '[name][extname]'
},
plugins: [
{
name: 'test',
buildStart() {
this.emitFile({
type: 'asset',
name: 'needs-reference1.txt',
needsCodeReference: true,
source: 'source1'
});
this.emitFile({
type: 'asset',
name: 'file1.txt',
source: 'source1'
});
this.emitFile({
type: 'asset',
name: 'file2.txt',
source: 'source2'
});
this.emitFile({
type: 'asset',
name: 'needs-reference2.txt',
needsCodeReference: true,
source: 'source2'
});
this.emitFile({
type: 'asset',
name: 'needs-reference3.txt',
needsCodeReference: true,
source: 'source3'
});
this.emitFile({
type: 'asset',
name: 'file4.txt',
source: 'source4'
});
this.emitFile({
type: 'asset',
name: 'needs-reference5.txt',
needsCodeReference: true,
source: 'source5'
});
this.emitFile({
type: 'asset',
name: 'file6.txt',
source: 'source6'
});
},
renderStart() {
this.emitFile({
type: 'asset',
name: 'file3.txt',
source: 'source3'
});
this.emitFile({
type: 'asset',
name: 'needs-reference4.txt',
needsCodeReference: true,
source: 'source4'
});
},
generateBundle(_, bundle) {
this.emitFile({
type: 'asset',
name: 'file5.txt',
source: 'source5'
});
this.emitFile({
type: 'asset',
name: 'needs-reference6.txt',
needsCodeReference: true,
source: 'source6'
});

assert.deepEqual(Object.keys(bundle).sort(), [
'file1.txt',
'file2.txt',
'file4.txt',
'file6.txt',
'main.js',
'needs-reference3.txt',
'needs-reference5.txt'
]);
}
}
]
}
};
@@ -0,0 +1 @@
source1
@@ -0,0 +1 @@
source2
@@ -0,0 +1 @@
source4
@@ -0,0 +1 @@
source6
@@ -0,0 +1,5 @@
define((function () { 'use strict';

console.log('main');

}));
@@ -0,0 +1 @@
source3
@@ -0,0 +1 @@
source5
@@ -0,0 +1 @@
source1
@@ -0,0 +1 @@
source2
@@ -0,0 +1 @@
source4
@@ -0,0 +1 @@
source6
@@ -0,0 +1,3 @@
'use strict';

console.log('main');
@@ -0,0 +1 @@
source3
@@ -0,0 +1 @@
source5
@@ -0,0 +1 @@
source1
@@ -0,0 +1 @@
source2
@@ -0,0 +1 @@
source4
@@ -0,0 +1 @@
source6
@@ -0,0 +1 @@
console.log('main');
@@ -0,0 +1 @@
source3
@@ -0,0 +1 @@
source5
@@ -0,0 +1 @@
source1
@@ -0,0 +1 @@
source2
@@ -0,0 +1 @@
source4
@@ -0,0 +1 @@
source6