Skip to content

Commit 6a69a18

Browse files
committed
fix #2473: yarn pnp exports in package.json
1 parent 5e085f5 commit 6a69a18

File tree

12 files changed

+246
-66
lines changed

12 files changed

+246
-66
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ jobs:
133133
if: matrix.os != 'ubuntu-latest'
134134
run: node scripts/wasm-tests.js
135135

136+
- name: Yarn PnP tests (non-Windows)
137+
if: matrix.os != 'windows-latest'
138+
run: make test-yarnpnp
139+
136140
- name: Sucrase Tests
137141
if: matrix.os == 'ubuntu-latest'
138142
run: make test-sucrase

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838

3939
For context: JavaScript files recently allowed using a [hashbang comment](https://github.com/tc39/proposal-hashbang), which starts with `#!` and which must start at the very first character of the file. It allows Unix systems to execute the file directly as a script without needing to prefix it by the `node` command. This comment typically has the value `#!/usr/bin/env node`. Hashbang comments will be a part of ES2023 when it's released next year.
4040

41+
* Fix `exports` maps with Yarn PnP path resolution ([#2473](https://github.com/evanw/esbuild/issues/2473))
42+
43+
The Yarn PnP specification says that to resolve a package path, you first resolve it to the absolute path of a directory, and then you run node's module resolution algorithm on it. Previously esbuild followed this part of the specification. However, doing this means that `exports` in `package.json` is not respected because node's module resolution algorithm doesn't interpret `exports` for absolute paths. So with this release, esbuild will now use a modified algorithm that deviates from both specifications but that should hopefully behave more similar to what Yarn actually does: node's module resolution algorithm is run with the original import path but starting from the directory returned by Yarn PnP.
44+
4145
## 0.15.3
4246

4347
* Change the Yarn PnP manifest to a singleton ([#2463](https://github.com/evanw/esbuild/issues/2463))

Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,32 @@ test-e2e-yarn-berry:
201201
# Clean up
202202
rm -fr e2e-yb
203203

204+
require/yarnpnp/.yarn/releases/yarn-3.2.2.cjs: require/yarnpnp/package.json require/yarnpnp/yarn.lock
205+
rm -fr require/yarnpnp/.pnp* require/yarnpnp/.yarn*
206+
which yarn || npm i -g yarn
207+
cd require/yarnpnp && yarn set version 3.2.2
208+
209+
require/yarnpnp/.yarn/cache: require/yarnpnp/.yarn/releases/yarn-3.2.2.cjs
210+
cd require/yarnpnp && yarn install
211+
212+
require/yarnpnp/in.js: Makefile
213+
@echo 'console.log("Running Yarn PnP tests...")' > require/yarnpnp/in.js
214+
@echo 'import * as rd from "react-dom"; if (rd.version !== "18.2.0") throw "❌ react-dom"' >> require/yarnpnp/in.js
215+
@echo 'import * as s3 from "strtok3"; if (!s3.fromFile) throw "❌ strtok3"' >> require/yarnpnp/in.js
216+
@echo 'import * as d3 from "d3-time"; if (!d3.utcDay) throw "❌ d3-time"' >> require/yarnpnp/in.js
217+
@echo 'import * as mm from "mime"; if (mm.getType("txt") !== "text/plain") throw "❌ mime"' >> require/yarnpnp/in.js
218+
@echo 'console.log("✅ Yarn PnP tests passed")' >> require/yarnpnp/in.js
219+
220+
test-yarnpnp: esbuild require/yarnpnp/in.js | require/yarnpnp/.yarn/cache
221+
cd require/yarnpnp && ../../esbuild --bundle in.js --log-level=debug --platform=node --outfile=out.js && node out.js
222+
223+
# Note: This is currently failing due to a bug in Yarn that generates invalid
224+
# file handles. I should update the Yarn version and then enable this test
225+
# when a new version of Yarn is released that fixes this bug. This test is
226+
# disabled for now.
227+
test-yarnpnp-wasm: platform-wasm require/yarnpnp/in.js | require/yarnpnp/.yarn/cache
228+
cd require/yarnpnp && yarn node ../../npm/esbuild-wasm/bin/esbuild --bundle in.js --log-level=debug --platform=node --outfile=out.js && node out.js
229+
204230
# Note: This used to only be rebuilt when "version.txt" was newer than
205231
# "cmd/esbuild/version.go", but that caused the publishing script to publish
206232
# invalid builds in the case when the publishing script failed once, the change
@@ -575,6 +601,7 @@ clean:
575601
rm -rf require/*/bench/
576602
rm -rf require/*/demo/
577603
rm -rf require/*/node_modules/
604+
rm -rf require/yarnpnp/.pnp* require/yarnpnp/.yarn*
578605
go clean -testcache ./internal/...
579606

580607
# This also cleans directories containing cached code from other projects

internal/bundler/bundler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,11 @@ func ResolveFailureErrorTextSuggestionNotes(
510510
}
511511

512512
if platform != config.PlatformNode {
513-
if _, ok := resolver.BuiltInNodeModules[path]; ok {
513+
pkg := path
514+
if strings.HasPrefix(pkg, "node:") {
515+
pkg = pkg[5:]
516+
}
517+
if resolver.BuiltInNodeModules[pkg] {
514518
var how string
515519
switch logger.API {
516520
case logger.CLIAPI:

internal/resolver/resolver.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -721,15 +721,6 @@ func (r resolverQuery) finalizeResolve(result *ResolveResult) {
721721
}
722722

723723
func (r resolverQuery) resolveWithoutSymlinks(sourceDir string, sourceDirInfo *dirInfo, importPath string) *ResolveResult {
724-
// If Yarn PnP is active, use it to rewrite the path
725-
if r.pnpManifest != nil {
726-
if result, ok := r.pnpResolve(importPath, sourceDirInfo.absPath, r.pnpManifest); ok {
727-
importPath = result // Continue with the module resolution algorithm from node.js
728-
} else {
729-
return nil // This is a module resolution error
730-
}
731-
}
732-
733724
// This implements the module resolution algorithm from node.js, which is
734725
// described here: https://nodejs.org/api/modules.html#modules_all_together
735726
var result ResolveResult
@@ -984,13 +975,13 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC
984975
}
985976

986977
absPath = r.fs.Join(current, ".pnp.cjs")
987-
if json := r.extractYarnPnPDataFromJSON(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
978+
if json := r.tryToExtractYarnPnPDataFromJS(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
988979
pnpData = compileYarnPnPData(absPath, current, json)
989980
break
990981
}
991982

992983
absPath = r.fs.Join(current, ".pnp.js")
993-
if json := r.extractYarnPnPDataFromJSON(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
984+
if json := r.tryToExtractYarnPnPDataFromJS(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
994985
pnpData = compileYarnPnPData(absPath, current, json)
995986
break
996987
}
@@ -1006,7 +997,7 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC
1006997
}
1007998

1008999
if pnpData != nil {
1009-
if result, ok := r.pnpResolve(extends, fileDir, pnpData); ok {
1000+
if result, ok := r.resolveToUnqualified(extends, fileDir, pnpData); ok {
10101001
extends = result // Continue with the module resolution algorithm from node.js
10111002
}
10121003
}
@@ -1991,6 +1982,21 @@ func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forb
19911982
}
19921983
}
19931984

1985+
// If Yarn PnP is active, use it to rewrite the path
1986+
if r.pnpManifest != nil {
1987+
if result, ok := r.resolveToUnqualified(importPath, dirInfo.absPath, r.pnpManifest); ok {
1988+
if resultDirInfo := r.dirInfoCached(result); resultDirInfo != nil {
1989+
// Continue with the module resolution algorithm from node.js but
1990+
// pretend that the request started from wherever Yarn resolved us to.
1991+
// This isn't in the Yarn PnP specification but it's what Yarn does:
1992+
// https://github.com/evanw/esbuild/issues/2473#issuecomment-1216774461
1993+
dirInfo = resultDirInfo
1994+
}
1995+
} else {
1996+
return PathPair{}, false, nil // This is a module resolution error
1997+
}
1998+
}
1999+
19942000
// Find the parent directory with the "package.json" file
19952001
dirInfoPackageJSON := dirInfo
19962002
for dirInfoPackageJSON != nil && dirInfoPackageJSON.packageJSON == nil {

internal/resolver/yarnpnp.go

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,28 +77,6 @@ type pnpPackageLocatorByLocation struct {
7777
discardFromLookup bool
7878
}
7979

80-
// Note: If this returns successfully then the node module resolution algorithm
81-
// (i.e. NM_RESOLVE in the Yarn PnP specification) is always run afterward
82-
func (r resolverQuery) pnpResolve(specifier string, parentURL string, parentManifest *pnpData) (string, bool) {
83-
// If specifier is a Node.js builtin, then
84-
if BuiltInNodeModules[specifier] {
85-
// Set resolved to specifier itself and return it
86-
return specifier, true
87-
}
88-
89-
// Otherwise, if `specifier` is either an absolute path or a path prefixed with "./" or "../", then
90-
if r.fs.IsAbs(specifier) || strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
91-
// Set resolved to NM_RESOLVE(specifier, parentURL) and return it
92-
return specifier, true
93-
}
94-
95-
// Otherwise,
96-
// Note: specifier is now a bare identifier
97-
// Let unqualified be RESOLVE_TO_UNQUALIFIED(specifier, parentURL)
98-
// Set resolved to NM_RESOLVE(unqualified, parentURL)
99-
return r.resolveToUnqualified(specifier, parentURL, parentManifest)
100-
}
101-
10280
func parseBareIdentifier(specifier string) (ident string, modulePath string, ok bool) {
10381
slash := strings.IndexByte(specifier, '/')
10482

@@ -135,6 +113,8 @@ func parseBareIdentifier(specifier string) (ident string, modulePath string, ok
135113
return
136114
}
137115

116+
// Note: If this returns successfully then the node module resolution algorithm
117+
// (i.e. NM_RESOLVE in the Yarn PnP specification) is always run afterward
138118
func (r resolverQuery) resolveToUnqualified(specifier string, parentURL string, manifest *pnpData) (string, bool) {
139119
// Let resolved be undefined
140120

internal/resolver/yarnpnp_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestYarnPnP(t *testing.T) {
6969
t.Run(current.It, func(t *testing.T) {
7070
rr := NewResolver(fs.MockFS(nil, fs.MockUnix), logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil), nil, config.Options{})
7171
r := resolverQuery{resolver: rr.(*resolver)}
72-
result, ok := r.pnpResolve(current.Imported, current.Importer, manifest)
72+
result, ok := r.resolveToUnqualified(current.Imported, current.Importer, manifest)
7373
if !ok {
7474
result = "error!"
7575
}

require/yarnpnp/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/.pnp*
2+
/.yarn*
3+
/in.js
4+
/out.js

require/yarnpnp/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"packageManager": "[email protected]",
3+
"dependencies": {
4+
"@vue/tsconfig": "0.1.3",
5+
"d3-time": "3.0.0",
6+
"mime": "3.0.0",
7+
"react": "18.2.0",
8+
"react-dom": "18.2.0",
9+
"strtok3": "7.0.0"
10+
}
11+
}

require/yarnpnp/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@vue/tsconfig/tsconfig.json"
3+
}

require/yarnpnp/yarn.lock

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# This file is generated by running "yarn install" inside your project.
2+
# Manual changes might be lost - proceed with caution!
3+
4+
__metadata:
5+
version: 6
6+
cacheKey: 8
7+
8+
"@tokenizer/token@npm:^0.3.0":
9+
version: 0.3.0
10+
resolution: "@tokenizer/token@npm:0.3.0"
11+
checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1
12+
languageName: node
13+
linkType: hard
14+
15+
"@vue/tsconfig@npm:0.1.3":
16+
version: 0.1.3
17+
resolution: "@vue/tsconfig@npm:0.1.3"
18+
peerDependencies:
19+
"@types/node": "*"
20+
peerDependenciesMeta:
21+
"@types/node":
22+
optional: true
23+
checksum: 8150a24497a5348bc342c27afb38ad989de2ce8e94c349020628065d2a8df6837cb8bb3012f9161eea716487832612ac71b5f910d95bac41539ac6021d6bd88d
24+
languageName: node
25+
linkType: hard
26+
27+
"d3-array@npm:2 - 3":
28+
version: 3.2.0
29+
resolution: "d3-array@npm:3.2.0"
30+
dependencies:
31+
internmap: 1 - 2
32+
checksum: e236f6670b60b64abb6c435da25b5cbbdc2c7c0decdbf9355bc4cf6803d6da4fa820b7b78b9cbd127edb493555934a9788d45084c2f39d7c2e1a2b7aa48264a4
33+
languageName: node
34+
linkType: hard
35+
36+
"d3-time@npm:3.0.0":
37+
version: 3.0.0
38+
resolution: "d3-time@npm:3.0.0"
39+
dependencies:
40+
d3-array: 2 - 3
41+
checksum: 01646568ef01682550b7ee9f32394e4eb116a29515564861958871ed8de8fff02a25cd50dd8c4413921e6d9ecb8c8ce39be3266f655c8c18599fe58bcb253d60
42+
languageName: node
43+
linkType: hard
44+
45+
"internmap@npm:1 - 2":
46+
version: 2.0.3
47+
resolution: "internmap@npm:2.0.3"
48+
checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241
49+
languageName: node
50+
linkType: hard
51+
52+
"js-tokens@npm:^3.0.0 || ^4.0.0":
53+
version: 4.0.0
54+
resolution: "js-tokens@npm:4.0.0"
55+
checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78
56+
languageName: node
57+
linkType: hard
58+
59+
"loose-envify@npm:^1.1.0":
60+
version: 1.4.0
61+
resolution: "loose-envify@npm:1.4.0"
62+
dependencies:
63+
js-tokens: ^3.0.0 || ^4.0.0
64+
bin:
65+
loose-envify: cli.js
66+
checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4
67+
languageName: node
68+
linkType: hard
69+
70+
"mime@npm:3.0.0":
71+
version: 3.0.0
72+
resolution: "mime@npm:3.0.0"
73+
bin:
74+
mime: cli.js
75+
checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928
76+
languageName: node
77+
linkType: hard
78+
79+
"peek-readable@npm:^5.0.0":
80+
version: 5.0.0
81+
resolution: "peek-readable@npm:5.0.0"
82+
checksum: bef5ceb50586eb42e14efba274ac57ffe97f0ed272df9239ce029f688f495d9bf74b2886fa27847c706a9db33acda4b7d23bbd09a2d21eb4c2a54da915117414
83+
languageName: node
84+
linkType: hard
85+
86+
"react-dom@npm:18.2.0":
87+
version: 18.2.0
88+
resolution: "react-dom@npm:18.2.0"
89+
dependencies:
90+
loose-envify: ^1.1.0
91+
scheduler: ^0.23.0
92+
peerDependencies:
93+
react: ^18.2.0
94+
checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc
95+
languageName: node
96+
linkType: hard
97+
98+
"react@npm:18.2.0":
99+
version: 18.2.0
100+
resolution: "react@npm:18.2.0"
101+
dependencies:
102+
loose-envify: ^1.1.0
103+
checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b
104+
languageName: node
105+
linkType: hard
106+
107+
"root-workspace-0b6124@workspace:.":
108+
version: 0.0.0-use.local
109+
resolution: "root-workspace-0b6124@workspace:."
110+
dependencies:
111+
"@vue/tsconfig": 0.1.3
112+
d3-time: 3.0.0
113+
mime: 3.0.0
114+
react: 18.2.0
115+
react-dom: 18.2.0
116+
strtok3: 7.0.0
117+
languageName: unknown
118+
linkType: soft
119+
120+
"scheduler@npm:^0.23.0":
121+
version: 0.23.0
122+
resolution: "scheduler@npm:0.23.0"
123+
dependencies:
124+
loose-envify: ^1.1.0
125+
checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a
126+
languageName: node
127+
linkType: hard
128+
129+
"strtok3@npm:7.0.0":
130+
version: 7.0.0
131+
resolution: "strtok3@npm:7.0.0"
132+
dependencies:
133+
"@tokenizer/token": ^0.3.0
134+
peek-readable: ^5.0.0
135+
checksum: 2ebe7ad8f2aea611dec6742cf6a42e82764892a362907f7ce493faf334501bf981ce21c828dcc300457e6d460dc9c34d644ededb3b01dcb9e37559203cf1748c
136+
languageName: node
137+
linkType: hard

0 commit comments

Comments
 (0)