Skip to content

Commit 6a69a18

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

12 files changed

Lines changed: 246 additions & 66 deletions

File tree

.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": "yarn@3.2.2",
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+
}

0 commit comments

Comments
 (0)