Skip to content

Commit 112b9aa

Browse files
committed
fix #3915: stack overflow with yarn + tsconfig
1 parent ed5a555 commit 112b9aa

File tree

5 files changed

+175
-8
lines changed

5 files changed

+175
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
./esbuild --version
2020
```
2121
22+
* Handle Yarn Plug'n'Play stack overflow with `tsconfig.json` ([#3915](https://github.com/evanw/esbuild/issues/3915))
23+
24+
Previously a `tsconfig.json` file that `extends` another file in a package with an `exports` map could cause a stack overflow when Yarn's Plug'n'Play resolution was active. This edge case should work now starting with this release.
25+
2226
## 0.23.1
2327
2428
* Allow using the `node:` import prefix with `es*` targets ([#3821](https://github.com/evanw/esbuild/issues/3821))

internal/bundler_tests/bundler_tsconfig_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,3 +2749,71 @@ func TestTsconfigJsonConfigDirBaseURLInheritedPaths(t *testing.T) {
27492749
},
27502750
})
27512751
}
2752+
2753+
// https://github.com/evanw/esbuild/issues/3915
2754+
func TestTsconfigStackOverflowYarnPnP(t *testing.T) {
2755+
tsconfig_suite.expectBundled(t, bundled{
2756+
files: map[string]string{
2757+
"/Users/user/project/entry.jsx": `
2758+
console.log(<div />)
2759+
`,
2760+
"/Users/user/project/tsconfig.json": `
2761+
{
2762+
"extends": "tsconfigs/config"
2763+
}
2764+
`,
2765+
"/Users/user/project/packages/tsconfigs/package.json": `
2766+
{
2767+
"exports": {
2768+
"./config": "./configs/tsconfig.json"
2769+
}
2770+
}
2771+
`,
2772+
"/Users/user/project/packages/tsconfigs/configs/tsconfig.json": `
2773+
{
2774+
"compilerOptions": {
2775+
"jsxFactory": "success"
2776+
}
2777+
}
2778+
`,
2779+
"/Users/user/project/.pnp.data.json": `
2780+
{
2781+
"packageRegistryData": [
2782+
[null, [
2783+
[null, {
2784+
"packageLocation": "./",
2785+
"packageDependencies": [
2786+
["tsconfigs", "virtual:some-path"]
2787+
],
2788+
"linkType": "SOFT"
2789+
}]
2790+
]],
2791+
["tsconfigs", [
2792+
["virtual:some-path", {
2793+
"packageLocation": "./packages/tsconfigs/",
2794+
"packageDependencies": [
2795+
["tsconfigs", "virtual:some-path"]
2796+
],
2797+
"packagePeers": [],
2798+
"linkType": "SOFT"
2799+
}],
2800+
["workspace:packages/tsconfigs", {
2801+
"packageLocation": "./packages/tsconfigs/",
2802+
"packageDependencies": [
2803+
["tsconfigs", "workspace:packages/tsconfigs"]
2804+
],
2805+
"linkType": "SOFT"
2806+
}]
2807+
]]
2808+
]
2809+
}
2810+
`,
2811+
},
2812+
entryPaths: []string{"/Users/user/project/entry.jsx"},
2813+
absWorkingDir: "/Users/user/project",
2814+
options: config.Options{
2815+
Mode: config.ModeBundle,
2816+
AbsOutputFile: "/Users/user/project/out.js",
2817+
},
2818+
})
2819+
}

internal/bundler_tests/snapshots/snapshots_tsconfig.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,12 @@ TestTsconfigRemoveUnusedImports
724724
// Users/user/project/src/entry.ts
725725
console.log(1);
726726

727+
================================================================================
728+
TestTsconfigStackOverflowYarnPnP
729+
---------- /Users/user/project/out.js ----------
730+
// entry.jsx
731+
console.log(/* @__PURE__ */ success("div", null));
732+
727733
================================================================================
728734
TestTsconfigUnrecognizedTargetWarning
729735
---------- /Users/user/project/out.js ----------

internal/resolver/resolver.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,12 +1132,19 @@ func (r resolverQuery) dirInfoCached(path string) *dirInfo {
11321132

11331133
// Cache hit: stop now
11341134
if !ok {
1135+
// Update the cache to indicate failure. Even if the read failed, we don't
1136+
// want to retry again later. The directory is inaccessible so trying again
1137+
// is wasted. Doing this before calling "dirInfoUncached" prevents stack
1138+
// overflow in case this directory is recursively encountered again.
1139+
r.dirCache[path] = nil
1140+
11351141
// Cache miss: read the info
11361142
cached = r.dirInfoUncached(path)
11371143

1138-
// Update the cache unconditionally. Even if the read failed, we don't want to
1139-
// retry again later. The directory is inaccessible so trying again is wasted.
1140-
r.dirCache[path] = cached
1144+
// Only update the cache again on success
1145+
if cached != nil {
1146+
r.dirCache[path] = cached
1147+
}
11411148
}
11421149

11431150
if r.debugLogs != nil {
@@ -1294,7 +1301,8 @@ func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map
12941301
if entry, _ := entries.Get("package.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry {
12951302
// Check the "exports" map
12961303
if packageJSON := r.parsePackageJSON(result.pkgDirPath); packageJSON != nil && packageJSON.exportsMap != nil {
1297-
if absolute, ok, _ := r.esmResolveAlgorithm(result.pkgIdent, "."+result.pkgSubpath, packageJSON, result.pkgDirPath, source.KeyPath.Text); ok {
1304+
if absolute, ok, _ := r.esmResolveAlgorithm(finalizeImportsExportsYarnPnPTSConfigExtends,
1305+
result.pkgIdent, "."+result.pkgSubpath, packageJSON, result.pkgDirPath, source.KeyPath.Text); ok {
12981306
base, err := r.parseTSConfig(absolute.Primary.Text, visited, configDir)
12991307
if result, shouldReturn := maybeFinishOurSearch(base, err, absolute.Primary.Text); shouldReturn {
13001308
return result
@@ -2236,14 +2244,22 @@ func (r resolverQuery) loadPackageImports(importPath string, dirInfoPackageJSON
22362244
}
22372245

22382246
absolute, ok, diffCase := r.finalizeImportsExportsResult(
2247+
finalizeImportsExportsNormal,
22392248
dirInfoPackageJSON.absPath, conditions, *packageJSON.importsMap, packageJSON,
22402249
resolvedPath, status, debug,
22412250
"", "", "",
22422251
)
22432252
return absolute, ok, diffCase, nil
22442253
}
22452254

2246-
func (r resolverQuery) esmResolveAlgorithm(esmPackageName string, esmPackageSubpath string, packageJSON *packageJSON, absPkgPath string, absPath string) (PathPair, bool, *fs.DifferentCase) {
2255+
func (r resolverQuery) esmResolveAlgorithm(
2256+
kind finalizeImportsExportsKind,
2257+
esmPackageName string,
2258+
esmPackageSubpath string,
2259+
packageJSON *packageJSON,
2260+
absPkgPath string,
2261+
absPath string,
2262+
) (PathPair, bool, *fs.DifferentCase) {
22472263
if r.debugLogs != nil {
22482264
r.debugLogs.addNote(fmt.Sprintf("Looking for %q in \"exports\" map in %q", esmPackageSubpath, packageJSON.source.KeyPath.Text))
22492265
r.debugLogs.increaseIndent()
@@ -2278,6 +2294,7 @@ func (r resolverQuery) esmResolveAlgorithm(esmPackageName string, esmPackageSubp
22782294
resolvedPath, status, debug = r.esmHandlePostConditions(resolvedPath, status, debug)
22792295

22802296
return r.finalizeImportsExportsResult(
2297+
kind,
22812298
absPkgPath, conditions, *packageJSON.exportsMap, packageJSON,
22822299
resolvedPath, status, debug,
22832300
esmPackageName, esmPackageSubpath, absPath,
@@ -2358,7 +2375,7 @@ func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forb
23582375
if pkgDirInfo := r.dirInfoCached(result.pkgDirPath); pkgDirInfo != nil {
23592376
// Check the "exports" map
23602377
if packageJSON := pkgDirInfo.packageJSON; packageJSON != nil && packageJSON.exportsMap != nil {
2361-
absolute, ok, diffCase := r.esmResolveAlgorithm(result.pkgIdent, "."+result.pkgSubpath, packageJSON, pkgDirInfo.absPath, absPath)
2378+
absolute, ok, diffCase := r.esmResolveAlgorithm(finalizeImportsExportsNormal, result.pkgIdent, "."+result.pkgSubpath, packageJSON, pkgDirInfo.absPath, absPath)
23622379
return absolute, ok, diffCase, nil
23632380
}
23642381

@@ -2393,7 +2410,7 @@ func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forb
23932410
// Check for self-references
23942411
if dirInfoPackageJSON != nil {
23952412
if packageJSON := dirInfoPackageJSON.packageJSON; packageJSON.name == esmPackageName && packageJSON.exportsMap != nil {
2396-
absolute, ok, diffCase := r.esmResolveAlgorithm(esmPackageName, esmPackageSubpath, packageJSON,
2413+
absolute, ok, diffCase := r.esmResolveAlgorithm(finalizeImportsExportsNormal, esmPackageName, esmPackageSubpath, packageJSON,
23972414
dirInfoPackageJSON.absPath, r.fs.Join(dirInfoPackageJSON.absPath, esmPackageSubpath))
23982415
return absolute, ok, diffCase, nil
23992416
}
@@ -2412,7 +2429,7 @@ func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forb
24122429
if pkgDirInfo := r.dirInfoCached(absPkgPath); pkgDirInfo != nil {
24132430
// Check the "exports" map
24142431
if packageJSON := pkgDirInfo.packageJSON; packageJSON != nil && packageJSON.exportsMap != nil {
2415-
absolute, ok, diffCase := r.esmResolveAlgorithm(esmPackageName, esmPackageSubpath, packageJSON, absPkgPath, absPath)
2432+
absolute, ok, diffCase := r.esmResolveAlgorithm(finalizeImportsExportsNormal, esmPackageName, esmPackageSubpath, packageJSON, absPkgPath, absPath)
24162433
return absolute, ok, diffCase, nil, true
24172434
}
24182435

@@ -2524,7 +2541,15 @@ func (r resolverQuery) checkForBuiltInNodeModules(importPath string) (PathPair,
25242541
return PathPair{}, false, nil
25252542
}
25262543

2544+
type finalizeImportsExportsKind uint8
2545+
2546+
const (
2547+
finalizeImportsExportsNormal finalizeImportsExportsKind = iota
2548+
finalizeImportsExportsYarnPnPTSConfigExtends
2549+
)
2550+
25272551
func (r resolverQuery) finalizeImportsExportsResult(
2552+
kind finalizeImportsExportsKind,
25282553
absDirPath string,
25292554
conditions map[string]bool,
25302555
importExportMap pjMap,
@@ -2551,6 +2576,14 @@ func (r resolverQuery) finalizeImportsExportsResult(
25512576
r.debugLogs.addNote(fmt.Sprintf("The resolved path %q is exact", absResolvedPath))
25522577
}
25532578

2579+
// Avoid calling "dirInfoCached" recursively for "tsconfig.json" extends with Yarn PnP
2580+
if kind == finalizeImportsExportsYarnPnPTSConfigExtends {
2581+
if r.debugLogs != nil {
2582+
r.debugLogs.addNote(fmt.Sprintf("Resolved to %q", absResolvedPath))
2583+
}
2584+
return PathPair{Primary: logger.Path{Text: absResolvedPath, Namespace: "file"}}, true, nil
2585+
}
2586+
25542587
resolvedDirInfo := r.dirInfoCached(r.fs.Dir(absResolvedPath))
25552588
base := r.fs.Base(absResolvedPath)
25562589
extensionOrder := r.options.ExtensionOrder

scripts/js-api-tests.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3738,6 +3738,62 @@ import "after/alias";
37383738
// .yarn/cache/dep-zip/node_modules/dep/index.js
37393739
success();
37403740
})();
3741+
`)
3742+
},
3743+
3744+
// https://github.com/evanw/esbuild/issues/3915
3745+
async yarnPnP_stackOverflow({ esbuild, testDir }) {
3746+
const entry = path.join(testDir, 'entry.jsx')
3747+
3748+
await writeFileAsync(entry, `console.log(<div />)`)
3749+
await writeFileAsync(path.join(testDir, 'tsconfig.json'), `{ "extends": "tsconfigs/config" }`)
3750+
await mkdirAsync(path.join(testDir, 'packages/tsconfigs/configs'), { recursive: true })
3751+
await writeFileAsync(path.join(testDir, 'packages/tsconfigs/package.json'), `{ "exports": { "./config": "./configs/tsconfig.json" } }`)
3752+
await writeFileAsync(path.join(testDir, 'packages/tsconfigs/configs/tsconfig.json'), `{ "compilerOptions": { "jsxFactory": "success" } }`)
3753+
3754+
await writeFileAsync(path.join(testDir, '.pnp.data.json'), `{
3755+
"packageRegistryData": [
3756+
[null, [
3757+
[null, {
3758+
"packageLocation": "./",
3759+
"packageDependencies": [
3760+
["tsconfigs", "virtual:some-path"]
3761+
],
3762+
"linkType": "SOFT"
3763+
}]
3764+
]],
3765+
["tsconfigs", [
3766+
["virtual:some-path", {
3767+
"packageLocation": "./.yarn/__virtual__/tsconfigs-virtual-f56a53910e/1/packages/tsconfigs/",
3768+
"packageDependencies": [
3769+
["tsconfigs", "virtual:some-path"]
3770+
],
3771+
"packagePeers": [],
3772+
"linkType": "SOFT"
3773+
}],
3774+
["workspace:packages/tsconfigs", {
3775+
"packageLocation": "./packages/tsconfigs/",
3776+
"packageDependencies": [
3777+
["tsconfigs", "workspace:packages/tsconfigs"]
3778+
],
3779+
"linkType": "SOFT"
3780+
}]
3781+
]]
3782+
]
3783+
}`)
3784+
3785+
const value = await esbuild.build({
3786+
entryPoints: [entry],
3787+
bundle: true,
3788+
write: false,
3789+
absWorkingDir: testDir,
3790+
})
3791+
3792+
assert.strictEqual(value.outputFiles.length, 1)
3793+
assert.strictEqual(value.outputFiles[0].text, `(() => {
3794+
// entry.jsx
3795+
console.log(/* @__PURE__ */ success("div", null));
3796+
})();
37413797
`)
37423798
},
37433799
}

0 commit comments

Comments
 (0)