diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a52dfb347..69f2a223f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ With this release, esbuild's built-in resolver will now automatically consider all import paths starting with `node:` as external. This new behavior is only active when the current platform is set to node such as with `--platform=node`. If you need to customize this behavior, you can write a plugin to intercept these paths and treat them differently. +* Consider `\` and `/` to be the same in file paths ([#1459](https://github.com/evanw/esbuild/issues/1459)) + + On Windows, there are many different file paths that can refer to the same underlying file. Windows uses a case-insensitive file system so for example `foo.js` and `Foo.js` are the same file. When bundling, esbuild needs to treat both of these paths as the same to avoid incorrectly bundling the file twice. This is case is already handled by identifying files by their lower-case file path. + + The case that wasn't being handled is the fact that Windows supports two different path separators, `/` and `\`, both of which mean the same thing. For example `foo/bar.js` and `foo\bar.js` are the same file. With this release, this case is also handled by esbuild. Files that are imported in multiple places with inconsistent path separators will now be considered the same file instead of bundling the file multiple times. + ## 0.12.15 * Fix a bug with `var()` in CSS color lowering ([#1421](https://github.com/evanw/esbuild/issues/1421)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 19bc632c570..c8f70ff76bd 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -898,11 +898,12 @@ func loaderFromFileExtension(extensionToLoader map[string]config.Loader, base st return config.LoaderNone } -// Identify the path by its lowercase absolute path name. This should -// hopefully avoid path case issues on Windows, which has case-insensitive -// file system paths. -func lowerCaseAbsPathForWindows(absPath string) string { - return strings.ToLower(absPath) +// Identify the path by its lowercase absolute path name with Windows-specific +// slashes substituted for standard slashes. This should hopefully avoid path +// issues on Windows where multiple different paths can refer to the same +// underlying file. +func canonicalFileSystemPathForWindows(absPath string) string { + return strings.ReplaceAll(strings.ToLower(absPath), "\\", "/") } func hashForFileName(hashBytes []byte) string { @@ -1042,7 +1043,7 @@ func (s *scanner) maybeParseFile( path := resolveResult.PathPair.Primary visitedKey := path if visitedKey.Namespace == "file" { - visitedKey.Text = lowerCaseAbsPathForWindows(visitedKey.Text) + visitedKey.Text = canonicalFileSystemPathForWindows(visitedKey.Text) } // Only parse a given file path once @@ -1209,14 +1210,14 @@ func (s *scanner) preprocessInjectedFiles() { j := 0 for _, absPath := range s.options.InjectAbsPaths { prettyPath := s.res.PrettyPath(logger.Path{Text: absPath, Namespace: "file"}) - lowerAbsPath := lowerCaseAbsPathForWindows(absPath) + absPathKey := canonicalFileSystemPathForWindows(absPath) - if duplicateInjectedFiles[lowerAbsPath] { + if duplicateInjectedFiles[absPathKey] { s.log.AddError(nil, logger.Loc{}, fmt.Sprintf("Duplicate injected file %q", prettyPath)) continue } - duplicateInjectedFiles[lowerAbsPath] = true + duplicateInjectedFiles[absPathKey] = true resolveResult := s.res.ResolveAbs(absPath) if resolveResult == nil { @@ -1596,7 +1597,7 @@ func (s *scanner) processScannedFiles() []scannerFile { if resolveResult.PathPair.HasSecondary() { secondaryKey := resolveResult.PathPair.Secondary if secondaryKey.Namespace == "file" { - secondaryKey.Text = lowerCaseAbsPathForWindows(secondaryKey.Text) + secondaryKey.Text = canonicalFileSystemPathForWindows(secondaryKey.Text) } if secondarySourceIndex, ok := s.visited[secondaryKey]; ok { record.SourceIndex = ast.MakeIndex32(secondarySourceIndex) @@ -1656,7 +1657,7 @@ func (s *scanner) processScannedFiles() []scannerFile { } else if !css.JSSourceIndex.IsValid() { stubKey := otherFile.inputFile.Source.KeyPath if stubKey.Namespace == "file" { - stubKey.Text = lowerCaseAbsPathForWindows(stubKey.Text) + stubKey.Text = canonicalFileSystemPathForWindows(stubKey.Text) } sourceIndex := s.allocateSourceIndex(stubKey, cache.SourceIndexJSStubForCSS) source := logger.Source{ @@ -2008,13 +2009,13 @@ func (b *Bundle) Compile(log logger.Log, options config.Options, timer *helpers. for _, sourceIndex := range allReachableFiles { keyPath := b.files[sourceIndex].inputFile.Source.KeyPath if keyPath.Namespace == "file" { - lowerAbsPath := lowerCaseAbsPathForWindows(keyPath.Text) - sourceAbsPaths[lowerAbsPath] = sourceIndex + absPathKey := canonicalFileSystemPathForWindows(keyPath.Text) + sourceAbsPaths[absPathKey] = sourceIndex } } for _, outputFile := range outputFiles { - lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath) - if sourceIndex, ok := sourceAbsPaths[lowerAbsPath]; ok { + absPathKey := canonicalFileSystemPathForWindows(outputFile.AbsPath) + if sourceIndex, ok := sourceAbsPaths[absPathKey]; ok { hint := "" switch logger.API { case logger.CLIAPI: @@ -2040,12 +2041,12 @@ func (b *Bundle) Compile(log logger.Log, options config.Options, timer *helpers. outputFileMap := make(map[string][]byte) end := 0 for _, outputFile := range outputFiles { - lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath) - contents, ok := outputFileMap[lowerAbsPath] + absPathKey := canonicalFileSystemPathForWindows(outputFile.AbsPath) + contents, ok := outputFileMap[absPathKey] // If this isn't a duplicate, keep the output file if !ok { - outputFileMap[lowerAbsPath] = outputFile.Contents + outputFileMap[absPathKey] = outputFile.Contents outputFiles[end] = outputFile end++ continue diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 33bb06d3da4..331adec495f 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -60,6 +60,30 @@ let buildTests = { } }, + async windowsBackslashPathTest({ esbuild, testDir }) { + let entry = path.join(testDir, 'entry.js'); + let nested = path.join(testDir, 'nested.js'); + let outfile = path.join(testDir, 'out.js'); + + // On Windows, backslash and forward slash should be treated the same + fs.writeFileSync(entry, ` + import ${JSON.stringify(nested)} + import ${JSON.stringify(nested.split(path.sep).join('/'))} + `); + fs.writeFileSync(nested, `console.log('once')`); + + const result = await esbuild.build({ + entryPoints: [entry], + outfile, + bundle: true, + write: false, + minify: true, + format: 'esm', + }) + + assert.strictEqual(result.outputFiles[0].text, 'console.log("once");\n') + }, + async workingDirTest({ esbuild, testDir }) { let aDir = path.join(testDir, 'a'); let bDir = path.join(testDir, 'b');