Skip to content

Commit

Permalink
fix #3247: use tsconfig in package.json
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 20, 2023
1 parent 60004e7 commit 629c921
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
134 changes: 134 additions & 0 deletions internal/bundler_tests/bundler_tsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(<div/>)
`,
"/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(<div/>)
`,
"/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(<div/>)
`,
"/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(<div/>)
`,
"/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{
Expand Down
24 changes: 24 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_tsconfig.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------
Expand Down
15 changes: 15 additions & 0 deletions internal/resolver/package_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
49 changes: 31 additions & 18 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1142,38 +1142,51 @@ 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
}
}
}
}
} else if r.debugLogs != nil && originalError != nil {
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)
Expand Down

0 comments on commit 629c921

Please sign in to comment.