From 6e6f15f9d6a32237ac68da2978de22ee25a2ae17 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 15 May 2024 13:50:29 -0400 Subject: [PATCH] fix #3767: `tsconfig.json` files inside symlinks --- CHANGELOG.md | 6 ++++++ internal/fs/fs.go | 1 + internal/fs/fs_mock.go | 4 ++++ internal/fs/fs_real.go | 7 +++++++ internal/fs/fs_zip.go | 4 ++++ internal/resolver/resolver.go | 7 +++++++ scripts/end-to-end-tests.js | 24 ++++++++++++++++++++++++ 7 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57169abe436..9b2d719c90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* Fix `tsconfig.json` files inside symlinked directories ([#3767](https://github.com/evanw/esbuild/issues/3767)) + + This release fixes an issue with a scenario involving a `tsconfig.json` file that `extends` another file from within a symlinked directory that uses the `paths` feature. In that case, the implicit `baseURL` value should be based on the real path (i.e. after expanding all symbolic links) instead of the original path. This was already done for other files that esbuild resolves but was not yet done for `tsconfig.json` because it's special-cased (the regular path resolver can't be used because the information inside `tsconfig.json` is involved in path resolution). Note that this fix no longer applies if the `--preserve-symlinks` setting is enabled. + ## 0.21.2 * Correct `this` in field and accessor decorators ([#3761](https://github.com/evanw/esbuild/issues/3761)) diff --git a/internal/fs/fs.go b/internal/fs/fs.go index b68da2c20d2..ccfcc6cded8 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -201,6 +201,7 @@ type FS interface { Join(parts ...string) string Cwd() string Rel(base string, target string) (string, bool) + EvalSymlinks(path string) (string, bool) // This is used in the implementation of "Entry" kind(dir string, base string) (symlink string, kind EntryKind) diff --git a/internal/fs/fs_mock.go b/internal/fs/fs_mock.go index 1bb62b97fd2..8626b59796d 100644 --- a/internal/fs/fs_mock.go +++ b/internal/fs/fs_mock.go @@ -281,6 +281,10 @@ func (fs *mockFS) Rel(base string, target string) (string, bool) { return target, true } +func (fs *mockFS) EvalSymlinks(path string) (string, bool) { + return "", false +} + func (fs *mockFS) kind(dir string, base string) (symlink string, kind EntryKind) { panic("This should never be called") } diff --git a/internal/fs/fs_real.go b/internal/fs/fs_real.go index 5b0ef3b4c55..412eb687d85 100644 --- a/internal/fs/fs_real.go +++ b/internal/fs/fs_real.go @@ -340,6 +340,13 @@ func (fs *realFS) Rel(base string, target string) (string, bool) { return "", false } +func (fs *realFS) EvalSymlinks(path string) (string, bool) { + if path, err := fs.fp.evalSymlinks(path); err == nil { + return path, true + } + return "", false +} + func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) { BeforeFileOpen() defer AfterFileClose() diff --git a/internal/fs/fs_zip.go b/internal/fs/fs_zip.go index 4f168f25fad..58a7b8ba494 100644 --- a/internal/fs/fs_zip.go +++ b/internal/fs/fs_zip.go @@ -322,6 +322,10 @@ func (fs *zipFS) Rel(base string, target string) (string, bool) { return fs.inner.Rel(base, target) } +func (fs *zipFS) EvalSymlinks(path string) (string, bool) { + return fs.inner.EvalSymlinks(path) +} + func (fs *zipFS) kind(dir string, base string) (symlink string, kind EntryKind) { return fs.inner.kind(dir, base) } diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 6e50acd9e1d..f53b5ab90c8 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -1165,6 +1165,13 @@ var errParseErrorAlreadyLogged = errors.New("(error already logged)") // Nested calls may also return "parseErrorImportCycle". In that case the // caller is responsible for logging an appropriate error message. func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSConfigJSON, error) { + // Resolve any symlinks first before parsing the file + if !r.options.PreserveSymlinks { + if real, ok := r.fs.EvalSymlinks(file); ok { + file = real + } + } + // Don't infinite loop if a series of "extends" links forms a cycle if visited[file] { return nil, errParseErrorImportCycle diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 5c3dc61c703..8766064738b 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -218,6 +218,30 @@ tests.push( export let bar = 'bar' `, }), + + // See: https://github.com/evanw/esbuild/issues/3767 + test(['apps/client/src/index.ts', '--bundle', '--outfile=node.js'], { + 'apps/client/src/index.ts': ` + import { foo } from '~/foo' + if (foo !== 'foo') throw 'fail' + `, + 'apps/client/src/foo.ts': ` + export const foo = 'foo' + `, + 'apps/client/tsconfig.json': `{ + "extends": "@repo/tsconfig/base" + }`, + 'apps/client/node_modules/@repo/tsconfig': { + symlink: `../../../../tooling/typescript`, + }, + 'tooling/typescript/base.json': `{ + "compilerOptions": { + "paths": { + "~/*": ["../../apps/client/src/*"] + } + } + }`, + }), ) // Test coverage for a special JSX error message