From 4d997d948579b2e4e9bd3bf820a0108b58b3732b Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 14 Mar 2024 14:01:02 -0400 Subject: [PATCH] fix #3698: yarn pnp edge case with `tsconfig.json` --- CHANGELOG.md | 4 + .../bundler_tests/bundler_tsconfig_test.go | 73 +++++++++++++++++++ .../snapshots/snapshots_tsconfig.txt | 6 ++ internal/resolver/resolver.go | 20 +++++ 4 files changed, 103 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e02f6409f3..9f0bef64f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ })(Foo || {}); ``` +* Handle Yarn Plug'n'Play edge case with `tsconfig.json` ([#3698](https://github.com/evanw/esbuild/issues/3698)) + + Previously a `tsconfig.json` file that `extends` another file in a package with an `exports` map failed to work when Yarn's Plug'n'Play resolution was active. This edge case should work now starting with this release. + * Work around issues with Deno 1.31+ ([#3682](https://github.com/evanw/esbuild/issues/3682)) Version 0.20.0 of esbuild changed how the esbuild child process is run in esbuild's API for Deno. Previously it used `Deno.run` but that API is being removed in favor of `Deno.Command`. As part of this change, esbuild is now calling the new `unref` function on esbuild's long-lived child process, which is supposed to allow Deno to exit when your code has finished running even though the child process is still around (previously you had to explicitly call esbuild's `stop()` function to terminate the child process for Deno to be able to exit). diff --git a/internal/bundler_tests/bundler_tsconfig_test.go b/internal/bundler_tests/bundler_tsconfig_test.go index f709dd4b86c..e2abe434211 100644 --- a/internal/bundler_tests/bundler_tsconfig_test.go +++ b/internal/bundler_tests/bundler_tsconfig_test.go @@ -2581,3 +2581,76 @@ func TestTsconfigJsonAsteriskNameCollisionIssue3354(t *testing.T) { }, }) } + +// https://github.com/evanw/esbuild/issues/3698 +func TestTsconfigPackageJsonExportsYarnPnP(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/packages/app/index.tsx": ` + console.log(
) + `, + "/Users/user/project/packages/app/tsconfig.json": ` + { + "extends": "tsconfigs/config" + } + `, + "/Users/user/project/packages/tsconfigs/package.json": ` + { + "exports": { + "./config": "./configs/tsconfig.json" + } + } + `, + "/Users/user/project/packages/tsconfigs/configs/tsconfig.json": ` + { + "compilerOptions": { + "jsxFactory": "success" + } + } + `, + "/Users/user/project/.pnp.data.json": ` + { + "packageRegistryData": [ + [ + "app", + [ + [ + "workspace:packages/app", + { + "packageLocation": "./packages/app/", + "packageDependencies": [ + [ + "tsconfigs", + "workspace:packages/tsconfigs" + ] + ], + "linkType": "SOFT" + } + ] + ] + ], + [ + "tsconfigs", + [ + [ + "workspace:packages/tsconfigs", + { + "packageLocation": "./packages/tsconfigs/", + "packageDependencies": [], + "linkType": "SOFT" + } + ] + ] + ] + ] + } + `, + }, + entryPaths: []string{"/Users/user/project/packages/app/index.tsx"}, + absWorkingDir: "/Users/user/project", + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + }) +} diff --git a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt index 5db4ed23092..f9616bf8aca 100644 --- a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt +++ b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt @@ -396,6 +396,12 @@ var both_default = /* @__PURE__ */ R.c(R.F, null, /* @__PURE__ */ R.c("div", nul // Users/user/project/entry.ts console.log(factory_default, fragment_default, both_default); +================================================================================ +TestTsconfigPackageJsonExportsYarnPnP +---------- /Users/user/project/out.js ---------- +// packages/app/index.tsx +console.log(/* @__PURE__ */ success("div", null)); + ================================================================================ TestTsconfigPaths ---------- /Users/user/project/out.js ---------- diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 147cd7f50e8..8d8a28d0127 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -1269,6 +1269,26 @@ func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map } goto pnpError } else if result.status == pnpSuccess { + // If Yarn PnP path resolution succeeded, run a custom abbreviated + // version of node's module resolution algorithm. The Yarn PnP + // specification says to use node's module resolution algorithm verbatim + // but that isn't what Yarn actually does. See this for more info: + // https://github.com/evanw/esbuild/issues/2473#issuecomment-1216774461 + if entries, _, dirErr := r.fs.ReadDirectory(result.pkgDirPath); dirErr == nil { + if entry, _ := entries.Get("package.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { + // Check the "exports" map + if packageJSON := r.parsePackageJSON(result.pkgDirPath); packageJSON != nil && packageJSON.exportsMap != nil { + if absolute, ok, _ := r.esmResolveAlgorithm(result.pkgIdent, "."+result.pkgSubpath, packageJSON, result.pkgDirPath, source.KeyPath.Text); ok { + base, err := r.parseTSConfig(absolute.Primary.Text, visited) + if result, shouldReturn := maybeFinishOurSearch(base, err, absolute.Primary.Text); shouldReturn { + return result + } + } + goto pnpError + } + } + } + // Continue with the module resolution algorithm from node.js extends = r.fs.Join(result.pkgDirPath, result.pkgSubpath) }