From 3fc691fccac88aa46507cbc4712ec639ff97130c Mon Sep 17 00:00:00 2001 From: jackeddisciple Date: Sat, 24 Oct 2026 01:50:00 +0530 Subject: [PATCH 1/2] Add program.getAllDependencies to the API --- packages/typescript/src/api/async/api.ts | 9 ++ .../typescript/src/api/proto.generated.ts | 3 + packages/typescript/src/api/sync/api.ts | 27 ++++ packages/typescript/test/async/api.test.ts | 42 ++++++ .../test/sync/api-generators.test.ts | 1 + packages/typescript/test/sync/api.test.ts | 42 ++++++ tsc/internal/api/proto.go | 2 + tsc/internal/api/session.go | 21 +++ tsc/internal/api/session_dependencies_test.go | 83 ++++++++++++ .../execute/incremental/dependencies.go | 43 ++++++ .../execute/incremental/dependencies_test.go | 124 ++++++++++++++++++ .../execute/incremental/programtosnapshot.go | 8 +- 12 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 tsc/internal/api/session_dependencies_test.go create mode 100644 tsc/internal/execute/incremental/dependencies.go create mode 100644 tsc/internal/execute/incremental/dependencies_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 64dbd13a61a75..1915c68f3cf1b 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1885,6 +1885,15 @@ export class Program implements FormatDiagnost return result ?? undefined; } + async getAllDependencies(file: DocumentIdentifier): Promise { + const data = await this.client.apiRequest("getAllDependencies", { + snapshot: this.snapshotId, + project: this.project.handle, + file, + }); + return data ?? []; + } + async getSourceFileNames(): Promise { const data = await this.client.apiRequest("getSourceFileNames", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 6f49fab3ca6ab..9e872e83447b4 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -87,6 +87,7 @@ export interface APIMethodInfo { getResolvedModuleFromModuleSpecifier: APIMethod; getResolvedTypeReferenceDirective: APIMethod; getResolvedTypeReferenceDirectiveFromTypeReferenceDirective: APIMethod; + getAllDependencies: APIMethod; getConfigFileNames: APIMethod; getConfigSourceFile: APIMethod; resolveName: APIMethod; @@ -1167,6 +1168,7 @@ export interface BatchRequest { | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" | "getAliasedSymbol" + | "getAllDependencies" | "getAnyType" | "getApparentPropertiesOfType" | "getApparentType" @@ -1349,6 +1351,7 @@ export interface BatchResponse { | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" | "getAliasedSymbol" + | "getAllDependencies" | "getAnyType" | "getApparentPropertiesOfType" | "getApparentType" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index f08ef255288b9..69b195c28bba3 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -3264,6 +3264,33 @@ export class Program implements FormatDiagnost ); } + get getAllDependencies(): { + (file: DocumentIdentifier): readonly RootedFilePath[]; + gen(file: DocumentIdentifier): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getAllDependencies", + function (file: DocumentIdentifier): readonly RootedFilePath[] { + const data = owner.client.apiRequest("getAllDependencies", { + snapshot: owner.snapshotId, + project: owner.project.handle, + file, + }); + return data ?? []; + }, + function* (file: DocumentIdentifier): Generator { + const data = yield* apiRequest("getAllDependencies", { + snapshot: owner.snapshotId, + project: owner.project.handle, + file, + }); + return data ?? []; + }, + ); + } + get getSourceFileNames(): { (): readonly RootedFilePath[]; gen(): Generator; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 379f07b6b68f7..0c8077a5fd779 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1322,6 +1322,48 @@ declare module "augmentation" {}`, await program.dispose(); }); + test("Program getAllDependencies", async () => { + await using api = spawnAPI({ + "/src/index.ts": `/// +import { a } from "./a"; +export const value = a;`, + "/src/a.ts": `import type { Shared } from "./shared"; +export const a: Shared["a"] = 1;`, + "/src/shared.ts": `export interface Shared { a: number; }`, + "/src/augment.ts": `export {}; +declare module "./shared" { interface Shared { b: string; } }`, + "/src/unrelated.ts": `export const unrelated = 1;`, + "/src/script.ts": `const scriptValue = 1;`, + "/node_modules/@types/pkg-types/package.json": JSON.stringify({ name: "@types/pkg-types", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/@types/pkg-types/index.d.ts": `export {};`, + }); + + const program = await api.createProgram( + ["/src/index.ts", "/src/augment.ts", "/src/unrelated.ts", "/src/script.ts"], + { module: ModuleKind.ESNext, moduleResolution: ModuleResolutionKind.Bundler, noLib: true, types: [] }, + ); + + assert.deepEqual(await program.getAllDependencies("/src/index.ts"), [ + "/src/index.ts", + "/node_modules/@types/pkg-types/index.d.ts", + "/src/shared.ts", + "/src/a.ts", + "/src/augment.ts", + ]); + assert.deepEqual(await program.getAllDependencies("/src/augment.ts"), ["/src/augment.ts", "/src/shared.ts"]); + assert.deepEqual(await program.getAllDependencies("/src/unrelated.ts"), ["/src/unrelated.ts"]); + + const sourceFileNames = await program.getSourceFileNames(); + assert.deepEqual(await program.getAllDependencies("/src/script.ts"), [ + "/src/script.ts", + ...sourceFileNames.filter(fileName => fileName !== "/src/script.ts"), + ]); + + await assert.rejects(program.getAllDependencies("/src/missing.ts"), /source file not found/); // @sync: assert.throws(() => program.getAllDependencies("/src/missing.ts"), /source file not found/); + + await program.dispose(); + }); + test("createProgram resolves raw compiler option paths", async () => { await using api = spawnAPI({ "/src/index.ts": `import { value } from "ba"; export { value };`, diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 314052b63f789..e9ff978e8f480 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1613,6 +1613,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getResolvedModuleFromModuleSpecifier", program.getResolvedModuleFromModuleSpecifier, assertDeepEquivalent, importSpecifier), parityCase("Program", "getResolvedTypeReferenceDirective", program.getResolvedTypeReferenceDirective, assertDeepEquivalent, "/src/index.ts", "parity", ModuleKind.CommonJS), parityCase("Program", "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", program.getResolvedTypeReferenceDirectiveFromTypeReferenceDirective, assertDeepEquivalent, typeReferenceDirective, "/src/index.ts"), + parityCase("Program", "getAllDependencies", program.getAllDependencies, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileByPath", program.getSourceFileByPath, assertOptionalSourceFilesEquivalent, indexFile.path), parityCase("Program", "getSourceFileNames", program.getSourceFileNames, assertDeepEquivalent), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f048e8ede87e6..89305f8ce7709 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -1309,6 +1309,48 @@ declare module "augmentation" {}`, program.dispose(); }); + test("Program getAllDependencies", () => { + using api = spawnAPI({ + "/src/index.ts": `/// +import { a } from "./a"; +export const value = a;`, + "/src/a.ts": `import type { Shared } from "./shared"; +export const a: Shared["a"] = 1;`, + "/src/shared.ts": `export interface Shared { a: number; }`, + "/src/augment.ts": `export {}; +declare module "./shared" { interface Shared { b: string; } }`, + "/src/unrelated.ts": `export const unrelated = 1;`, + "/src/script.ts": `const scriptValue = 1;`, + "/node_modules/@types/pkg-types/package.json": JSON.stringify({ name: "@types/pkg-types", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/@types/pkg-types/index.d.ts": `export {};`, + }); + + const program = api.createProgram( + ["/src/index.ts", "/src/augment.ts", "/src/unrelated.ts", "/src/script.ts"], + { module: ModuleKind.ESNext, moduleResolution: ModuleResolutionKind.Bundler, noLib: true, types: [] }, + ); + + assert.deepEqual(program.getAllDependencies("/src/index.ts"), [ + "/src/index.ts", + "/node_modules/@types/pkg-types/index.d.ts", + "/src/shared.ts", + "/src/a.ts", + "/src/augment.ts", + ]); + assert.deepEqual(program.getAllDependencies("/src/augment.ts"), ["/src/augment.ts", "/src/shared.ts"]); + assert.deepEqual(program.getAllDependencies("/src/unrelated.ts"), ["/src/unrelated.ts"]); + + const sourceFileNames = program.getSourceFileNames(); + assert.deepEqual(program.getAllDependencies("/src/script.ts"), [ + "/src/script.ts", + ...sourceFileNames.filter(fileName => fileName !== "/src/script.ts"), + ]); + + assert.throws(() => program.getAllDependencies("/src/missing.ts"), /source file not found/); + + program.dispose(); + }); + test("createProgram resolves raw compiler option paths", () => { using api = spawnAPI({ "/src/index.ts": `import { value } from "ba"; export { value };`, diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index c03e7ff85ed46..ace1b6991aecb 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -118,6 +118,7 @@ const ( MethodGetResolvedModuleFromModuleSpecifier Method = "getResolvedModuleFromModuleSpecifier" MethodGetResolvedTypeReferenceDirective Method = "getResolvedTypeReferenceDirective" MethodGetResolvedTypeReferenceDirectiveFromReference Method = "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective" + MethodGetAllDependencies Method = "getAllDependencies" MethodGetConfigFileNames Method = "getConfigFileNames" MethodGetConfigSourceFile Method = "getConfigSourceFile" MethodResolveName Method = "resolveName" @@ -633,6 +634,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetResolvedModuleFromModuleSpecifier: unmarshallerFor[GetResolvedModuleFromModuleSpecifierParams], MethodGetResolvedTypeReferenceDirective: unmarshallerFor[GetResolvedTypeReferenceDirectiveParams], MethodGetResolvedTypeReferenceDirectiveFromReference: unmarshallerFor[GetResolvedTypeReferenceDirectiveFromReferenceParams], + MethodGetAllDependencies: unmarshallerFor[GetSourceFileParams], MethodGetConfigFileNames: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetConfigSourceFile: unmarshallerFor[GetSourceFileParams], MethodGetSymbolAtPosition: unmarshallerFor[GetSymbolAtPositionParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index c88ef9a1b3bee..65060343cb6e0 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -26,6 +26,7 @@ import ( compilerdebug "github.com/microsoft/TypeScript/tsc/internal/debug" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/execute/build" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/format" "github.com/microsoft/TypeScript/tsc/internal/ipc" @@ -965,6 +966,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetResolvedTypeReferenceDirective(ctx, parsed.(*GetResolvedTypeReferenceDirectiveParams)) case string(MethodGetResolvedTypeReferenceDirectiveFromReference): return s.handleGetResolvedTypeReferenceDirectiveFromReference(ctx, parsed.(*GetResolvedTypeReferenceDirectiveFromReferenceParams)) + case string(MethodGetAllDependencies): + return s.handleGetAllDependencies(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetConfigFileNames): return s.handleGetConfigFileNames(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetConfigSourceFile): @@ -2666,6 +2669,24 @@ func (s *Session) handleGetResolvedTypeReferenceDirectiveFromReference(ctx conte return newResolvedTypeReferenceDirectiveResponse(program.GetResolvedTypeReferenceDirective(sourceFile, params.TypeDirectiveName, mode)), nil } +func (s *Session) handleGetAllDependencies(ctx context.Context, params *GetSourceFileParams) ([]tspath.RootedFilePath, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + sourceFile, err := s.resolveOptionalSourceFile(program, ¶ms.File) + if err != nil { + return nil, err + } + ch, done := program.GetTypeCheckerForFile(ctx, sourceFile) + defer done() + return core.Map(incremental.GetAllDependencies(program, ch, sourceFile), (*ast.SourceFile).FileName), nil +} + // handleGetSymbolAtPosition returns the symbol at a position in a file. // @gen-proto-nullable func (s *Session) handleGetSymbolAtPosition(ctx context.Context, params *GetSymbolAtPositionParams) (*SymbolResponse, error) { diff --git a/tsc/internal/api/session_dependencies_test.go b/tsc/internal/api/session_dependencies_test.go new file mode 100644 index 0000000000000..ec59ec8bffbd8 --- /dev/null +++ b/tsc/internal/api/session_dependencies_test.go @@ -0,0 +1,83 @@ +package api + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +func TestGetAllDependencies(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + disableSourceRedirect bool + expected []tspath.RootedFilePath + }{ + { + name: "uses sources of project references", + expected: []tspath.RootedFilePath{"/home/projects/app/src/index.ts", "/home/projects/shared/src/util.ts"}, + }, + { + name: "uses outputs of project references", + disableSourceRedirect: true, + expected: []tspath.RootedFilePath{"/home/projects/app/src/index.ts", "/home/projects/shared/dist/util.d.ts"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + const configFileName = "/home/projects/app/tsconfig.json" + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/shared/tsconfig.json": `{ "compilerOptions": { "composite": true, "noLib": true, "rootDir": "src", "outDir": "dist" }, "include": ["src"] }`, + "/home/projects/shared/src/util.ts": `export const util = 1;`, + "/home/projects/shared/dist/util.d.ts": `export declare const util = 1;`, + configFileName: fmt.Sprintf( + `{ "compilerOptions": { "noLib": true, "disableSourceOfProjectReferenceRedirect": %t }, "references": [{ "path": "../shared" }], "include": ["src"] }`, + test.disableSourceRedirect, + ), + "/home/projects/app/src/index.ts": `import { util } from "../../shared/src/util"; export const value = util;`, + "/home/projects/app/src/unrelated.ts": `export const unrelated = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + snapshot, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, + }) + assert.NilError(t, err) + projectHandle := snapshot.Projects[0].Handle + + dependencies, err := session.handleGetAllDependencies(context.Background(), &GetSourceFileParams{ + Snapshot: snapshot.Snapshot, + Project: projectHandle, + File: DocumentIdentifier{FileName: "/home/projects/app/src/index.ts"}, + }) + assert.NilError(t, err) + assert.DeepEqual(t, dependencies, test.expected) + + dependencies, err = session.handleGetAllDependencies(context.Background(), &GetSourceFileParams{ + Snapshot: snapshot.Snapshot, + Project: projectHandle, + File: DocumentIdentifier{FileName: "/home/projects/app/src/unrelated.ts"}, + }) + assert.NilError(t, err) + assert.DeepEqual(t, dependencies, []tspath.RootedFilePath{"/home/projects/app/src/unrelated.ts"}) + + _, err = session.handleGetAllDependencies(context.Background(), &GetSourceFileParams{ + Snapshot: snapshot.Snapshot, + Project: projectHandle, + File: DocumentIdentifier{FileName: "/home/projects/app/src/missing.ts"}, + }) + assert.Assert(t, errors.Is(err, ErrClientError)) + }) + } +} diff --git a/tsc/internal/execute/incremental/dependencies.go b/tsc/internal/execute/incremental/dependencies.go new file mode 100644 index 0000000000000..bdfde61cfced5 --- /dev/null +++ b/tsc/internal/execute/incremental/dependencies.go @@ -0,0 +1,43 @@ +package incremental + +import ( + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +func GetAllDependencies(program *compiler.Program, checker *checker.Checker, file *ast.SourceFile) []*ast.SourceFile { + files := program.GetSourceFiles() + if !fileAffectsGlobalScope(file) { + seen := collections.Set[tspath.PathKey]{} + queue := []tspath.PathKey{file.PathKey()} + for len(queue) > 0 { + path := queue[len(queue)-1] + queue = queue[:len(queue)-1] + if !seen.AddIfAbsent(path) { + continue + } + if current := program.GetSourceFileByPath(path); current != nil { + if references := getReferencedFilesWithChecker(program, checker, current); references != nil { + for reference := range references.Keys() { + queue = append(queue, reference) + } + } + } + } + files = core.Filter(files, func(f *ast.SourceFile) bool { + return seen.Has(f.PathKey()) + }) + } + dependencies := make([]*ast.SourceFile, 0, len(files)) + dependencies = append(dependencies, file) + for _, f := range files { + if f != file { + dependencies = append(dependencies, f) + } + } + return dependencies +} diff --git a/tsc/internal/execute/incremental/dependencies_test.go b/tsc/internal/execute/incremental/dependencies_test.go new file mode 100644 index 0000000000000..8b9e098d39d41 --- /dev/null +++ b/tsc/internal/execute/incremental/dependencies_test.go @@ -0,0 +1,124 @@ +package incremental + +import ( + "context" + "maps" + "slices" + "strings" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func TestGetAllDependencies(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + files map[string]string + expected map[string][]string + }{ + { + name: "follows imports transitively", + files: map[string]string{ + "/project/index.ts": `import { a } from "./a"; export const value = a;`, + "/project/a.ts": `import { shared } from "./shared"; export const a = shared;`, + "/project/b.ts": `import { shared } from "./shared"; export const b = shared;`, + "/project/shared.ts": `export const shared = 1;`, + "/project/unrelated.ts": `export const unrelated = 1;`, + }, + expected: map[string][]string{ + "index.ts": {"index.ts", "shared.ts", "a.ts"}, + "b.ts": {"b.ts", "shared.ts"}, + "shared.ts": {"shared.ts"}, + "unrelated.ts": {"unrelated.ts"}, + }, + }, + { + name: "includes module augmentations of imported modules", + files: map[string]string{ + "/project/index.ts": `import type { Shared } from "./shared"; export type Value = Shared;`, + "/project/shared.ts": `export interface Shared { a: number; }`, + "/project/augment.ts": `export {}; declare module "./shared" { interface Shared { b: string; } }`, + }, + expected: map[string][]string{ + "index.ts": {"index.ts", "augment.ts", "shared.ts"}, + "augment.ts": {"augment.ts", "shared.ts"}, + }, + }, + { + name: "includes ambient module declarations", + files: map[string]string{ + "/project/index.ts": `import { value } from "virtual"; export const local = value;`, + "/project/ambient.d.ts": `declare module "virtual" { export const value: number; }`, + }, + expected: map[string][]string{ + "index.ts": {"index.ts", "ambient.d.ts"}, + }, + }, + { + name: "includes triple slash references and type reference directives", + files: map[string]string{ + "/project/index.ts": `/// +/// +export const value = 1;`, + "/project/globals.d.ts": `declare const globalValue: number;`, + "/project/node_modules/@types/pkg/index.d.ts": `declare const pkgValue: string;`, + "/project/unrelated.ts": `export const unrelated = 1;`, + }, + expected: map[string][]string{ + "index.ts": {"index.ts", "globals.d.ts", "node_modules/@types/pkg/index.d.ts"}, + "unrelated.ts": {"unrelated.ts"}, + }, + }, + { + name: "returns every file for files that affect the global scope", + files: map[string]string{ + "/project/script.ts": `const scriptValue = 1;`, + "/project/globalAugment.ts": `export {}; declare global { var augmented: number; }`, + "/project/module.ts": `export const value = 1;`, + }, + expected: map[string][]string{ + "script.ts": {"script.ts", "globalAugment.ts", "module.ts"}, + "globalAugment.ts": {"globalAugment.ts", "module.ts", "script.ts"}, + "module.ts": {"module.ts"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + program := newDependenciesTestProgram(test.files) + checker, done := program.GetTypeChecker(context.Background()) + defer done() + for root, expected := range test.expected { + file := program.GetSourceFile(tspath.ToRootedFilePath(root, "/project")) + assert.Assert(t, file != nil, root) + actual := core.Map(GetAllDependencies(program, checker, file), func(f *ast.SourceFile) string { + return strings.TrimPrefix(f.FileName().AsString(), "/project/") + }) + assert.DeepEqual(t, actual, expected) + } + }) + } +} + +func newDependenciesTestProgram(files map[string]string) *compiler.Program { + host := compiler.NewCompilerHost(vfstest.FromMap(files, tspath.CaseSensitive), "/lib", nil, nil, nil) + var roots []tspath.RootedFilePath + for _, name := range slices.Sorted(maps.Keys(files)) { + if !strings.Contains(name, "/node_modules/") { + roots = append(roots, tspath.ToRootedFilePath(name, "/project")) + } + } + options := &core.CompilerOptions{NoLib: core.TSTrue, Module: core.ModuleKindESNext, ModuleResolution: core.ModuleResolutionKindBundler} + config := tsoptions.NewParsedCommandLine(options, roots, nil, "/project", tspath.CaseSensitive) + return compiler.NewProgram(compiler.ProgramOptions{Config: config, Host: host}) +} diff --git a/tsc/internal/execute/incremental/programtosnapshot.go b/tsc/internal/execute/incremental/programtosnapshot.go index 14f7b99fc1815..7e9f99755150c 100644 --- a/tsc/internal/execute/incremental/programtosnapshot.go +++ b/tsc/internal/execute/incremental/programtosnapshot.go @@ -315,13 +315,17 @@ func addReferencedFileFromFileName(program *compiler.Program, fileName tspath.Ro // Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collections.Set[tspath.PathKey] { + checker, done := program.GetTypeCheckerForFileExclusive(context.TODO(), file) + defer done() + return getReferencedFilesWithChecker(program, checker, file) +} + +func getReferencedFilesWithChecker(program *compiler.Program, checker *checker.Checker, file *ast.SourceFile) *collections.Set[tspath.PathKey] { referencedFiles := collections.Set[tspath.PathKey]{} // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - checker, done := program.GetTypeCheckerForFileExclusive(context.TODO(), file) - defer done() for _, importName := range file.Imports() { addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, importName) } From ac25939b0051f0c8f35401166ce02f5b884d025c Mon Sep 17 00:00:00 2001 From: jackeddisciple Date: Sat, 24 Oct 2026 08:42:45 +0530 Subject: [PATCH 2/2] Track canonical file paths for deduplicated packages in getAllDependencies --- .../execute/incremental/dependencies.go | 11 +++++------ .../execute/incremental/dependencies_test.go | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tsc/internal/execute/incremental/dependencies.go b/tsc/internal/execute/incremental/dependencies.go index bdfde61cfced5..b9a0771d1128d 100644 --- a/tsc/internal/execute/incremental/dependencies.go +++ b/tsc/internal/execute/incremental/dependencies.go @@ -17,14 +17,13 @@ func GetAllDependencies(program *compiler.Program, checker *checker.Checker, fil for len(queue) > 0 { path := queue[len(queue)-1] queue = queue[:len(queue)-1] - if !seen.AddIfAbsent(path) { + current := program.GetSourceFileByPath(path) + if current == nil || !seen.AddIfAbsent(current.PathKey()) { continue } - if current := program.GetSourceFileByPath(path); current != nil { - if references := getReferencedFilesWithChecker(program, checker, current); references != nil { - for reference := range references.Keys() { - queue = append(queue, reference) - } + if references := getReferencedFilesWithChecker(program, checker, current); references != nil { + for reference := range references.Keys() { + queue = append(queue, reference) } } } diff --git a/tsc/internal/execute/incremental/dependencies_test.go b/tsc/internal/execute/incremental/dependencies_test.go index 8b9e098d39d41..094c62d92ae99 100644 --- a/tsc/internal/execute/incremental/dependencies_test.go +++ b/tsc/internal/execute/incremental/dependencies_test.go @@ -77,6 +77,25 @@ export const value = 1;`, "unrelated.ts": {"unrelated.ts"}, }, }, + { + name: "includes deduplicated package installations reached through type reference directives", + files: map[string]string{ + "/project/a.ts": `import { a } from "a"; export const value = a;`, + "/project/b.ts": `import { b } from "b"; export const value = b;`, + "/project/node_modules/a/package.json": `{ "name": "a", "version": "1.0.0" }`, + "/project/node_modules/a/index.d.ts": `/// ` + "\n" + `export declare const a: number;`, + "/project/node_modules/a/node_modules/@types/pkg/package.json": `{ "name": "@types/pkg", "version": "1.0.0" }`, + "/project/node_modules/a/node_modules/@types/pkg/index.d.ts": `declare const pkgValue: string;`, + "/project/node_modules/b/package.json": `{ "name": "b", "version": "1.0.0" }`, + "/project/node_modules/b/index.d.ts": `/// ` + "\n" + `export declare const b: number;`, + "/project/node_modules/b/node_modules/@types/pkg/package.json": `{ "name": "@types/pkg", "version": "1.0.0" }`, + "/project/node_modules/b/node_modules/@types/pkg/index.d.ts": `declare const pkgValue: string;`, + }, + expected: map[string][]string{ + "a.ts": {"a.ts", "node_modules/a/node_modules/@types/pkg/index.d.ts", "node_modules/a/index.d.ts"}, + "b.ts": {"b.ts", "node_modules/a/node_modules/@types/pkg/index.d.ts", "node_modules/b/index.d.ts"}, + }, + }, { name: "returns every file for files that affect the global scope", files: map[string]string{