Skip to content

Commit a375b37

Browse files
committed
fix #2071: remap bytesInOutput for substitutions
closes #2091
1 parent 0e12657 commit a375b37

File tree

4 files changed

+89
-44
lines changed

4 files changed

+89
-44
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
}
2727
```
2828

29+
* Fix byte counts in metafile regarding references to other output files ([#2071](https://github.com/evanw/esbuild/issues/2071))
30+
31+
Previously files that contained references to other output files had slightly incorrect metadata for the byte counts of input files which contributed to that output file. So for example if `app.js` imports `image.png` using the file loader and esbuild generates `out.js` and `image-LSAMBFUD.png`, the metadata for how many bytes of `out.js` are from `app.js` was slightly off (the metadata for the byte count of `out.js` was still correct). The reason is because esbuild substitutes the final paths for references between output files toward the end of the build to handle cyclic references, and the byte counts needed to be adjusted as well during the path substitution. This release fixes these byte counts (specifically the `bytesInOutput` values).
32+
2933
* The alias feature now strips a trailing slash ([#2730](https://github.com/evanw/esbuild/issues/2730))
3034

3135
People sometimes add a trailing slash to the name of one of node's built-in modules to force node to import from the file system instead of importing the built-in module. For example, importing `util` imports node's built-in module called `util` but importing `util/` tries to find a package called `util` on the file system. Previously attempting to use esbuild's package alias feature to replace imports to `util` with a specific file would fail because the file path would also gain a trailing slash (e.g. mapping `util` to `./file.js` turned `util/` into `./file.js/`). With this release, esbuild will now omit the path suffix if it's a single trailing slash, which should now allow you to successfully apply aliases to these import paths.

internal/bundler/linker.go

+77-36
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
614614
// Path substitution for the JSON metadata
615615
var jsonMetadataChunk string
616616
if c.options.NeedsMetafile {
617-
jsonMetadataChunkPieces := c.breakOutputIntoPieces(chunk.jsonMetadataChunkCallback(len(outputContents)))
617+
jsonMetadataChunkPieces := c.breakJoinerIntoPieces(chunk.jsonMetadataChunkCallback(len(outputContents)))
618618
jsonMetadataChunkBytes, _ := c.substituteFinalPaths(jsonMetadataChunkPieces, func(finalRelPathForImport string) string {
619619
return c.res.PrettyPath(logger.Path{Text: c.fs.Join(c.options.AbsOutputDir, finalRelPathForImport), Namespace: "file"})
620620
})
@@ -704,6 +704,37 @@ func (c *linkerContext) substituteFinalPaths(
704704
return
705705
}
706706

707+
func (c *linkerContext) accurateFinalByteCount(output intermediateOutput, chunkFinalRelDir string) int {
708+
count := 0
709+
710+
// Note: The paths generated here must match "substituteFinalPaths" above
711+
for _, piece := range output.pieces {
712+
count += len(piece.data)
713+
714+
switch piece.kind {
715+
case outputPieceAssetIndex:
716+
file := c.graph.Files[piece.index]
717+
if len(file.InputFile.AdditionalFiles) != 1 {
718+
panic("Internal error")
719+
}
720+
relPath, _ := c.fs.Rel(c.options.AbsOutputDir, file.InputFile.AdditionalFiles[0].AbsPath)
721+
722+
// Make sure to always use forward slashes, even on Windows
723+
relPath = strings.ReplaceAll(relPath, "\\", "/")
724+
725+
importPath := c.pathBetweenChunks(chunkFinalRelDir, relPath)
726+
count += len(importPath)
727+
728+
case outputPieceChunkIndex:
729+
chunk := c.chunks[piece.index]
730+
importPath := c.pathBetweenChunks(chunkFinalRelDir, chunk.finalRelPath)
731+
count += len(importPath)
732+
}
733+
}
734+
735+
return count
736+
}
737+
707738
func (c *linkerContext) pathBetweenChunks(fromRelDir string, toRelPath string) string {
708739
// Join with the public path if it has been configured
709740
if c.options.PublicPath != "" {
@@ -5036,12 +5067,12 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai
50365067
var compileResultsForSourceMap []compileResultForSourceMap
50375068
var legalCommentList []string
50385069
var metaOrder []uint32
5039-
var metaByteCount map[string]int
5070+
var metaBytes map[uint32][][]byte
50405071
legalCommentSet := make(map[string]bool)
50415072
prevFileNameComment := uint32(0)
50425073
if c.options.NeedsMetafile {
50435074
metaOrder = make([]uint32, 0, len(compileResults))
5044-
metaByteCount = make(map[string]int, len(compileResults))
5075+
metaBytes = make(map[uint32][][]byte, len(compileResults))
50455076
}
50465077
for _, compileResult := range compileResults {
50475078
isRuntime := compileResult.sourceIndex == runtime.SourceIndex
@@ -5104,13 +5135,11 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai
51045135
// Include this file in the metadata
51055136
if c.options.NeedsMetafile {
51065137
// Accumulate file sizes since a given file may be split into multiple parts
5107-
path := c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath
5108-
if count, ok := metaByteCount[path]; ok {
5109-
metaByteCount[path] = count + len(compileResult.JS)
5110-
} else {
5138+
bytes, ok := metaBytes[compileResult.sourceIndex]
5139+
if !ok {
51115140
metaOrder = append(metaOrder, compileResult.sourceIndex)
5112-
metaByteCount[path] = len(compileResult.JS)
51135141
}
5142+
metaBytes[compileResult.sourceIndex] = append(bytes, compileResult.JS)
51145143
}
51155144
}
51165145

@@ -5148,7 +5177,7 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai
51485177
}
51495178

51505179
// The JavaScript contents are done now that the source map comment is in
5151-
chunk.intermediateOutput = c.breakOutputIntoPieces(j)
5180+
chunk.intermediateOutput = c.breakJoinerIntoPieces(j)
51525181
timer.End("Join JavaScript files")
51535182

51545183
if c.options.SourceMap != config.SourceMapNone {
@@ -5161,20 +5190,30 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai
51615190
// End the metadata lazily. The final output size is not known until the
51625191
// final import paths are substituted into the output pieces generated below.
51635192
if c.options.NeedsMetafile {
5193+
pieces := make([][]intermediateOutput, len(metaOrder))
5194+
for i, sourceIndex := range metaOrder {
5195+
slices := metaBytes[sourceIndex]
5196+
outputs := make([]intermediateOutput, len(slices))
5197+
for j, slice := range slices {
5198+
outputs[j] = c.breakOutputIntoPieces(slice)
5199+
}
5200+
pieces[i] = outputs
5201+
}
51645202
chunk.jsonMetadataChunkCallback = func(finalOutputSize int) helpers.Joiner {
5165-
isFirstMeta := true
5166-
for _, sourceIndex := range metaOrder {
5167-
if isFirstMeta {
5168-
isFirstMeta = false
5169-
} else {
5203+
finalRelDir := c.fs.Dir(chunk.finalRelPath)
5204+
for i, sourceIndex := range metaOrder {
5205+
if i > 0 {
51705206
jMeta.AddString(",")
51715207
}
5172-
path := c.graph.Files[sourceIndex].InputFile.Source.PrettyPath
5173-
extra := c.generateExtraDataForFileJS(sourceIndex)
5174-
jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n %s}",
5175-
helpers.QuoteForJSON(path, c.options.ASCIIOnly), metaByteCount[path], extra))
5208+
count := 0
5209+
for _, output := range pieces[i] {
5210+
count += c.accurateFinalByteCount(output, finalRelDir)
5211+
}
5212+
jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n }",
5213+
helpers.QuoteForJSON(c.graph.Files[sourceIndex].InputFile.Source.PrettyPath, c.options.ASCIIOnly),
5214+
count))
51765215
}
5177-
if !isFirstMeta {
5216+
if len(metaOrder) > 0 {
51785217
jMeta.AddString("\n ")
51795218
}
51805219
jMeta.AddString(fmt.Sprintf("},\n \"bytes\": %d\n }", finalOutputSize))
@@ -5461,7 +5500,6 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
54615500
jMeta.AddString("],\n \"inputs\": {")
54625501
}
54635502
}
5464-
isFirstMeta := true
54655503

54665504
// Concatenate the generated CSS chunks together
54675505
var compileResultsForSourceMap []compileResultForSourceMap
@@ -5507,18 +5545,6 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
55075545
})
55085546
}
55095547
}
5510-
5511-
// Include this file in the metadata
5512-
if c.options.NeedsMetafile {
5513-
if isFirstMeta {
5514-
isFirstMeta = false
5515-
} else {
5516-
jMeta.AddString(",")
5517-
}
5518-
jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n }",
5519-
helpers.QuoteForJSON(c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath, c.options.ASCIIOnly),
5520-
len(compileResult.CSS)))
5521-
}
55225548
}
55235549

55245550
// Make sure the file ends with a newline
@@ -5531,7 +5557,7 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
55315557
}
55325558

55335559
// The CSS contents are done now that the source map comment is in
5534-
chunk.intermediateOutput = c.breakOutputIntoPieces(j)
5560+
chunk.intermediateOutput = c.breakJoinerIntoPieces(j)
55355561
timer.End("Join CSS files")
55365562

55375563
if c.options.SourceMap != config.SourceMapNone {
@@ -5544,8 +5570,21 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
55445570
// End the metadata lazily. The final output size is not known until the
55455571
// final import paths are substituted into the output pieces generated below.
55465572
if c.options.NeedsMetafile {
5573+
pieces := make([]intermediateOutput, len(compileResults))
5574+
for i, compileResult := range compileResults {
5575+
pieces[i] = c.breakOutputIntoPieces(compileResult.CSS)
5576+
}
55475577
chunk.jsonMetadataChunkCallback = func(finalOutputSize int) helpers.Joiner {
5548-
if !isFirstMeta {
5578+
finalRelDir := c.fs.Dir(chunk.finalRelPath)
5579+
for i, compileResult := range compileResults {
5580+
if i > 0 {
5581+
jMeta.AddString(",")
5582+
}
5583+
jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n }",
5584+
helpers.QuoteForJSON(c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath, c.options.ASCIIOnly),
5585+
c.accurateFinalByteCount(pieces[i], finalRelDir)))
5586+
}
5587+
if len(compileResults) > 0 {
55495588
jMeta.AddString("\n ")
55505589
}
55515590
jMeta.AddString(fmt.Sprintf("},\n \"bytes\": %d\n }", finalOutputSize))
@@ -5632,16 +5671,18 @@ func (c *linkerContext) appendIsolatedHashesForImportedChunks(
56325671
hash.Write(chunk.waitForIsolatedHash())
56335672
}
56345673

5635-
func (c *linkerContext) breakOutputIntoPieces(j helpers.Joiner) intermediateOutput {
5674+
func (c *linkerContext) breakJoinerIntoPieces(j helpers.Joiner) intermediateOutput {
56365675
// Optimization: If there can be no substitutions, just reuse the initial
56375676
// joiner that was used when generating the intermediate chunk output
56385677
// instead of creating another one and copying the whole file into it.
56395678
if !j.Contains(c.uniqueKeyPrefix, c.uniqueKeyPrefixBytes) {
56405679
return intermediateOutput{joiner: j}
56415680
}
5681+
return c.breakOutputIntoPieces(j.Done())
5682+
}
56425683

5684+
func (c *linkerContext) breakOutputIntoPieces(output []byte) intermediateOutput {
56435685
var pieces []outputPiece
5644-
output := j.Done()
56455686
prefix := c.uniqueKeyPrefixBytes
56465687
for {
56475688
// Scan for the next piece boundary

internal/bundler/snapshots/snapshots_default.txt

+7-7
Original file line numberDiff line numberDiff line change
@@ -3165,7 +3165,7 @@ d {
31653165
"bytesInOutput": 101
31663166
},
31673167
"project/entry.js": {
3168-
"bytesInOutput": 242
3168+
"bytesInOutput": 233
31693169
},
31703170
"project/esm.js": {
31713171
"bytesInOutput": 21
@@ -3174,7 +3174,7 @@ d {
31743174
"bytesInOutput": 24
31753175
},
31763176
"project/file.file": {
3177-
"bytesInOutput": 48
3177+
"bytesInOutput": 43
31783178
}
31793179
},
31803180
"bytes": 642
@@ -3234,7 +3234,7 @@ d {
32343234
"entryPoint": "project/entry.css",
32353235
"inputs": {
32363236
"project/entry.css": {
3237-
"bytesInOutput": 193
3237+
"bytesInOutput": 183
32383238
}
32393239
},
32403240
"bytes": 230
@@ -3351,7 +3351,7 @@ a {
33513351
"entryPoint": "project/bytesInOutput should be at least 99 (1).js",
33523352
"inputs": {
33533353
"project/111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.file": {
3354-
"bytesInOutput": 45
3354+
"bytesInOutput": 135
33553355
},
33563356
"project/bytesInOutput should be at least 99 (1).js": {
33573357
"bytesInOutput": 24
@@ -3380,7 +3380,7 @@ a {
33803380
"entryPoint": "project/bytesInOutput should be at least 99 (2).js",
33813381
"inputs": {
33823382
"project/bytesInOutput should be at least 99 (2).js": {
3383-
"bytesInOutput": 59
3383+
"bytesInOutput": 149
33843384
}
33853385
},
33863386
"bytes": 203
@@ -3396,7 +3396,7 @@ a {
33963396
"entryPoint": "project/bytesInOutput should be at least 99 (3).js",
33973397
"inputs": {
33983398
"project/bytesInOutput should be at least 99 (3).js": {
3399-
"bytesInOutput": 55
3399+
"bytesInOutput": 143
34003400
}
34013401
},
34023402
"bytes": 197
@@ -3432,7 +3432,7 @@ a {
34323432
"entryPoint": "project/bytesInOutput should be at least 99.css",
34333433
"inputs": {
34343434
"project/bytesInOutput should be at least 99.css": {
3435-
"bytesInOutput": 52
3435+
"bytesInOutput": 142
34363436
}
34373437
},
34383438
"bytes": 196

scripts/js-api-tests.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ body {
12411241
assert.deepStrictEqual(json.outputs[outImport2].exports, [])
12421242
assert.deepStrictEqual(json.outputs[outChunk].exports, [])
12431243

1244-
assert.deepStrictEqual(json.outputs[outEntry].inputs, { [inEntry]: { bytesInOutput: 74 } })
1244+
assert.deepStrictEqual(json.outputs[outEntry].inputs, { [inEntry]: { bytesInOutput: 66 } })
12451245
assert.deepStrictEqual(json.outputs[outImport1].inputs, { [inImport1]: { bytesInOutput: 0 } })
12461246
assert.deepStrictEqual(json.outputs[outImport2].inputs, { [inImport2]: { bytesInOutput: 0 } })
12471247
assert.deepStrictEqual(json.outputs[outChunk].inputs, { [inShared]: { bytesInOutput: 28 } })

0 commit comments

Comments
 (0)