Skip to content

Commit

Permalink
fix #1044: correct relative paths for file loader
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 28, 2021
1 parent 776ffc3 commit c7be8d8
Show file tree
Hide file tree
Showing 7 changed files with 525 additions and 42 deletions.
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
# Changelog

## Unreleased

* Fix `file` loader import paths when subdirectories are present ([#1044](https:/evanw/esbuild/issues/1044))

Using the `file` loader for a file type causes importing affected files to copy the file into the output directory and to embed the path to the copied file into the code that imported it. However, esbuild previously always embedded the path relative to the output directory itself. This is problematic when the importing code is generated within a subdirectory inside the output directory, since then the relative path is wrong. For example:

```
$ cat src/example/entry.css
div { background: url(../images/image.png) }
$ esbuild --bundle src/example/entry.css --outdir=out --outbase=src --loader:.png=file
$ find out -type f
out/example/entry.css
out/image-55DNWN2R.png
$ cat out/example/entry.css
/* src/example/entry.css */
div {
background: url(./image-55DNWN2R.png);
}
```

This is output from the previous version of esbuild. The above asset reference in `out/example/entry.css` is wrong. The path should start with `../` because the two files are in different directories.

With this release, the asset references present in output files will now be the full relative path from the output file to the asset, so imports should now work correctly when the entry point is in a subdirectory within the output directory. This change affects asset reference paths in both CSS and JS output files.

Note that if you want asset reference paths to be independent of the subdirectory in which they reside, you can use the `--public-path` setting to provide the common path that all asset reference paths should be constructed relative to.

* Add support for `[dir]` in `--asset-names` ([#1196](https:/evanw/esbuild/pull/1196))

You can now use path templates such as `--asset-names=[dir]/[name]-[hash]` to copy the input directory structure of your asset files (i.e. input files loaded with the `file` loader) to the output directory. Here's an example:

```
$ cat entry.css
header {
background: url(images/common/header.png);
}
main {
background: url(images/home/hero.png);
}
$ esbuild --bundle entry.css --outdir=out --asset-names=[dir]/[name]-[hash] --loader:.png=file
$ find out -type f
out/images/home/hero-55DNWN2R.png
out/images/common/header-55DNWN2R.png
out/entry.css
$ cat out/entry.css
/* entry.css */
header {
background: url(./images/common/header-55DNWN2R.png);
}
main {
background: url(./images/home/hero-55DNWN2R.png);
}
```

## 0.12.11

* Enable faster synchronous transforms with the JS API by default ([#1000](https:/evanw/esbuild/issues/1000))
Expand Down
92 changes: 52 additions & 40 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type parseArgs struct {
results chan parseResult
inject chan config.InjectedFile
skipResolve bool
uniqueKeyPrefix string
}

type parseResult struct {
Expand Down Expand Up @@ -274,28 +275,11 @@ func parseFile(args parseArgs) {
result.ok = true

case config.LoaderFile:
// Add a hash to the file name to prevent multiple files with the same name
// but different contents from colliding
var hash string
if config.HasPlaceholder(args.options.AssetPathTemplate, config.HashPlaceholder) {
h := xxhash.New()
h.Write([]byte(source.Contents))
hash = hashForFileName(h.Sum(nil))
}
dir := "/"
relPath := config.TemplateToString(config.SubstituteTemplate(args.options.AssetPathTemplate, config.PathPlaceholders{
Dir: &dir,
Name: &base,
Hash: &hash,
})) + ext

// Determine the final path that this asset will have in the output directory
publicPath := joinWithPublicPath(args.options.PublicPath, relPath+source.KeyPath.IgnoredSuffix)

// Export the resulting relative path as a string
expr := js_ast.Expr{Data: &js_ast.EString{Value: js_lexer.StringToUTF16(publicPath)}}
uniqueKey := fmt.Sprintf("%sA%08d", args.uniqueKeyPrefix, args.sourceIndex)
uniqueKeyPath := uniqueKey + source.KeyPath.IgnoredSuffix
expr := js_ast.Expr{Data: &js_ast.EString{Value: js_lexer.StringToUTF16(uniqueKeyPath)}}
ast := js_parser.LazyExportAST(args.log, source, js_parser.OptionsFromConfig(&args.options), expr, "")
ast.URLForCSS = publicPath
ast.URLForCSS = uniqueKeyPath
if pluginName != "" {
result.file.inputFile.SideEffects.Kind = graph.NoSideEffects_PureData_FromPlugin
} else {
Expand All @@ -320,8 +304,9 @@ func parseFile(args parseArgs) {

// Copy the file using an additional file payload to make sure we only copy
// the file if the module isn't removed due to tree shaking.
result.file.inputFile.UniqueKey = uniqueKey
result.file.inputFile.AdditionalFiles = []graph.OutputFile{{
AbsPath: args.fs.Join(args.options.AbsOutputDir, relPath),
AbsPath: source.KeyPath.Text,
Contents: []byte(source.Contents),
JSONMetadataChunk: jsonMetadataChunk,
}}
Expand Down Expand Up @@ -945,12 +930,13 @@ func hashForFileName(hashBytes []byte) string {
}

type scanner struct {
log logger.Log
fs fs.FS
res resolver.Resolver
caches *cache.CacheSet
options config.Options
timer *helpers.Timer
log logger.Log
fs fs.FS
res resolver.Resolver
caches *cache.CacheSet
options config.Options
timer *helpers.Timer
uniqueKeyPrefix string

// This is not guarded by a mutex because it's only ever modified by a single
// thread. Note that not all results in the "results" array are necessarily
Expand Down Expand Up @@ -1005,24 +991,25 @@ func ScanBundle(
}
}

s := scanner{
log: log,
fs: fs,
res: res,
caches: caches,
options: options,
timer: timer,
results: make([]parseResult, 0, caches.SourceIndexCache.LenHint()),
visited: make(map[logger.Path]uint32),
resultChannel: make(chan parseResult),
}

// Each bundling operation gets a separate unique key
uniqueKeyPrefix, err := generateUniqueKeyPrefix()
if err != nil {
log.AddError(nil, logger.Loc{}, fmt.Sprintf("Failed to read from randomness source: %s", err.Error()))
}

s := scanner{
log: log,
fs: fs,
res: res,
caches: caches,
options: options,
timer: timer,
results: make([]parseResult, 0, caches.SourceIndexCache.LenHint()),
visited: make(map[logger.Path]uint32),
resultChannel: make(chan parseResult),
uniqueKeyPrefix: uniqueKeyPrefix,
}

// Always start by parsing the runtime file
s.results = append(s.results, parseResult{})
s.remaining++
Expand Down Expand Up @@ -1160,6 +1147,7 @@ func (s *scanner) maybeParseFile(
results: s.resultChannel,
inject: inject,
skipResolve: skipResolve,
uniqueKeyPrefix: s.uniqueKeyPrefix,
})

return sourceIndex
Expand Down Expand Up @@ -1765,6 +1753,30 @@ func (s *scanner) processScannedFiles() []scannerFile {
sb.WriteString("]\n }")
}

// Turn all additional file paths from input paths into output paths by
// rewriting the output base directory to the output directory
for j, additionalFile := range result.file.inputFile.AdditionalFiles {
if relPath, ok := s.fs.Rel(s.options.AbsOutputBase, additionalFile.AbsPath); ok {
var hash string

// Add a hash to the file name to prevent multiple files with the same name
// but different contents from colliding
if config.HasPlaceholder(s.options.AssetPathTemplate, config.HashPlaceholder) {
h := xxhash.New()
h.Write(additionalFile.Contents)
hash = hashForFileName(h.Sum(nil))
}

dir, base, ext := logger.PlatformIndependentPathDirBaseExt(relPath)
relPath = config.TemplateToString(config.SubstituteTemplate(s.options.AssetPathTemplate, config.PathPlaceholders{
Dir: &dir,
Name: &base,
Hash: &hash,
})) + ext
result.file.inputFile.AdditionalFiles[j].AbsPath = s.fs.Join(s.options.AbsOutputDir, relPath)
}
}

s.results[i].file.jsonMetadataChunk = sb.String()
}

Expand Down
Loading

0 comments on commit c7be8d8

Please sign in to comment.