diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d101020e8..e2c274aa94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ ./esbuild --version ``` +* Avoid incorrect cycle warning with `tsconfig.json` multiple inheritance ([#3898](https://github.com/evanw/esbuild/issues/3898)) + + TypeScript 5.0 introduced multiple inheritance for `tsconfig.json` files where `extends` can be an array of file paths. Previously esbuild would incorrectly treat files encountered more than once when processing separate subtrees of the multiple inheritance hierarchy as an inheritance cycle. With this release, `tsconfig.json` files containing this edge case should work correctly without generating a warning. + * Handle Yarn Plug'n'Play stack overflow with `tsconfig.json` ([#3915](https://github.com/evanw/esbuild/issues/3915)) 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. diff --git a/internal/bundler_tests/bundler_tsconfig_test.go b/internal/bundler_tests/bundler_tsconfig_test.go index 12d2a72077..c7c0795b4e 100644 --- a/internal/bundler_tests/bundler_tsconfig_test.go +++ b/internal/bundler_tests/bundler_tsconfig_test.go @@ -2817,3 +2817,55 @@ func TestTsconfigStackOverflowYarnPnP(t *testing.T) { }, }) } + +func TestTsconfigJsonExtendsArrayIssue3898(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/index.tsx": ` + import { type SomeType } from 'MUST_KEEP' + console.log(<> +
+ ) + `, + "/Users/user/project/tsconfig.json": ` + { + "extends": [ + "./tsconfigs/a.json", + "./tsconfigs/b.json", + ] + } + `, + "/Users/user/project/tsconfigs/base.json": ` + { + "compilerOptions": { + "verbatimModuleSyntax": true, + } + } + `, + "/Users/user/project/tsconfigs/a.json": ` + { + "extends": "./base.json", + "compilerOptions": { + "jsxFactory": "SUCCESS", + } + } + `, + "/Users/user/project/tsconfigs/b.json": ` + { + "extends": "./base.json", + "compilerOptions": { + "jsxFragmentFactory": "WORKS", + } + } + `, + }, + entryPaths: []string{"/Users/user/project/index.tsx"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/Users/user/project/out.js", + JSX: config.JSXOptions{ + SideEffects: true, + }, + }, + }) +} diff --git a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt index c96c6d8658..f77255a3a9 100644 --- a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt +++ b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt @@ -290,6 +290,12 @@ TestTsconfigJsonExtendsAbsolute // Users/user/project/entry.jsx console.log(/* @__PURE__ */ baseFactory("div", null), /* @__PURE__ */ baseFactory(derivedFragment, null)); +================================================================================ +TestTsconfigJsonExtendsArrayIssue3898 +---------- /Users/user/project/out.js ---------- +import {} from "MUST_KEEP"; +console.log(SUCCESS(WORKS, null, SUCCESS("div", null))); + ================================================================================ TestTsconfigJsonExtendsLoop ---------- /out.js ---------- diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 3a4ae894db..b3f6c8b565 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -1183,11 +1183,6 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool, confi if visited[file] { return nil, errParseErrorImportCycle } - if visited != nil { - // This is only non-nil for "build" API calls. This is nil for "transform" - // API calls, which tells us to not process "extends" fields. - visited[file] = true - } contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, file) if r.debugLogs != nil && originalError != nil { @@ -1206,7 +1201,20 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool, confi PrettyPath: PrettyPath(r.fs, keyPath), Contents: contents, } - return r.parseTSConfigFromSource(source, visited, configDir) + if visited != nil { + // This is only non-nil for "build" API calls. This is nil for "transform" + // API calls, which tells us to not process "extends" fields. + visited[file] = true + } + result, err := r.parseTSConfigFromSource(source, visited, configDir) + if visited != nil { + // Reset this to back false in case something uses TypeScript 5.0's multiple + // inheritance feature for "tsconfig.json" files. It should be valid to visit + // the same base "tsconfig.json" file multiple times from different multiple + // inheritance subtrees. + visited[file] = false + } + return result, err } func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map[string]bool, configDir string) (*TSConfigJSON, error) {