Skip to content

Commit 3dac33f

Browse files
committed
fix #3131, fix #3663: yarnpnp + windows + D drive
1 parent 0f2c5c8 commit 3dac33f

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
## Unreleased
44

5+
* Better support building projects that use Yarn on Windows ([#3131](https://github.com/evanw/esbuild/issues/3131), [#3663](https://github.com/evanw/esbuild/issues/3663))
6+
7+
With this release, you can now use esbuild to bundle projects that use Yarn Plug'n'Play on Windows on drives other than the `C:` drive. The problem was as follows:
8+
9+
1. Yarn in Plug'n'Play mode on Windows stores its global module cache on the `C:` drive
10+
2. Some developers put their projects on the `D:` drive
11+
3. Yarn generates relative paths that use `../..` to get from the project directory to the cache directory
12+
4. Windows-style paths don't support directory traversal between drives via `..` (so `D:\..` is just `D:`)
13+
5. I didn't have access to a Windows machine for testing this edge case
14+
15+
Yarn works around this edge case by pretending Windows-style paths beginning with `C:\` are actually Unix-style paths beginning with `/C:/`, so the `../..` path segments are able to navigate across drives inside Yarn's implementation. This was broken for a long time in esbuild but I finally got access to a Windows machine and was able to debug and fix this edge case. So you should now be able to bundle these projects with esbuild.
16+
517
* Preserve parentheses around function expressions ([#4252](https://github.com/evanw/esbuild/issues/4252))
618

719
The V8 JavaScript VM uses parentheses around function expressions as an optimization hint to immediately compile the function. Otherwise the function would be lazily-compiled, which has additional overhead if that function is always called immediately as lazy compilation involves parsing the function twice. You can read [V8's blog post about this](https://v8.dev/blog/preparser) for more details.

internal/bundler_tests/bundler_yarnpnp_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,57 @@ func TestTsconfigStackOverflowYarnPnP(t *testing.T) {
150150
},
151151
})
152152
}
153+
func TestWindowsCrossVolumeReferenceYarnPnP(t *testing.T) {
154+
yarnpnp_suite.expectBundledWindows(t, bundled{
155+
files: map[string]string{
156+
"D:\\project\\entry.jsx": `
157+
import * as React from 'react'
158+
console.log(<div />)
159+
`,
160+
"C:\\Users\\user\\AppData\\Local\\Yarn\\Berry\\cache\\react.zip\\node_modules\\react\\index.js": `
161+
export function createElement() {}
162+
`,
163+
"D:\\project\\.pnp.data.json": `
164+
{
165+
"packageRegistryData": [
166+
[null, [
167+
[null, {
168+
"packageLocation": "./",
169+
"packageDependencies": [
170+
["react", "npm:19.1.1"],
171+
["project", "workspace:."]
172+
],
173+
"linkType": "SOFT"
174+
}]
175+
]],
176+
["react", [
177+
["npm:19.1.1", {
178+
"packageLocation": "../../C:/Users/user/AppData/Local/Yarn/Berry/cache/react.zip/node_modules/react/",
179+
"packageDependencies": [
180+
["react", "npm:19.1.1"]
181+
],
182+
"linkType": "HARD"
183+
}]
184+
]],
185+
["project", [
186+
["workspace:.", {
187+
"packageLocation": "./",
188+
"packageDependencies": [
189+
["react", "npm:19.1.1"],
190+
["project", "workspace:."]
191+
],
192+
"linkType": "SOFT"
193+
}]
194+
]]
195+
]
196+
}
197+
`,
198+
},
199+
entryPaths: []string{"D:\\project\\entry.jsx"},
200+
absWorkingDir: "D:\\project",
201+
options: config.Options{
202+
Mode: config.ModeBundle,
203+
AbsOutputFile: "D:\\project\\out.js",
204+
},
205+
})
206+
}

internal/bundler_tests/snapshots/snapshots_yarnpnp.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ TestTsconfigStackOverflowYarnPnP
88
---------- /Users/user/project/out.js ----------
99
// entry.jsx
1010
console.log(/* @__PURE__ */ success("div", null));
11+
12+
================================================================================
13+
TestWindowsCrossVolumeReferenceYarnPnP
14+
---------- D:/project/out.js ----------
15+
// C:/Users/user/AppData/Local/Yarn/Berry/cache/react.zip/node_modules/react/index.js
16+
function createElement() {
17+
}
18+
19+
// entry.jsx
20+
console.log(/* @__PURE__ */ createElement("div", null));

internal/resolver/yarnpnp.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package resolver
44

55
import (
66
"fmt"
7+
"path"
78
"regexp"
89
"strings"
910
"syscall"
@@ -278,7 +279,25 @@ func (r resolverQuery) resolveToUnqualified(specifier string, parentURL string,
278279
}
279280

280281
// Return path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
281-
pkgDirPath := r.fs.Join(manifest.absDirPath, dependencyPkg.packageLocation)
282+
absDirPath := manifest.absDirPath
283+
isWindows := !strings.HasPrefix(absDirPath, "/")
284+
if isWindows {
285+
// Yarn converts Windows-style paths with volume labels into Unix-style
286+
// paths with a "/" prefix for the purpose of joining them together here.
287+
// So "C:\foo\bar.txt" becomes "/C:/foo/bar.txt". This is very important
288+
// because Yarn also stores a single global cache on the "C:" drive, many
289+
// developers do their work on the "D:" drive, and Yarn uses "../C:" to
290+
// traverse between the "D:" drive and the "C:" drive. Windows doesn't
291+
// allow you to do that ("D:\.." is just "D:\") so without temporarily
292+
// swapping to Unix-style paths here, esbuild would otherwise fail in this
293+
// case while Yarn itself would succeed.
294+
absDirPath = "/" + strings.ReplaceAll(absDirPath, "\\", "/")
295+
}
296+
pkgDirPath := path.Join(absDirPath, dependencyPkg.packageLocation)
297+
if isWindows && strings.HasPrefix(pkgDirPath, "/") {
298+
// Convert the Unix-style path back into a Windows-style path afterwards
299+
pkgDirPath = strings.ReplaceAll(pkgDirPath[1:], "\\", "//")
300+
}
282301
if r.debugLogs != nil {
283302
r.debugLogs.addNote(fmt.Sprintf(" Resolved %q via Yarn PnP to %q with subpath %q", specifier, pkgDirPath, modulePath))
284303
}

0 commit comments

Comments
 (0)