From 01a253f4d933398815bfb0bc5aa9944fa4ab379c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:15:08 -0700 Subject: [PATCH 1/3] Disable automatic type acquisition when no user cache is available Automatic type acquisition should stay disabled when a user-specific cache directory cannot be determined, rather than installing typings into a temporary directory. --- tsc/cmd/tsc/lsp.go | 3 +-- tsc/internal/lsp/replay_test.go | 3 +-- tsc/internal/project/ata/ata_test.go | 31 ++++++++++++++++++++++++ tsc/internal/vfs/osvfs/os.go | 6 ++--- tsc/internal/vfs/osvfs/os_test.go | 35 ++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/tsc/cmd/tsc/lsp.go b/tsc/cmd/tsc/lsp.go index b294ad5688416..0c6a3a6c71e81 100644 --- a/tsc/cmd/tsc/lsp.go +++ b/tsc/cmd/tsc/lsp.go @@ -44,7 +44,6 @@ func runLSP(args []string) int { fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() - typingsLocation := osvfs.GetGlobalTypingsCacheLocation() cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -57,7 +56,7 @@ func runLSP(args []string) int { Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: tspath.ToRootedDirectoryPath(typingsLocation, cwd), + TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(), NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { cmd := exec.CommandContext(ctx, "npm", args...) cmd.Dir = cwd diff --git a/tsc/internal/lsp/replay_test.go b/tsc/internal/lsp/replay_test.go index d0b40f33042b6..4f9b06e377ee5 100644 --- a/tsc/internal/lsp/replay_test.go +++ b/tsc/internal/lsp/replay_test.go @@ -51,14 +51,13 @@ func TestReplay(t *testing.T) { fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() - typingsLocation := osvfs.GetGlobalTypingsCacheLocation() cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) serverOpts := lsp.ServerOptions{ Err: os.Stderr, Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: tspath.ToRootedDirectoryPath(typingsLocation, cwd), + TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(), NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { cmd := exec.CommandContext(ctx, "npm", args...) cmd.Dir = cwd diff --git a/tsc/internal/project/ata/ata_test.go b/tsc/internal/project/ata/ata_test.go index 258788185c9c0..60021e4573adc 100644 --- a/tsc/internal/project/ata/ata_test.go +++ b/tsc/internal/project/ata/ata_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -19,6 +20,36 @@ func TestATA(t *testing.T) { t.Skip("bundled files are not embedded") } + t.Run("no typings cache disables acquisition", func(t *testing.T) { + t.Parallel() + + files := map[string]any{ + "/user/username/projects/project/app.js": `const $ = require("jquery");`, + "/user/username/projects/project/jsconfig.json": `{ + "typeAcquisition": { "enable": true } + }`, + } + session, utils := projecttestutil.SetupWithOptionsAndTypingsInstaller(files, &project.SessionOptions{ + CurrentDirectory: tspath.RootedDirectoryPathFromNormalized("/"), + DefaultLibraryPath: bundled.LibPath(), + WatchEnabled: true, + }, &projecttestutil.TypingsInstallerOptions{ + PackageToFile: map[string]string{ + "jquery": `export const x: number;`, + }, + }) + + uri := lsproto.DocumentUri("file:///user/username/projects/project/app.js") + session.DidOpenFile(context.Background(), uri, 1, files["/user/username/projects/project/app.js"].(string), lsproto.LanguageKindJavaScript) + session.WaitForBackgroundTasks() + + ls, err := session.GetLanguageService(context.Background(), uri) + assert.NilError(t, err) + assert.Assert(t, ls.GetProgram().GetSourceFile("/user/username/projects/project/app.js") != nil) + assert.Equal(t, len(utils.NpmExecutor().NpmInstallCalls()), 0) + assert.Assert(t, !session.FS().FileExists(projecttestutil.TestTypingsDirectory.ResolveFile("package.json"))) + }) + t.Run("local module should not be picked up", func(t *testing.T) { t.Parallel() files := map[string]any{ diff --git a/tsc/internal/vfs/osvfs/os.go b/tsc/internal/vfs/osvfs/os.go index dbd0911ab5d77..0bc4f542b93e6 100644 --- a/tsc/internal/vfs/osvfs/os.go +++ b/tsc/internal/vfs/osvfs/os.go @@ -188,10 +188,10 @@ func (vfs *osFS) Chtimes(path tspath.RootedPath, aTime time.Time, mTime time.Tim return os.Chtimes(path.AsString(), aTime, mTime) } -func GetGlobalTypingsCacheLocation() string { +func GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { cacheDir, err := os.UserCacheDir() if err != nil { - cacheDir = os.TempDir() + return "" } var subdir string @@ -200,5 +200,5 @@ func GetGlobalTypingsCacheLocation() string { } else { subdir = "typescript" } - return tspath.CombinePaths(cacheDir, subdir, core.VersionMajorMinor()) + return tspath.RootedDirectoryPathFromAbsolute(cacheDir).ResolveDirectory(subdir).ResolveDirectory(core.VersionMajorMinor()) } diff --git a/tsc/internal/vfs/osvfs/os_test.go b/tsc/internal/vfs/osvfs/os_test.go index cb6bfdef53c39..35842826bd863 100644 --- a/tsc/internal/vfs/osvfs/os_test.go +++ b/tsc/internal/vfs/osvfs/os_test.go @@ -7,12 +7,47 @@ import ( "strings" "testing" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/repo" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" "gotest.tools/v3/assert" ) +func TestGetGlobalTypingsCacheLocation(t *testing.T) { + t.Run("user cache", func(t *testing.T) { + cacheRoot := t.TempDir() + t.Setenv("HOME", cacheRoot) + t.Setenv("home", cacheRoot) + t.Setenv("LocalAppData", cacheRoot) + t.Setenv("XDG_CACHE_HOME", cacheRoot) + cacheDir, err := os.UserCacheDir() + assert.NilError(t, err) + + subdir := "typescript" + if runtime.GOOS == "windows" { + subdir = "Microsoft/TypeScript" + } + assert.Equal(t, osvfs.GetGlobalTypingsCacheLocation(), tspath.RootedDirectoryPathFromAbsolute(tspath.CombinePaths(cacheDir, subdir, core.VersionMajorMinor()))) + }) + + t.Run("missing user cache", func(t *testing.T) { + for _, name := range []string{"HOME", "home", "LocalAppData", "XDG_CACHE_HOME"} { + t.Setenv(name, "") + } + assert.Equal(t, osvfs.GetGlobalTypingsCacheLocation(), tspath.RootedDirectoryPath("")) + }) + + t.Run("relative XDG cache", func(t *testing.T) { + switch runtime.GOOS { + case "windows", "darwin", "ios", "plan9": + t.Skip("XDG_CACHE_HOME is not used on this platform") + } + t.Setenv("XDG_CACHE_HOME", "relative") + assert.Equal(t, osvfs.GetGlobalTypingsCacheLocation(), tspath.RootedDirectoryPath("")) + }) +} + func TestOS(t *testing.T) { t.Parallel() From eb883673207c3aeac0a07ba878add7b3c4c4ffc5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:15:57 -0700 Subject: [PATCH 2/3] Keep typings cache lookup in its original position --- tsc/cmd/tsc/lsp.go | 3 ++- tsc/internal/lsp/replay_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tsc/cmd/tsc/lsp.go b/tsc/cmd/tsc/lsp.go index 0c6a3a6c71e81..3bb3affd2b160 100644 --- a/tsc/cmd/tsc/lsp.go +++ b/tsc/cmd/tsc/lsp.go @@ -44,6 +44,7 @@ func runLSP(args []string) int { fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() + typingsLocation := osvfs.GetGlobalTypingsCacheLocation() cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -56,7 +57,7 @@ func runLSP(args []string) int { Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(), + TypingsLocation: typingsLocation, NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { cmd := exec.CommandContext(ctx, "npm", args...) cmd.Dir = cwd diff --git a/tsc/internal/lsp/replay_test.go b/tsc/internal/lsp/replay_test.go index 4f9b06e377ee5..55264326a44bf 100644 --- a/tsc/internal/lsp/replay_test.go +++ b/tsc/internal/lsp/replay_test.go @@ -51,13 +51,14 @@ func TestReplay(t *testing.T) { fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() + typingsLocation := osvfs.GetGlobalTypingsCacheLocation() cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) serverOpts := lsp.ServerOptions{ Err: os.Stderr, Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(), + TypingsLocation: typingsLocation, NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { cmd := exec.CommandContext(ctx, "npm", args...) cmd.Dir = cwd From 526968bdcaf8c24e5ad6d06c59b499413eabbb80 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:27:57 -0700 Subject: [PATCH 3/3] Disable type acquisition for relative user cache paths UserCacheDir can return a relative path without an error when the home or local app data directory is relative. Such a cache is unavailable for global typings and must not prevent the language server from starting. --- tsc/internal/vfs/osvfs/os.go | 6 +++++- tsc/internal/vfs/osvfs/os_test.go | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tsc/internal/vfs/osvfs/os.go b/tsc/internal/vfs/osvfs/os.go index 0bc4f542b93e6..33e5bce39a8ad 100644 --- a/tsc/internal/vfs/osvfs/os.go +++ b/tsc/internal/vfs/osvfs/os.go @@ -193,6 +193,10 @@ func GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { if err != nil { return "" } + cachePath, ok := tspath.TryRootedPathFromAbsolute(cacheDir) + if !ok { + return "" + } var subdir string if runtime.GOOS == "windows" { @@ -200,5 +204,5 @@ func GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { } else { subdir = "typescript" } - return tspath.RootedDirectoryPathFromAbsolute(cacheDir).ResolveDirectory(subdir).ResolveDirectory(core.VersionMajorMinor()) + return tspath.RootedDirectoryPathFromPath(cachePath).ResolveDirectory(subdir).ResolveDirectory(core.VersionMajorMinor()) } diff --git a/tsc/internal/vfs/osvfs/os_test.go b/tsc/internal/vfs/osvfs/os_test.go index 35842826bd863..9baab0859cc26 100644 --- a/tsc/internal/vfs/osvfs/os_test.go +++ b/tsc/internal/vfs/osvfs/os_test.go @@ -46,6 +46,14 @@ func TestGetGlobalTypingsCacheLocation(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "relative") assert.Equal(t, osvfs.GetGlobalTypingsCacheLocation(), tspath.RootedDirectoryPath("")) }) + + t.Run("relative user cache", func(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", "") + for _, name := range []string{"HOME", "home", "LocalAppData"} { + t.Setenv(name, "relative-home") + } + assert.Equal(t, osvfs.GetGlobalTypingsCacheLocation(), tspath.RootedDirectoryPath("")) + }) } func TestOS(t *testing.T) {