Skip to content

Commit efd0af6

Browse files
committedSep 23, 2022
fix #2569: support node's "pattern trailer" syntax
1 parent 23709e2 commit efd0af6

File tree

4 files changed

+173
-29
lines changed

4 files changed

+173
-29
lines changed
 

‎CHANGELOG.md

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* Add support for node's "pattern trailers" syntax ([#2569](https://github.com/evanw/esbuild/issues/2569))
6+
7+
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:
8+
9+
```json
10+
{
11+
"exports": {
12+
"./features/*": "./features/*.js",
13+
"./features/*.js": "./features/*.js"
14+
}
15+
}
16+
```
17+
18+
With this release, esbuild now supports these types of patterns too.
19+
320
## 0.15.9
421

522
* Fix an obscure npm package installation issue with `--omit=optional` ([#2558](https://github.com/evanw/esbuild/issues/2558))

‎internal/bundler/bundler_packagejson_test.go

+44-4
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,9 @@ func TestPackageJsonExportsWildcard(t *testing.T) {
18221822
}
18231823
}
18241824
`,
1825+
"/Users/user/project/node_modules/pkg1/file.js": `
1826+
console.log('SUCCESS')
1827+
`,
18251828
"/Users/user/project/node_modules/pkg1/file2.js": `
18261829
console.log('SUCCESS')
18271830
`,
@@ -1831,10 +1834,6 @@ func TestPackageJsonExportsWildcard(t *testing.T) {
18311834
Mode: config.ModeBundle,
18321835
AbsOutputFile: "/Users/user/project/out.js",
18331836
},
1834-
expectedScanLog: `Users/user/project/src/entry.js: ERROR: Could not resolve "pkg1/foo"
1835-
Users/user/project/node_modules/pkg1/package.json: NOTE: The path "./foo" is not exported by package "pkg1":
1836-
NOTE: You can mark the path "pkg1/foo" as external to exclude it from the bundle, which will remove this error.
1837-
`,
18381837
})
18391838
}
18401839

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

2141+
func TestPackageJsonExportsPatternTrailers(t *testing.T) {
2142+
packagejson_suite.expectBundled(t, bundled{
2143+
files: map[string]string{
2144+
"/Users/user/project/src/entry.js": `
2145+
import 'pkg/path/foo.js/bar.js'
2146+
import 'pkg2/features/abc'
2147+
import 'pkg2/features/xyz.js'
2148+
`,
2149+
"/Users/user/project/node_modules/pkg/package.json": `
2150+
{
2151+
"exports": {
2152+
"./path/*/bar.js": "./dir/baz-*"
2153+
}
2154+
}
2155+
`,
2156+
"/Users/user/project/node_modules/pkg/dir/baz-foo.js": `
2157+
console.log('works')
2158+
`,
2159+
"/Users/user/project/node_modules/pkg2/package.json": `
2160+
{
2161+
"exports": {
2162+
"./features/*": "./public/*.js",
2163+
"./features/*.js": "./public/*.js"
2164+
}
2165+
}
2166+
`,
2167+
"/Users/user/project/node_modules/pkg2/public/abc.js": `
2168+
console.log('abc')
2169+
`,
2170+
"/Users/user/project/node_modules/pkg2/public/xyz.js": `
2171+
console.log('xyz')
2172+
`,
2173+
},
2174+
entryPaths: []string{"/Users/user/project/src/entry.js"},
2175+
options: config.Options{
2176+
Mode: config.ModeBundle,
2177+
AbsOutputFile: "/Users/user/project/out.js",
2178+
},
2179+
})
2180+
}
2181+
21422182
func TestPackageJsonImports(t *testing.T) {
21432183
packagejson_suite.expectBundled(t, bundled{
21442184
files: map[string]string{

‎internal/bundler/snapshots/snapshots_packagejson.txt

+21
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,18 @@ console.log("SUCCESS");
609609
// Users/user/project/node_modules/pkg2/1/bar.js
610610
console.log("SUCCESS");
611611

612+
================================================================================
613+
TestPackageJsonExportsPatternTrailers
614+
---------- /Users/user/project/out.js ----------
615+
// Users/user/project/node_modules/pkg/dir/baz-foo.js
616+
console.log("works");
617+
618+
// Users/user/project/node_modules/pkg2/public/abc.js
619+
console.log("abc");
620+
621+
// Users/user/project/node_modules/pkg2/public/xyz.js
622+
console.log("xyz");
623+
612624
================================================================================
613625
TestPackageJsonExportsRequireOverImport
614626
---------- /Users/user/project/out.js ----------
@@ -622,6 +634,15 @@ var require_require = __commonJS({
622634
// Users/user/project/src/entry.js
623635
require_require();
624636

637+
================================================================================
638+
TestPackageJsonExportsWildcard
639+
---------- /Users/user/project/out.js ----------
640+
// Users/user/project/node_modules/pkg1/file.js
641+
console.log("SUCCESS");
642+
643+
// Users/user/project/node_modules/pkg1/file2.js
644+
console.log("SUCCESS");
645+
625646
================================================================================
626647
TestPackageJsonImportSelfUsingImport
627648
---------- /Users/user/project/out.js ----------

‎internal/resolver/package_json.go

+91-25
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,56 @@ func (a expansionKeysArray) Len() int { return len(a) }
560560
func (a expansionKeysArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
561561

562562
func (a expansionKeysArray) Less(i int, j int) bool {
563-
return len(a[i].key) > len(a[j].key)
563+
// Assert: keyA ends with "/" or contains only a single "*".
564+
// Assert: keyB ends with "/" or contains only a single "*".
565+
keyA := a[i].key
566+
keyB := a[j].key
567+
568+
// Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
569+
// Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
570+
starA := strings.IndexByte(keyA, '*')
571+
starB := strings.IndexByte(keyB, '*')
572+
var baseLengthA int
573+
var baseLengthB int
574+
if starA >= 0 {
575+
baseLengthA = starA
576+
} else {
577+
baseLengthA = len(keyA)
578+
}
579+
if starB >= 0 {
580+
baseLengthB = starB
581+
} else {
582+
baseLengthB = len(keyB)
583+
}
584+
585+
// If baseLengthA is greater than baseLengthB, return -1.
586+
// If baseLengthB is greater than baseLengthA, return 1.
587+
if baseLengthA > baseLengthB {
588+
return true
589+
}
590+
if baseLengthB > baseLengthA {
591+
return false
592+
}
593+
594+
// If keyA does not contain "*", return 1.
595+
// If keyB does not contain "*", return -1.
596+
if starA < 0 {
597+
return false
598+
}
599+
if starB < 0 {
600+
return true
601+
}
602+
603+
// If the length of keyA is greater than the length of keyB, return -1.
604+
// If the length of keyB is greater than the length of keyA, return 1.
605+
if len(keyA) > len(keyB) {
606+
return true
607+
}
608+
if len(keyB) > len(keyA) {
609+
return false
610+
}
611+
612+
return false
564613
}
565614

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

641-
if strings.HasSuffix(key, "/") || strings.HasSuffix(key, "*") {
690+
if strings.HasSuffix(key, "/") || strings.IndexByte(key, '*') >= 0 {
642691
expansionKeys = append(expansionKeys, entry)
643692
}
644693

645694
mapData[i] = entry
646695
}
647696

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

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

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

872923
for _, expansion := range matchObj.expansionKeys {
873-
// If expansionKey ends in "*" and matchKey starts with but is not equal to
874-
// the substring of expansionKey excluding the last "*" character
875-
if strings.HasSuffix(expansion.key, "*") {
876-
if substr := expansion.key[:len(expansion.key)-1]; strings.HasPrefix(matchKey, substr) && matchKey != substr {
924+
// If expansionKey contains "*", set patternBase to the substring of
925+
// expansionKey up to but excluding the first "*" character
926+
if star := strings.IndexByte(expansion.key, '*'); star >= 0 {
927+
patternBase := expansion.key[:star]
928+
929+
// If patternBase is not null and matchKey starts with but is not equal
930+
// to patternBase, then
931+
if strings.HasPrefix(matchKey, patternBase) {
932+
// Let patternTrailer be the substring of expansionKey from the index
933+
// after the first "*" character.
934+
patternTrailer := expansion.key[star+1:]
935+
936+
// If patternTrailer has zero length, or if matchKey ends with
937+
// patternTrailer and the length of matchKey is greater than or
938+
// equal to the length of expansionKey, then
939+
if patternTrailer == "" || (strings.HasSuffix(matchKey, patternTrailer) && len(matchKey) >= len(expansion.key)) {
940+
target := expansion.value
941+
subpath := matchKey[len(patternBase) : len(matchKey)-len(patternTrailer)]
942+
if r.debugLogs != nil {
943+
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
944+
}
945+
return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions)
946+
}
947+
}
948+
} else {
949+
// Otherwise if patternBase is null and matchKey starts with
950+
// expansionKey, then
951+
if strings.HasPrefix(matchKey, expansion.key) {
877952
target := expansion.value
878-
subpath := matchKey[len(expansion.key)-1:]
953+
subpath := matchKey[len(expansion.key):]
879954
if r.debugLogs != nil {
880955
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
881956
}
882-
return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions)
883-
}
884-
}
885-
886-
if strings.HasPrefix(matchKey, expansion.key) {
887-
target := expansion.value
888-
subpath := matchKey[len(expansion.key):]
889-
if r.debugLogs != nil {
890-
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
891-
}
892-
result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions)
893-
if status == pjStatusExact || status == pjStatusExactEndsWithStar {
894-
// Return the object { resolved, exact: false }.
895-
status = pjStatusInexact
957+
result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions)
958+
if status == pjStatusExact || status == pjStatusExactEndsWithStar {
959+
// Return the object { resolved, exact: false }.
960+
status = pjStatusInexact
961+
}
962+
return result, status, debug
896963
}
897-
return result, status, debug
898964
}
899965

900966
if r.debugLogs != nil {

0 commit comments

Comments
 (0)
Please sign in to comment.