diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbe071b6c2..b806a42e76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Starting with this release, that last step can now be replaced with telling esbuild to serve a specific HTML file using the `--serve-fallback=` option. This can be used to provide a "not found" page for missing URLs. It can also be used to implement a [single-page app](https://en.wikipedia.org/wiki/Single-page_application) that mutates the current URL and therefore requires the single app entry point to be served when the page is loaded regardless of whatever the current URL is. +* Use the `tsconfig` field in `package.json` during `extends` resolution ([#3247](https://github.com/evanw/esbuild/issues/3247)) + + This release adds a feature from [TypeScript 3.2](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#tsconfigjson-inheritance-via-nodejs-packages) where if a `tsconfig.json` file specifies a package name in the `extends` field and that package's `package.json` file has a `tsconfig` field, the contents of that field are used in the search for the base `tsconfig.json` file. + * Implement CSS nesting without `:is()` when possible ([#1945](https://github.com/evanw/esbuild/issues/1945)) Previously esbuild would always produce a warning when transforming nested CSS for a browser that doesn't support the `:is()` pseudo-class. This was because the nesting transform needs to generate an `:is()` in some complex cases which means the transformed CSS would then not work in that browser. However, the CSS nesting transform can often be done without generating an `:is()`. So with this release, esbuild will no longer warn when targeting browsers that don't support `:is()` in the cases where an `:is()` isn't needed to represent the nested CSS. diff --git a/internal/bundler_tests/bundler_tsconfig_test.go b/internal/bundler_tests/bundler_tsconfig_test.go index 6768c80eec6..d58e9680bd1 100644 --- a/internal/bundler_tests/bundler_tsconfig_test.go +++ b/internal/bundler_tests/bundler_tsconfig_test.go @@ -1199,6 +1199,140 @@ func TestTsconfigJsonNodeModulesImplicitFile(t *testing.T) { }) } +func TestTsconfigJsonNodeModulesTsconfigPathExact(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/app/entry.tsx": ` + console.log(
) + `, + "/Users/user/project/src/tsconfig.json": ` + { + "extends": "foo" + } + `, + "/Users/user/project/src/node_modules/foo/package.json": ` + { + "tsconfig": "over/here.json" + } + `, + "/Users/user/project/src/node_modules/foo/over/here.json": ` + { + "compilerOptions": { + "jsx": "react", + "jsxFactory": "worked" + } + } + `, + }, + entryPaths: []string{"/Users/user/project/src/app/entry.tsx"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + }) +} + +func TestTsconfigJsonNodeModulesTsconfigPathImplicitJson(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/app/entry.tsx": ` + console.log() + `, + "/Users/user/project/src/tsconfig.json": ` + { + "extends": "foo" + } + `, + "/Users/user/project/src/node_modules/foo/package.json": ` + { + "tsconfig": "over/here" + } + `, + "/Users/user/project/src/node_modules/foo/over/here.json": ` + { + "compilerOptions": { + "jsx": "react", + "jsxFactory": "worked" + } + } + `, + }, + entryPaths: []string{"/Users/user/project/src/app/entry.tsx"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + }) +} + +func TestTsconfigJsonNodeModulesTsconfigPathDirectory(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/app/entry.tsx": ` + console.log() + `, + "/Users/user/project/src/tsconfig.json": ` + { + "extends": "foo" + } + `, + "/Users/user/project/src/node_modules/foo/package.json": ` + { + "tsconfig": "over/here" + } + `, + "/Users/user/project/src/node_modules/foo/over/here/tsconfig.json": ` + { + "compilerOptions": { + "jsx": "react", + "jsxFactory": "worked" + } + } + `, + }, + entryPaths: []string{"/Users/user/project/src/app/entry.tsx"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + }) +} + +func TestTsconfigJsonNodeModulesTsconfigPathBad(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/app/entry.tsx": ` + console.log() + `, + "/Users/user/project/src/tsconfig.json": ` + { + "extends": "foo" + } + `, + "/Users/user/project/src/node_modules/foo/package.json": ` + { + "tsconfig": "over/here.json" + } + `, + "/Users/user/project/src/node_modules/foo/tsconfig.json": ` + { + "compilerOptions": { + "jsx": "react", + "jsxFactory": "THIS SHOULD NOT BE LOADED" + } + } + `, + }, + entryPaths: []string{"/Users/user/project/src/app/entry.tsx"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + expectedScanLog: `Users/user/project/src/tsconfig.json: WARNING: Cannot find base config file "foo" +`, + }) +} + func TestTsconfigJsonInsideNodeModules(t *testing.T) { tsconfig_suite.expectBundled(t, bundled{ files: map[string]string{ diff --git a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt index 29cba788315..b70cf0d70aa 100644 --- a/internal/bundler_tests/snapshots/snapshots_tsconfig.txt +++ b/internal/bundler_tests/snapshots/snapshots_tsconfig.txt @@ -283,6 +283,30 @@ TestTsconfigJsonNodeModulesImplicitFile // Users/user/project/src/app/entry.tsx console.log(/* @__PURE__ */ worked("div", null)); +================================================================================ +TestTsconfigJsonNodeModulesTsconfigPathBad +---------- /Users/user/project/out.js ---------- +// Users/user/project/src/app/entry.tsx +console.log(/* @__PURE__ */ React.createElement("div", null)); + +================================================================================ +TestTsconfigJsonNodeModulesTsconfigPathDirectory +---------- /Users/user/project/out.js ---------- +// Users/user/project/src/app/entry.tsx +console.log(/* @__PURE__ */ worked("div", null)); + +================================================================================ +TestTsconfigJsonNodeModulesTsconfigPathExact +---------- /Users/user/project/out.js ---------- +// Users/user/project/src/app/entry.tsx +console.log(/* @__PURE__ */ worked("div", null)); + +================================================================================ +TestTsconfigJsonNodeModulesTsconfigPathImplicitJson +---------- /Users/user/project/out.js ---------- +// Users/user/project/src/app/entry.tsx +console.log(/* @__PURE__ */ worked("div", null)); + ================================================================================ TestTsconfigJsonOverrideMissing ---------- /Users/user/project/out.js ---------- diff --git a/internal/resolver/package_json.go b/internal/resolver/package_json.go index 0257ba9457a..04fd6a820b5 100644 --- a/internal/resolver/package_json.go +++ b/internal/resolver/package_json.go @@ -21,6 +21,14 @@ type packageJSON struct { mainFields map[string]mainField moduleTypeData js_ast.ModuleTypeData + // "TypeScript will first check whether package.json contains a "tsconfig" + // field, and if it does, TypeScript will try to load a configuration file + // from that field. If neither exists, TypeScript will try to read from a + // tsconfig.json at the root." + // + // See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#tsconfigjson-inheritance-via-nodejs-packages + tsconfig string + // Present if the "browser" field is present. This field is intended to be // used by bundlers and lets you redirect the paths of certain 3rd-party // modules that don't work in the browser to other modules that shim that @@ -323,6 +331,13 @@ func (r resolverQuery) parsePackageJSON(inputPath string) *packageJSON { } } + // Read the "tsconfig" field + if tsconfigJSON, _, ok := getProperty(json, "tsconfig"); ok { + if tsconfigValue, ok := getString(tsconfigJSON); ok { + packageJSON.tsconfig = tsconfigValue + } + } + // Read the "main" fields mainFields := r.options.MainFields if mainFields == nil { diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 85bb5ca5063..45ba7d5b05c 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -1142,30 +1142,44 @@ func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map for { // Skip "node_modules" folders if r.fs.Base(current) != "node_modules" { - // if "package.json" exists, try checking the "exports" map. The - // ability to use "extends" like this was added in TypeScript 5.0. + join := r.fs.Join(current, "node_modules", extends) + + // Check to see if "package.json" exists pkgDir := r.fs.Join(current, "node_modules", esmPackageName) pjFile := r.fs.Join(pkgDir, "package.json") if _, err, originalError := r.fs.ReadFile(pjFile); err == nil { - if packageJSON := r.parsePackageJSON(pkgDir); packageJSON != nil && packageJSON.exportsMap != nil { - if r.debugLogs != nil { - r.debugLogs.addNote(fmt.Sprintf("Looking for %q in \"exports\" map in %q", esmPackageSubpath, packageJSON.source.KeyPath.Text)) - r.debugLogs.increaseIndent() - defer r.debugLogs.decreaseIndent() + if packageJSON := r.parsePackageJSON(pkgDir); packageJSON != nil { + // Try checking the "tsconfig" field of "package.json". The ability to use "extends" like this was added in TypeScript 3.2: + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#tsconfigjson-inheritance-via-nodejs-packages + if packageJSON.tsconfig != "" { + join = packageJSON.tsconfig + if !r.fs.IsAbs(join) { + join = r.fs.Join(pkgDir, join) + } } - // Note: TypeScript appears to always treat this as a "require" import - conditions := r.esmConditionsRequire - resolvedPath, status, debug := r.esmPackageExportsResolve("/", esmPackageSubpath, packageJSON.exportsMap.root, conditions) - resolvedPath, status, debug = r.esmHandlePostConditions(resolvedPath, status, debug) + // Try checking the "exports" map. The ability to use "extends" like this was added in TypeScript 5.0: + // https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/ + if packageJSON.exportsMap != nil { + if r.debugLogs != nil { + r.debugLogs.addNote(fmt.Sprintf("Looking for %q in \"exports\" map in %q", esmPackageSubpath, packageJSON.source.KeyPath.Text)) + r.debugLogs.increaseIndent() + defer r.debugLogs.decreaseIndent() + } + + // Note: TypeScript appears to always treat this as a "require" import + conditions := r.esmConditionsRequire + resolvedPath, status, debug := r.esmPackageExportsResolve("/", esmPackageSubpath, packageJSON.exportsMap.root, conditions) + resolvedPath, status, debug = r.esmHandlePostConditions(resolvedPath, status, debug) - // This is a very abbreviated version of our ESM resolution - if status == pjStatusExact || status == pjStatusExactEndsWithStar { - fileToCheck := r.fs.Join(pkgDir, resolvedPath) - base, err := r.parseTSConfig(fileToCheck, visited) + // This is a very abbreviated version of our ESM resolution + if status == pjStatusExact || status == pjStatusExactEndsWithStar { + fileToCheck := r.fs.Join(pkgDir, resolvedPath) + base, err := r.parseTSConfig(fileToCheck, visited) - if result, shouldReturn := maybeFinishOurSearch(base, err, fileToCheck); shouldReturn { - return result + if result, shouldReturn := maybeFinishOurSearch(base, err, fileToCheck); shouldReturn { + return result + } } } } @@ -1173,7 +1187,6 @@ func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", pjFile, originalError.Error())) } - join := r.fs.Join(current, "node_modules", extends) filesToCheck := []string{r.fs.Join(join, "tsconfig.json"), join, join + ".json"} for _, fileToCheck := range filesToCheck { base, err := r.parseTSConfig(fileToCheck, visited)