Skip to content

Commit

Permalink
fix #2569: support node's "pattern trailer" syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Sep 23, 2022
1 parent 23709e2 commit efd0af6
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 29 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## Unreleased

* Add support for node's "pattern trailers" syntax ([#2569](https://github.com/evanw/esbuild/issues/2569))

After esbuild implemented node's `exports` feature in `package.json`, node changed the feature to also allow text after `*` wildcards in patterns. Previously the `*` was required to be at the end of the pattern. It lets you do something like this:

```json
{
"exports": {
"./features/*": "./features/*.js",
"./features/*.js": "./features/*.js"
}
}
```

With this release, esbuild now supports these types of patterns too.

## 0.15.9

* Fix an obscure npm package installation issue with `--omit=optional` ([#2558](https://github.com/evanw/esbuild/issues/2558))
Expand Down
48 changes: 44 additions & 4 deletions internal/bundler/bundler_packagejson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,9 @@ func TestPackageJsonExportsWildcard(t *testing.T) {
}
}
`,
"/Users/user/project/node_modules/pkg1/file.js": `
console.log('SUCCESS')
`,
"/Users/user/project/node_modules/pkg1/file2.js": `
console.log('SUCCESS')
`,
Expand All @@ -1831,10 +1834,6 @@ func TestPackageJsonExportsWildcard(t *testing.T) {
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
expectedScanLog: `Users/user/project/src/entry.js: ERROR: Could not resolve "pkg1/foo"
Users/user/project/node_modules/pkg1/package.json: NOTE: The path "./foo" is not exported by package "pkg1":
NOTE: You can mark the path "pkg1/foo" as external to exclude it from the bundle, which will remove this error.
`,
})
}

Expand Down Expand Up @@ -2139,6 +2138,47 @@ NOTE: You can mark the path "pkg/path/to/other/file" as external to exclude it f
})
}

func TestPackageJsonExportsPatternTrailers(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg/path/foo.js/bar.js'
import 'pkg2/features/abc'
import 'pkg2/features/xyz.js'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"./path/*/bar.js": "./dir/baz-*"
}
}
`,
"/Users/user/project/node_modules/pkg/dir/baz-foo.js": `
console.log('works')
`,
"/Users/user/project/node_modules/pkg2/package.json": `
{
"exports": {
"./features/*": "./public/*.js",
"./features/*.js": "./public/*.js"
}
}
`,
"/Users/user/project/node_modules/pkg2/public/abc.js": `
console.log('abc')
`,
"/Users/user/project/node_modules/pkg2/public/xyz.js": `
console.log('xyz')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
})
}

func TestPackageJsonImports(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
21 changes: 21 additions & 0 deletions internal/bundler/snapshots/snapshots_packagejson.txt
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,18 @@ console.log("SUCCESS");
// Users/user/project/node_modules/pkg2/1/bar.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsPatternTrailers
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/dir/baz-foo.js
console.log("works");

// Users/user/project/node_modules/pkg2/public/abc.js
console.log("abc");

// Users/user/project/node_modules/pkg2/public/xyz.js
console.log("xyz");

================================================================================
TestPackageJsonExportsRequireOverImport
---------- /Users/user/project/out.js ----------
Expand All @@ -622,6 +634,15 @@ var require_require = __commonJS({
// Users/user/project/src/entry.js
require_require();

================================================================================
TestPackageJsonExportsWildcard
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg1/file.js
console.log("SUCCESS");

// Users/user/project/node_modules/pkg1/file2.js
console.log("SUCCESS");

================================================================================
TestPackageJsonImportSelfUsingImport
---------- /Users/user/project/out.js ----------
Expand Down
116 changes: 91 additions & 25 deletions internal/resolver/package_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,56 @@ func (a expansionKeysArray) Len() int { return len(a) }
func (a expansionKeysArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }

func (a expansionKeysArray) Less(i int, j int) bool {
return len(a[i].key) > len(a[j].key)
// Assert: keyA ends with "/" or contains only a single "*".
// Assert: keyB ends with "/" or contains only a single "*".
keyA := a[i].key
keyB := a[j].key

// Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
// Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
starA := strings.IndexByte(keyA, '*')
starB := strings.IndexByte(keyB, '*')
var baseLengthA int
var baseLengthB int
if starA >= 0 {
baseLengthA = starA
} else {
baseLengthA = len(keyA)
}
if starB >= 0 {
baseLengthB = starB
} else {
baseLengthB = len(keyB)
}

// If baseLengthA is greater than baseLengthB, return -1.
// If baseLengthB is greater than baseLengthA, return 1.
if baseLengthA > baseLengthB {
return true
}
if baseLengthB > baseLengthA {
return false
}

// If keyA does not contain "*", return 1.
// If keyB does not contain "*", return -1.
if starA < 0 {
return false
}
if starB < 0 {
return true
}

// If the length of keyA is greater than the length of keyB, return -1.
// If the length of keyB is greater than the length of keyA, return 1.
if len(keyA) > len(keyB) {
return true
}
if len(keyB) > len(keyA) {
return false
}

return false
}

func (entry pjEntry) valueForKey(key string) (pjEntry, bool) {
Expand Down Expand Up @@ -638,15 +687,16 @@ func parseImportsExportsMap(source logger.Source, log logger.Log, json js_ast.Ex
value: visit(property.ValueOrNil),
}

if strings.HasSuffix(key, "/") || strings.HasSuffix(key, "*") {
if strings.HasSuffix(key, "/") || strings.IndexByte(key, '*') >= 0 {
expansionKeys = append(expansionKeys, entry)
}

mapData[i] = entry
}

// Let expansionKeys be the list of keys of matchObj ending in "/" or "*",
// sorted by length descending.
// Let expansionKeys be the list of keys of matchObj either ending in "/"
// or containing only a single "*", sorted by the sorting function
// PATTERN_KEY_COMPARE which orders in descending order of specificity.
sort.Stable(expansionKeys)

return pjEntry{
Expand Down Expand Up @@ -860,7 +910,8 @@ func (r resolverQuery) esmPackageImportsExportsResolve(
r.debugLogs.addNote(fmt.Sprintf("Checking object path map for %q", matchKey))
}

if !strings.HasSuffix(matchKey, "*") {
// If matchKey is a key of matchObj and does not end in "/" or contain "*", then
if !strings.HasSuffix(matchKey, "/") && strings.IndexByte(matchKey, '*') < 0 {
if target, ok := matchObj.valueForKey(matchKey); ok {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Found exact match for %q", matchKey))
Expand All @@ -870,31 +921,46 @@ func (r resolverQuery) esmPackageImportsExportsResolve(
}

for _, expansion := range matchObj.expansionKeys {
// If expansionKey ends in "*" and matchKey starts with but is not equal to
// the substring of expansionKey excluding the last "*" character
if strings.HasSuffix(expansion.key, "*") {
if substr := expansion.key[:len(expansion.key)-1]; strings.HasPrefix(matchKey, substr) && matchKey != substr {
// If expansionKey contains "*", set patternBase to the substring of
// expansionKey up to but excluding the first "*" character
if star := strings.IndexByte(expansion.key, '*'); star >= 0 {
patternBase := expansion.key[:star]

// If patternBase is not null and matchKey starts with but is not equal
// to patternBase, then
if strings.HasPrefix(matchKey, patternBase) {
// Let patternTrailer be the substring of expansionKey from the index
// after the first "*" character.
patternTrailer := expansion.key[star+1:]

// If patternTrailer has zero length, or if matchKey ends with
// patternTrailer and the length of matchKey is greater than or
// equal to the length of expansionKey, then
if patternTrailer == "" || (strings.HasSuffix(matchKey, patternTrailer) && len(matchKey) >= len(expansion.key)) {
target := expansion.value
subpath := matchKey[len(patternBase) : len(matchKey)-len(patternTrailer)]
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
}
return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions)
}
}
} else {
// Otherwise if patternBase is null and matchKey starts with
// expansionKey, then
if strings.HasPrefix(matchKey, expansion.key) {
target := expansion.value
subpath := matchKey[len(expansion.key)-1:]
subpath := matchKey[len(expansion.key):]
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
}
return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions)
}
}

if strings.HasPrefix(matchKey, expansion.key) {
target := expansion.value
subpath := matchKey[len(expansion.key):]
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
}
result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions)
if status == pjStatusExact || status == pjStatusExactEndsWithStar {
// Return the object { resolved, exact: false }.
status = pjStatusInexact
result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions)
if status == pjStatusExact || status == pjStatusExactEndsWithStar {
// Return the object { resolved, exact: false }.
status = pjStatusInexact
}
return result, status, debug
}
return result, status, debug
}

if r.debugLogs != nil {
Expand Down

0 comments on commit efd0af6

Please sign in to comment.