Skip to content

Commit 1978946

Browse files
committed
fix #2963: a non-determinism bug with alias
1 parent 2176b35 commit 1978946

File tree

4 files changed

+85
-33
lines changed

4 files changed

+85
-33
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 the `alias` feature to always prefer the longest match ([#2963](https://github.com/evanw/esbuild/issues/2963))
6+
7+
It's possible to configure conflicting aliases such as `--alias:a=b` and `--alias:a/c=d`, which is ambiguous for the import path `a/c/x` (since it could map to either `b/c/x` or `d/x`). Previously esbuild would pick the first matching `alias`, which would non-deterministically pick between one of the possible matches. This release fixes esbuild to always deterministically pick the longest possible match.
8+
59
* Minify calls to some global primitive constructors ([#2962](https://github.com/evanw/esbuild/issues/2962))
610

711
With this release, esbuild's minifier now replaces calls to `Boolean`/`Number`/`String`/`BigInt` with equivalent shorter code when relevant:

internal/bundler_tests/bundler_default_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7551,6 +7551,36 @@ func TestPackageAlias(t *testing.T) {
75517551
})
75527552
}
75537553

7554+
func TestPackageAliasMatchLongest(t *testing.T) {
7555+
default_suite.expectBundled(t, bundled{
7556+
files: map[string]string{
7557+
"/entry.js": `
7558+
import "pkg"
7559+
import "pkg/foo"
7560+
import "pkg/foo/bar"
7561+
import "pkg/foo/bar/baz"
7562+
import "pkg/bar/baz"
7563+
import "pkg/baz"
7564+
`,
7565+
},
7566+
entryPaths: []string{"/entry.js"},
7567+
options: config.Options{
7568+
Mode: config.ModeBundle,
7569+
AbsOutputFile: "/out.js",
7570+
PackageAliases: map[string]string{
7571+
"pkg": "alias/pkg",
7572+
"pkg/foo": "alias/pkg_foo",
7573+
"pkg/foo/bar": "alias/pkg_foo_bar",
7574+
},
7575+
ExternalSettings: config.ExternalSettings{
7576+
PreResolve: config.ExternalMatchers{
7577+
Patterns: []config.WildcardPattern{{Prefix: "alias/"}},
7578+
},
7579+
},
7580+
},
7581+
})
7582+
}
7583+
75547584
func TestErrorsForAssertTypeJSON(t *testing.T) {
75557585
default_suite.expectBundled(t, bundled{
75567586
files: map[string]string{

internal/bundler_tests/snapshots/snapshots_default.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4697,6 +4697,17 @@ console.log(10);
46974697
// node_modules/@scope/prefix-foo/index.js
46984698
console.log(11);
46994699

4700+
================================================================================
4701+
TestPackageAliasMatchLongest
4702+
---------- /out.js ----------
4703+
// entry.js
4704+
import "alias/pkg";
4705+
import "alias/pkg_foo";
4706+
import "alias/pkg_foo_bar";
4707+
import "alias/pkg_foo_bar/baz";
4708+
import "alias/pkg/bar/baz";
4709+
import "alias/pkg/baz";
4710+
47004711
================================================================================
47014712
TestQuotedProperty
47024713
---------- /out/entry.js ----------

internal/resolver/resolver.go

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -293,42 +293,49 @@ func (res *Resolver) Resolve(sourceDir string, importPath string, kind ast.Impor
293293
if r.debugLogs != nil {
294294
r.debugLogs.addNote("Checking for package alias matches")
295295
}
296-
foundMatch := false
296+
longestKey := ""
297+
longestValue := ""
298+
297299
for key, value := range r.options.PackageAliases {
298-
if strings.HasPrefix(importPath, key) && (len(importPath) == len(key) || importPath[len(key)] == '/') {
299-
// Resolve the package using the current path instead of the original
300-
// path. This is trying to resolve the substitute in the top-level
301-
// package instead of the nested package, which lets the top-level
302-
// package control the version of the substitution. It's also critical
303-
// when using Yarn PnP because Yarn PnP doesn't allow nested packages
304-
// to "reach outside" of their normal dependency lists.
305-
sourceDir = r.fs.Cwd()
306-
if tail := importPath[len(key):]; tail != "/" {
307-
// Don't include the trailing characters if they are equal to a
308-
// single slash. This comes up because you can abuse this quirk of
309-
// node's path resolution to force node to load the package from the
310-
// file system instead of as a built-in module. For example, "util"
311-
// is node's built-in module while "util/" is one on the file system.
312-
// Leaving the trailing slash in place causes problems for people:
313-
// https://github.com/evanw/esbuild/issues/2730. It should be ok to
314-
// always strip the trailing slash even when using the alias feature
315-
// to swap one package for another (except when you swap a reference
316-
// to one built-in node module with another but really why would you
317-
// do that).
318-
value += tail
319-
}
320-
debugMeta.ModifiedImportPath = value
321-
if r.debugLogs != nil {
322-
r.debugLogs.addNote(fmt.Sprintf(" Matched with alias from %q to %q", key, value))
323-
r.debugLogs.addNote(fmt.Sprintf(" Modified import path from %q to %q", importPath, debugMeta.ModifiedImportPath))
324-
r.debugLogs.addNote(fmt.Sprintf(" Changed resolve directory to %q", sourceDir))
325-
}
326-
importPath = debugMeta.ModifiedImportPath
327-
foundMatch = true
328-
break
300+
if len(key) > len(longestKey) && strings.HasPrefix(importPath, key) && (len(importPath) == len(key) || importPath[len(key)] == '/') {
301+
longestKey = key
302+
longestValue = value
329303
}
330304
}
331-
if r.debugLogs != nil && !foundMatch {
305+
306+
if longestKey != "" {
307+
debugMeta.ModifiedImportPath = longestValue
308+
if tail := importPath[len(longestKey):]; tail != "/" {
309+
// Don't include the trailing characters if they are equal to a
310+
// single slash. This comes up because you can abuse this quirk of
311+
// node's path resolution to force node to load the package from the
312+
// file system instead of as a built-in module. For example, "util"
313+
// is node's built-in module while "util/" is one on the file system.
314+
// Leaving the trailing slash in place causes problems for people:
315+
// https://github.com/evanw/esbuild/issues/2730. It should be ok to
316+
// always strip the trailing slash even when using the alias feature
317+
// to swap one package for another (except when you swap a reference
318+
// to one built-in node module with another but really why would you
319+
// do that).
320+
debugMeta.ModifiedImportPath += tail
321+
}
322+
if r.debugLogs != nil {
323+
r.debugLogs.addNote(fmt.Sprintf(" Matched with alias from %q to %q", longestKey, longestValue))
324+
r.debugLogs.addNote(fmt.Sprintf(" Modified import path from %q to %q", importPath, debugMeta.ModifiedImportPath))
325+
}
326+
importPath = debugMeta.ModifiedImportPath
327+
328+
// Resolve the package using the current path instead of the original
329+
// path. This is trying to resolve the substitute in the top-level
330+
// package instead of the nested package, which lets the top-level
331+
// package control the version of the substitution. It's also critical
332+
// when using Yarn PnP because Yarn PnP doesn't allow nested packages
333+
// to "reach outside" of their normal dependency lists.
334+
sourceDir = r.fs.Cwd()
335+
if r.debugLogs != nil {
336+
r.debugLogs.addNote(fmt.Sprintf(" Changed resolve directory to %q", sourceDir))
337+
}
338+
} else if r.debugLogs != nil {
332339
r.debugLogs.addNote(" Failed to find any package alias matches")
333340
}
334341
}

0 commit comments

Comments
 (0)