Skip to content

Commit

Permalink
fix #2456: extends in tsconfig.json with pnp
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Aug 12, 2022
1 parent 201c1f6 commit 4e68c27
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Fix Yarn PnP support for `extends` in `tsconfig.json` ([#2456](https://github.com/evanw/esbuild/issues/2456))

Previously using `extends` in `tsconfig.json` with a path in a Yarn PnP package didn't work. This is because the process of setting up package path resolution rules requires parsing `tsconfig.json` files (due to the `baseUrl` and `paths` features) and resolving `extends` to a package path requires package path resolution rules to already be set up, which is a circular dependency. This cycle is broken by using special rules for `extends` in `tsconfig.json` that bypasses esbuild's normal package path resolution process. This is why using `extends` with a Yarn PnP package didn't automatically work. With this release, these special rules have been modified to check for a Yarn PnP manifest so this case should work now.

* Fix Yarn PnP support in `esbuild-wasm` ([#2458](https://github.com/evanw/esbuild/issues/2458))

When running esbuild via WebAssembly, Yarn PnP support previously failed because Go's file system internals return `EINVAL` when trying to read a `.zip` file as a directory when run with WebAssembly. This was unexpected because Go's file system internals return `ENOTDIR` for this case on native. This release updates esbuild to treat `EINVAL` like `ENOTDIR` in this case, which fixes using `esbuild-wasm` to bundle a Yarn PnP project.
Expand Down
61 changes: 53 additions & 8 deletions internal/resolver/resolver.go
Expand Up @@ -936,12 +936,57 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC
fileDir := r.fs.Dir(file)

result := ParseTSConfigJSON(r.log, source, &r.caches.JSONCache, func(extends string, extendsRange logger.Range) *TSConfigJSON {
// Note: 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.

// Check for a Yarn PnP manifest and use that to rewrite the path
isAbsolutePathFromYarnPnP := false
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 := fileDir
for {
if _, _, ok := fs.ParseYarnPnPVirtualPath(current); !ok {
var pnpData *pnpData
absPath := r.fs.Join(current, ".pnp.data.json")
if json := r.extractYarnPnPDataFromJSON(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
pnpData = compileYarnPnPData(absPath, current, json)
} else {
absPath := r.fs.Join(current, ".pnp.cjs")
if json := r.extractYarnPnPDataFromJSON(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
pnpData = compileYarnPnPData(absPath, current, json)
} else {
absPath := r.fs.Join(current, ".pnp.js")
if json := r.extractYarnPnPDataFromJSON(absPath, pnpIgnoreErrorsAboutMissingFiles); json.Data != nil {
pnpData = compileYarnPnPData(absPath, current, json)
}
}
}
if pnpData != nil {
if result, ok := r.pnpResolve(extends, current, pnpData); ok {
extends = result // Continue with the module resolution algorithm from node.js
if r.fs.IsAbs(result) {
// Windows-style absolute paths are considered package paths
// because they do not start with a "/", but they should
// not go through package path resolution
isAbsolutePathFromYarnPnP = true
}
}
break
}
}

// Go to the parent directory, stopping at the file system root
next := r.fs.Dir(current)
if current == next {
break
}
current = next
}
}

if IsPackagePath(extends) && !isAbsolutePathFromYarnPnP {
// If this is still a package path, try to resolve it to a "node_modules" directory
current := fileDir
for {
// Skip "node_modules" folders
Expand Down Expand Up @@ -1215,17 +1260,17 @@ func (r resolverQuery) dirInfoUncached(path string) *dirInfo {
if _, _, ok := fs.ParseYarnPnPVirtualPath(path); !ok {
if pnp, _ := entries.Get(".pnp.data.json"); pnp != nil && pnp.Kind(r.fs) == fs.FileEntry {
absPath := r.fs.Join(path, ".pnp.data.json")
if json := r.extractYarnPnPDataFromJSON(absPath, &r.caches.JSONCache); json.Data != nil {
if json := r.extractYarnPnPDataFromJSON(absPath, pnpReportErrorsAboutMissingFiles); json.Data != nil {
info.pnpData = compileYarnPnPData(absPath, path, json)
}
} else if pnp, _ := entries.Get(".pnp.cjs"); pnp != nil && pnp.Kind(r.fs) == fs.FileEntry {
absPath := r.fs.Join(path, ".pnp.cjs")
if json := r.tryToExtractYarnPnPDataFromJS(absPath, &r.caches.JSONCache); json.Data != nil {
if json := r.tryToExtractYarnPnPDataFromJS(absPath, pnpReportErrorsAboutMissingFiles); json.Data != nil {
info.pnpData = compileYarnPnPData(absPath, path, json)
}
} else if pnp, _ := entries.Get(".pnp.js"); pnp != nil && pnp.Kind(r.fs) == fs.FileEntry {
absPath := r.fs.Join(path, ".pnp.js")
if json := r.tryToExtractYarnPnPDataFromJS(absPath, &r.caches.JSONCache); json.Data != nil {
if json := r.tryToExtractYarnPnPDataFromJS(absPath, pnpReportErrorsAboutMissingFiles); json.Data != nil {
info.pnpData = compileYarnPnPData(absPath, path, json)
}
}
Expand Down
33 changes: 22 additions & 11 deletions internal/resolver/yarnpnp.go
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"regexp"
"strings"
"syscall"

"github.com/evanw/esbuild/internal/cache"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_parser"
Expand Down Expand Up @@ -577,15 +577,24 @@ func getDependencyTarget(json js_ast.Expr) (pnpIdentAndReference, bool) {
return pnpIdentAndReference{}, false
}

func (r resolverQuery) extractYarnPnPDataFromJSON(pnpDataPath string, jsonCache *cache.JSONCache) (result js_ast.Expr) {
type pnpDataMode uint8

const (
pnpIgnoreErrorsAboutMissingFiles pnpDataMode = iota
pnpReportErrorsAboutMissingFiles
)

func (r resolverQuery) extractYarnPnPDataFromJSON(pnpDataPath string, mode pnpDataMode) (result js_ast.Expr) {
contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, pnpDataPath)
if r.debugLogs != nil && originalError != nil {
r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", pnpDataPath, originalError.Error()))
}
if err != nil {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
r.PrettyPath(logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
if mode == pnpReportErrorsAboutMissingFiles || err != syscall.ENOENT {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
r.PrettyPath(logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
}
return
}
if r.debugLogs != nil {
Expand All @@ -597,19 +606,21 @@ func (r resolverQuery) extractYarnPnPDataFromJSON(pnpDataPath string, jsonCache
PrettyPath: r.PrettyPath(keyPath),
Contents: contents,
}
result, _ = jsonCache.Parse(r.log, source, js_parser.JSONOptions{})
result, _ = r.caches.JSONCache.Parse(r.log, source, js_parser.JSONOptions{})
return
}

func (r resolverQuery) tryToExtractYarnPnPDataFromJS(pnpDataPath string, jsonCache *cache.JSONCache) (result js_ast.Expr) {
func (r resolverQuery) tryToExtractYarnPnPDataFromJS(pnpDataPath string, mode pnpDataMode) (result js_ast.Expr) {
contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, pnpDataPath)
if r.debugLogs != nil && originalError != nil {
r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", pnpDataPath, originalError.Error()))
}
if err != nil {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
r.PrettyPath(logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
if mode == pnpReportErrorsAboutMissingFiles || err != syscall.ENOENT {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
r.PrettyPath(logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
}
return
}
if r.debugLogs != nil {
Expand All @@ -622,7 +633,7 @@ func (r resolverQuery) tryToExtractYarnPnPDataFromJS(pnpDataPath string, jsonCac
PrettyPath: r.PrettyPath(keyPath),
Contents: contents,
}
ast, _ := js_parser.Parse(r.log, source, js_parser.OptionsForYarnPnP())
ast, _ := r.caches.JSCache.Parse(r.log, source, js_parser.OptionsForYarnPnP())

if r.debugLogs != nil && ast.ManifestForYarnPnP.Data != nil {
r.debugLogs.addNote(fmt.Sprintf(" Extracted JSON data from %q", pnpDataPath))
Expand Down
58 changes: 58 additions & 0 deletions scripts/js-api-tests.js
Expand Up @@ -3058,6 +3058,64 @@ require("/assets/file.png");
// scripts/.js-api-tests/yarnPnP_ignoreNestedManifests/entry.js
console.log(foo_default);
})();
`)
},

async yarnPnP_tsconfig({ esbuild, testDir }) {
const entry = path.join(testDir, 'entry.js')
const tsconfigExtends = path.join(testDir, 'tsconfig.json')
const tsconfigBase = path.join(testDir, 'foo', 'tsconfig.json')
const manifest = path.join(testDir, '.pnp.data.json')

await writeFileAsync(entry, `
x **= 2
`)

await writeFileAsync(tsconfigExtends, `{
"extends": "@scope/base/tsconfig.json",
}`)

await mkdirAsync(path.dirname(tsconfigBase), { recursive: true })
await writeFileAsync(tsconfigBase, `{
"compilerOptions": {
"target": "ES5"
}
}`)

await writeFileAsync(manifest, `{
"packageRegistryData": [
[null, [
[null, {
"packageLocation": "./",
"packageDependencies": [
["@scope/base", "npm:1.0.0"]
],
"linkType": "SOFT"
}]
]],
["@scope/base", [
["npm:1.0.0", {
"packageLocation": "./foo/",
"packageDependencies": [],
"linkType": "HARD"
}]
]]
]
}`)

const value = await esbuild.build({
entryPoints: [entry],
bundle: true,
write: false,
})

assert.strictEqual(value.outputFiles.length, 1)
assert.strictEqual(value.outputFiles[0].text, `(() => {
var __pow = Math.pow;
// scripts/.js-api-tests/yarnPnP_tsconfig/entry.js
x = __pow(x, 2);
})();
`)
},
}
Expand Down

0 comments on commit 4e68c27

Please sign in to comment.