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

enable hashes in entry file paths #1001

Merged
merged 5 commits into from
Mar 19, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

## Unreleased

* Enable hashes in entry point file paths ([#518](https://github.com/evanw/esbuild/issues/518))

This release adds the new `--entry-names=` flag. It's similar to the `--chunk-names=` and `--asset-names=` flags except it sets the output paths for entry point files. The pattern defaults to `[dir]/[name]` which should be equivalent to the previous entry point output path behavior, so this should be a backward-compatible change.

This change has the following consequences:

* It is now possible for entry point output paths to contain a hash. For example, this now happens if you pass `--entry-names=[dir]/[name]-[hash]`. This means you can now use esbuild to generate output files such that all output paths have a hash in them, which means it should now be possible to serve the output files with an infinite cache lifetime so they are only downloaded once and then cached by the browser forever.

* It is now possible to prevent the generation of subdirectories inside the output directory. Previously esbuild replicated the directory structure of the input entry points relative to the `outbase` directory (which defaults to the [lowest common ancestor](https://en.wikipedia.org/wiki/Lowest_common_ancestor) directory across all entry points). This value is substituted into the newly-added `[dir]` placeholder. But you can now omit it by omitting that placeholder, like this: `--entry-names=[name]`.

* Source map names should now be equal to the corresponding output file name plus an additional `.map` extension. Previously the hashes were content hashes, so the source map had a different hash than the corresponding output file because they had different contents. Now they have the same hash so finding the source map should now be easier (just add `.map`).

* Due to the way the new hashing algorithm works, all chunks can now be generated fully in parallel instead of some chunks having to wait until their dependency chunks have been generated first. The import paths for dependency chunks are now swapped in after chunk generation in a second pass (detailed below). This could theoretically result in a speedup although I haven't done any benchmarks around this.

Implementing this feature required overhauling how hashes are calculated to prevent the chicken-and-egg hashing problem due to dynamic imports, which can cause cycles in the import graph of the resulting output files when code splitting is enabled. Since generating a hash involved first hashing all of your dependencies, you could end up in a situation where you needed to know the hash to calculate the hash (if a file was a dependency of itself).

The hashing algorithm now works in three steps (potentially subject to change in the future):

1. The initial versions of all output files are generated in parallel, with temporary paths used for any imports of other output files. Each temporary path is a randomly-generated string that is unique for each output file. An initial source map is also generated at this step if source maps are enabled.

The hash for the first step includes: the raw content of the output file excluding the temporary paths, the relative file paths of all input files present in that output file, the relative output path for the resulting output file (with `[hash]` for the hash that hasn't been computed yet), and contents of the initial source map.

2. After the initial versions of all output files have been generated, calculate the final hash and final output path for each output file. Calculating the final output path involves substituting the final hash for the `[hash]` placeholder in the entry name template.

The hash for the second step includes: the hash from the first step for this file and all of its transitive dependencies.

3. After all output files have a final output path, the import paths in each output file for importing other output files are substituted. Source map offsets also have to be adjusted because the final output path is likely a different length than the temporary path used in the first step. This is also done in parallel for each output file.

This whole algorithm roughly means the hash of a given output file should change if an only if any input file in that output file or any output file it depends on is changed. So the output path and therefore the browser's cache key should not change for a given output file in between builds if none of the relevant input files were changed.

* Fix importing a path containing a `?` character on Windows ([#989](https://github.com/evanw/esbuild/issues/989))

On Windows, the `?` character is not allowed in path names. This causes esbuild to fail to import paths containing this character. This is usually fine because people don't put `?` in their file names for this reason. However, the import paths for some ancient CSS code contains the `?` character as a hack to work around a bug in Internet Explorer:
Expand Down
8 changes: 7 additions & 1 deletion cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ var helpText = func(colors logger.Colors) string {
--watch Watch mode: rebuild on file system changes
` + colors.Bold + `Advanced options:` + colors.Default + `
--asset-names=... Path template to use for "file" loader files
(default "[name]-[hash]")
--banner:T=... Text to be prepended to each output file of type T
where T is one of: css | js
--charset=utf8 Do not escape UTF-8 code points
--chunk-names=... Path template to use for code splitting chunks
(default "[name]-[hash]")
--color=... Force use of color terminal escapes (true | false)
--log-limit=... Maximum message count or 0 to disable (default 10)
--entry-names=... Path template to use for entry point output paths
(default "[dir]/[name]", can also use "[hash]")
--footer:T=... Text to be appended to each output file of type T
where T is one of: css | js
--global-name=... The name of the global for the IIFE format
Expand All @@ -60,6 +65,7 @@ var helpText = func(colors logger.Colors) string {
--keep-names Preserve "name" on functions and classes
--log-level=... Disable logging (info | warning | error | silent,
default info)
--log-limit=... Maximum message count or 0 to disable (default 10)
--main-fields=... Override the main file order in package.json
(default "browser,module,main" when platform is
browser and "main,module" when platform is node)
Expand Down
19 changes: 13 additions & 6 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ type file struct {
// fully assembled later.
jsonMetadataChunk string

// The path of this entry point relative to the lowest common ancestor
// directory containing all entry points. Note: this must have OS-independent
// path separators (i.e. '/' not '\').
entryPointRelPath string
// If "isEntryPoint" is true, this is the index of the corresponding entry
// point chunk.
entryPointChunkIndex uint32

// If this file ends up being used in the bundle, these are additional files
// that must be written to the output directory. It's used by the "file"
Expand Down Expand Up @@ -321,7 +320,9 @@ func parseFile(args parseArgs) {
hashBytes := sha1.Sum([]byte(source.Contents))
hash = hashForFileName(hashBytes)
}
dir := "./"
relPath := config.TemplateToString(config.SubstituteTemplate(args.options.AssetPathTemplate, config.PathPlaceholders{
Dir: &dir,
Name: &base,
Hash: &hash,
})) + ext
Expand Down Expand Up @@ -1516,16 +1517,22 @@ func applyOptionDefaults(options *config.Options) {
}

// Configure default path templates
if len(options.EntryPathTemplate) == 0 {
options.EntryPathTemplate = []config.PathTemplate{
{Data: "./", Placeholder: config.DirPlaceholder},
{Data: "/", Placeholder: config.NamePlaceholder},
}
}
if len(options.ChunkPathTemplate) == 0 {
options.ChunkPathTemplate = []config.PathTemplate{
{Data: "./", Placeholder: config.NamePlaceholder},
{Data: ".", Placeholder: config.HashPlaceholder},
{Data: "-", Placeholder: config.HashPlaceholder},
}
}
if len(options.AssetPathTemplate) == 0 {
options.AssetPathTemplate = []config.PathTemplate{
{Data: "./", Placeholder: config.NamePlaceholder},
{Data: ".", Placeholder: config.HashPlaceholder},
{Data: "-", Placeholder: config.HashPlaceholder},
}
}
}
Expand Down
Loading