Skip to content

Commit

Permalink
fix #269: package paths for tsconfig.json extends
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 19, 2020
1 parent 5750c81 commit 5f37fe7
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

* Allow extending `tsconfig.json` paths inside packages ([#269](https://github.com/evanw/esbuild/issues/269))

Previously the `extends` field in `tsconfig.json` only worked with relative paths (paths starting with `./` or `../`). Now this field can also take a package path, which will be resolved by looking for the package in the `node_modules` directory.

## 0.6.3

* Fix `/* @__PURE__ */` IIFEs at start of statement ([#258](https://github.com/evanw/esbuild/issues/258))
Expand Down
32 changes: 32 additions & 0 deletions internal/bundler/bundler_tsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,35 @@ console.log(123);
},
})
}

func TestTsconfigJsonExtendsPackage(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/app/entry.jsx": `
console.log(<div/>)
`,
"/Users/user/project/src/tsconfig.json": `
{
"extends": "@package/foo/tsconfig.json"
}
`,
"/Users/user/project/node_modules/@package/foo/tsconfig.json": `
{
"compilerOptions": {
"jsxFactory": "worked"
}
}
`,
},
entryPaths: []string{"/Users/user/project/src/app/entry.jsx"},
options: config.Options{
IsBundling: true,
AbsOutputFile: "/Users/user/project/out.js",
},
expected: map[string]string{
"/Users/user/project/out.js": `// /Users/user/project/src/app/entry.jsx
console.log(/* @__PURE__ */ worked("div", null));
`,
},
})
}
72 changes: 54 additions & 18 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,13 @@ func (r *resolver) parseMemberExpressionForJSX(source logging.Source, loc ast.Lo
return parts
}

func (r *resolver) parseJsTsConfig(file string, path string, visited map[string]bool) (*tsConfigJson, parseStatus) {
func (r *resolver) parseJsTsConfig(file string, visited map[string]bool) (*tsConfigJson, parseStatus) {
// Don't infinite loop if a series of "extends" links forms a cycle
if visited[file] {
return nil, parseImportCycle
}
visited[file] = true
filePath := r.fs.Dir(file)

// Unfortunately "tsconfig.json" isn't actually JSON. It's some other
// format that appears to be defined by the implementation details of the
Expand All @@ -414,22 +415,57 @@ func (r *resolver) parseJsTsConfig(file string, path string, visited map[string]
if extendsJson, _, ok := getProperty(json, "extends"); ok {
if extends, ok := getString(extendsJson); ok {
warnRange := tsConfigSource.RangeOfString(extendsJson.Loc)
extendsFile := r.fs.Join(path, extends)
extendsDir := r.fs.Dir(extendsFile)
found := false

for _, file := range []string{extendsFile, extendsFile + ".json"} {
base, baseStatus := r.parseJsTsConfig(file, extendsDir, visited)
if baseStatus == parseReadFailure {
continue
} else if baseStatus == parseImportCycle {
r.log.AddRangeWarning(&tsConfigSource, warnRange,
fmt.Sprintf("Base config file %q forms cycle", extends))
} else if baseStatus == parseSuccess {
result = *base
if IsPackagePath(extends) {
// If this is a package path, try to resolve it to a "node_modules"
// folder. This doesn't use the normal node module resolution algorithm
// both because it's different (e.g. we don't want to match a directory)
// and because it would deadlock since we're currently in the middle of
// populating the directory info cache.
current := filePath
for !found {
// Skip "node_modules" folders
if r.fs.Base(current) != "node_modules" {
extendsFile := r.fs.Join(current, "node_modules", extends)
for _, fileToCheck := range []string{extendsFile, extendsFile + ".json"} {
base, baseStatus := r.parseJsTsConfig(fileToCheck, visited)
if baseStatus == parseReadFailure {
continue
} else if baseStatus == parseImportCycle {
r.log.AddRangeWarning(&tsConfigSource, warnRange,
fmt.Sprintf("Base config file %q forms cycle", extends))
} else if baseStatus == parseSuccess {
result = *base
}
found = true
break
}
}

// Go to the parent directory, stopping at the file system root
next := r.fs.Dir(current)
if current == next {
break
}
current = next
}
} else {
// If this is a regular path, search relative to the enclosing directory
extendsFile := r.fs.Join(filePath, extends)
for _, fileToCheck := range []string{extendsFile, extendsFile + ".json"} {
base, baseStatus := r.parseJsTsConfig(fileToCheck, visited)
if baseStatus == parseReadFailure {
continue
} else if baseStatus == parseImportCycle {
r.log.AddRangeWarning(&tsConfigSource, warnRange,
fmt.Sprintf("Base config file %q forms cycle", extends))
} else if baseStatus == parseSuccess {
result = *base
}
found = true
break
}
found = true
break
}

if !found {
Expand All @@ -444,7 +480,7 @@ func (r *resolver) parseJsTsConfig(file string, path string, visited map[string]
// Parse "baseUrl"
if baseUrlJson, _, ok := getProperty(compilerOptionsJson, "baseUrl"); ok {
if baseUrl, ok := getString(baseUrlJson); ok {
baseUrl = r.fs.Join(path, baseUrl)
baseUrl = r.fs.Join(filePath, baseUrl)
result.absPathBaseUrl = &baseUrl
}
}
Expand Down Expand Up @@ -594,13 +630,13 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo {
if forceTsConfig := r.options.TsConfigOverride; forceTsConfig == "" {
// Record if this directory has a tsconfig.json or jsconfig.json file
if entries["tsconfig.json"].Kind == fs.FileEntry {
info.tsConfigJson, _ = r.parseJsTsConfig(r.fs.Join(path, "tsconfig.json"), path, make(map[string]bool))
info.tsConfigJson, _ = r.parseJsTsConfig(r.fs.Join(path, "tsconfig.json"), make(map[string]bool))
} else if entries["jsconfig.json"].Kind == fs.FileEntry {
info.tsConfigJson, _ = r.parseJsTsConfig(r.fs.Join(path, "jsconfig.json"), path, make(map[string]bool))
info.tsConfigJson, _ = r.parseJsTsConfig(r.fs.Join(path, "jsconfig.json"), make(map[string]bool))
}
} else if parentInfo == nil {
// If there is a tsconfig.json override, mount it at the root directory
info.tsConfigJson, _ = r.parseJsTsConfig(forceTsConfig, r.fs.Dir(forceTsConfig), make(map[string]bool))
info.tsConfigJson, _ = r.parseJsTsConfig(forceTsConfig, make(map[string]bool))
}

// Is the "main" field from "package.json" missing?
Expand Down

0 comments on commit 5f37fe7

Please sign in to comment.