Skip to content

Commit d3a89e7

Browse files
committed
fix #1957: include "file" loader paths in hashes
1 parent 9411fc1 commit d3a89e7

File tree

3 files changed

+95
-3
lines changed

3 files changed

+95
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
* Fix bug with filename hashes and the `file` loader ([#1957](https://github.com/evanw/esbuild/issues/1957))
6+
7+
This release fixes a bug where if a file name template has the `[hash]` placeholder (either `--entry-names=` or `--chunk-names=`), the hash that esbuild generates didn't include the content of the string generated by the `file` loader. Importing a file with the `file` loader causes the imported file to be copied to the output directory and causes the imported value to be the relative path from the output JS file to that copied file. This bug meant that if the `--asset-names=` setting also contained `[hash]` and the file loaded with the `file` loader was changed, the hash in the copied file name would change but the hash of the JS file would not change, which could potentially result in a stale JS file being loaded. Now the hash of the JS file will be changed too which fixes the reload issue.
8+
59
* Prefer the `import` condition for entry points ([#1956](https://github.com/evanw/esbuild/issues/1956))
610

711
The `exports` field in `package.json` maps package subpaths to file paths. The mapping can be conditional, which lets it vary in different situations. For example, you can have an `import` condition that applies when the subpath originated from a JS import statement, and a `require` condition that applies when the subpath originated from a JS require call. These are supposed to be mutually exclusive according to the specification: https://nodejs.org/api/packages.html#conditional-exports.

internal/bundler/linker.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func (c *linkerContext) generateChunksInParallel(chunks []chunkInfo) []graph.Out
374374
if config.HasPlaceholder(chunk.finalTemplate, config.HashPlaceholder) {
375375
// Compute the final hash using the isolated hashes of the dependencies
376376
hash := xxhash.New()
377-
appendIsolatedHashesForImportedChunks(hash, chunks, uint32(chunkIndex), visited, ^uint32(chunkIndex))
377+
c.appendIsolatedHashesForImportedChunks(hash, chunks, uint32(chunkIndex), visited, ^uint32(chunkIndex))
378378
finalBytes = hash.Sum(finalBytes[:0])
379379
finalString := hashForFileName(finalBytes)
380380
hashSubstitution = &finalString
@@ -5200,7 +5200,7 @@ func maybeAppendLegalComments(
52005200
}
52015201
}
52025202

5203-
func appendIsolatedHashesForImportedChunks(
5203+
func (c *linkerContext) appendIsolatedHashesForImportedChunks(
52045204
hash hash.Hash,
52055205
chunks []chunkInfo,
52065206
chunkIndex uint32,
@@ -5219,7 +5219,24 @@ func appendIsolatedHashesForImportedChunks(
52195219

52205220
// Visit the other chunks that this chunk imports before visiting this chunk
52215221
for _, chunkImport := range chunk.crossChunkImports {
5222-
appendIsolatedHashesForImportedChunks(hash, chunks, chunkImport.chunkIndex, visited, visitedKey)
5222+
c.appendIsolatedHashesForImportedChunks(hash, chunks, chunkImport.chunkIndex, visited, visitedKey)
5223+
}
5224+
5225+
// Mix in hashes for referenced asset paths (i.e. the "file" loader)
5226+
for _, piece := range chunk.intermediateOutput.pieces {
5227+
if piece.kind == outputPieceAssetIndex {
5228+
file := c.graph.Files[piece.index]
5229+
if len(file.InputFile.AdditionalFiles) != 1 {
5230+
panic("Internal error")
5231+
}
5232+
relPath, _ := c.fs.Rel(c.options.AbsOutputDir, file.InputFile.AdditionalFiles[0].AbsPath)
5233+
5234+
// Make sure to always use forward slashes, even on Windows
5235+
relPath = strings.ReplaceAll(relPath, "\\", "/")
5236+
5237+
// Mix in the hash for the relative path, which ends up as a JS string
5238+
hashWriteLengthPrefixed(hash, []byte(relPath))
5239+
}
52235240
}
52245241

52255242
// Mix in the hash for this chunk

scripts/js-api-tests.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,77 @@ let buildTests = {
583583
assert.strictEqual(result.__esModule, true)
584584
},
585585

586+
async fileLoaderEntryHash({ esbuild, testDir }) {
587+
const input = path.join(testDir, 'index.js')
588+
const data = path.join(testDir, 'data.bin')
589+
const outdir = path.join(testDir, 'out')
590+
await writeFileAsync(input, `export {default as value} from ${JSON.stringify(data)}`)
591+
await writeFileAsync(data, `stuff`)
592+
const result1 = await esbuild.build({
593+
entryPoints: [input],
594+
bundle: true,
595+
outdir,
596+
format: 'cjs',
597+
loader: { '.bin': 'file' },
598+
entryNames: '[name]-[hash]',
599+
write: false,
600+
})
601+
await writeFileAsync(data, `more stuff`)
602+
const result2 = await esbuild.build({
603+
entryPoints: [input],
604+
bundle: true,
605+
outdir,
606+
format: 'cjs',
607+
loader: { '.bin': 'file' },
608+
entryNames: '[name]-[hash]',
609+
write: false,
610+
})
611+
assert.strictEqual(result1.outputFiles.length, 2)
612+
assert.strictEqual(result2.outputFiles.length, 2)
613+
614+
// Make sure each path is unique. This tests for a bug where the hash in
615+
// the output filename corresponding to the "index.js" entry point wasn't
616+
// including the filename for the "file" loader.
617+
assert.strictEqual(new Set(result1.outputFiles.concat(result2.outputFiles).map(x => x.path)).size, 4)
618+
},
619+
620+
async fileLoaderEntryHashNoChange({ esbuild, testDir }) {
621+
const input = path.join(testDir, 'index.js')
622+
const data = path.join(testDir, 'data.bin')
623+
const outdir = path.join(testDir, 'out')
624+
await writeFileAsync(input, `export {default as value} from ${JSON.stringify(data)}`)
625+
await writeFileAsync(data, `stuff`)
626+
const result1 = await esbuild.build({
627+
entryPoints: [input],
628+
bundle: true,
629+
outdir,
630+
format: 'cjs',
631+
loader: { '.bin': 'file' },
632+
entryNames: '[name]-[hash]',
633+
assetNames: '[name]',
634+
write: false,
635+
})
636+
await writeFileAsync(data, `more stuff`)
637+
const result2 = await esbuild.build({
638+
entryPoints: [input],
639+
bundle: true,
640+
outdir,
641+
format: 'cjs',
642+
loader: { '.bin': 'file' },
643+
entryNames: '[name]-[hash]',
644+
assetNames: '[name]',
645+
write: false,
646+
})
647+
assert.strictEqual(result1.outputFiles.length, 2)
648+
assert.strictEqual(result2.outputFiles.length, 2)
649+
650+
// The paths should be the same. The hash augmentation from the previous
651+
// test should only be checking for a file name difference, not a difference
652+
// in content, because the JS output file only contains the file name for
653+
// something using the "file" loader.
654+
assert.strictEqual(new Set(result1.outputFiles.concat(result2.outputFiles).map(x => x.path)).size, 2)
655+
},
656+
586657
async splittingPublicPath({ esbuild, testDir }) {
587658
const input1 = path.join(testDir, 'a', 'in1.js')
588659
const input2 = path.join(testDir, 'b', 'in2.js')

0 commit comments

Comments
 (0)