From 71d1d9cebd24ae3aa423ed0f0230d80f63acc7e5 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 15 Sep 2026 08:37:28 -0700 Subject: [PATCH 01/20] [api] Add module resolution APIs Add snapshot-scoped reusable module resolvers and serializable resolution overrides for standalone resolution and createProgram.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/typescript/src/api/async/api.ts | 103 ++++++- .../typescript/src/api/proto.generated.ts | 63 +++++ packages/typescript/src/api/sync/api.ts | 176 +++++++++++- packages/typescript/test/async/api.test.ts | 79 +++++- .../test/sync/api-generators.test.ts | 22 ++ packages/typescript/test/sync/api.test.ts | 79 +++++- tools/gen-proto/main.go | 3 + tsc/internal/api/module_resolution.go | 254 ++++++++++++++++++ tsc/internal/api/proto.go | 83 +++++- tsc/internal/api/session.go | 36 ++- .../api/session_module_resolution_test.go | 219 +++++++++++++++ tsc/internal/compiler/fileloader.go | 16 +- tsc/internal/compiler/program.go | 12 + tsc/internal/module/resolver.go | 32 ++- tsc/internal/module/types.go | 11 + tsc/internal/project/project.go | 6 + tsc/internal/project/refcountcache_test.go | 2 + tsc/internal/project/snapshot.go | 34 +++ tsc/internal/project/snapshothost.go | 3 + 19 files changed, 1212 insertions(+), 21 deletions(-) create mode 100644 tsc/internal/api/module_resolution.go create mode 100644 tsc/internal/api/session_module_resolution_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index e80759433c3f0..455850c49e433 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -60,7 +60,7 @@ import { import type { APIFileChanges, CompilerOptions, - CreateProgramOptions, + CreateProgramOptions as ProtocolCreateProgramOptions, CreateProgramResponse, Diagnostic, DocumentIdentifier, @@ -70,6 +70,9 @@ import type { IntrinsicTypeMethod, LSPUpdateSnapshotParams, PackageId, + ModuleResolutionInvocationResult, + ModuleResolutionSource, + ModuleResolutionSpec, ParsedCommandLine, ProjectReference, ProjectResponse, @@ -168,7 +171,6 @@ export type { CompletionInfo, CompletionOptions, ConditionalType, - CreateProgramOptions, Diagnostic, DocumentIdentifier, DocumentPosition, @@ -189,6 +191,7 @@ export type { JSDocTagInfo, LiteralType, LSPConnectionOptions, + ModuleResolutionSpec, NumberLiteralType, ObjectType, PackageId, @@ -232,6 +235,21 @@ export interface TranspileOutput { sourceMapText?: string | undefined; } +export interface ModuleResolverOptions { + moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; +} + +export type CreateProgramOptions = Omit & ModuleResolverOptions; + +function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { + if (input === undefined) return undefined; + if (input instanceof ModuleResolutionSet) { + input.ensureNotDisposed(); + return { set: input.id }; + } + return { spec: input }; +} + // @sync-only-start // export { all, defer, type APIRequestGenerator, type AnyAPIRequestGenerator, type AllAPIRequestGenerator, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts"; // import {executeRequestGenerators, type ExecutedGeneratorsResults, type AnyAPIRequestGenerator} from "./generatorSupport.ts"; @@ -504,6 +522,12 @@ export class API implements FormatDiagnosticsHo return false; } + async createModuleResolutionSet(spec: ModuleResolutionSpec): Promise { + await this.ensureInitialized(); + const id = await this.client.apiRequest("createModuleResolutionSet", { spec }); + return new ModuleResolutionSet(id, this.client); + } + /** * Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges. */ @@ -522,9 +546,14 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } + const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); const data: CreateProgramResponse = await this.client.apiRequest("createProgram", { rootFiles, - createProgramOptions, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }, oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, fileChanges, }); @@ -549,6 +578,34 @@ export class API implements FormatDiagnosticsHo } } +export class ModuleResolutionSet { + readonly id: number; + private readonly client: Client; + private disposed = false; + + constructor(id: number, client: Client) { + this.id = id; + this.client = client; + } + + [globalThis.Symbol.asyncDispose](): Promise { // @sync: [globalThis.Symbol.dispose](): void { + return this.dispose(); + } + + async dispose(): Promise { + if (this.disposed) return; + await this.client.apiRequest("releaseModuleResolutionSet", { set: this.id }); + this.disposed = true; + } + + /** @internal */ + ensureNotDisposed(): void { + if (this.disposed) { + throw new Error("ModuleResolutionSet is disposed"); + } + } +} + type EnsureInitialized = () => Promise; // @sync: type EnsureInitialized = (() => void) & { gen(): Generator; }; interface SnapshotOwner extends FormatDiagnosticsHost { @@ -648,6 +705,17 @@ export class Snapshot { return this.api.updateSnapshotFrom(this, params); } + async createModuleResolver(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Promise { + this.ensureNotDisposed(); + const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); + const id = await this.client.apiRequest("createModuleResolver", { + snapshot: this.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }); + return new ModuleResolver(id, this.id, this.client, () => this.ensureNotDisposed()); + } + [globalThis.Symbol.dispose](): void { void this.dispose(); } @@ -682,6 +750,35 @@ export class Snapshot { } } +export class ModuleResolver { + private readonly id: number; + private readonly snapshotId: number; + private readonly client: Client; + private readonly ensureSnapshotActive: () => void; + + constructor(id: number, snapshotId: number, client: Client, ensureSnapshotActive: () => void) { + this.id = id; + this.snapshotId = snapshotId; + this.client = client; + this.ensureSnapshotActive = ensureSnapshotActive; + } + + async resolveModuleName( + moduleName: string, + containingDirectory: DocumentIdentifier, + resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext, + ): Promise { + this.ensureSnapshotActive(); + return this.client.apiRequest("resolveModuleName", { + snapshot: this.snapshotId, + resolver: this.id, + moduleName, + containingDirectory, + ...(resolutionMode !== undefined ? { resolutionMode } : {}), + }); + } +} + class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 6feefcc38038a..9f267f9fa1aaa 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -23,6 +23,10 @@ export interface APIMethodInfo { updateSnapshot: APIMethod; updateTemporarySnapshot: APIMethod; createProgram: APIMethod; + createModuleResolutionSet: APIMethod; + releaseModuleResolutionSet: APIMethod; + createModuleResolver: APIMethod; + resolveModuleName: APIMethod; parseCommandLine: APIMethod; readConfigFile: APIMethod; parseJsonConfigFileContent: APIMethod; @@ -280,6 +284,33 @@ export interface CreateProgramResponse { project: ProjectResponse | null; } +export interface CreateModuleResolutionSetParams { + spec: ModuleResolutionSpec; +} + +export interface ReleaseModuleResolutionSetParams { + set: number; +} + +export interface CreateModuleResolverParams { + snapshot: number; + compilerOptions: CompilerOptions; + moduleResolutions?: ModuleResolutionSource | undefined; +} + +export interface ResolveModuleNameParams { + snapshot: number; + resolver: number; + moduleName: string; + containingDirectory: DocumentIdentifier; + resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; +} + +export interface ModuleResolutionInvocationResult { + result?: ResolvedModule | undefined; + trace?: string[] | undefined; +} + export interface ParseCommandLineParams { commandLine: readonly string[] | null; } @@ -999,6 +1030,8 @@ export interface ProfileResult { export interface BatchRequest { method: | "batchRequests" + | "createModuleResolutionSet" + | "createModuleResolver" | "createProgram" | "emit" | "emitToString" @@ -1138,6 +1171,8 @@ export interface BatchRequest { | "printNode" | "readConfigFile" | "release" + | "releaseModuleResolutionSet" + | "resolveModuleName" | "resolveName" | "saveHeapProfile" | "signatureToSignatureDeclaration" @@ -1157,6 +1192,8 @@ export interface BatchRequest { export interface BatchResponse { method: | "batchRequests" + | "createModuleResolutionSet" + | "createModuleResolver" | "createProgram" | "emit" | "emitToString" @@ -1296,6 +1333,8 @@ export interface BatchResponse { | "printNode" | "readConfigFile" | "release" + | "releaseModuleResolutionSet" + | "resolveModuleName" | "resolveName" | "saveHeapProfile" | "signatureToSignatureDeclaration" @@ -1366,6 +1405,7 @@ export interface CreateProgramOptions { compilerOptions: CompilerOptions; projectReferences?: ProjectReference[] | undefined; configFileParsingDiagnostics?: DiagnosticResponse[] | undefined; + moduleResolutions?: ModuleResolutionSource | undefined; } export interface CreateProgramOldProgramParams { @@ -1373,6 +1413,11 @@ export interface CreateProgramOldProgramParams { project?: string | undefined; } +export interface ModuleResolutionSpec { + fallback: "resolve" | "unresolved"; + entries: ModuleResolutionEntry[]; +} + /** CompilerOptions contains the compiler options exposed by the API. */ export interface CompilerOptions { allowJs?: boolean | undefined; @@ -1479,6 +1524,11 @@ export interface CompilerOptions { configFilePath?: string | undefined; } +export interface ModuleResolutionSource { + spec?: ModuleResolutionSpec | undefined; + set?: number | undefined; +} + export interface ProjectReference { /** Path is a normalized path on disk. */ path: string; @@ -1573,8 +1623,21 @@ export interface ProjectFileChanges { deletedFiles?: string[] | undefined; } +export interface ModuleResolutionEntry { + moduleName: string; + containingDirectory?: DocumentIdentifier | undefined; + resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; + result: ProvidedModuleResolution; +} + /** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */ export interface CompletionEntryLabelDetailsResponse { detail?: string | undefined; description?: string | undefined; } + +export interface ProvidedModuleResolution { + resolvedFileName?: DocumentIdentifier | undefined; + originalPath?: DocumentIdentifier | undefined; + packageId?: PackageId | undefined; +} diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 5a0972e0f0b00..06cbe9d7db195 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -77,7 +77,7 @@ import { import type { APIFileChanges, CompilerOptions, - CreateProgramOptions, + CreateProgramOptions as ProtocolCreateProgramOptions, CreateProgramResponse, Diagnostic, DocumentIdentifier, @@ -86,6 +86,9 @@ import type { ImportAdderAction, IntrinsicTypeMethod, LSPUpdateSnapshotParams, + ModuleResolutionInvocationResult, + ModuleResolutionSource, + ModuleResolutionSpec, PackageId, ParsedCommandLine, ProjectReference, @@ -185,7 +188,6 @@ export type { CompletionInfo, CompletionOptions, ConditionalType, - CreateProgramOptions, Diagnostic, DocumentIdentifier, DocumentPosition, @@ -206,6 +208,7 @@ export type { JSDocTagInfo, LiteralType, LSPConnectionOptions, + ModuleResolutionSpec, NumberLiteralType, ObjectType, PackageId, @@ -249,6 +252,21 @@ export interface TranspileOutput { sourceMapText?: string | undefined; } +export interface ModuleResolverOptions { + moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; +} + +export type CreateProgramOptions = Omit & ModuleResolverOptions; + +function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { + if (input === undefined) return undefined; + if (input instanceof ModuleResolutionSet) { + input.ensureNotDisposed(); + return { set: input.id }; + } + return { spec: input }; +} + export { all, type AllAPIRequestGenerator, type AnyAPIRequestGenerator, type APIRequestGenerator, defer, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts"; import { type AnyAPIRequestGenerator, @@ -865,6 +883,27 @@ export class API implements FormatDiagnosticsHo return false; } + get createModuleResolutionSet(): { + (spec: ModuleResolutionSpec): ModuleResolutionSet; + gen(spec: ModuleResolutionSpec): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "createModuleResolutionSet", + function (spec: ModuleResolutionSpec): ModuleResolutionSet { + owner.ensureInitialized(); + const id = owner.client.apiRequest("createModuleResolutionSet", { spec }); + return new ModuleResolutionSet(id, owner.client); + }, + function* (spec: ModuleResolutionSpec): Generator { + yield* owner.ensureInitialized.gen(); + const id = yield* apiRequest("createModuleResolutionSet", { spec }); + return new ModuleResolutionSet(id, owner.client); + }, + ); + } + /** * Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges. */ @@ -886,9 +925,14 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } + const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); const data: CreateProgramResponse = owner.client.apiRequest("createProgram", { rootFiles, - createProgramOptions, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }, oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, fileChanges, }); @@ -921,9 +965,14 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } + const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); const data: CreateProgramResponse = yield* apiRequest("createProgram", { rootFiles, - createProgramOptions, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }, oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, fileChanges, }); @@ -950,6 +999,49 @@ export class API implements FormatDiagnosticsHo } } +export class ModuleResolutionSet { + readonly id: number; + private readonly client: Client; + private disposed = false; + + constructor(id: number, client: Client) { + this.id = id; + this.client = client; + } + + [globalThis.Symbol.dispose](): void { + return this.dispose(); + } + + get dispose(): { + (): void; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "dispose", + function (): void { + if (owner.disposed) return; + owner.client.apiRequest("releaseModuleResolutionSet", { set: owner.id }); + owner.disposed = true; + }, + function* (): Generator { + if (owner.disposed) return; + yield* apiRequest("releaseModuleResolutionSet", { set: owner.id }); + owner.disposed = true; + }, + ); + } + + /** @internal */ + ensureNotDisposed(): void { + if (this.disposed) { + throw new Error("ModuleResolutionSet is disposed"); + } + } +} + type EnsureInitialized = (() => void) & { gen(): Generator; }; interface SnapshotOwner extends FormatDiagnosticsHost { @@ -1129,6 +1221,37 @@ export class Snapshot { ); } + get createModuleResolver(): { + (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver; + gen(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "createModuleResolver", + function (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver { + owner.ensureNotDisposed(); + const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); + const id = owner.client.apiRequest("createModuleResolver", { + snapshot: owner.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }); + return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + }, + function* (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator { + owner.ensureNotDisposed(); + const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); + const id = yield* apiRequest("createModuleResolver", { + snapshot: owner.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + }); + return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + }, + ); + } + [globalThis.Symbol.dispose](): void { void this.dispose(); } @@ -1201,6 +1324,51 @@ export class Snapshot { } } +export class ModuleResolver { + private readonly id: number; + private readonly snapshotId: number; + private readonly client: Client; + private readonly ensureSnapshotActive: () => void; + + constructor(id: number, snapshotId: number, client: Client, ensureSnapshotActive: () => void) { + this.id = id; + this.snapshotId = snapshotId; + this.client = client; + this.ensureSnapshotActive = ensureSnapshotActive; + } + + get resolveModuleName(): { + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ModuleResolutionInvocationResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "resolveModuleName", + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ModuleResolutionInvocationResult { + owner.ensureSnapshotActive(); + return owner.client.apiRequest("resolveModuleName", { + snapshot: owner.snapshotId, + resolver: owner.id, + moduleName, + containingDirectory, + ...(resolutionMode !== undefined ? { resolutionMode } : {}), + }); + }, + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator { + owner.ensureSnapshotActive(); + return yield* apiRequest("resolveModuleName", { + snapshot: owner.snapshotId, + resolver: owner.id, + moduleName, + containingDirectory, + ...(resolutionMode !== undefined ? { resolutionMode } : {}), + }); + }, + ); + } +} + class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 2dda1f8544278..be87a5712c7fb 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -481,13 +481,90 @@ import "missing";`, "/src/main.ts": `import { dependency } from "./dependency"; export const value = dependency;`, "/src/dependency.ts": `export const dependency = 1;`, }); - const program = await api.createProgram(["/src/main.ts"], { compilerOptions: { noLib: true } }); assert.deepEqual([...await program.getSourceFileNames()].sort(), ["/src/dependency.ts", "/src/main.ts"]); await program.dispose(); }); + test("module resolver and provided resolution sets", async () => { + await using api = spawnAPI({ + "/src/main.ts": `import { value } from "pkg"; export { value };`, + "/src/extension.ts": `import { value } from "./dep.ts"; value;`, + "/src/dep.ts": `export const value = 1;`, + "/provided.d.ts": `export declare const value: string;`, + "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, + "/node_modules/pkg/index.d.ts": `export declare const value: number;`, + }); + const snapshot = await api.updateSnapshot(); + const compilerOptions = { + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + traceResolution: true, + }; + const resolver = await snapshot.createModuleResolver(compilerOptions); + const defaultResolution = await resolver.resolveModuleName("pkg", "/src"); + assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); + assert.ok(defaultResolution.trace?.length); + const fallbackResolver = await snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { fallback: "resolve", entries: [] }, + }); + assert.equal( + (await fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + + const set = await api.createModuleResolutionSet({ + fallback: "unresolved", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }); + const overriddenResolver = await snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + assert.equal( + (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); + + const program = await api.createProgram( + ["/src/main.ts"], + { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, + ); + assert.deepEqual([...await program.getSourceFileNames()].sort(), ["/provided.d.ts", "/src/main.ts"]); + + const extensionProgram = await api.createProgram( + ["/src/extension.ts"], + { + compilerOptions: { ...compilerOptions, noLib: true }, + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./dep.ts", + containingDirectory: "/src", + result: { resolvedFileName: "/src/dep.ts" }, + }], + }, + }, + ); + assert.ok((await extensionProgram.getSemanticDiagnostics("/src/extension.ts")).some(diagnostic => diagnostic.code === 5097)); + + await set.dispose(); + assert.equal( + (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + await assert.rejects(createFromDisposedSet, /ModuleResolutionSet is disposed/); // @sync: assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); + + await program.dispose(); + await extensionProgram.dispose(); + await snapshot.dispose(); + const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); + await assert.rejects(resolveAfterSnapshotDisposal, /Snapshot is disposed/); // @sync: assert.throws(resolveAfterSnapshotDisposal, /Snapshot is disposed/); + }); + test("createProgram updates an old program with file changes", async () => { const fileName = "/src/index.ts"; const options = { compilerOptions: { noLib: true, strict: true } }; diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 5c7cfa96e0870..0284ce61452d2 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -39,6 +39,7 @@ import { type InterfaceType, type LiteralType, ModuleKind, + ModuleResolutionKind, type NodeHandle, type Program, type Project, @@ -1462,6 +1463,25 @@ describe("API - generator batching", () => { const nodeHandle = combineSymbol.valueDeclaration ?? combineSymbol.declarations[0]; const completionPosition = parityFiles["/src/index.ts"].indexOf("semanticIssue"); const temporaryProjects: string[] = []; + const moduleResolutionSpec = { + fallback: "unresolved" as const, + entries: [{ + moduleName: "models", + result: { resolvedFileName: "/src/models.ts" }, + }], + }; + const generatedResolutionSet = api.batch(api.createModuleResolutionSet.gen(moduleResolutionSpec))[0]; + const directResolutionSet = api.createModuleResolutionSet(moduleResolutionSpec); + exercisedMethods.add("API.createModuleResolutionSet"); + const generatedModuleResolver = api.batch(snapshot.createModuleResolver.gen( + { moduleResolution: ModuleResolutionKind.NodeNext }, + { moduleResolutions: generatedResolutionSet }, + ))[0]; + const directModuleResolver = snapshot.createModuleResolver( + { moduleResolution: ModuleResolutionKind.NodeNext }, + { moduleResolutions: directResolutionSet }, + ); + exercisedMethods.add("Snapshot.createModuleResolver"); const orderedArguments = Array.from({ length: 128 }, (_, index) => ["--strict", `--outDir=out-${index}`] as const); const orderedGenerated = api.batch(...orderedArguments.map(args => api.parseCommandLine.gen(args))); @@ -1513,6 +1533,7 @@ describe("API - generator batching", () => { temporaryProjects.push(temporarySnapshot.getProjects()[0].configFileName); }), parityCase("Snapshot", "getDefaultProjectForFile", snapshot.getDefaultProjectForFile, assertOptionalProjectsEquivalent, "/src/index.ts"), + parityCase("ModuleResolver", "resolveModuleName", generatedModuleResolver.resolveModuleName, assertDeepEquivalent, "models", "/src"), parityCase("Project", "getImportAdderEdits", project.getImportAdderEdits, assertDeepEquivalent, "/src/index.ts", [{ kind: "importSymbol", symbol: unimportedSymbol }]), parityCase("Project", "getImportEditsForSymbols", project.getImportEditsForSymbols, assertDeepEquivalent, "/src/index.ts", [unimportedSymbol]), @@ -1733,6 +1754,7 @@ describe("API - generator batching", () => { { name: "API", value: api.constructor as object, own: true }, { name: "InternalAPI", value: api.internal }, { name: "Snapshot", value: snapshot }, + { name: "ModuleResolver", value: directModuleResolver }, { name: "Project", value: project }, { name: "LanguageService", value: languageService }, { name: "Program", value: program }, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index a7678eed037aa..62140c7e914f4 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -470,13 +470,90 @@ import "missing";`, "/src/main.ts": `import { dependency } from "./dependency"; export const value = dependency;`, "/src/dependency.ts": `export const dependency = 1;`, }); - const program = api.createProgram(["/src/main.ts"], { compilerOptions: { noLib: true } }); assert.deepEqual([...program.getSourceFileNames()].sort(), ["/src/dependency.ts", "/src/main.ts"]); program.dispose(); }); + test("module resolver and provided resolution sets", () => { + using api = spawnAPI({ + "/src/main.ts": `import { value } from "pkg"; export { value };`, + "/src/extension.ts": `import { value } from "./dep.ts"; value;`, + "/src/dep.ts": `export const value = 1;`, + "/provided.d.ts": `export declare const value: string;`, + "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, + "/node_modules/pkg/index.d.ts": `export declare const value: number;`, + }); + const snapshot = api.updateSnapshot(); + const compilerOptions = { + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + traceResolution: true, + }; + const resolver = snapshot.createModuleResolver(compilerOptions); + const defaultResolution = resolver.resolveModuleName("pkg", "/src"); + assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); + assert.ok(defaultResolution.trace?.length); + const fallbackResolver = snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { fallback: "resolve", entries: [] }, + }); + assert.equal( + (fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + + const set = api.createModuleResolutionSet({ + fallback: "unresolved", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }); + const overriddenResolver = snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + assert.equal( + (overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + assert.equal((overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); + + const program = api.createProgram( + ["/src/main.ts"], + { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, + ); + assert.deepEqual([...program.getSourceFileNames()].sort(), ["/provided.d.ts", "/src/main.ts"]); + + const extensionProgram = api.createProgram( + ["/src/extension.ts"], + { + compilerOptions: { ...compilerOptions, noLib: true }, + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./dep.ts", + containingDirectory: "/src", + result: { resolvedFileName: "/src/dep.ts" }, + }], + }, + }, + ); + assert.ok((extensionProgram.getSemanticDiagnostics("/src/extension.ts")).some(diagnostic => diagnostic.code === 5097)); + + set.dispose(); + assert.equal( + (overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); + + program.dispose(); + extensionProgram.dispose(); + snapshot.dispose(); + const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); + assert.throws(resolveAfterSnapshotDisposal, /Snapshot is disposed/); + }); + test("createProgram updates an old program with file changes", () => { const fileName = "/src/index.ts"; const options = { compilerOptions: { noLib: true, strict: true } }; diff --git a/tools/gen-proto/main.go b/tools/gen-proto/main.go index ea26612e6fad1..0ad72408e1731 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -492,6 +492,9 @@ func (r *typeRenderer) namedType(named *types.Named) string { case r.apiPackagePath + ".DocumentIdentifier": r.documentIdentifier = obj return "DocumentIdentifier" + case r.apiPackagePath + ".ResolutionMode": + r.importType("ModuleKind", "#enums/moduleKind") + return "ModuleKind.CommonJS | ModuleKind.ESNext" case "github.com/microsoft/TypeScript/tsc/internal/packagejson.JSONValue": return "unknown" case "github.com/microsoft/TypeScript/tsc/internal/json.Value": diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go new file mode 100644 index 0000000000000..bf85a1e6c3e9a --- /dev/null +++ b/tsc/internal/api/module_resolution.go @@ -0,0 +1,254 @@ +package api + +import ( + "fmt" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +type moduleResolutionMatchKey struct { + moduleName string + directory tspath.Path + mode core.ResolutionMode + hasDirectory bool + hasMode bool +} + +type providedModuleResolutions struct { + identity uint64 + fallbackToResolution bool + entries map[moduleResolutionMatchKey]*module.ProvidedModuleResolution + currentDirectory string + useCaseSensitive bool +} + +func (p *providedModuleResolutions) Identity() uint64 { + return p.identity +} + +func (p *providedModuleResolutions) GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ProvidedModuleResolution, bool) { + directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitive) + keys := [...]moduleResolutionMatchKey{ + {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, + {moduleName: moduleName, directory: directory, hasDirectory: true}, + {moduleName: moduleName, mode: resolutionMode, hasMode: true}, + {moduleName: moduleName}, + } + for _, key := range keys { + if result, ok := p.entries[key]; ok { + return result, true + } + } + if p.fallbackToResolution { + return nil, false + } + return nil, true +} + +func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { + if spec == nil { + return nil, nil + } + fallbackToResolution := false + switch spec.Fallback { + case ModuleResolutionFallbackResolve: + fallbackToResolution = true + case ModuleResolutionFallbackUnresolved: + fallbackToResolution = false + default: + return nil, fmt.Errorf("%w: invalid module resolution fallback %q", ErrClientError, spec.Fallback) + } + + provider := &providedModuleResolutions{ + identity: identity, + fallbackToResolution: fallbackToResolution, + entries: make(map[moduleResolutionMatchKey]*module.ProvidedModuleResolution, len(spec.Entries)), + currentDirectory: currentDirectory, + useCaseSensitive: useCaseSensitive, + } + for i, entry := range spec.Entries { + if entry == nil { + return nil, fmt.Errorf("%w: module resolution entry %d is null", ErrClientError, i) + } + if entry.ModuleName == "" { + return nil, fmt.Errorf("%w: module resolution entry %d has an empty moduleName", ErrClientError, i) + } + if entry.Result == nil { + return nil, fmt.Errorf("%w: module resolution entry %d has no result", ErrClientError, i) + } + + key := moduleResolutionMatchKey{moduleName: entry.ModuleName} + if entry.ContainingDirectory != nil { + directory := tspath.GetNormalizedAbsolutePath(entry.ContainingDirectory.ToAbsoluteFileName(currentDirectory), currentDirectory) + key.directory = tspath.ToPath(directory, currentDirectory, useCaseSensitive) + key.hasDirectory = true + } + if entry.ResolutionMode != nil { + mode := core.ModuleKind(*entry.ResolutionMode) + if mode != core.ModuleKindCommonJS && mode != core.ModuleKindESNext { + return nil, fmt.Errorf("%w: module resolution entry %d has invalid resolutionMode %s", ErrClientError, i, mode.String()) + } + key.mode = mode + key.hasMode = true + } + if _, exists := provider.entries[key]; exists { + return nil, fmt.Errorf("%w: duplicate module resolution entry for %q", ErrClientError, entry.ModuleName) + } + + var provided *module.ProvidedModuleResolution + if entry.Result.ResolvedFileName != nil { + provided = &module.ProvidedModuleResolution{ + ResolvedFileName: tspath.GetNormalizedAbsolutePath(entry.Result.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), + } + if entry.Result.OriginalPath != nil { + provided.OriginalPath = tspath.GetNormalizedAbsolutePath(entry.Result.OriginalPath.ToAbsoluteFileName(currentDirectory), currentDirectory) + } + if entry.Result.PackageID != nil { + provided.PackageId = module.PackageId{ + Name: entry.Result.PackageID.Name, + SubModuleName: entry.Result.PackageID.SubModuleName, + Version: entry.Result.PackageID.Version, + PeerDependencies: entry.Result.PackageID.PeerDependencies, + } + } + } + provider.entries[key] = provided + } + + return provider, nil +} + +func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { + return core.Map(trace, func(entry module.DiagAndArgs) string { + return entry.Message.Localize(locale.Default, entry.Args...) + }) +} + +func (s *Session) resolveModuleResolutionSource(source *ModuleResolutionSource) (module.ResolutionProvider, error) { + if source == nil { + return nil, nil + } + if source.Spec != nil && source.Set != 0 { + return nil, fmt.Errorf("%w: moduleResolutions cannot contain both spec and set", ErrClientError) + } + if source.Spec != nil { + return compileModuleResolutionSpec( + source.Spec, + s.nextModuleResolutionIdentity.Add(1), + s.currentDirectory(), + s.fileSystem().UseCaseSensitiveFileNames(), + ) + } + if source.Set == 0 { + return nil, fmt.Errorf("%w: moduleResolutions must contain spec or set", ErrClientError) + } + s.moduleResolutionSetsMu.RLock() + provider := s.moduleResolutionSets[source.Set] + s.moduleResolutionSetsMu.RUnlock() + if provider == nil { + return nil, fmt.Errorf("%w: module resolution set %d not found", ErrClientError, source.Set) + } + return provider, nil +} + +func (s *Session) handleCreateModuleResolutionSet(params *CreateModuleResolutionSetParams) (ModuleResolutionSetID, error) { + provider, err := compileModuleResolutionSpec( + ¶ms.Spec, + s.nextModuleResolutionIdentity.Add(1), + s.currentDirectory(), + s.fileSystem().UseCaseSensitiveFileNames(), + ) + if err != nil { + return 0, err + } + id := ModuleResolutionSetID(s.nextModuleResolutionSetID.Add(1)) + s.moduleResolutionSetsMu.Lock() + s.moduleResolutionSets[id] = provider + s.moduleResolutionSetsMu.Unlock() + return id, nil +} + +func (s *Session) handleReleaseModuleResolutionSet(params *ReleaseModuleResolutionSetParams) (any, error) { + if params.Set == 0 { + return nil, fmt.Errorf("%w: empty module resolution set handle", ErrClientError) + } + s.moduleResolutionSetsMu.Lock() + _, ok := s.moduleResolutionSets[params.Set] + if ok { + delete(s.moduleResolutionSets, params.Set) + } + s.moduleResolutionSetsMu.Unlock() + if !ok { + return nil, fmt.Errorf("%w: module resolution set %d not found", ErrClientError, params.Set) + } + return nil, nil +} + +func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) (ModuleResolverID, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return 0, err + } + provider, err := s.resolveModuleResolutionSource(params.ModuleResolutions) + if err != nil { + return 0, err + } + id := ModuleResolverID(s.nextModuleResolverID.Add(1)) + data := &moduleResolverData{ + resolver: module.NewResolver(sd.snapshot, ¶ms.CompilerOptions, "", "", sd.snapshot.ContentMapperExtensions()), + provider: provider, + } + sd.moduleResolversMu.Lock() + if sd.moduleResolvers == nil { + sd.moduleResolvers = make(map[ModuleResolverID]*moduleResolverData) + } + sd.moduleResolvers[id] = data + sd.moduleResolversMu.Unlock() + return id, nil +} + +func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*ModuleResolutionInvocationResult, error) { + if params.ModuleName == "" { + return nil, fmt.Errorf("%w: moduleName is empty", ErrClientError) + } + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + sd.moduleResolversMu.RLock() + data := sd.moduleResolvers[params.Resolver] + sd.moduleResolversMu.RUnlock() + if data == nil { + return nil, fmt.Errorf("%w: module resolver %d not found in snapshot %d", ErrClientError, params.Resolver, params.Snapshot) + } + + mode := core.ResolutionModeNone + if params.ResolutionMode != nil { + mode = core.ModuleKind(*params.ResolutionMode) + if mode != core.ResolutionModeCommonJS && mode != core.ResolutionModeESM { + return nil, fmt.Errorf("%w: invalid resolutionMode %s", ErrClientError, mode.String()) + } + } + containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.currentDirectory()), s.currentDirectory()) + + var result *module.ResolvedModule + var trace []module.DiagAndArgs + var provided bool + if data.provider != nil { + var providedResolution *module.ProvidedModuleResolution + providedResolution, provided = data.provider.GetModuleResolution(params.ModuleName, containingDirectory, mode) + if provided { + result = data.resolver.ResolveProvidedModule(params.ModuleName, providedResolution) + } + } + if !provided { + result, trace = data.resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) + } + return &ModuleResolutionInvocationResult{ + Result: newResolvedModuleResponse(result), + Trace: moduleResolutionTraceToStrings(trace), + }, nil +} diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 14cb062642920..b6114726002ed 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -34,12 +34,14 @@ var ( type Method string type ( - SnapshotID uint64 - ProjectID string - SymbolID uint64 - TypeID uint32 - SignatureID uint64 - NodeHandle string + SnapshotID uint64 + ProjectID string + SymbolID uint64 + TypeID uint32 + SignatureID uint64 + ModuleResolutionSetID uint64 + ModuleResolverID uint64 + NodeHandle string ) func ProjectHandle(p *project.Project) ProjectID { @@ -71,6 +73,10 @@ const ( MethodUpdateSnapshot Method = "updateSnapshot" MethodUpdateTemporarySnapshot Method = "updateTemporarySnapshot" MethodCreateProgram Method = "createProgram" + MethodCreateModuleResolutionSet Method = "createModuleResolutionSet" + MethodReleaseModuleResolutionSet Method = "releaseModuleResolutionSet" + MethodCreateModuleResolver Method = "createModuleResolver" + MethodResolveModuleName Method = "resolveModuleName" MethodParseCommandLine Method = "parseCommandLine" MethodReadConfigFile Method = "readConfigFile" MethodParseJsonConfigFile Method = "parseJsonConfigFileContent" @@ -406,6 +412,67 @@ type CreateProgramOptions struct { CompilerOptions core.CompilerOptions `json:"compilerOptions"` ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` + ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` +} + +type ( + ModuleResolutionFallback string + ResolutionMode core.ModuleKind +) + +const ( + ModuleResolutionFallbackResolve ModuleResolutionFallback = "resolve" + ModuleResolutionFallbackUnresolved ModuleResolutionFallback = "unresolved" +) + +type ModuleResolutionSpec struct { + Fallback ModuleResolutionFallback `json:"fallback"` + Entries []*ModuleResolutionEntry `json:"entries" nonnil:"true"` +} + +type ModuleResolutionEntry struct { + ModuleName string `json:"moduleName"` + ContainingDirectory *DocumentIdentifier `json:"containingDirectory,omitempty"` + ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` + Result *ProvidedModuleResolution `json:"result" nonnil:"true"` +} + +type ProvidedModuleResolution struct { + ResolvedFileName *DocumentIdentifier `json:"resolvedFileName,omitempty"` + OriginalPath *DocumentIdentifier `json:"originalPath,omitempty"` + PackageID *PackageId `json:"packageId,omitempty"` +} + +type ModuleResolutionSource struct { + Spec *ModuleResolutionSpec `json:"spec,omitempty"` + Set ModuleResolutionSetID `json:"set,omitempty"` +} + +type CreateModuleResolutionSetParams struct { + Spec ModuleResolutionSpec `json:"spec"` +} + +type ReleaseModuleResolutionSetParams struct { + Set ModuleResolutionSetID `json:"set"` +} + +type CreateModuleResolverParams struct { + Snapshot SnapshotID `json:"snapshot"` + CompilerOptions core.CompilerOptions `json:"compilerOptions"` + ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` +} + +type ResolveModuleNameParams struct { + Snapshot SnapshotID `json:"snapshot"` + Resolver ModuleResolverID `json:"resolver"` + ModuleName string `json:"moduleName"` + ContainingDirectory DocumentIdentifier `json:"containingDirectory"` + ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` +} + +type ModuleResolutionInvocationResult struct { + Result *ResolvedModule `json:"result,omitempty"` + Trace []string `json:"trace,omitempty"` } type CreateProgramOldProgramParams struct { @@ -456,6 +523,10 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodUpdateSnapshot: unmarshallerFor[UpdateSnapshotParams], MethodUpdateTemporarySnapshot: unmarshallerFor[UpdateTemporarySnapshotParams], MethodCreateProgram: unmarshallerFor[CreateProgramParams], + MethodCreateModuleResolutionSet: unmarshallerFor[CreateModuleResolutionSetParams], + MethodReleaseModuleResolutionSet: unmarshallerFor[ReleaseModuleResolutionSetParams], + MethodCreateModuleResolver: unmarshallerFor[CreateModuleResolverParams], + MethodResolveModuleName: unmarshallerFor[ResolveModuleNameParams], MethodParseCommandLine: unmarshallerFor[ParseCommandLineParams], MethodReadConfigFile: unmarshallerFor[ReadConfigFileParams], MethodParseJsonConfigFile: unmarshallerFor[ParseJsonConfigFileContentParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 1f0faff868ce7..fe5d7454e383c 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -50,6 +50,9 @@ type snapshotData struct { fileSystem vfs.FS refCount int + moduleResolvers map[ModuleResolverID]*moduleResolverData + moduleResolversMu sync.RWMutex + // Symbol IDs come from ast.GetSymbolId, a global atomic counter, so the same // *ast.Symbol pointer always has the same unique ID across all projects in the // snapshot. Symbols are registered snapshot-wide to ensure identity semantics: @@ -69,6 +72,11 @@ type snapshotData struct { projectRegistriesMu sync.RWMutex } +type moduleResolverData struct { + resolver *module.Resolver + provider module.ResolutionProvider +} + // projectRegistryData holds per-project type and signature registries. // Types and signatures use per-checker sequential IDs, so the same local ID // can appear in multiple projects. Separate maps per project prevent collisions @@ -434,6 +442,12 @@ type Session struct { // snapshots. Lock ordering is updateMu -> snapshotsMu (never the reverse). updateMu sync.Mutex + moduleResolutionSets map[ModuleResolutionSetID]*providedModuleResolutions + moduleResolutionSetsMu sync.RWMutex + nextModuleResolutionSetID atomic.Uint64 + nextModuleResolutionIdentity atomic.Uint64 + nextModuleResolverID atomic.Uint64 + cpuProfiler pprof.CPUProfiler } @@ -475,10 +489,11 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont withLocale = func(ctx context.Context) context.Context { return ctx } } s := &Session{ - id: formatSessionID(id), - snapshotHost: snapshotHost, - withLocale: withLocale, - snapshots: make(map[SnapshotID]*snapshotData), + id: formatSessionID(id), + snapshotHost: snapshotHost, + withLocale: withLocale, + snapshots: make(map[SnapshotID]*snapshotData), + moduleResolutionSets: make(map[ModuleResolutionSetID]*providedModuleResolutions), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses @@ -769,6 +784,14 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleParseJsonConfigFileContent(ctx, parsed.(*ParseJsonConfigFileContentParams)) case string(MethodCreateProgram): return s.handleCreateProgram(ctx, parsed.(*CreateProgramParams)) + case string(MethodCreateModuleResolutionSet): + return s.handleCreateModuleResolutionSet(parsed.(*CreateModuleResolutionSetParams)) + case string(MethodReleaseModuleResolutionSet): + return s.handleReleaseModuleResolutionSet(parsed.(*ReleaseModuleResolutionSetParams)) + case string(MethodCreateModuleResolver): + return s.handleCreateModuleResolver(parsed.(*CreateModuleResolverParams)) + case string(MethodResolveModuleName): + return s.handleResolveModuleName(parsed.(*ResolveModuleNameParams)) case string(MethodParseConfigFile): return s.handleParseConfigFile(ctx, parsed.(*ParseConfigFileParams)) case string(MethodTranspileModule): @@ -1407,6 +1430,10 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram if params.FileChanges != nil && params.OldProgram == nil { return nil, fmt.Errorf("%w: fileChanges requires an oldProgram", ErrClientError) } + moduleResolutionProvider, err := s.resolveModuleResolutionSource(params.CreateProgramOptions.ModuleResolutions) + if err != nil { + return nil, err + } rootFileNames := make([]string, len(params.RootFiles)) for i, rootFile := range params.RootFiles { @@ -1454,6 +1481,7 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram ¶ms.CreateProgramOptions.CompilerOptions, params.CreateProgramOptions.ProjectReferences, core.Map(params.CreateProgramOptions.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), + moduleResolutionProvider, oldProject, fileChanges, ) diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go new file mode 100644 index 0000000000000..87482f18cd56d --- /dev/null +++ b/tsc/internal/api/session_module_resolution_test.go @@ -0,0 +1,219 @@ +package api + +import ( + "context" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { + t.Parallel() + + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/p/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","exports":{".":{"types":"./index.d.ts","default":"./index.js"}}}`, + "/home/projects/p/node_modules/pkg/index.d.ts": `export declare const value: string;`, + "/home/projects/p/node_modules/pkg/index.js": `exports.value = "value";`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + snapshot, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{}) + assert.NilError(t, err) + resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ + Snapshot: snapshot.Snapshot, + CompilerOptions: core.CompilerOptions{ + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + TraceResolution: core.TSTrue, + }, + }) + assert.NilError(t, err) + result, resolutionErr := session.handleResolveModuleName(&ResolveModuleNameParams{ + Snapshot: snapshot.Snapshot, + Resolver: resolver, + ModuleName: "pkg", + ContainingDirectory: DocumentIdentifier{FileName: "/home/projects/p/src"}, + }) + assert.NilError(t, resolutionErr) + assert.Equal(t, result.Result.ResolvedFileName, "/home/projects/p/node_modules/pkg/index.d.ts") + assert.Equal(t, result.Result.PackageId.Name, "pkg") + assert.Assert(t, len(result.Trace) > 0) +} + +func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { + t.Parallel() + + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/p/global.d.ts": `export declare const value: "global";`, + "/home/projects/p/mode.d.ts": `export declare const value: "mode";`, + "/home/projects/p/dir.d.ts": `export declare const value: "dir";`, + "/home/projects/p/exact.d.ts": `export declare const value: "exact";`, + "/home/projects/p/default.d.ts": `export declare const value: "default";`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + snapshot, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{}) + assert.NilError(t, err) + esm := core.ModuleKindESNext + spec := ModuleResolutionSpec{ + Fallback: ModuleResolutionFallbackUnresolved, + Entries: []*ModuleResolutionEntry{ + providedResolutionEntry("pkg", "", nil, "/home/projects/p/global.d.ts"), + providedResolutionEntry("pkg", "", &esm, "/home/projects/p/mode.d.ts"), + providedResolutionEntry("pkg", "/home/projects/p/src", nil, "/home/projects/p/dir.d.ts"), + providedResolutionEntry("pkg", "/home/projects/p/src", &esm, "/home/projects/p/exact.d.ts"), + }, + } + setID, err := session.handleCreateModuleResolutionSet(&CreateModuleResolutionSetParams{Spec: spec}) + assert.NilError(t, err) + resolverID, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ + Snapshot: snapshot.Snapshot, + CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + ModuleResolutions: &ModuleResolutionSource{Set: setID}, + }) + assert.NilError(t, err) + + assertResolution := func(directory string, mode core.ModuleKind, expected string) { + t.Helper() + resolutionMode := ResolutionMode(mode) + result, resolutionErr := session.handleResolveModuleName(&ResolveModuleNameParams{ + Snapshot: snapshot.Snapshot, + Resolver: resolverID, + ModuleName: "pkg", + ContainingDirectory: DocumentIdentifier{FileName: directory}, + ResolutionMode: &resolutionMode, + }) + assert.NilError(t, resolutionErr) + assert.Equal(t, result.Result.ResolvedFileName, expected) + assert.Equal(t, len(result.Trace), 0) + } + assertResolution("/home/projects/p/src", core.ModuleKindESNext, "/home/projects/p/exact.d.ts") + assertResolution("/home/projects/p/src", core.ModuleKindCommonJS, "/home/projects/p/dir.d.ts") + assertResolution("/home/projects/p/other", core.ModuleKindESNext, "/home/projects/p/mode.d.ts") + assertResolution("/home/projects/p/other", core.ModuleKindCommonJS, "/home/projects/p/global.d.ts") + + unresolved, err := session.handleResolveModuleName(&ResolveModuleNameParams{ + Snapshot: snapshot.Snapshot, + Resolver: resolverID, + ModuleName: "other", + ContainingDirectory: DocumentIdentifier{FileName: "/home/projects/p/src"}, + }) + assert.NilError(t, err) + assert.Assert(t, unresolved.Result == nil) + + _, err = session.handleReleaseModuleResolutionSet(&ReleaseModuleResolutionSetParams{Set: setID}) + assert.NilError(t, err) + assertResolution("/home/projects/p/src", core.ModuleKindESNext, "/home/projects/p/exact.d.ts") + _, err = session.handleCreateModuleResolver(&CreateModuleResolverParams{ + Snapshot: snapshot.Snapshot, + CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + ModuleResolutions: &ModuleResolutionSource{Set: setID}, + }) + assert.ErrorContains(t, err, "not found") +} + +func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { + t.Parallel() + + const root = "/home/projects/p/src/index.ts" + const provided = "/home/projects/p/provided.d.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + root: `import { value } from "pkg"; export { value };`, + provided: `export declare const value: string;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + response, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{ + RootFiles: []DocumentIdentifier{{FileName: root}}, + CreateProgramOptions: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ModuleResolutions: &ModuleResolutionSource{ + Spec: &ModuleResolutionSpec{ + Fallback: ModuleResolutionFallbackUnresolved, + Entries: []*ModuleResolutionEntry{ + providedResolutionEntry("pkg", "", nil, provided), + }, + }, + }, + }, + }) + assert.NilError(t, err) + fileNames, err := session.handleGetSourceFileNames(context.Background(), &GetSourceFileNamesParams{ + Snapshot: response.Snapshot, + Project: response.Project.Id, + }) + assert.NilError(t, err) + assert.DeepEqual(t, fileNames, []string{provided, root}) +} + +func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { + t.Parallel() + + projectSession, _ := projecttestutil.Setup(map[string]any{}) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + snapshot, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{}) + assert.NilError(t, err) + resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ + Snapshot: snapshot.Snapshot, + CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + ModuleResolutions: &ModuleResolutionSource{Spec: &ModuleResolutionSpec{ + Fallback: ModuleResolutionFallbackUnresolved, + Entries: []*ModuleResolutionEntry{{ + ModuleName: "pkg", + Result: &ProvidedModuleResolution{ + ResolvedFileName: &DocumentIdentifier{FileName: "/store/pkg/index.d.ts"}, + OriginalPath: &DocumentIdentifier{FileName: "/node_modules/pkg/index.d.ts"}, + PackageID: &PackageId{ + Name: "pkg", + SubModuleName: "", + Version: "1.2.3", + }, + }, + }}, + }}, + }) + assert.NilError(t, err) + result, err := session.handleResolveModuleName(&ResolveModuleNameParams{ + Snapshot: snapshot.Snapshot, + Resolver: resolver, + ModuleName: "pkg", + ContainingDirectory: DocumentIdentifier{FileName: "/src"}, + }) + assert.NilError(t, err) + assert.Equal(t, result.Result.OriginalPath, "/node_modules/pkg/index.d.ts") + assert.Equal(t, result.Result.PackageId.Name, "pkg") + assert.Equal(t, result.Result.PackageId.Version, "1.2.3") + assert.Equal(t, result.Result.IsExternalLibraryImport, true) +} + +func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { + entry := &ModuleResolutionEntry{ + ModuleName: moduleName, + Result: &ProvidedModuleResolution{ + ResolvedFileName: &DocumentIdentifier{FileName: fileName}, + }, + } + if mode != nil { + resolutionMode := ResolutionMode(*mode) + entry.ResolutionMode = &resolutionMode + } + if directory != "" { + entry.ContainingDirectory = &DocumentIdentifier{FileName: directory} + } + return entry +} diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 1885c55056045..59157261e235d 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -872,7 +872,21 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { } mode := getModeForUsageLocation(file.FileName(), meta, entry, optionsForFile) - resolvedModule, trace := p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + var resolvedModule *module.ResolvedModule + var trace []module.DiagAndArgs + if p.opts.ModuleResolutionProvider != nil { + var provided bool + var providedResolution *module.ProvidedModuleResolution + providedResolution, provided = p.opts.ModuleResolutionProvider.GetModuleResolution(moduleName, tspath.GetDirectoryPath(fileName), mode) + if provided { + resolvedModule = p.resolver.ResolveProvidedModule(moduleName, providedResolution) + } + if !provided { + resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + } + } else { + resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + } resolutionsInFile[module.ModeAwareCacheKey{Name: moduleName, Mode: mode}] = resolvedModule resolutionsTrace = append(resolutionsTrace, trace...) diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 2d0461124718c..84bde5b9782e7 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -43,6 +43,7 @@ type ProgramOptions struct { TypingsLocation string ProjectName string Tracing *tracing.Tracing + ModuleResolutionProvider module.ResolutionProvider // SkipModuleResolution avoids all module and type reference resolution while // still collecting import metadata needed for emit. SkipModuleResolution bool @@ -326,6 +327,9 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost if createCheckerPool != nil { newOpts.CreateCheckerPool = createCheckerPool } + if resolutionProviderIdentity(p.opts.ModuleResolutionProvider) != resolutionProviderIdentity(newOpts.ModuleResolutionProvider) { + return nil, nil, false + } oldFile := p.filesByPath[changedFilePath] var newFile *ast.SourceFile @@ -342,6 +346,7 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost if err != nil { return nil, nil, false } + oldSupplementalFiles = oldFile.SupplementalSourceFiles() newSupplementalFiles = files.Supplemental } else { @@ -415,6 +420,13 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost return result, newFile, true } +func resolutionProviderIdentity(provider module.ResolutionProvider) uint64 { + if provider == nil { + return 0 + } + return provider.Identity() +} + func (p *Program) initCheckerPool() { if !p.finishedProcessing { panic("Program must finish processing files before initializing checker pool") diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 2363435bb2207..e49f0d338477f 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -265,7 +265,37 @@ func (r *Resolver) ResolveTypeReferenceDirective( } func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { - containingDirectory := tspath.GetDirectoryPath(containingFile) + return r.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) +} + +func (r *Resolver) ResolveModuleNameFromDirectory(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, []DiagAndArgs) { + return r.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) +} + +func (r *Resolver) ResolveProvidedModule(moduleName string, provided *ProvidedModuleResolution) *ResolvedModule { + if provided == nil { + return &ResolvedModule{} + } + externalPath := provided.ResolvedFileName + if provided.OriginalPath != "" { + externalPath = provided.OriginalPath + } + extension := tspath.TryGetExtensionFromPath(provided.ResolvedFileName) + isStandardExtension := tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) || + tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedJSExtensionsFlat) + return &ResolvedModule{ + ResolvedFileName: provided.ResolvedFileName, + OriginalPath: provided.OriginalPath, + Extension: extension, + ResolvedUsingTsExtension: tspath.IsExternalModuleNameRelative(moduleName) && + tspath.TryExtractTSExtension(moduleName) != "", + ResolvedUsingExtraExtensions: !isStandardExtension, + PackageId: provided.PackageId, + IsExternalLibraryImport: strings.Contains(externalPath, "/node_modules/"), + } +} + +func (r *Resolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { traceBuilder := r.newTraceBuilder() cacheKey := moduleResolutionCacheKey{ diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 72e2573adf3fe..a9c3cdfa6f4b3 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -16,6 +16,17 @@ type ResolutionHost interface { GetCurrentDirectory() string } +type ResolutionProvider interface { + Identity() uint64 + GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (resolution *ProvidedModuleResolution, found bool) +} + +type ProvidedModuleResolution struct { + ResolvedFileName string + OriginalPath string + PackageId PackageId +} + type ModeAwareCacheKey struct { Name string Mode core.ResolutionMode diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 80cacf0b7b834..9de4b54fa98c4 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/ls" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project/ata" "github.com/microsoft/TypeScript/tsc/internal/project/logging" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" @@ -83,6 +84,8 @@ type Project struct { checkerPool *checkerPool + moduleResolutionProvider module.ResolutionProvider + // installedTypingsInfo is the value of `project.ComputeTypingsInfo()` that was // used during the most recently completed typings installation. installedTypingsInfo *ata.TypingsInfo @@ -313,6 +316,8 @@ func (p *Project) Clone() *Project { checkerPool: p.checkerPool, + moduleResolutionProvider: p.moduleResolutionProvider, + installedTypingsInfo: p.installedTypingsInfo, typingsFiles: p.typingsFiles, } @@ -454,6 +459,7 @@ func (p *Project) CreateProgram() CreateProgramResult { UseSourceOfProjectReference: true, TypingsLocation: typingsLocation, CreateCheckerPool: createCheckerPool, + ModuleResolutionProvider: p.moduleResolutionProvider, }, ) } diff --git a/tsc/internal/project/refcountcache_test.go b/tsc/internal/project/refcountcache_test.go index 2edd1d4d3f970..c53cd5f42a9de 100644 --- a/tsc/internal/project/refcountcache_test.go +++ b/tsc/internal/project/refcountcache_test.go @@ -529,6 +529,7 @@ func TestRefCountingCaches(t *testing.T) { appProject.CommandLine.CompilerOptions(), appProject.CommandLine.ProjectReferences(), appProject.CommandLine.Errors, + nil, appProject, FileChangeSummary{}, ) @@ -559,6 +560,7 @@ func TestRefCountingCaches(t *testing.T) { programProject.CommandLine.CompilerOptions(), programProject.CommandLine.ProjectReferences(), programProject.CommandLine.Errors, + nil, programProject, fileChanges, ) diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index ad4426ea3ddc0..672fb9843181a 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -18,6 +18,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project/ata" "github.com/microsoft/TypeScript/tsc/internal/project/dirty" "github.com/microsoft/TypeScript/tsc/internal/project/logging" @@ -113,6 +114,7 @@ func (s *Snapshot) cloneForProgram( compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, + moduleResolutionProvider module.ResolutionProvider, oldProject *Project, fileChanges FileChangeSummary, sessionLogger logging.Logger, @@ -158,6 +160,14 @@ func (s *Snapshot) cloneForProgram( ) projectCollectionBuilder.seedInferredProjectForProgram(oldProject, logger) + if project := projectCollectionBuilder.inferredProject.Value(); project != nil && + resolutionProviderIdentity(project.moduleResolutionProvider) != resolutionProviderIdentity(moduleResolutionProvider) { + projectCollectionBuilder.inferredProject.Change(func(project *Project) { + project.moduleResolutionProvider = moduleResolutionProvider + project.dirty = true + project.dirtyFilePath = "" + }) + } if !fileChanges.IsEmpty() { changeLogger := logger if changeLogger != nil { @@ -177,11 +187,15 @@ func (s *Snapshot) cloneForProgram( s.inferredProjectContentMappers, updateLogger, ) + projectCollectionBuilder.inferredProject.Change(func(project *Project) { + project.moduleResolutionProvider = moduleResolutionProvider + }) if projectCollectionBuilder.inferredProject.Value().dirty { createLogger := logger if createLogger != nil { createLogger = logger.Fork("CreateProgram") } + projectCollectionBuilder.updateProgram(projectCollectionBuilder.inferredProject, createLogger) } projectCollectionBuilder.cleanupAllConfiguredProjects(logger.Fork("cleanupAllConfiguredProjects")) @@ -282,6 +296,13 @@ func (s *Snapshot) cloneWithTemporaryFile( }, overlays, nil), nil } +func resolutionProviderIdentity(provider module.ResolutionProvider) uint64 { + if provider == nil { + return 0 + } + return provider.Identity() +} + func (s *Snapshot) processFileChanges( fs *snapshotFSBuilder, fileChanges FileChangeSummary, @@ -344,6 +365,14 @@ func (s *Snapshot) GetFile(fileName string) FileHandle { return s.fs.GetFile(fileName) } +func (s *Snapshot) FS() vfs.FS { + return newSourceFS(false, s.fs, s.toPath) +} + +func (s *Snapshot) GetCurrentDirectory() string { + return s.host.GetCurrentDirectory() +} + func (s *Snapshot) LSPLineMap(fileName string) *lsconv.LSPLineMap { if file := s.fs.GetFile(fileName); file != nil { return file.LSPLineMap() @@ -374,6 +403,11 @@ func (s *Snapshot) AutoImportRegistry() *autoimport.Registry { return s.AutoImports } +func (s *Snapshot) ContentMapperExtensions() []string { + extensions, _ := s.contentMapperWatchState() + return extensions +} + func (s *Snapshot) ID() uint64 { return s.id } diff --git a/tsc/internal/project/snapshothost.go b/tsc/internal/project/snapshothost.go index faaa4c4321b20..bf3379326efe6 100644 --- a/tsc/internal/project/snapshothost.go +++ b/tsc/internal/project/snapshothost.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project/logging" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" @@ -119,6 +120,7 @@ func (s *SnapshotHost) CloneSnapshotForProgram( options *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, + moduleResolutionProvider module.ResolutionProvider, oldProject *Project, fileChanges FileChangeSummary, ) *Snapshot { @@ -129,6 +131,7 @@ func (s *SnapshotHost) CloneSnapshotForProgram( options, projectReferences, configFileParsingDiagnostics, + moduleResolutionProvider, oldProject, fileChanges, nil, From 89bdab8051ca3583364ba62c91c2a13bc6842b40 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 15 Sep 2026 08:46:22 -0700 Subject: [PATCH 02/20] Format rebased module resolution changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/typescript/src/api/async/api.ts | 2 +- packages/typescript/test/async/api.test.ts | 134 ++++++++++----------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 455850c49e433..4400ee7efe69c 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -69,10 +69,10 @@ import type { ImportAdderAction, IntrinsicTypeMethod, LSPUpdateSnapshotParams, - PackageId, ModuleResolutionInvocationResult, ModuleResolutionSource, ModuleResolutionSpec, + PackageId, ParsedCommandLine, ProjectReference, ProjectResponse, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index be87a5712c7fb..d604b71f058d3 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -489,79 +489,79 @@ import "missing";`, test("module resolver and provided resolution sets", async () => { await using api = spawnAPI({ - "/src/main.ts": `import { value } from "pkg"; export { value };`, - "/src/extension.ts": `import { value } from "./dep.ts"; value;`, - "/src/dep.ts": `export const value = 1;`, - "/provided.d.ts": `export declare const value: string;`, - "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, - "/node_modules/pkg/index.d.ts": `export declare const value: number;`, + "/src/main.ts": `import { value } from "pkg"; export { value };`, + "/src/extension.ts": `import { value } from "./dep.ts"; value;`, + "/src/dep.ts": `export const value = 1;`, + "/provided.d.ts": `export declare const value: string;`, + "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, + "/node_modules/pkg/index.d.ts": `export declare const value: number;`, }); const snapshot = await api.updateSnapshot(); - const compilerOptions = { - module: ModuleKind.NodeNext, - moduleResolution: ModuleResolutionKind.NodeNext, - traceResolution: true, - }; - const resolver = await snapshot.createModuleResolver(compilerOptions); - const defaultResolution = await resolver.resolveModuleName("pkg", "/src"); - assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); - assert.ok(defaultResolution.trace?.length); - const fallbackResolver = await snapshot.createModuleResolver(compilerOptions, { - moduleResolutions: { fallback: "resolve", entries: [] }, - }); - assert.equal( - (await fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, - "/node_modules/pkg/index.d.ts", - ); + const compilerOptions = { + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + traceResolution: true, + }; + const resolver = await snapshot.createModuleResolver(compilerOptions); + const defaultResolution = await resolver.resolveModuleName("pkg", "/src"); + assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); + assert.ok(defaultResolution.trace?.length); + const fallbackResolver = await snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { fallback: "resolve", entries: [] }, + }); + assert.equal( + (await fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + + const set = await api.createModuleResolutionSet({ + fallback: "unresolved", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }); + const overriddenResolver = await snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + assert.equal( + (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); + + const program = await api.createProgram( + ["/src/main.ts"], + { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, + ); + assert.deepEqual([...await program.getSourceFileNames()].sort(), ["/provided.d.ts", "/src/main.ts"]); - const set = await api.createModuleResolutionSet({ + const extensionProgram = await api.createProgram( + ["/src/extension.ts"], + { + compilerOptions: { ...compilerOptions, noLib: true }, + moduleResolutions: { fallback: "unresolved", entries: [{ - moduleName: "pkg", - result: { resolvedFileName: "/provided.d.ts" }, + moduleName: "./dep.ts", + containingDirectory: "/src", + result: { resolvedFileName: "/src/dep.ts" }, }], - }); - const overriddenResolver = await snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); - assert.equal( - (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, - "/provided.d.ts", - ); - assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); - - const program = await api.createProgram( - ["/src/main.ts"], - { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, - ); - assert.deepEqual([...await program.getSourceFileNames()].sort(), ["/provided.d.ts", "/src/main.ts"]); - - const extensionProgram = await api.createProgram( - ["/src/extension.ts"], - { - compilerOptions: { ...compilerOptions, noLib: true }, - moduleResolutions: { - fallback: "unresolved", - entries: [{ - moduleName: "./dep.ts", - containingDirectory: "/src", - result: { resolvedFileName: "/src/dep.ts" }, - }], - }, - }, - ); - assert.ok((await extensionProgram.getSemanticDiagnostics("/src/extension.ts")).some(diagnostic => diagnostic.code === 5097)); - - await set.dispose(); - assert.equal( - (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, - "/provided.d.ts", - ); - const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); - await assert.rejects(createFromDisposedSet, /ModuleResolutionSet is disposed/); // @sync: assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); - - await program.dispose(); - await extensionProgram.dispose(); - await snapshot.dispose(); - const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); + }, + }, + ); + assert.ok((await extensionProgram.getSemanticDiagnostics("/src/extension.ts")).some(diagnostic => diagnostic.code === 5097)); + + await set.dispose(); + assert.equal( + (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + "/provided.d.ts", + ); + const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); + await assert.rejects(createFromDisposedSet, /ModuleResolutionSet is disposed/); // @sync: assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); + + await program.dispose(); + await extensionProgram.dispose(); + await snapshot.dispose(); + const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); await assert.rejects(resolveAfterSnapshotDisposal, /Snapshot is disposed/); // @sync: assert.throws(resolveAfterSnapshotDisposal, /Snapshot is disposed/); }); From c49318e10875b524f489c15d1a172276e74f62e8 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 15 Sep 2026 09:51:06 -0700 Subject: [PATCH 03/20] Support module resolution overrides and programless resolveModuleName --- packages/typescript/src/api/async/api.ts | 9 +++-- .../typescript/src/api/proto.generated.ts | 7 ++-- packages/typescript/src/api/sync/api.ts | 15 +++++--- packages/typescript/test/async/api.test.ts | 10 +++--- packages/typescript/test/sync/api.test.ts | 10 +++--- tsc/internal/api/module_resolution.go | 35 +++++++++++-------- tsc/internal/api/proto.go | 7 ++-- .../api/session_module_resolution_test.go | 16 ++++----- tsc/internal/compiler/fileloader.go | 7 ++-- tsc/internal/module/resolver.go | 23 ------------ tsc/internal/module/types.go | 8 +---- 11 files changed, 68 insertions(+), 79 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 4400ee7efe69c..964894511d31b 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -69,16 +69,18 @@ import type { ImportAdderAction, IntrinsicTypeMethod, LSPUpdateSnapshotParams, - ModuleResolutionInvocationResult, + ModuleResolutionEntry, ModuleResolutionSource, ModuleResolutionSpec, PackageId, ParsedCommandLine, ProjectReference, ProjectResponse, + ProvidedModuleResolution, ReadConfigFileResponse, ResolvedModule, ResolvedTypeReferenceDirective, + ResolveModuleNameResult, SignaturePropertyMethod, SignatureResponse, SourceFileMetadata, @@ -191,16 +193,19 @@ export type { JSDocTagInfo, LiteralType, LSPConnectionOptions, + ModuleResolutionEntry, ModuleResolutionSpec, NumberLiteralType, ObjectType, PackageId, ParsedCommandLine, ProjectReference, + ProvidedModuleResolution, ReadConfigFileResponse, RequestTiming, ResolvedModule, ResolvedTypeReferenceDirective, + ResolveModuleNameResult, SourceFileMetadata, StringLiteralType, StringMappingType, @@ -767,7 +772,7 @@ export class ModuleResolver { moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext, - ): Promise { + ): Promise { this.ensureSnapshotActive(); return this.client.apiRequest("resolveModuleName", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 9f267f9fa1aaa..97a70ca47b91e 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -26,7 +26,7 @@ export interface APIMethodInfo { createModuleResolutionSet: APIMethod; releaseModuleResolutionSet: APIMethod; createModuleResolver: APIMethod; - resolveModuleName: APIMethod; + resolveModuleName: APIMethod; parseCommandLine: APIMethod; readConfigFile: APIMethod; parseJsonConfigFileContent: APIMethod; @@ -306,8 +306,9 @@ export interface ResolveModuleNameParams { resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; } -export interface ModuleResolutionInvocationResult { - result?: ResolvedModule | undefined; +export interface ResolveModuleNameResult { + resolvedModule?: ResolvedModule | undefined; + /** Trace is provided when compilerOptions.traceResolution is true. */ trace?: string[] | undefined; } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 06cbe9d7db195..80375d3efbec3 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -86,16 +86,18 @@ import type { ImportAdderAction, IntrinsicTypeMethod, LSPUpdateSnapshotParams, - ModuleResolutionInvocationResult, + ModuleResolutionEntry, ModuleResolutionSource, ModuleResolutionSpec, PackageId, ParsedCommandLine, ProjectReference, ProjectResponse, + ProvidedModuleResolution, ReadConfigFileResponse, ResolvedModule, ResolvedTypeReferenceDirective, + ResolveModuleNameResult, SignaturePropertyMethod, SignatureResponse, SourceFileMetadata, @@ -208,16 +210,19 @@ export type { JSDocTagInfo, LiteralType, LSPConnectionOptions, + ModuleResolutionEntry, ModuleResolutionSpec, NumberLiteralType, ObjectType, PackageId, ParsedCommandLine, ProjectReference, + ProvidedModuleResolution, ReadConfigFileResponse, RequestTiming, ResolvedModule, ResolvedTypeReferenceDirective, + ResolveModuleNameResult, SourceFileMetadata, StringLiteralType, StringMappingType, @@ -1338,14 +1343,14 @@ export class ModuleResolver { } get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ModuleResolutionInvocationResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator; + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ModuleResolutionInvocationResult { + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ResolveModuleNameResult { owner.ensureSnapshotActive(); return owner.client.apiRequest("resolveModuleName", { snapshot: owner.snapshotId, @@ -1355,7 +1360,7 @@ export class ModuleResolver { ...(resolutionMode !== undefined ? { resolutionMode } : {}), }); }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator { + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator { owner.ensureSnapshotActive(); return yield* apiRequest("resolveModuleName", { snapshot: owner.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index d604b71f058d3..72d4b099ba703 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -504,13 +504,13 @@ import "missing";`, }; const resolver = await snapshot.createModuleResolver(compilerOptions); const defaultResolution = await resolver.resolveModuleName("pkg", "/src"); - assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); + assert.equal(defaultResolution.resolvedModule?.resolvedFileName, "/node_modules/pkg/index.d.ts"); assert.ok(defaultResolution.trace?.length); const fallbackResolver = await snapshot.createModuleResolver(compilerOptions, { moduleResolutions: { fallback: "resolve", entries: [] }, }); assert.equal( - (await fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (await fallbackResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/node_modules/pkg/index.d.ts", ); @@ -523,10 +523,10 @@ import "missing";`, }); const overriddenResolver = await snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); assert.equal( - (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (await overriddenResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/provided.d.ts", ); - assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); + assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); const program = await api.createProgram( ["/src/main.ts"], @@ -552,7 +552,7 @@ import "missing";`, await set.dispose(); assert.equal( - (await overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (await overriddenResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/provided.d.ts", ); const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 62140c7e914f4..b459acffdf082 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -493,13 +493,13 @@ import "missing";`, }; const resolver = snapshot.createModuleResolver(compilerOptions); const defaultResolution = resolver.resolveModuleName("pkg", "/src"); - assert.equal(defaultResolution.result?.resolvedFileName, "/node_modules/pkg/index.d.ts"); + assert.equal(defaultResolution.resolvedModule?.resolvedFileName, "/node_modules/pkg/index.d.ts"); assert.ok(defaultResolution.trace?.length); const fallbackResolver = snapshot.createModuleResolver(compilerOptions, { moduleResolutions: { fallback: "resolve", entries: [] }, }); assert.equal( - (fallbackResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (fallbackResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/node_modules/pkg/index.d.ts", ); @@ -512,10 +512,10 @@ import "missing";`, }); const overriddenResolver = snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); assert.equal( - (overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (overriddenResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/provided.d.ts", ); - assert.equal((overriddenResolver.resolveModuleName("missing", "/src")).result, undefined); + assert.equal((overriddenResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); const program = api.createProgram( ["/src/main.ts"], @@ -541,7 +541,7 @@ import "missing";`, set.dispose(); assert.equal( - (overriddenResolver.resolveModuleName("pkg", "/src")).result?.resolvedFileName, + (overriddenResolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, "/provided.d.ts", ); const createFromDisposedSet = () => snapshot.createModuleResolver(compilerOptions, { moduleResolutions: set }); diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index bf85a1e6c3e9a..67c7a8bbce789 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "strings" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/locale" @@ -20,7 +21,7 @@ type moduleResolutionMatchKey struct { type providedModuleResolutions struct { identity uint64 fallbackToResolution bool - entries map[moduleResolutionMatchKey]*module.ProvidedModuleResolution + entries map[moduleResolutionMatchKey]*module.ResolvedModule currentDirectory string useCaseSensitive bool } @@ -29,7 +30,7 @@ func (p *providedModuleResolutions) Identity() uint64 { return p.identity } -func (p *providedModuleResolutions) GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ProvidedModuleResolution, bool) { +func (p *providedModuleResolutions) GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitive) keys := [...]moduleResolutionMatchKey{ {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, @@ -65,7 +66,7 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu provider := &providedModuleResolutions{ identity: identity, fallbackToResolution: fallbackToResolution, - entries: make(map[moduleResolutionMatchKey]*module.ProvidedModuleResolution, len(spec.Entries)), + entries: make(map[moduleResolutionMatchKey]*module.ResolvedModule, len(spec.Entries)), currentDirectory: currentDirectory, useCaseSensitive: useCaseSensitive, } @@ -98,9 +99,9 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu return nil, fmt.Errorf("%w: duplicate module resolution entry for %q", ErrClientError, entry.ModuleName) } - var provided *module.ProvidedModuleResolution + var provided *module.ResolvedModule if entry.Result.ResolvedFileName != nil { - provided = &module.ProvidedModuleResolution{ + provided = &module.ResolvedModule{ ResolvedFileName: tspath.GetNormalizedAbsolutePath(entry.Result.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), } if entry.Result.OriginalPath != nil { @@ -114,6 +115,16 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu PeerDependencies: entry.Result.PackageID.PeerDependencies, } } + externalPath := provided.ResolvedFileName + if provided.OriginalPath != "" { + externalPath = provided.OriginalPath + } + provided.Extension = tspath.TryGetExtensionFromPath(provided.ResolvedFileName) + provided.ResolvedUsingTsExtension = tspath.IsExternalModuleNameRelative(entry.ModuleName) && + tspath.TryExtractTSExtension(entry.ModuleName) != "" + provided.ResolvedUsingExtraExtensions = !tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && + !tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedJSExtensionsFlat) + provided.IsExternalLibraryImport = strings.Contains(externalPath, "/node_modules/") } provider.entries[key] = provided } @@ -210,7 +221,7 @@ func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) return id, nil } -func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*ModuleResolutionInvocationResult, error) { +func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*ResolveModuleNameResult, error) { if params.ModuleName == "" { return nil, fmt.Errorf("%w: moduleName is empty", ErrClientError) } @@ -238,17 +249,13 @@ func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*Mod var trace []module.DiagAndArgs var provided bool if data.provider != nil { - var providedResolution *module.ProvidedModuleResolution - providedResolution, provided = data.provider.GetModuleResolution(params.ModuleName, containingDirectory, mode) - if provided { - result = data.resolver.ResolveProvidedModule(params.ModuleName, providedResolution) - } + result, provided = data.provider.GetModuleResolution(params.ModuleName, containingDirectory, mode) } if !provided { result, trace = data.resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) } - return &ModuleResolutionInvocationResult{ - Result: newResolvedModuleResponse(result), - Trace: moduleResolutionTraceToStrings(trace), + return &ResolveModuleNameResult{ + ResolvedModule: newResolvedModuleResponse(result), + Trace: moduleResolutionTraceToStrings(trace), }, nil } diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index b6114726002ed..c7d4517f01b51 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -470,9 +470,10 @@ type ResolveModuleNameParams struct { ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` } -type ModuleResolutionInvocationResult struct { - Result *ResolvedModule `json:"result,omitempty"` - Trace []string `json:"trace,omitempty"` +type ResolveModuleNameResult struct { + ResolvedModule *ResolvedModule `json:"resolvedModule,omitempty"` + // Trace is provided when compilerOptions.traceResolution is true. + Trace []string `json:"trace,omitempty"` } type CreateProgramOldProgramParams struct { diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 87482f18cd56d..b88616d67f3a7 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -39,8 +39,8 @@ func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { ContainingDirectory: DocumentIdentifier{FileName: "/home/projects/p/src"}, }) assert.NilError(t, resolutionErr) - assert.Equal(t, result.Result.ResolvedFileName, "/home/projects/p/node_modules/pkg/index.d.ts") - assert.Equal(t, result.Result.PackageId.Name, "pkg") + assert.Equal(t, result.ResolvedModule.ResolvedFileName, "/home/projects/p/node_modules/pkg/index.d.ts") + assert.Equal(t, result.ResolvedModule.PackageId.Name, "pkg") assert.Assert(t, len(result.Trace) > 0) } @@ -90,7 +90,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { ResolutionMode: &resolutionMode, }) assert.NilError(t, resolutionErr) - assert.Equal(t, result.Result.ResolvedFileName, expected) + assert.Equal(t, result.ResolvedModule.ResolvedFileName, expected) assert.Equal(t, len(result.Trace), 0) } assertResolution("/home/projects/p/src", core.ModuleKindESNext, "/home/projects/p/exact.d.ts") @@ -105,7 +105,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { ContainingDirectory: DocumentIdentifier{FileName: "/home/projects/p/src"}, }) assert.NilError(t, err) - assert.Assert(t, unresolved.Result == nil) + assert.Assert(t, unresolved.ResolvedModule == nil) _, err = session.handleReleaseModuleResolutionSet(&ReleaseModuleResolutionSetParams{Set: setID}) assert.NilError(t, err) @@ -195,10 +195,10 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { ContainingDirectory: DocumentIdentifier{FileName: "/src"}, }) assert.NilError(t, err) - assert.Equal(t, result.Result.OriginalPath, "/node_modules/pkg/index.d.ts") - assert.Equal(t, result.Result.PackageId.Name, "pkg") - assert.Equal(t, result.Result.PackageId.Version, "1.2.3") - assert.Equal(t, result.Result.IsExternalLibraryImport, true) + assert.Equal(t, result.ResolvedModule.OriginalPath, "/node_modules/pkg/index.d.ts") + assert.Equal(t, result.ResolvedModule.PackageId.Name, "pkg") + assert.Equal(t, result.ResolvedModule.PackageId.Version, "1.2.3") + assert.Equal(t, result.ResolvedModule.IsExternalLibraryImport, true) } func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 59157261e235d..80a0a250236af 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -876,10 +876,9 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { var trace []module.DiagAndArgs if p.opts.ModuleResolutionProvider != nil { var provided bool - var providedResolution *module.ProvidedModuleResolution - providedResolution, provided = p.opts.ModuleResolutionProvider.GetModuleResolution(moduleName, tspath.GetDirectoryPath(fileName), mode) - if provided { - resolvedModule = p.resolver.ResolveProvidedModule(moduleName, providedResolution) + resolvedModule, provided = p.opts.ModuleResolutionProvider.GetModuleResolution(moduleName, tspath.GetDirectoryPath(fileName), mode) + if provided && resolvedModule == nil { + resolvedModule = &module.ResolvedModule{} } if !provided { resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index e49f0d338477f..4a73978f2dc6a 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -272,29 +272,6 @@ func (r *Resolver) ResolveModuleNameFromDirectory(moduleName string, containingD return r.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) } -func (r *Resolver) ResolveProvidedModule(moduleName string, provided *ProvidedModuleResolution) *ResolvedModule { - if provided == nil { - return &ResolvedModule{} - } - externalPath := provided.ResolvedFileName - if provided.OriginalPath != "" { - externalPath = provided.OriginalPath - } - extension := tspath.TryGetExtensionFromPath(provided.ResolvedFileName) - isStandardExtension := tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) || - tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedJSExtensionsFlat) - return &ResolvedModule{ - ResolvedFileName: provided.ResolvedFileName, - OriginalPath: provided.OriginalPath, - Extension: extension, - ResolvedUsingTsExtension: tspath.IsExternalModuleNameRelative(moduleName) && - tspath.TryExtractTSExtension(moduleName) != "", - ResolvedUsingExtraExtensions: !isStandardExtension, - PackageId: provided.PackageId, - IsExternalLibraryImport: strings.Contains(externalPath, "/node_modules/"), - } -} - func (r *Resolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { traceBuilder := r.newTraceBuilder() diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index a9c3cdfa6f4b3..0d202e3362619 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -18,13 +18,7 @@ type ResolutionHost interface { type ResolutionProvider interface { Identity() uint64 - GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (resolution *ProvidedModuleResolution, found bool) -} - -type ProvidedModuleResolution struct { - ResolvedFileName string - OriginalPath string - PackageId PackageId + GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (resolution *ResolvedModule, found bool) } type ModeAwareCacheKey struct { From cf4a7b7803095c481a804bb4de165e5a5f630868 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 15 Sep 2026 13:35:36 -0700 Subject: [PATCH 04/20] Add callback as a way of overriding too --- packages/typescript/src/api/async/api.ts | 98 +++++++--- packages/typescript/src/api/async/client.ts | 8 + .../typescript/src/api/proto.generated.ts | 12 +- packages/typescript/src/api/sync/api.ts | 170 +++++++++++++----- packages/typescript/src/api/sync/client.ts | 6 + packages/typescript/src/api/syncChannel.ts | 4 + packages/typescript/test/async/api.test.ts | 73 ++++++++ packages/typescript/test/sync/api.test.ts | 73 ++++++++ tools/gen-proto/main.go | 7 +- tsc/internal/api/module_resolution.go | 170 ++++++++++++++---- tsc/internal/api/proto.go | 34 ++-- tsc/internal/api/server.go | 1 + tsc/internal/api/session.go | 39 +++- .../api/session_module_resolution_test.go | 45 ++++- tsc/internal/compiler/fileloader.go | 33 ++-- tsc/internal/compiler/filesparser.go | 1 + tsc/internal/compiler/program.go | 4 + tsc/internal/lsp/server.go | 1 + tsc/internal/module/types.go | 7 +- 19 files changed, 643 insertions(+), 143 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 964894511d31b..407e6e1f85504 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -242,9 +242,21 @@ export interface TranspileOutput { export interface ModuleResolverOptions { moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; + /** + * Resolves module names not matched by `moduleResolutions`. + * Returning `undefined` marks the module unresolved. + */ + resolveModuleName?: ResolveModuleNameCallback; } -export type CreateProgramOptions = Omit & ModuleResolverOptions; +/** + * Resolves a module name from a directory. + */ +export type ResolutionMode = ModuleKind.None | ModuleKind.CommonJS | ModuleKind.ESNext; + +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; + +export type CreateProgramOptions = Omit & ModuleResolverOptions; function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { if (input === undefined) return undefined; @@ -267,6 +279,7 @@ export class API implements FormatDiagnosticsHo private currentDirectory: string | undefined; private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; private initialized: boolean = false; + private nextModuleResolutionCallbackId = 0; private initializing: Promise | undefined; private activeSnapshots: Set = new Set(); private latestSnapshot: Snapshot | undefined; @@ -533,6 +546,22 @@ export class API implements FormatDiagnosticsHo return new ModuleResolutionSet(id, this.client); } + /** @internal */ + registerModuleResolutionCallback(callback: ResolveModuleNameCallback): { name: string; dispose: () => void; } { + const name = `resolveModuleName/${++this.nextModuleResolutionCallbackId}`; + return { + name, + dispose: this.client.registerCallback(name, params => { + const { moduleName, containingDirectory, resolutionMode } = params as { + moduleName: string; + containingDirectory: string; + resolutionMode?: ResolutionMode; + }; + return callback(moduleName, containingDirectory, resolutionMode); + }), + }; + } + /** * Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges. */ @@ -551,18 +580,28 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } - const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const { moduleResolutions, resolveModuleName, ...wireCreateProgramOptions } = createProgramOptions; const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); - const data: CreateProgramResponse = await this.client.apiRequest("createProgram", { - rootFiles, - createProgramOptions: { - ...wireCreateProgramOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }, - oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, - fileChanges, - }); + const callback = resolveModuleName ? this.registerModuleResolutionCallback(resolveModuleName) : undefined; + let data: CreateProgramResponse; + try { + data = await this.client.apiRequest("createProgram", { + rootFiles, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }, + oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, + fileChanges, + }); + } + catch (error) { + callback?.dispose(); + throw error; + } if (!data.project) { + callback?.dispose(); throw new Error("createProgram did not return a project"); } const snapshot = new Snapshot( @@ -576,6 +615,7 @@ export class API implements FormatDiagnosticsHo this.sourceFileCache.releaseSnapshot(snapshot.id); }, ); + if (callback) snapshot.addModuleResolutionCallbackDisposer(callback.dispose); const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); this.activeSnapshots.add(snapshot); @@ -615,6 +655,7 @@ type EnsureInitialized = () => Promise; // @sync: type EnsureInitialized = interface SnapshotOwner extends FormatDiagnosticsHost { updateSnapshotFrom(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Promise; + registerModuleResolutionCallback(callback: ResolveModuleNameCallback): { name: string; dispose: () => void; }; } export class InternalAPI { @@ -655,6 +696,7 @@ export class Snapshot { private onDispose: () => void; private api: SnapshotOwner; private snapshotRegistry: SnapshotObjectRegistry; + private resolverCallbackDisposers: (() => void)[] = []; readonly internal: SnapshotInternalAPI; constructor( @@ -713,12 +755,26 @@ export class Snapshot { async createModuleResolver(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Promise { this.ensureNotDisposed(); const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); - const id = await this.client.apiRequest("createModuleResolver", { - snapshot: this.id, - compilerOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }); - return new ModuleResolver(id, this.id, this.client, () => this.ensureNotDisposed()); + const callback = options?.resolveModuleName ? this.api.registerModuleResolutionCallback(options.resolveModuleName) : undefined; + try { + const id = await this.client.apiRequest("createModuleResolver", { + snapshot: this.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }); + if (callback) this.addModuleResolutionCallbackDisposer(callback.dispose); + return new ModuleResolver(id, this.id, this.client, () => this.ensureNotDisposed()); + } + catch (error) { + callback?.dispose(); + throw error; + } + } + + /** @internal */ + addModuleResolutionCallbackDisposer(dispose: () => void): void { + this.resolverCallbackDisposers.push(dispose); } [globalThis.Symbol.dispose](): void { @@ -736,6 +792,8 @@ export class Snapshot { } this.projectMap.clear(); this.snapshotRegistry.clear(); + for (const dispose of this.resolverCallbackDisposers) dispose(); + this.resolverCallbackDisposers = []; try { await this.client.apiRequest("release", { snapshot: this.id }); } @@ -771,7 +829,7 @@ export class ModuleResolver { async resolveModuleName( moduleName: string, containingDirectory: DocumentIdentifier, - resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext, + resolutionMode?: ResolutionMode, ): Promise { this.ensureSnapshotActive(); return this.client.apiRequest("resolveModuleName", { @@ -1338,7 +1396,7 @@ export class Program implements FormatDiagnosticsHost { async getResolvedModule( file: DocumentIdentifier, moduleName: string, - mode: ModuleKind, + mode: ResolutionMode, ): Promise { const result = await this.client.apiRequest("getResolvedModule", { snapshot: this.snapshotId, @@ -1366,7 +1424,7 @@ export class Program implements FormatDiagnosticsHost { async getResolvedTypeReferenceDirective( file: DocumentIdentifier, typeDirectiveName: string, - mode: ModuleKind, + mode: ResolutionMode, ): Promise { const result = await this.client.apiRequest("getResolvedTypeReferenceDirective", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/async/client.ts b/packages/typescript/src/api/async/client.ts index 03809285c9b4d..ff7b9c2f60f8f 100644 --- a/packages/typescript/src/api/async/client.ts +++ b/packages/typescript/src/api/async/client.ts @@ -199,6 +199,14 @@ export class Client { return result; } + registerCallback(name: string, callback: (params: unknown) => unknown | Promise): () => void { + if (!this.connection) { + throw new Error("Connection not established"); + } + const disposable = this.connection.onRequest(new RequestType(name), callback); + return () => disposable.dispose(); + } + private async doBatch(): Promise { this.nextBatch = undefined; if (!this.batchedRequests.length) return; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 97a70ca47b91e..7a001c02eff4f 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -296,6 +296,7 @@ export interface CreateModuleResolverParams { snapshot: number; compilerOptions: CompilerOptions; moduleResolutions?: ModuleResolutionSource | undefined; + resolveModuleNameCallback?: string | undefined; } export interface ResolveModuleNameParams { @@ -303,7 +304,7 @@ export interface ResolveModuleNameParams { resolver: number; moduleName: string; containingDirectory: DocumentIdentifier; - resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; + resolutionMode?: ResolutionMode | undefined; } export interface ResolveModuleNameResult { @@ -525,7 +526,7 @@ export interface GetResolvedModuleParams { project: string; file: DocumentIdentifier; moduleName: string; - mode: ModuleKind; + mode: ResolutionMode; } export interface ResolvedModule { @@ -551,7 +552,7 @@ export interface GetResolvedTypeReferenceDirectiveParams { project: string; file: DocumentIdentifier; typeDirectiveName: string; - mode: ModuleKind; + mode: ResolutionMode; } export interface ResolvedTypeReferenceDirective { @@ -1407,6 +1408,7 @@ export interface CreateProgramOptions { projectReferences?: ProjectReference[] | undefined; configFileParsingDiagnostics?: DiagnosticResponse[] | undefined; moduleResolutions?: ModuleResolutionSource | undefined; + resolveModuleNameCallback?: string | undefined; } export interface CreateProgramOldProgramParams { @@ -1627,7 +1629,7 @@ export interface ProjectFileChanges { export interface ModuleResolutionEntry { moduleName: string; containingDirectory?: DocumentIdentifier | undefined; - resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; + resolutionMode?: ResolutionMode | undefined; result: ProvidedModuleResolution; } @@ -1642,3 +1644,5 @@ export interface ProvidedModuleResolution { originalPath?: DocumentIdentifier | undefined; packageId?: PackageId | undefined; } + +export type ResolutionMode = ModuleKind.None | ModuleKind.CommonJS | ModuleKind.ESNext; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 80375d3efbec3..5289aa90c31d4 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -259,9 +259,21 @@ export interface TranspileOutput { export interface ModuleResolverOptions { moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; + /** + * Resolves module names not matched by `moduleResolutions`. + * Returning `undefined` marks the module unresolved. + */ + resolveModuleName?: ResolveModuleNameCallback; } -export type CreateProgramOptions = Omit & ModuleResolverOptions; +/** + * Resolves a module name from a directory. + */ +export type ResolutionMode = ModuleKind.None | ModuleKind.CommonJS | ModuleKind.ESNext; + +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; + +export type CreateProgramOptions = Omit & ModuleResolverOptions; function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { if (input === undefined) return undefined; @@ -286,6 +298,7 @@ export class API implements FormatDiagnosticsHo private currentDirectory: string | undefined; private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; private initialized: boolean = false; + private nextModuleResolutionCallbackId = 0; private initializing: void | undefined; private activeSnapshots: Set = new Set(); private latestSnapshot: Snapshot | undefined; @@ -909,6 +922,22 @@ export class API implements FormatDiagnosticsHo ); } + /** @internal */ + registerModuleResolutionCallback(callback: ResolveModuleNameCallback): { name: string; dispose: () => void; } { + const name = `resolveModuleName/${++this.nextModuleResolutionCallbackId}`; + return { + name, + dispose: this.client.registerCallback(name, params => { + const { moduleName, containingDirectory, resolutionMode } = params as { + moduleName: string; + containingDirectory: string; + resolutionMode?: ResolutionMode; + }; + return callback(moduleName, containingDirectory, resolutionMode); + }), + }; + } + /** * Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges. */ @@ -930,18 +959,28 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } - const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const { moduleResolutions, resolveModuleName, ...wireCreateProgramOptions } = createProgramOptions; const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); - const data: CreateProgramResponse = owner.client.apiRequest("createProgram", { - rootFiles, - createProgramOptions: { - ...wireCreateProgramOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }, - oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, - fileChanges, - }); + const callback = resolveModuleName ? owner.registerModuleResolutionCallback(resolveModuleName) : undefined; + let data: CreateProgramResponse; + try { + data = owner.client.apiRequest("createProgram", { + rootFiles, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }, + oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, + fileChanges, + }); + } + catch (error) { + callback?.dispose(); + throw error; + } if (!data.project) { + callback?.dispose(); throw new Error("createProgram did not return a project"); } const snapshot = new Snapshot( @@ -955,6 +994,7 @@ export class API implements FormatDiagnosticsHo owner.sourceFileCache.releaseSnapshot(snapshot.id); }, ); + if (callback) snapshot.addModuleResolutionCallbackDisposer(callback.dispose); const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); owner.activeSnapshots.add(snapshot); @@ -970,18 +1010,28 @@ export class API implements FormatDiagnosticsHo throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); } - const { moduleResolutions, ...wireCreateProgramOptions } = createProgramOptions; + const { moduleResolutions, resolveModuleName, ...wireCreateProgramOptions } = createProgramOptions; const moduleResolutionSource = toModuleResolutionSource(moduleResolutions); - const data: CreateProgramResponse = yield* apiRequest("createProgram", { - rootFiles, - createProgramOptions: { - ...wireCreateProgramOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }, - oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, - fileChanges, - }); + const callback = resolveModuleName ? owner.registerModuleResolutionCallback(resolveModuleName) : undefined; + let data: CreateProgramResponse; + try { + data = yield* apiRequest("createProgram", { + rootFiles, + createProgramOptions: { + ...wireCreateProgramOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }, + oldProgram: oldProgram ? { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } : undefined, + fileChanges, + }); + } + catch (error) { + callback?.dispose(); + throw error; + } if (!data.project) { + callback?.dispose(); throw new Error("createProgram did not return a project"); } const snapshot = new Snapshot( @@ -995,6 +1045,7 @@ export class API implements FormatDiagnosticsHo owner.sourceFileCache.releaseSnapshot(snapshot.id); }, ); + if (callback) snapshot.addModuleResolutionCallbackDisposer(callback.dispose); const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); owner.activeSnapshots.add(snapshot); @@ -1054,6 +1105,7 @@ interface SnapshotOwner extends FormatDiagnosticsHost { (baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Snapshot; gen(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Generator; }; + registerModuleResolutionCallback(callback: ResolveModuleNameCallback): { name: string; dispose: () => void; }; } export class InternalAPI { @@ -1138,6 +1190,7 @@ export class Snapshot { private onDispose: () => void; private api: SnapshotOwner; private snapshotRegistry: SnapshotObjectRegistry; + private resolverCallbackDisposers: (() => void)[] = []; readonly internal: SnapshotInternalAPI; constructor( @@ -1237,26 +1290,49 @@ export class Snapshot { function (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver { owner.ensureNotDisposed(); const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); - const id = owner.client.apiRequest("createModuleResolver", { - snapshot: owner.id, - compilerOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }); - return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + const callback = options?.resolveModuleName ? owner.api.registerModuleResolutionCallback(options.resolveModuleName) : undefined; + try { + const id = owner.client.apiRequest("createModuleResolver", { + snapshot: owner.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }); + if (callback) owner.addModuleResolutionCallbackDisposer(callback.dispose); + return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + } + catch (error) { + callback?.dispose(); + throw error; + } }, function* (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator { owner.ensureNotDisposed(); const moduleResolutionSource = toModuleResolutionSource(options?.moduleResolutions); - const id = yield* apiRequest("createModuleResolver", { - snapshot: owner.id, - compilerOptions, - ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), - }); - return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + const callback = options?.resolveModuleName ? owner.api.registerModuleResolutionCallback(options.resolveModuleName) : undefined; + try { + const id = yield* apiRequest("createModuleResolver", { + snapshot: owner.id, + compilerOptions, + ...(moduleResolutionSource ? { moduleResolutions: moduleResolutionSource } : {}), + ...(callback ? { resolveModuleNameCallback: callback.name } : {}), + }); + if (callback) owner.addModuleResolutionCallbackDisposer(callback.dispose); + return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); + } + catch (error) { + callback?.dispose(); + throw error; + } }, ); } + /** @internal */ + addModuleResolutionCallbackDisposer(dispose: () => void): void { + this.resolverCallbackDisposers.push(dispose); + } + [globalThis.Symbol.dispose](): void { void this.dispose(); } @@ -1293,6 +1369,8 @@ export class Snapshot { } owner.projectMap.clear(); owner.snapshotRegistry.clear(); + for (const dispose of owner.resolverCallbackDisposers) dispose(); + owner.resolverCallbackDisposers = []; try { owner.client.apiRequest("release", { snapshot: owner.id }); } @@ -1308,6 +1386,8 @@ export class Snapshot { } owner.projectMap.clear(); owner.snapshotRegistry.clear(); + for (const dispose of owner.resolverCallbackDisposers) dispose(); + owner.resolverCallbackDisposers = []; try { yield* apiRequest("release", { snapshot: owner.id }); } @@ -1343,14 +1423,14 @@ export class ModuleResolver { } get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ResolveModuleNameResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator; + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ResolveModuleNameResult { + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): ResolveModuleNameResult { owner.ensureSnapshotActive(); return owner.client.apiRequest("resolveModuleName", { snapshot: owner.snapshotId, @@ -1360,7 +1440,7 @@ export class ModuleResolver { ...(resolutionMode !== undefined ? { resolutionMode } : {}), }); }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): Generator { + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): Generator { owner.ensureSnapshotActive(); return yield* apiRequest("resolveModuleName", { snapshot: owner.snapshotId, @@ -2465,14 +2545,14 @@ export class Program implements FormatDiagnosticsHost { } get getResolvedModule(): { - (file: DocumentIdentifier, moduleName: string, mode: ModuleKind): ResolvedModule | undefined; - gen(file: DocumentIdentifier, moduleName: string, mode: ModuleKind): Generator; + (file: DocumentIdentifier, moduleName: string, mode: ResolutionMode): ResolvedModule | undefined; + gen(file: DocumentIdentifier, moduleName: string, mode: ResolutionMode): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getResolvedModule", - function (file: DocumentIdentifier, moduleName: string, mode: ModuleKind): ResolvedModule | undefined { + function (file: DocumentIdentifier, moduleName: string, mode: ResolutionMode): ResolvedModule | undefined { const result = owner.client.apiRequest("getResolvedModule", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2482,7 +2562,7 @@ export class Program implements FormatDiagnosticsHost { }); return result ?? undefined; }, - function* (file: DocumentIdentifier, moduleName: string, mode: ModuleKind): Generator { + function* (file: DocumentIdentifier, moduleName: string, mode: ResolutionMode): Generator { const result = yield* apiRequest("getResolvedModule", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2525,14 +2605,14 @@ export class Program implements FormatDiagnosticsHost { } get getResolvedTypeReferenceDirective(): { - (file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): ResolvedTypeReferenceDirective | undefined; - gen(file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): Generator; + (file: DocumentIdentifier, typeDirectiveName: string, mode: ResolutionMode): ResolvedTypeReferenceDirective | undefined; + gen(file: DocumentIdentifier, typeDirectiveName: string, mode: ResolutionMode): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getResolvedTypeReferenceDirective", - function (file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): ResolvedTypeReferenceDirective | undefined { + function (file: DocumentIdentifier, typeDirectiveName: string, mode: ResolutionMode): ResolvedTypeReferenceDirective | undefined { const result = owner.client.apiRequest("getResolvedTypeReferenceDirective", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2542,7 +2622,7 @@ export class Program implements FormatDiagnosticsHost { }); return result ?? undefined; }, - function* (file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): Generator { + function* (file: DocumentIdentifier, typeDirectiveName: string, mode: ResolutionMode): Generator { const result = yield* apiRequest("getResolvedTypeReferenceDirective", { snapshot: owner.snapshotId, project: owner.project.id, diff --git a/packages/typescript/src/api/sync/client.ts b/packages/typescript/src/api/sync/client.ts index b174784d7006e..d3c3ccc64b9fc 100644 --- a/packages/typescript/src/api/sync/client.ts +++ b/packages/typescript/src/api/sync/client.ts @@ -98,9 +98,15 @@ export class Client { if (result.length) { return JSON.parse(result) as APIMethodInfo[K]["result"]; } + return undefined as APIMethodInfo[K]["result"]; } + registerCallback(name: string, callback: (params: unknown) => unknown): () => void { + this.channel.registerCallback(name, (_, payload) => JSON.stringify(callback(JSON.parse(payload))) ?? ""); + return () => this.channel.unregisterCallback(name); + } + batchRequests(requests: readonly APIRequest[]): BatchRequestsResponse { const params: BatchRequestsParams = { requests }; if (this.maxResponseBytesPerPage !== undefined) { diff --git a/packages/typescript/src/api/syncChannel.ts b/packages/typescript/src/api/syncChannel.ts index 122203c85e38e..8814e905a0949 100644 --- a/packages/typescript/src/api/syncChannel.ts +++ b/packages/typescript/src/api/syncChannel.ts @@ -244,6 +244,10 @@ export class SyncRpcChannel { this.callbacks.set(name, callback); } + unregisterCallback(name: string): void { + this.callbacks.delete(name); + } + /** Kill the child process and release resources. */ close(): void { try { diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 72d4b099ba703..44c38bb38727e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -490,9 +490,11 @@ import "missing";`, test("module resolver and provided resolution sets", async () => { await using api = spawnAPI({ "/src/main.ts": `import { value } from "pkg"; export { value };`, + "/src/callback-main.ts": `import { value as provided } from "pkg"; import { value as callback } from "callback"; export { provided, callback };`, "/src/extension.ts": `import { value } from "./dep.ts"; value;`, "/src/dep.ts": `export const value = 1;`, "/provided.d.ts": `export declare const value: string;`, + "/callback.d.ts": `export declare const value: boolean;`, "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, "/node_modules/pkg/index.d.ts": `export declare const value: number;`, }); @@ -528,6 +530,76 @@ import "missing";`, ); assert.equal((await overriddenResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); + const callbackCalls: unknown[][] = []; + const callbackResolver = await snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { + fallback: "resolve", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }, + resolveModuleName: (moduleName, containingDirectory, resolutionMode) => { + callbackCalls.push([moduleName, containingDirectory, resolutionMode]); + return moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined; + }, + }); + assert.equal( + (await callbackResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/provided.d.ts", + ); + assert.deepEqual(callbackCalls, []); + assert.equal( + (await callbackResolver.resolveModuleName("callback", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/callback.d.ts", + ); + assert.deepEqual(callbackCalls, [["callback", "/src", ModuleKind.ESNext]]); + assert.equal((await callbackResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); + assert.deepEqual(callbackCalls.at(-1), ["missing", "/src", undefined]); + + const noSpecCallbackResolver = await snapshot.createModuleResolver(compilerOptions, { + resolveModuleName: moduleName => moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined, + }); + assert.equal( + (await noSpecCallbackResolver.resolveModuleName("callback", "/src")).resolvedModule?.resolvedFileName, + "/callback.d.ts", + ); + + let authoritativeCallbackCalled = false; + const authoritativeResolver = await snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { fallback: "unresolved", entries: [] }, + resolveModuleName: () => { + authoritativeCallbackCalled = true; + return { resolvedFileName: "/callback.d.ts" }; + }, + }); + assert.equal((await authoritativeResolver.resolveModuleName("callback", "/src")).resolvedModule, undefined); + assert.equal(authoritativeCallbackCalled, false); + + const programCallbackCalls: string[] = []; + const callbackProgram = await api.createProgram( + ["/src/callback-main.ts"], + { + compilerOptions: { ...compilerOptions, noLib: true }, + moduleResolutions: { + fallback: "resolve", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }, + resolveModuleName: moduleName => { + programCallbackCalls.push(moduleName); + return moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined; + }, + }, + ); + assert.deepEqual( + [...await callbackProgram.getSourceFileNames()].sort(), + ["/callback.d.ts", "/provided.d.ts", "/src/callback-main.ts"], + ); + assert.deepEqual(programCallbackCalls, ["callback"]); + const program = await api.createProgram( ["/src/main.ts"], { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, @@ -559,6 +631,7 @@ import "missing";`, await assert.rejects(createFromDisposedSet, /ModuleResolutionSet is disposed/); // @sync: assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); await program.dispose(); + await callbackProgram.dispose(); await extensionProgram.dispose(); await snapshot.dispose(); const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index b459acffdf082..a22efce467b56 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -479,9 +479,11 @@ import "missing";`, test("module resolver and provided resolution sets", () => { using api = spawnAPI({ "/src/main.ts": `import { value } from "pkg"; export { value };`, + "/src/callback-main.ts": `import { value as provided } from "pkg"; import { value as callback } from "callback"; export { provided, callback };`, "/src/extension.ts": `import { value } from "./dep.ts"; value;`, "/src/dep.ts": `export const value = 1;`, "/provided.d.ts": `export declare const value: string;`, + "/callback.d.ts": `export declare const value: boolean;`, "/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts"}`, "/node_modules/pkg/index.d.ts": `export declare const value: number;`, }); @@ -517,6 +519,76 @@ import "missing";`, ); assert.equal((overriddenResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); + const callbackCalls: unknown[][] = []; + const callbackResolver = snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { + fallback: "resolve", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }, + resolveModuleName: (moduleName, containingDirectory, resolutionMode) => { + callbackCalls.push([moduleName, containingDirectory, resolutionMode]); + return moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined; + }, + }); + assert.equal( + (callbackResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/provided.d.ts", + ); + assert.deepEqual(callbackCalls, []); + assert.equal( + (callbackResolver.resolveModuleName("callback", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/callback.d.ts", + ); + assert.deepEqual(callbackCalls, [["callback", "/src", ModuleKind.ESNext]]); + assert.equal((callbackResolver.resolveModuleName("missing", "/src")).resolvedModule, undefined); + assert.deepEqual(callbackCalls.at(-1), ["missing", "/src", undefined]); + + const noSpecCallbackResolver = snapshot.createModuleResolver(compilerOptions, { + resolveModuleName: moduleName => moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined, + }); + assert.equal( + (noSpecCallbackResolver.resolveModuleName("callback", "/src")).resolvedModule?.resolvedFileName, + "/callback.d.ts", + ); + + let authoritativeCallbackCalled = false; + const authoritativeResolver = snapshot.createModuleResolver(compilerOptions, { + moduleResolutions: { fallback: "unresolved", entries: [] }, + resolveModuleName: () => { + authoritativeCallbackCalled = true; + return { resolvedFileName: "/callback.d.ts" }; + }, + }); + assert.equal((authoritativeResolver.resolveModuleName("callback", "/src")).resolvedModule, undefined); + assert.equal(authoritativeCallbackCalled, false); + + const programCallbackCalls: string[] = []; + const callbackProgram = api.createProgram( + ["/src/callback-main.ts"], + { + compilerOptions: { ...compilerOptions, noLib: true }, + moduleResolutions: { + fallback: "resolve", + entries: [{ + moduleName: "pkg", + result: { resolvedFileName: "/provided.d.ts" }, + }], + }, + resolveModuleName: moduleName => { + programCallbackCalls.push(moduleName); + return moduleName === "callback" ? { resolvedFileName: "/callback.d.ts" } : undefined; + }, + }, + ); + assert.deepEqual( + [...callbackProgram.getSourceFileNames()].sort(), + ["/callback.d.ts", "/provided.d.ts", "/src/callback-main.ts"], + ); + assert.deepEqual(programCallbackCalls, ["callback"]); + const program = api.createProgram( ["/src/main.ts"], { compilerOptions: { ...compilerOptions, noLib: true }, moduleResolutions: set }, @@ -548,6 +620,7 @@ import "missing";`, assert.throws(createFromDisposedSet, /ModuleResolutionSet is disposed/); program.dispose(); + callbackProgram.dispose(); extensionProgram.dispose(); snapshot.dispose(); const resolveAfterSnapshotDisposal = () => overriddenResolver.resolveModuleName("pkg", "/src"); diff --git a/tools/gen-proto/main.go b/tools/gen-proto/main.go index 0ad72408e1731..5fd9cfc80636f 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -353,6 +353,7 @@ type typeRenderer struct { docs map[types.Object]string packages map[string]*packages.Package documentIdentifier *types.TypeName + resolutionMode bool } func newTypeRenderer(apiPackage *packages.Package) *typeRenderer { @@ -493,8 +494,9 @@ func (r *typeRenderer) namedType(named *types.Named) string { r.documentIdentifier = obj return "DocumentIdentifier" case r.apiPackagePath + ".ResolutionMode": + r.resolutionMode = true r.importType("ModuleKind", "#enums/moduleKind") - return "ModuleKind.CommonJS | ModuleKind.ESNext" + return "ResolutionMode" case "github.com/microsoft/TypeScript/tsc/internal/packagejson.JSONValue": return "unknown" case "github.com/microsoft/TypeScript/tsc/internal/json.Value": @@ -627,6 +629,9 @@ func (r *typeRenderer) declarations() (string, error) { } out.WriteString("}\n\n") } + if r.resolutionMode { + out.WriteString("export type ResolutionMode = ModuleKind.None | ModuleKind.CommonJS | ModuleKind.ESNext;\n\n") + } return strings.TrimRight(out.String(), "\n") + "\n", nil } diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 67c7a8bbce789..59aaf08492c8b 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -1,10 +1,13 @@ package api import ( + "context" "fmt" "strings" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/ipc" + "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/tspath" @@ -26,11 +29,58 @@ type providedModuleResolutions struct { useCaseSensitive bool } +type callbackModuleResolutionProvider struct { + identity uint64 + base *providedModuleResolutions + conn ipc.Conn + ctx context.Context + callback string + currentDirectory string +} + +func (p *callbackModuleResolutionProvider) Identity() uint64 { + return p.identity +} + +func (p *callbackModuleResolutionProvider) ResolveModuleName( + moduleName string, + containingDirectory string, + resolutionMode core.ResolutionMode, + _ func() *module.ResolvedModule, +) (*module.ResolvedModule, error) { + if p.base != nil { + if result, found := p.base.lookup(moduleName, containingDirectory, resolutionMode); found { + return result, nil + } + if !p.base.fallbackToResolution { + return nil, nil + } + } + params := &ResolveModuleNameCallbackParams{ + ModuleName: moduleName, + ContainingDirectory: containingDirectory, + } + mode := ResolutionMode(resolutionMode) + params.ResolutionMode = &mode + callbackResult, err := p.conn.Call(p.ctx, p.callback, params) + if err != nil { + return nil, fmt.Errorf("resolveModuleName callback failed: %w", err) + } + if len(callbackResult) == 0 || string(callbackResult) == "null" { + return nil, nil + } + var providedResolution ProvidedModuleResolution + if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { + return nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) + } + return providedModuleResolutionToResolvedModule(moduleName, &providedResolution, p.currentDirectory), nil +} + func (p *providedModuleResolutions) Identity() uint64 { return p.identity } -func (p *providedModuleResolutions) GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { +func (p *providedModuleResolutions) lookup(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitive) keys := [...]moduleResolutionMatchKey{ {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, @@ -43,10 +93,22 @@ func (p *providedModuleResolutions) GetModuleResolution(moduleName string, conta return result, true } } + return nil, false +} + +func (p *providedModuleResolutions) ResolveModuleName( + moduleName string, + containingDirectory string, + resolutionMode core.ResolutionMode, + fallback func() *module.ResolvedModule, +) (*module.ResolvedModule, error) { + if result, found := p.lookup(moduleName, containingDirectory, resolutionMode); found { + return result, nil + } if p.fallbackToResolution { - return nil, false + return fallback(), nil } - return nil, true + return nil, nil } func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { @@ -89,7 +151,7 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu } if entry.ResolutionMode != nil { mode := core.ModuleKind(*entry.ResolutionMode) - if mode != core.ModuleKindCommonJS && mode != core.ModuleKindESNext { + if mode != core.ModuleKindNone && mode != core.ModuleKindCommonJS && mode != core.ModuleKindESNext { return nil, fmt.Errorf("%w: module resolution entry %d has invalid resolutionMode %s", ErrClientError, i, mode.String()) } key.mode = mode @@ -99,46 +161,51 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu return nil, fmt.Errorf("%w: duplicate module resolution entry for %q", ErrClientError, entry.ModuleName) } - var provided *module.ResolvedModule - if entry.Result.ResolvedFileName != nil { - provided = &module.ResolvedModule{ - ResolvedFileName: tspath.GetNormalizedAbsolutePath(entry.Result.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), - } - if entry.Result.OriginalPath != nil { - provided.OriginalPath = tspath.GetNormalizedAbsolutePath(entry.Result.OriginalPath.ToAbsoluteFileName(currentDirectory), currentDirectory) - } - if entry.Result.PackageID != nil { - provided.PackageId = module.PackageId{ - Name: entry.Result.PackageID.Name, - SubModuleName: entry.Result.PackageID.SubModuleName, - Version: entry.Result.PackageID.Version, - PeerDependencies: entry.Result.PackageID.PeerDependencies, - } - } - externalPath := provided.ResolvedFileName - if provided.OriginalPath != "" { - externalPath = provided.OriginalPath - } - provided.Extension = tspath.TryGetExtensionFromPath(provided.ResolvedFileName) - provided.ResolvedUsingTsExtension = tspath.IsExternalModuleNameRelative(entry.ModuleName) && - tspath.TryExtractTSExtension(entry.ModuleName) != "" - provided.ResolvedUsingExtraExtensions = !tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && - !tspath.FileExtensionIsOneOf(provided.ResolvedFileName, tspath.SupportedJSExtensionsFlat) - provided.IsExternalLibraryImport = strings.Contains(externalPath, "/node_modules/") - } + provided := providedModuleResolutionToResolvedModule(entry.ModuleName, entry.Result, currentDirectory) provider.entries[key] = provided } return provider, nil } +func providedModuleResolutionToResolvedModule(moduleName string, provided *ProvidedModuleResolution, currentDirectory string) *module.ResolvedModule { + if provided == nil || provided.ResolvedFileName == nil { + return nil + } + result := &module.ResolvedModule{ + ResolvedFileName: tspath.GetNormalizedAbsolutePath(provided.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), + } + if provided.OriginalPath != nil { + result.OriginalPath = tspath.GetNormalizedAbsolutePath(provided.OriginalPath.ToAbsoluteFileName(currentDirectory), currentDirectory) + } + if provided.PackageID != nil { + result.PackageId = module.PackageId{ + Name: provided.PackageID.Name, + SubModuleName: provided.PackageID.SubModuleName, + Version: provided.PackageID.Version, + PeerDependencies: provided.PackageID.PeerDependencies, + } + } + externalPath := result.ResolvedFileName + if result.OriginalPath != "" { + externalPath = result.OriginalPath + } + result.Extension = tspath.TryGetExtensionFromPath(result.ResolvedFileName) + result.ResolvedUsingTsExtension = tspath.IsExternalModuleNameRelative(moduleName) && + tspath.TryExtractTSExtension(moduleName) != "" + result.ResolvedUsingExtraExtensions = !tspath.FileExtensionIsOneOf(result.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && + !tspath.FileExtensionIsOneOf(result.ResolvedFileName, tspath.SupportedJSExtensionsFlat) + result.IsExternalLibraryImport = strings.Contains(externalPath, "/node_modules/") + return result +} + func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { return core.Map(trace, func(entry module.DiagAndArgs) string { return entry.Message.Localize(locale.Default, entry.Args...) }) } -func (s *Session) resolveModuleResolutionSource(source *ModuleResolutionSource) (module.ResolutionProvider, error) { +func (s *Session) resolveModuleResolutionSource(source *ModuleResolutionSource) (*providedModuleResolutions, error) { if source == nil { return nil, nil } @@ -209,8 +276,9 @@ func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) } id := ModuleResolverID(s.nextModuleResolverID.Add(1)) data := &moduleResolverData{ - resolver: module.NewResolver(sd.snapshot, ¶ms.CompilerOptions, "", "", sd.snapshot.ContentMapperExtensions()), - provider: provider, + resolver: module.NewResolver(sd.snapshot, ¶ms.CompilerOptions, "", "", sd.snapshot.ContentMapperExtensions()), + provider: provider, + resolveModuleNameCallback: params.ResolveModuleNameCallback, } sd.moduleResolversMu.Lock() if sd.moduleResolvers == nil { @@ -221,7 +289,7 @@ func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) return id, nil } -func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*ResolveModuleNameResult, error) { +func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveModuleNameParams) (*ResolveModuleNameResult, error) { if params.ModuleName == "" { return nil, fmt.Errorf("%w: moduleName is empty", ErrClientError) } @@ -239,7 +307,7 @@ func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*Res mode := core.ResolutionModeNone if params.ResolutionMode != nil { mode = core.ModuleKind(*params.ResolutionMode) - if mode != core.ResolutionModeCommonJS && mode != core.ResolutionModeESM { + if mode != core.ResolutionModeNone && mode != core.ResolutionModeCommonJS && mode != core.ResolutionModeESM { return nil, fmt.Errorf("%w: invalid resolutionMode %s", ErrClientError, mode.String()) } } @@ -249,10 +317,34 @@ func (s *Session) handleResolveModuleName(params *ResolveModuleNameParams) (*Res var trace []module.DiagAndArgs var provided bool if data.provider != nil { - result, provided = data.provider.GetModuleResolution(params.ModuleName, containingDirectory, mode) + result, provided = data.provider.lookup(params.ModuleName, containingDirectory, mode) } - if !provided { - result, trace = data.resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) + if !provided && (data.provider == nil || data.provider.fallbackToResolution) { + if data.resolveModuleNameCallback != "" { + if s.conn == nil { + return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) + } + callbackParams := &ResolveModuleNameCallbackParams{ + ModuleName: params.ModuleName, + ContainingDirectory: containingDirectory, + } + if params.ResolutionMode != nil { + callbackParams.ResolutionMode = params.ResolutionMode + } + callbackResult, err := s.conn.Call(ctx, data.resolveModuleNameCallback, callbackParams) + if err != nil { + return nil, fmt.Errorf("resolveModuleName callback failed: %w", err) + } + if len(callbackResult) != 0 && string(callbackResult) != "null" { + var providedResolution ProvidedModuleResolution + if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { + return nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) + } + result = providedModuleResolutionToResolvedModule(params.ModuleName, &providedResolution, s.currentDirectory()) + } + } else { + result, trace = data.resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) + } } return &ResolveModuleNameResult{ ResolvedModule: newResolvedModuleResponse(result), diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index c7d4517f01b51..6fc429acb2abf 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -413,6 +413,7 @@ type CreateProgramOptions struct { ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` + ResolveModuleNameCallback string `json:"resolveModuleNameCallback,omitempty"` } type ( @@ -457,9 +458,10 @@ type ReleaseModuleResolutionSetParams struct { } type CreateModuleResolverParams struct { - Snapshot SnapshotID `json:"snapshot"` - CompilerOptions core.CompilerOptions `json:"compilerOptions"` - ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` + Snapshot SnapshotID `json:"snapshot"` + CompilerOptions core.CompilerOptions `json:"compilerOptions"` + ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` + ResolveModuleNameCallback string `json:"resolveModuleNameCallback,omitempty"` } type ResolveModuleNameParams struct { @@ -470,6 +472,12 @@ type ResolveModuleNameParams struct { ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` } +type ResolveModuleNameCallbackParams struct { + ModuleName string `json:"moduleName"` + ContainingDirectory string `json:"containingDirectory"` + ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` +} + type ResolveModuleNameResult struct { ResolvedModule *ResolvedModule `json:"resolvedModule,omitempty"` // Trace is provided when compilerOptions.traceResolution is true. @@ -1191,11 +1199,11 @@ type GetSourceFileNamesParams struct { } type GetResolvedModuleParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project ProjectID `json:"project"` - File DocumentIdentifier `json:"file"` - ModuleName string `json:"moduleName"` - Mode core.ResolutionMode `json:"mode"` + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + File DocumentIdentifier `json:"file"` + ModuleName string `json:"moduleName"` + Mode ResolutionMode `json:"mode"` } type GetResolvedModuleFromModuleSpecifierParams struct { @@ -1206,11 +1214,11 @@ type GetResolvedModuleFromModuleSpecifierParams struct { } type GetResolvedTypeReferenceDirectiveParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project ProjectID `json:"project"` - File DocumentIdentifier `json:"file"` - TypeDirectiveName string `json:"typeDirectiveName"` - Mode core.ResolutionMode `json:"mode"` + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + File DocumentIdentifier `json:"file"` + TypeDirectiveName string `json:"typeDirectiveName"` + Mode ResolutionMode `json:"mode"` } type GetResolvedTypeReferenceDirectiveFromReferenceParams struct { diff --git a/tsc/internal/api/server.go b/tsc/internal/api/server.go index d5e55fc74be8f..d63397e1d0e14 100644 --- a/tsc/internal/api/server.go +++ b/tsc/internal/api/server.go @@ -126,6 +126,7 @@ func (s *StdioServer) Run(ctx context.Context) error { if callbackFS != nil { callbackFS.SetConnection(ctx, conn) } + session.SetConnection(conn) return conn.Run(ctx) } diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index fe5d7454e383c..9ae122a84fed8 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -73,8 +73,9 @@ type snapshotData struct { } type moduleResolverData struct { - resolver *module.Resolver - provider module.ResolutionProvider + resolver *module.Resolver + provider *providedModuleResolutions + resolveModuleNameCallback string } // projectRegistryData holds per-project type and signature registries. @@ -447,6 +448,7 @@ type Session struct { nextModuleResolutionSetID atomic.Uint64 nextModuleResolutionIdentity atomic.Uint64 nextModuleResolverID atomic.Uint64 + conn ipc.Conn cpuProfiler pprof.CPUProfiler } @@ -506,6 +508,10 @@ func (s *Session) ID() string { return s.id } +func (s *Session) SetConnection(conn ipc.Conn) { + s.conn = conn +} + func (s *Session) currentDirectory() string { return s.snapshotHost.GetCurrentDirectory() } @@ -791,7 +797,7 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. case string(MethodCreateModuleResolver): return s.handleCreateModuleResolver(parsed.(*CreateModuleResolverParams)) case string(MethodResolveModuleName): - return s.handleResolveModuleName(parsed.(*ResolveModuleNameParams)) + return s.handleResolveModuleName(ctx, parsed.(*ResolveModuleNameParams)) case string(MethodParseConfigFile): return s.handleParseConfigFile(ctx, parsed.(*ParseConfigFileParams)) case string(MethodTranspileModule): @@ -1430,10 +1436,27 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram if params.FileChanges != nil && params.OldProgram == nil { return nil, fmt.Errorf("%w: fileChanges requires an oldProgram", ErrClientError) } - moduleResolutionProvider, err := s.resolveModuleResolutionSource(params.CreateProgramOptions.ModuleResolutions) + providedModuleResolutions, err := s.resolveModuleResolutionSource(params.CreateProgramOptions.ModuleResolutions) if err != nil { return nil, err } + var moduleResolutionProvider module.ResolutionProvider + if providedModuleResolutions != nil { + moduleResolutionProvider = providedModuleResolutions + } + if callback := params.CreateProgramOptions.ResolveModuleNameCallback; callback != "" { + if s.conn == nil { + return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) + } + moduleResolutionProvider = &callbackModuleResolutionProvider{ + identity: s.nextModuleResolutionIdentity.Add(1), + base: providedModuleResolutions, + conn: s.conn, + ctx: ctx, + callback: callback, + currentDirectory: s.currentDirectory(), + } + } rootFileNames := make([]string, len(params.RootFiles)) for i, rootFile := range params.RootFiles { @@ -1490,6 +1513,10 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram snapshot.Deref() return nil, fmt.Errorf("%w: failed to create synthetic project", ErrClientError) } + if err := project.Program.ModuleResolutionError(); err != nil { + snapshot.Deref() + return nil, err + } sd.snapshot = snapshot handle, _ := s.registerSnapshotData(sd, false) @@ -1851,7 +1878,7 @@ func (s *Session) handleGetResolvedModule(ctx context.Context, params *GetResolv if err != nil { return nil, err } - return newResolvedModuleResponse(program.GetResolvedModule(sourceFile, params.ModuleName, params.Mode)), nil + return newResolvedModuleResponse(program.GetResolvedModule(sourceFile, params.ModuleName, core.ResolutionMode(params.Mode))), nil } // @gen-proto-nullable @@ -1899,7 +1926,7 @@ func (s *Session) handleGetResolvedTypeReferenceDirective(ctx context.Context, p if err != nil { return nil, err } - return newResolvedTypeReferenceDirectiveResponse(program.GetResolvedTypeReferenceDirective(sourceFile, params.TypeDirectiveName, params.Mode)), nil + return newResolvedTypeReferenceDirectiveResponse(program.GetResolvedTypeReferenceDirective(sourceFile, params.TypeDirectiveName, core.ResolutionMode(params.Mode))), nil } // @gen-proto-nullable diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index b88616d67f3a7..0c49ccfd5e17e 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -2,13 +2,32 @@ package api import ( "context" + "errors" "testing" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) +type failingModuleResolutionConn struct { + calls int +} + +func (c *failingModuleResolutionConn) Run(context.Context) error { + return nil +} + +func (c *failingModuleResolutionConn) Call(context.Context, string, any) (json.Value, error) { + c.calls++ + return nil, errors.New("callback error") +} + +func (c *failingModuleResolutionConn) Notify(context.Context, string, any) error { + return nil +} + func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { t.Parallel() @@ -32,7 +51,7 @@ func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { }, }) assert.NilError(t, err) - result, resolutionErr := session.handleResolveModuleName(&ResolveModuleNameParams{ + result, resolutionErr := session.handleResolveModuleName(context.Background(), &ResolveModuleNameParams{ Snapshot: snapshot.Snapshot, Resolver: resolver, ModuleName: "pkg", @@ -82,7 +101,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { assertResolution := func(directory string, mode core.ModuleKind, expected string) { t.Helper() resolutionMode := ResolutionMode(mode) - result, resolutionErr := session.handleResolveModuleName(&ResolveModuleNameParams{ + result, resolutionErr := session.handleResolveModuleName(context.Background(), &ResolveModuleNameParams{ Snapshot: snapshot.Snapshot, Resolver: resolverID, ModuleName: "pkg", @@ -98,7 +117,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { assertResolution("/home/projects/p/other", core.ModuleKindESNext, "/home/projects/p/mode.d.ts") assertResolution("/home/projects/p/other", core.ModuleKindCommonJS, "/home/projects/p/global.d.ts") - unresolved, err := session.handleResolveModuleName(&ResolveModuleNameParams{ + unresolved, err := session.handleResolveModuleName(context.Background(), &ResolveModuleNameParams{ Snapshot: snapshot.Snapshot, Resolver: resolverID, ModuleName: "other", @@ -188,7 +207,7 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { }}, }) assert.NilError(t, err) - result, err := session.handleResolveModuleName(&ResolveModuleNameParams{ + result, err := session.handleResolveModuleName(context.Background(), &ResolveModuleNameParams{ Snapshot: snapshot.Snapshot, Resolver: resolver, ModuleName: "pkg", @@ -201,6 +220,24 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { assert.Equal(t, result.ResolvedModule.IsExternalLibraryImport, true) } +func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { + t.Parallel() + + conn := &failingModuleResolutionConn{} + provider := &callbackModuleResolutionProvider{ + identity: 1, + conn: conn, + ctx: context.Background(), + callback: "resolveModuleName/1", + currentDirectory: "/", + } + for range 2 { + _, err := provider.ResolveModuleName("pkg", "/src", core.ResolutionModeESM, nil) + assert.ErrorContains(t, err, "callback error") + } + assert.Equal(t, conn.calls, 2) +} + func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { entry := &ModuleResolutionEntry{ ModuleName: moduleName, diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 80a0a250236af..21bf05d5331bb 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -70,10 +70,12 @@ type fileLoader struct { // contentMapperMu guards the content-mapper bookkeeping below, which is written concurrently as // content-mapped files are parsed across worker goroutines. - contentMapperMu sync.Mutex - contentMapperFailures map[*contentmapper.Mapper]int - contentMapperInitFailed collections.Set[*contentmapper.Mapper] - contentMapperDiagnostics []*ast.Diagnostic + contentMapperMu sync.Mutex + contentMapperFailures map[*contentmapper.Mapper]int + contentMapperInitFailed collections.Set[*contentmapper.Mapper] + contentMapperDiagnostics []*ast.Diagnostic + moduleResolutionErrorOnce sync.Once + moduleResolutionError error } type redirectsFile struct { @@ -138,6 +140,7 @@ type processedFiles struct { redirectFilesByPath map[tspath.Path]*redirectsFile // Program-level diagnostics reported when a content mapper fails fatally (reported once per mapper). contentMapperDiagnostics []*ast.Diagnostic + moduleResolutionError error finishedProcessing bool } @@ -875,13 +878,23 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { var resolvedModule *module.ResolvedModule var trace []module.DiagAndArgs if p.opts.ModuleResolutionProvider != nil { - var provided bool - resolvedModule, provided = p.opts.ModuleResolutionProvider.GetModuleResolution(moduleName, tspath.GetDirectoryPath(fileName), mode) - if provided && resolvedModule == nil { - resolvedModule = &module.ResolvedModule{} + var err error + resolvedModule, err = p.opts.ModuleResolutionProvider.ResolveModuleName( + moduleName, + tspath.GetDirectoryPath(fileName), + mode, + func() *module.ResolvedModule { + resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + return resolvedModule + }, + ) + if err != nil { + p.moduleResolutionErrorOnce.Do(func() { + p.moduleResolutionError = err + }) } - if !provided { - resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + if resolvedModule == nil { + resolvedModule = &module.ResolvedModule{} } } else { resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) diff --git a/tsc/internal/compiler/filesparser.go b/tsc/internal/compiler/filesparser.go index 8ec9b43dc827a..dc232f819fa7f 100644 --- a/tsc/internal/compiler/filesparser.go +++ b/tsc/internal/compiler/filesparser.go @@ -582,6 +582,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { redirectTargetsMap: redirectTargetsMap, redirectFilesByPath: redirectFilesByPath, contentMapperDiagnostics: loader.contentMapperDiagnostics, + moduleResolutionError: loader.moduleResolutionError, } } diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 84bde5b9782e7..a62ef4261a3c8 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -636,6 +636,10 @@ func (p *Program) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*mo return p.resolvedModules } +func (p *Program) ModuleResolutionError() error { + return p.moduleResolutionError +} + // GetPackagesMap returns a lazily-cached map of package names to whether they bundle types. // This is used by incremental diagnostic repopulation. func (p *Program) GetPackagesMap() map[string]bool { diff --git a/tsc/internal/lsp/server.go b/tsc/internal/lsp/server.go index f23a7fcadcd3c..b0a76779ff209 100644 --- a/tsc/internal/lsp/server.go +++ b/tsc/internal/lsp/server.go @@ -2334,6 +2334,7 @@ func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto }() conn := ipc.NewAsyncConn(rwc, apiSession) + apiSession.SetConnection(conn) if apiErr := conn.Run(apiCtx); apiErr != nil { s.logger.Errorf("API session %s: %v", apiSession.ID(), apiErr) } diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 0d202e3362619..89de51d92444e 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -18,7 +18,12 @@ type ResolutionHost interface { type ResolutionProvider interface { Identity() uint64 - GetModuleResolution(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (resolution *ResolvedModule, found bool) + ResolveModuleName( + moduleName string, + containingDirectory string, + resolutionMode core.ResolutionMode, + fallback func() *ResolvedModule, + ) (*ResolvedModule, error) } type ModeAwareCacheKey struct { From 5cfe713ca3046ca217d4ff4826e6362c282b5de6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 17 Sep 2026 16:41:45 -0700 Subject: [PATCH 05/20] Test module resolution state updates Cover reusable sets, changed and removed providers, repeated inline specs, and callbacks under the snapshot reconfiguration model.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/sync/api-generators.test.ts | 15 ++ .../api/session_module_resolution_test.go | 223 +++++++++++++++++- 2 files changed, 237 insertions(+), 1 deletion(-) diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 7979cf39112e5..1083b55da02eb 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -39,6 +39,7 @@ import { type InterfaceType, type LiteralType, ModuleKind, + ModuleResolutionKind, type NodeHandle, type Program, type Project, @@ -1501,6 +1502,18 @@ describe("API - generator batching", () => { assert.equal(checker.isArgumentsSymbol(argumentsSymbol), true); assert.equal(checker.isUnknownSignature(unknownSignature), true); + const moduleResolutionSpec = { + fallback: "unresolved" as const, + entries: [{ moduleName: "models", result: { resolvedFileName: "/src/models.ts" } }], + }; + const moduleResolutionSet = api.batch(api.createModuleResolutionSet.gen(moduleResolutionSpec))[0]; + exercisedMethods.add("API.createModuleResolutionSet"); + const moduleResolver = api.batch(snapshot.createModuleResolver.gen( + { moduleResolution: ModuleResolutionKind.NodeNext }, + { moduleResolutions: moduleResolutionSet }, + ))[0]; + exercisedMethods.add("Snapshot.createModuleResolver"); + const cases: ParityCase[] = [ parityCase("API", "parseConfigFile", api.parseConfigFile, assertDeepEquivalent, "/tsconfig.json"), parityCase("API", "parseCommandLine", api.parseCommandLine, assertDeepEquivalent, ["--strict", "--noEmit"]), @@ -1517,6 +1530,7 @@ describe("API - generator batching", () => { temporaryProjects.push(temporarySnapshot.getProjects()[0].configFileName); }), parityCase("Snapshot", "getDefaultProjectForFile", snapshot.getDefaultProjectForFile, assertOptionalProjectsEquivalent, "/src/index.ts"), + parityCase("ModuleResolver", "resolveModuleName", moduleResolver.resolveModuleName, assertDeepEquivalent, "models", "/src"), parityCase("Snapshot", "update", snapshot.update, assertSnapshotsEquivalent, {}), parityCase("Project", "getImportAdderEdits", project.getImportAdderEdits, assertDeepEquivalent, "/src/index.ts", [{ kind: "importSymbol", symbol: unimportedSymbol }]), @@ -1740,6 +1754,7 @@ describe("API - generator batching", () => { { name: "API", value: api.constructor as object, own: true }, { name: "InternalAPI", value: api.internal }, { name: "Snapshot", value: snapshot }, + { name: "ModuleResolver", value: moduleResolver }, { name: "Project", value: project }, { name: "LanguageService", value: languageService }, { name: "Program", value: program }, diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 8e6cc69f13d21..4fdb97699db8a 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -7,7 +7,9 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/json" + "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" ) @@ -28,6 +30,26 @@ func (c *failingModuleResolutionConn) Notify(context.Context, string, any) error return nil } +type staticModuleResolutionConn struct { + calls int + fileName string +} + +func (c *staticModuleResolutionConn) Run(context.Context) error { + return nil +} + +func (c *staticModuleResolutionConn) Call(context.Context, string, any) (json.Value, error) { + c.calls++ + return json.Marshal(&ProvidedModuleResolution{ + ResolvedFileName: &DocumentIdentifier{FileName: c.fileName}, + }) +} + +func (c *staticModuleResolutionConn) Notify(context.Context, string, any) error { + return nil +} + func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { t.Parallel() @@ -151,7 +173,7 @@ func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { defer session.Close() response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize CreatePrograms: []*CreateSnapshotProgramParams{{ RootFiles: []DocumentIdentifier{{FileName: root}}, Options: CreateProgramOptions{ @@ -243,6 +265,205 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { assert.Equal(t, conn.calls, 2) } +func TestModuleResolutionProviderStateAcrossProgramUpdates(t *testing.T) { + t.Parallel() + + const root = "/home/projects/p/src/index.ts" + const providedA = "/home/projects/p/a.d.ts" + const providedB = "/home/projects/p/b.d.ts" + projectSession, utils := projecttestutil.Setup(map[string]any{ + root: `import { value } from "pkg"; export { value };`, + providedA: `export declare const value: "a";`, + providedB: `export declare const value: "b";`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + spec := func(fileName string) *ModuleResolutionSpec { + return &ModuleResolutionSpec{ + Fallback: ModuleResolutionFallbackUnresolved, + Entries: []*ModuleResolutionEntry{ + providedResolutionEntry("pkg", "", nil, fileName), + }, + } + } + createSet := func(fileName string) ModuleResolutionSetID { + t.Helper() + id, err := session.handleCreateModuleResolutionSet(&CreateModuleResolutionSetParams{Spec: *spec(fileName)}) + assert.NilError(t, err) + return id + } + setA := createSet(providedA) + setB := createSet(providedB) + createOptions := func(source *ModuleResolutionSource) CreateProgramOptions { + return CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ModuleResolutions: source, + } + } + + response, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(&ModuleResolutionSource{Set: setA}), + }}, + }, + }) + assert.NilError(t, err) + programID := (*response.Operation.CreatedPrograms)[0] + getProject := func(snapshotID SnapshotID) *project.Project { + t.Helper() + sd, snapshotErr := session.getSnapshotData(snapshotID) + assert.NilError(t, snapshotErr) + proj := sd.snapshot.ProjectCollection.GetProjectByPath(tspath.Path(programID)) + assert.Assert(t, proj != nil) + return proj + } + initialProject := getProject(response.Snapshot) + + sameSetResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: response.Snapshot, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: programID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(&ModuleResolutionSource{Set: setA}), + }}, + }, + }, + }) + assert.NilError(t, err) + sameSetProject := getProject(sameSetResponse.Snapshot) + assert.Assert(t, sameSetProject.Program == initialProject.Program) + + assert.NilError(t, utils.FS().WriteFile(root, `import { value } from "pkg"; export const updated = value;`)) + clonedResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: sameSetResponse.Snapshot, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: programID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(&ModuleResolutionSource{Set: setA}), + }}, + }, + FileNotifications: &FileNotifications{Changed: []DocumentIdentifier{{FileName: root}}}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, getProject(clonedResponse.Snapshot).ProgramUpdateKind, project.ProgramUpdateKindCloned) + + for _, test := range []struct { + name string + source *ModuleResolutionSource + expected project.ProgramUpdateKind + }{ + {name: "changed set", source: &ModuleResolutionSource{Set: setB}, expected: project.ProgramUpdateKindNewFiles}, + {name: "removed set", expected: project.ProgramUpdateKindNewFiles}, + {name: "inline spec", source: &ModuleResolutionSource{Spec: spec(providedA)}, expected: project.ProgramUpdateKindSameFileNames}, + } { + updated, updateErr := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: clonedResponse.Snapshot, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: programID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(test.source), + }}, + }, + }, + }) + assert.NilError(t, updateErr, test.name) + assert.Equal(t, getProject(updated.Snapshot).ProgramUpdateKind, test.expected, test.name) + } + + inlineResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: clonedResponse.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: programID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(&ModuleResolutionSource{Spec: spec(providedA)}), + }}, + }}, + }) + assert.NilError(t, err) + inlineProgram := getProject(inlineResponse.Snapshot).Program + repeatedInlineResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: inlineResponse.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: programID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: createOptions(&ModuleResolutionSource{Spec: spec(providedA)}), + }}, + }}, + }) + assert.NilError(t, err) + repeatedInlineProject := getProject(repeatedInlineResponse.Snapshot) + assert.Equal(t, repeatedInlineProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) + assert.Assert(t, repeatedInlineProject.Program != inlineProgram) + + callbackConn := &staticModuleResolutionConn{fileName: providedA} + session.SetConnection(callbackConn) + callbackResponse, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ResolveModuleNameCallback: "resolveModuleName/1", + }, + }}, + }, + }) + assert.NilError(t, err) + callbackProgramID := (*callbackResponse.Operation.CreatedPrograms)[0] + callbackProject := func(snapshotID SnapshotID) *project.Project { + t.Helper() + sd, snapshotErr := session.getSnapshotData(snapshotID) + assert.NilError(t, snapshotErr) + proj := sd.snapshot.ProjectCollection.GetProjectByPath(tspath.Path(callbackProgramID)) + assert.Assert(t, proj != nil) + return proj + } + initialCallbackProgram := callbackProject(callbackResponse.Snapshot).Program + updatedCallbackResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: callbackResponse.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: callbackProgramID, + RootFiles: []DocumentIdentifier{{FileName: root}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ResolveModuleNameCallback: "resolveModuleName/1", + }, + }}, + }}, + }) + assert.NilError(t, err) + updatedCallbackProject := callbackProject(updatedCallbackResponse.Snapshot) + assert.Equal(t, updatedCallbackProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) + assert.Assert(t, updatedCallbackProject.Program != initialCallbackProgram) + assert.Equal(t, callbackConn.calls, 2) +} + func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { entry := &ModuleResolutionEntry{ ModuleName: moduleName, From 67b47d43eb13c1c25525b8c696c1e3d4243b256e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 17 Sep 2026 17:20:50 -0700 Subject: [PATCH 06/20] Move module resolution state tests to API client Exercise synthetic-program reconfiguration end to end for reusable sets, changed and removed providers, repeated inline specs, and callbacks.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/typescript/test/async/api.test.ts | 83 +++++++ packages/typescript/test/sync/api.test.ts | 83 +++++++ .../api/session_module_resolution_test.go | 221 ------------------ 3 files changed, 166 insertions(+), 221 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 0a15cdff436bc..3448d7f2312ab 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -72,6 +72,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type ModuleResolverOptions, type NumberLiteralType, ObjectFlags, type Program, @@ -429,6 +430,88 @@ describe("API", () => { assert.deepEqual(await originalProgram.getSourceFileNames(), ["/src/a.ts"]); }); + test("snapshot.update reconfigures module resolution providers", async () => { + const root = "/src/index.ts"; + const providedA = "/a.d.ts"; + const providedB = "/b.d.ts"; + const { api: disposableAPI, fs } = spawnAPIWithFS({ + [root]: `import { value } from "pkg"; export { value };`, + [providedA]: `export declare const value: "a";`, + [providedB]: `export declare const value: "b";`, + }); + await using api = disposableAPI; + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const spec = (resolvedFileName: string) => ({ + fallback: "unresolved" as const, + entries: [{ moduleName: "pkg", result: { resolvedFileName } }], + }); + const setA = await api.createModuleResolutionSet(spec(providedA)); + const setB = await api.createModuleResolutionSet(spec(providedB)); + const createProgram = (moduleResolutions: ModuleResolverOptions["moduleResolutions"]) => ({ + rootFiles: [root], + options: { + compilerOptions, + ...(moduleResolutions ? { moduleResolutions } : {}), + }, + }); + + const initial = await api.createSnapshot({ createPrograms: [createProgram(setA)] }); + const programId = initial.operation.createdPrograms[0].id; + assert.deepEqual([...await initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + const sameSet = await initial.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + }); + assert.deepEqual([...await sameSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + fs.writeFile!(root, `import { value } from "pkg"; export const updated = value;`); + const sameSetAfterEdit = await sameSet.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + fileNotifications: { changed: [root] }, + }); + assert.deepEqual([...await sameSetAfterEdit.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + const changedSet = await sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setB) }], + }); + assert.deepEqual([...await changedSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedB, root]); + + const removedSet = await sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(undefined) }], + }); + assert.deepEqual(await removedSet.getProgram(programId)!.getSourceFileNames(), [root]); + + const inline = await sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + }); + const repeatedInline = await inline.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + }); + assert.deepEqual([...await repeatedInline.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + let callbackCalls = 0; + const callbackOptions = { + compilerOptions, + resolveModuleName: (moduleName: string) => { + callbackCalls++; + return moduleName === "pkg" ? { resolvedFileName: providedA } : undefined; + }, + }; + const callbackSnapshot = await api.createSnapshot({ + createPrograms: [{ rootFiles: [root], options: callbackOptions }], + }); + const callbackProgramId = callbackSnapshot.operation.createdPrograms[0].id; + const repeatedCallback = await callbackSnapshot.update({ + reconfigurePrograms: [{ id: callbackProgramId, rootFiles: [root], options: callbackOptions }], + }); + assert.deepEqual([...await repeatedCallback.getProgram(callbackProgramId)!.getSourceFileNames()].sort(), [providedA, root]); + assert.equal(callbackCalls, 2); + }); + test("Program resolved modules and type reference directives", async () => { await using api = spawnAPI({ "/src/index.ts": `/// diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f20c229bbfce3..1663b94f8fb1b 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -87,6 +87,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type ModuleResolverOptions, type NumberLiteralType, ObjectFlags, type Program, @@ -418,6 +419,88 @@ describe("API", () => { assert.deepEqual(originalProgram.getSourceFileNames(), ["/src/a.ts"]); }); + test("snapshot.update reconfigures module resolution providers", () => { + const root = "/src/index.ts"; + const providedA = "/a.d.ts"; + const providedB = "/b.d.ts"; + const { api: disposableAPI, fs } = spawnAPIWithFS({ + [root]: `import { value } from "pkg"; export { value };`, + [providedA]: `export declare const value: "a";`, + [providedB]: `export declare const value: "b";`, + }); + using api = disposableAPI; + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const spec = (resolvedFileName: string) => ({ + fallback: "unresolved" as const, + entries: [{ moduleName: "pkg", result: { resolvedFileName } }], + }); + const setA = api.createModuleResolutionSet(spec(providedA)); + const setB = api.createModuleResolutionSet(spec(providedB)); + const createProgram = (moduleResolutions: ModuleResolverOptions["moduleResolutions"]) => ({ + rootFiles: [root], + options: { + compilerOptions, + ...(moduleResolutions ? { moduleResolutions } : {}), + }, + }); + + const initial = api.createSnapshot({ createPrograms: [createProgram(setA)] }); + const programId = initial.operation.createdPrograms[0].id; + assert.deepEqual([...initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + const sameSet = initial.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + }); + assert.deepEqual([...sameSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + fs.writeFile!(root, `import { value } from "pkg"; export const updated = value;`); + const sameSetAfterEdit = sameSet.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + fileNotifications: { changed: [root] }, + }); + assert.deepEqual([...sameSetAfterEdit.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + const changedSet = sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(setB) }], + }); + assert.deepEqual([...changedSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedB, root]); + + const removedSet = sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(undefined) }], + }); + assert.deepEqual(removedSet.getProgram(programId)!.getSourceFileNames(), [root]); + + const inline = sameSetAfterEdit.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + }); + const repeatedInline = inline.update({ + reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + }); + assert.deepEqual([...repeatedInline.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + + let callbackCalls = 0; + const callbackOptions = { + compilerOptions, + resolveModuleName: (moduleName: string) => { + callbackCalls++; + return moduleName === "pkg" ? { resolvedFileName: providedA } : undefined; + }, + }; + const callbackSnapshot = api.createSnapshot({ + createPrograms: [{ rootFiles: [root], options: callbackOptions }], + }); + const callbackProgramId = callbackSnapshot.operation.createdPrograms[0].id; + const repeatedCallback = callbackSnapshot.update({ + reconfigurePrograms: [{ id: callbackProgramId, rootFiles: [root], options: callbackOptions }], + }); + assert.deepEqual([...repeatedCallback.getProgram(callbackProgramId)!.getSourceFileNames()].sort(), [providedA, root]); + assert.equal(callbackCalls, 2); + }); + test("Program resolved modules and type reference directives", () => { using api = spawnAPI({ "/src/index.ts": `/// diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 4fdb97699db8a..8bd50aaab0d1c 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -7,9 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/json" - "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" ) @@ -30,26 +28,6 @@ func (c *failingModuleResolutionConn) Notify(context.Context, string, any) error return nil } -type staticModuleResolutionConn struct { - calls int - fileName string -} - -func (c *staticModuleResolutionConn) Run(context.Context) error { - return nil -} - -func (c *staticModuleResolutionConn) Call(context.Context, string, any) (json.Value, error) { - c.calls++ - return json.Marshal(&ProvidedModuleResolution{ - ResolvedFileName: &DocumentIdentifier{FileName: c.fileName}, - }) -} - -func (c *staticModuleResolutionConn) Notify(context.Context, string, any) error { - return nil -} - func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { t.Parallel() @@ -265,205 +243,6 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { assert.Equal(t, conn.calls, 2) } -func TestModuleResolutionProviderStateAcrossProgramUpdates(t *testing.T) { - t.Parallel() - - const root = "/home/projects/p/src/index.ts" - const providedA = "/home/projects/p/a.d.ts" - const providedB = "/home/projects/p/b.d.ts" - projectSession, utils := projecttestutil.Setup(map[string]any{ - root: `import { value } from "pkg"; export { value };`, - providedA: `export declare const value: "a";`, - providedB: `export declare const value: "b";`, - }) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - defer session.Close() - ctx := context.Background() - spec := func(fileName string) *ModuleResolutionSpec { - return &ModuleResolutionSpec{ - Fallback: ModuleResolutionFallbackUnresolved, - Entries: []*ModuleResolutionEntry{ - providedResolutionEntry("pkg", "", nil, fileName), - }, - } - } - createSet := func(fileName string) ModuleResolutionSetID { - t.Helper() - id, err := session.handleCreateModuleResolutionSet(&CreateModuleResolutionSetParams{Spec: *spec(fileName)}) - assert.NilError(t, err) - return id - } - setA := createSet(providedA) - setB := createSet(providedB) - createOptions := func(source *ModuleResolutionSource) CreateProgramOptions { - return CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Module: core.ModuleKindNodeNext, - ModuleResolution: core.ModuleResolutionKindNodeNext, - }, - ModuleResolutions: source, - } - } - - response, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - CreatePrograms: []*CreateSnapshotProgramParams{{ - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(&ModuleResolutionSource{Set: setA}), - }}, - }, - }) - assert.NilError(t, err) - programID := (*response.Operation.CreatedPrograms)[0] - getProject := func(snapshotID SnapshotID) *project.Project { - t.Helper() - sd, snapshotErr := session.getSnapshotData(snapshotID) - assert.NilError(t, snapshotErr) - proj := sd.snapshot.ProjectCollection.GetProjectByPath(tspath.Path(programID)) - assert.Assert(t, proj != nil) - return proj - } - initialProject := getProject(response.Snapshot) - - sameSetResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: response.Snapshot, - Changes: &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: programID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(&ModuleResolutionSource{Set: setA}), - }}, - }, - }, - }) - assert.NilError(t, err) - sameSetProject := getProject(sameSetResponse.Snapshot) - assert.Assert(t, sameSetProject.Program == initialProject.Program) - - assert.NilError(t, utils.FS().WriteFile(root, `import { value } from "pkg"; export const updated = value;`)) - clonedResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: sameSetResponse.Snapshot, - Changes: &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: programID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(&ModuleResolutionSource{Set: setA}), - }}, - }, - FileNotifications: &FileNotifications{Changed: []DocumentIdentifier{{FileName: root}}}, - }, - }) - assert.NilError(t, err) - assert.Equal(t, getProject(clonedResponse.Snapshot).ProgramUpdateKind, project.ProgramUpdateKindCloned) - - for _, test := range []struct { - name string - source *ModuleResolutionSource - expected project.ProgramUpdateKind - }{ - {name: "changed set", source: &ModuleResolutionSource{Set: setB}, expected: project.ProgramUpdateKindNewFiles}, - {name: "removed set", expected: project.ProgramUpdateKindNewFiles}, - {name: "inline spec", source: &ModuleResolutionSource{Spec: spec(providedA)}, expected: project.ProgramUpdateKindSameFileNames}, - } { - updated, updateErr := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: clonedResponse.Snapshot, - Changes: &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: programID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(test.source), - }}, - }, - }, - }) - assert.NilError(t, updateErr, test.name) - assert.Equal(t, getProject(updated.Snapshot).ProgramUpdateKind, test.expected, test.name) - } - - inlineResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: clonedResponse.Snapshot, - Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: programID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(&ModuleResolutionSource{Spec: spec(providedA)}), - }}, - }}, - }) - assert.NilError(t, err) - inlineProgram := getProject(inlineResponse.Snapshot).Program - repeatedInlineResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: inlineResponse.Snapshot, - Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: programID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: createOptions(&ModuleResolutionSource{Spec: spec(providedA)}), - }}, - }}, - }) - assert.NilError(t, err) - repeatedInlineProject := getProject(repeatedInlineResponse.Snapshot) - assert.Equal(t, repeatedInlineProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) - assert.Assert(t, repeatedInlineProject.Program != inlineProgram) - - callbackConn := &staticModuleResolutionConn{fileName: providedA} - session.SetConnection(callbackConn) - callbackResponse, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ - SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - CreatePrograms: []*CreateSnapshotProgramParams{{ - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Module: core.ModuleKindNodeNext, - ModuleResolution: core.ModuleResolutionKindNodeNext, - }, - ResolveModuleNameCallback: "resolveModuleName/1", - }, - }}, - }, - }) - assert.NilError(t, err) - callbackProgramID := (*callbackResponse.Operation.CreatedPrograms)[0] - callbackProject := func(snapshotID SnapshotID) *project.Project { - t.Helper() - sd, snapshotErr := session.getSnapshotData(snapshotID) - assert.NilError(t, snapshotErr) - proj := sd.snapshot.ProjectCollection.GetProjectByPath(tspath.Path(callbackProgramID)) - assert.Assert(t, proj != nil) - return proj - } - initialCallbackProgram := callbackProject(callbackResponse.Snapshot).Program - updatedCallbackResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - Snapshot: callbackResponse.Snapshot, - Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize - ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ - Id: callbackProgramID, - RootFiles: []DocumentIdentifier{{FileName: root}}, - Options: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Module: core.ModuleKindNodeNext, - ModuleResolution: core.ModuleResolutionKindNodeNext, - }, - ResolveModuleNameCallback: "resolveModuleName/1", - }, - }}, - }}, - }) - assert.NilError(t, err) - updatedCallbackProject := callbackProject(updatedCallbackResponse.Snapshot) - assert.Equal(t, updatedCallbackProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) - assert.Assert(t, updatedCallbackProject.Program != initialCallbackProgram) - assert.Equal(t, callbackConn.calls, 2) -} - func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { entry := &ModuleResolutionEntry{ ModuleName: moduleName, From 314fd684214c273672ffb7ca870435db7f833782 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 08:34:32 -0700 Subject: [PATCH 07/20] Format --- tsc/internal/ls/lsutil/formatcodeoptions.go | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tsc/internal/ls/lsutil/formatcodeoptions.go b/tsc/internal/ls/lsutil/formatcodeoptions.go index 73c8a899a63a1..fe39b21f4d1e6 100644 --- a/tsc/internal/ls/lsutil/formatcodeoptions.go +++ b/tsc/internal/ls/lsutil/formatcodeoptions.go @@ -113,18 +113,18 @@ func (settings FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOption func GetDefaultFormatCodeSettings() FormatCodeSettings { return FormatCodeSettings{ - IndentSize: printer.GetDefaultIndentSize(), - TabSize: printer.GetDefaultIndentSize(), - NewLineCharacter: "\n", - ConvertTabsToSpaces: core.TSTrue, - IndentStyle: IndentStyleSmart, - TrimTrailingWhitespace: core.TSTrue, - InsertSpaceAfterConstructor: core.TSFalse, - InsertSpaceAfterCommaDelimiter: core.TSTrue, - InsertSpaceAfterSemicolonInForStatements: core.TSTrue, - InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue, - InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue, - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse, + IndentSize: printer.GetDefaultIndentSize(), + TabSize: printer.GetDefaultIndentSize(), + NewLineCharacter: "\n", + ConvertTabsToSpaces: core.TSTrue, + IndentStyle: IndentStyleSmart, + TrimTrailingWhitespace: core.TSTrue, + InsertSpaceAfterConstructor: core.TSFalse, + InsertSpaceAfterCommaDelimiter: core.TSTrue, + InsertSpaceAfterSemicolonInForStatements: core.TSTrue, + InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue, + InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue, + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse, InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.TSFalse, InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.TSFalse, InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.TSTrue, From f91176d04fed00ffdfed241c4a1f3a9a6356ed00 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 10:11:38 -0700 Subject: [PATCH 08/20] Fix snapshot params, add test for lack of resolvedUsingTsExtension --- packages/typescript/src/api/async/api.ts | 15 ++++++-- packages/typescript/src/api/sync/api.ts | 17 ++++++--- packages/typescript/test/async/api.test.ts | 42 ++++++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 42 ++++++++++++++++++++++ tsc/internal/api/module_resolution.go | 10 ++---- 5 files changed, 112 insertions(+), 14 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index f3aa381e5c3de..17c28017d6cb0 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -74,7 +74,7 @@ import type { ImportAdderAction, InferredProjectId, IntrinsicTypeMethod, - LanguageServerSnapshotChanges, + LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, ModuleResolutionEntry, ModuleResolutionSource, ModuleResolutionSpec, @@ -202,7 +202,6 @@ export type { IntersectionType, IntrinsicType, JSDocTagInfo, - LanguageServerSnapshotChanges, LiteralType, LSPConnectionOptions, ModuleResolutionEntry, @@ -257,6 +256,10 @@ export type CreateSnapshotParams = Omit & { + createPrograms?: readonly CreateSnapshotProgramParams[] | undefined; + reconfigurePrograms?: readonly ReconfigureSnapshotProgramParams[] | undefined; +}; function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { if (input === undefined) return undefined; @@ -514,6 +517,12 @@ export class API implements FormatDiagnosticsHo }; } + private prepareLanguageServerSnapshotChanges(changes: LanguageServerSnapshotChanges | undefined): ProtocolLanguageServerSnapshotChanges | undefined { + if (!changes) return undefined; + const prepared = this.prepareCreateSnapshotParams(changes); + return prepared; + } + private createSnapshotUpdater(getSnapshot: () => Snapshot): SnapshotUpdater { const update: SnapshotUpdater = params => this.updateSnapshot(getSnapshot(), params); // @sync: const update = ((params: CreateSnapshotParams) => this.updateSnapshot(getSnapshot(), params)) as SnapshotUpdater; // @sync-only-start @@ -549,7 +558,7 @@ export class API implements FormatDiagnosticsHo } const data = await this.client.apiRequest("getCurrentLanguageServerSnapshot", { ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), - ...(changes ? { changes } : {}), + ...(changes ? { changes: this.prepareLanguageServerSnapshotChanges(changes) } : {}), }); if (baseSnapshot) { this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 7ff9f99d1f207..b58f20d70ab40 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -91,7 +91,7 @@ import type { ImportAdderAction, InferredProjectId, IntrinsicTypeMethod, - LanguageServerSnapshotChanges, + LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, ModuleResolutionEntry, ModuleResolutionSource, ModuleResolutionSpec, @@ -219,7 +219,6 @@ export type { IntersectionType, IntrinsicType, JSDocTagInfo, - LanguageServerSnapshotChanges, LiteralType, LSPConnectionOptions, ModuleResolutionEntry, @@ -274,6 +273,10 @@ export type CreateSnapshotParams = Omit & { + createPrograms?: readonly CreateSnapshotProgramParams[] | undefined; + reconfigurePrograms?: readonly ReconfigureSnapshotProgramParams[] | undefined; +}; function toModuleResolutionSource(input: ModuleResolutionSpec | ModuleResolutionSet | undefined): ModuleResolutionSource | undefined { if (input === undefined) return undefined; @@ -811,6 +814,12 @@ export class API implements FormatDiagnosticsHo }; } + private prepareLanguageServerSnapshotChanges(changes: LanguageServerSnapshotChanges | undefined): ProtocolLanguageServerSnapshotChanges | undefined { + if (!changes) return undefined; + const prepared = this.prepareCreateSnapshotParams(changes); + return prepared; + } + private createSnapshotUpdater(getSnapshot: () => Snapshot): SnapshotUpdater { const update = ((params: CreateSnapshotParams) => this.updateSnapshot(getSnapshot(), params)) as SnapshotUpdater; const owner = this; @@ -852,7 +861,7 @@ export class API implements FormatDiagnosticsHo } const data = owner.client.apiRequest("getCurrentLanguageServerSnapshot", { ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), - ...(changes ? { changes } : {}), + ...(changes ? { changes: owner.prepareLanguageServerSnapshotChanges(changes) } : {}), }); if (baseSnapshot) { owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); @@ -888,7 +897,7 @@ export class API implements FormatDiagnosticsHo } const data = yield* apiRequest("getCurrentLanguageServerSnapshot", { ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), - ...(changes ? { changes } : {}), + ...(changes ? { changes: owner.prepareLanguageServerSnapshotChanges(changes) } : {}), }); if (baseSnapshot) { owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 8023e397c3564..edc919568652a 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -72,6 +72,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type ModuleResolutionSet, type ModuleResolverOptions, type NumberLiteralType, ObjectFlags, @@ -125,6 +126,17 @@ describe("API", () => { const lsp = undefined! as API; void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); + const resolutionSet = undefined! as ModuleResolutionSet; + void lsp.getCurrentLanguageServerSnapshot({ + createPrograms: [{ + rootFiles: ["/index.ts"], + options: { + compilerOptions: {}, + moduleResolutions: resolutionSet, + resolveModuleName: () => ({ resolvedFileName: "/resolved.ts" }), + }, + }], + }); const baseSnapshot = undefined! as Snapshot; void lsp.getCurrentLanguageServerSnapshot(undefined, baseSnapshot); @@ -560,6 +572,36 @@ describe("API", () => { assert.equal(callbackCalls, 2); }); + test("provided resolutions do not report native resolution provenance diagnostics", async () => { + await using api = spawnAPI({ + "/src/index.ts": `import { value } from "./value.ts"; export { value };`, + "/value.ts": `export const value = 1;`, + }); + const snapshot = await api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { + compilerOptions: { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }, + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./value.ts", + result: { + resolvedFileName: "/value.ts", + }, + }], + }, + }, + }], + }); + const program = snapshot.operation.createdPrograms[0]; + assert.deepEqual(await program.getSemanticDiagnostics("/src/index.ts"), []); + }); + test("Program resolved modules and type reference directives", async () => { await using api = spawnAPI({ "/src/index.ts": `/// diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index fddf249466a25..e991b1ecce79e 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -87,6 +87,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type ModuleResolutionSet, type ModuleResolverOptions, type NumberLiteralType, ObjectFlags, @@ -133,6 +134,17 @@ describe("API", () => { const lsp = undefined! as API; void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); + const resolutionSet = undefined! as ModuleResolutionSet; + void lsp.getCurrentLanguageServerSnapshot({ + createPrograms: [{ + rootFiles: ["/index.ts"], + options: { + compilerOptions: {}, + moduleResolutions: resolutionSet, + resolveModuleName: () => ({ resolvedFileName: "/resolved.ts" }), + }, + }], + }); const baseSnapshot = undefined! as Snapshot; void lsp.getCurrentLanguageServerSnapshot(undefined, baseSnapshot); @@ -549,6 +561,36 @@ describe("API", () => { assert.equal(callbackCalls, 2); }); + test("provided resolutions do not report native resolution provenance diagnostics", () => { + using api = spawnAPI({ + "/src/index.ts": `import { value } from "./value.ts"; export { value };`, + "/value.ts": `export const value = 1;`, + }); + const snapshot = api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { + compilerOptions: { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }, + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./value.ts", + result: { + resolvedFileName: "/value.ts", + }, + }], + }, + }, + }], + }); + const program = snapshot.operation.createdPrograms[0]; + assert.deepEqual(program.getSemanticDiagnostics("/src/index.ts"), []); + }); + test("Program resolved modules and type reference directives", () => { using api = spawnAPI({ "/src/index.ts": `/// diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 51c98fbcb4b40..f503739bf3ece 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -187,16 +187,12 @@ func providedModuleResolutionToResolvedModule(moduleName string, provided *Provi PeerDependencies: provided.PackageID.PeerDependencies, } } - externalPath := result.ResolvedFileName + originalPath := result.ResolvedFileName if result.OriginalPath != "" { - externalPath = result.OriginalPath + originalPath = result.OriginalPath } result.Extension = tspath.TryGetExtensionFromPath(result.ResolvedFileName) - result.ResolvedUsingTsExtension = tspath.IsExternalModuleNameRelative(moduleName) && - tspath.TryExtractTSExtension(moduleName) != "" - result.ResolvedUsingExtraExtensions = !tspath.FileExtensionIsOneOf(result.ResolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && - !tspath.FileExtensionIsOneOf(result.ResolvedFileName, tspath.SupportedJSExtensionsFlat) - result.IsExternalLibraryImport = strings.Contains(externalPath, "/node_modules/") + result.IsExternalLibraryImport = strings.Contains(originalPath, "/node_modules/") return result } From 404b3baa70aec93918922522875972c9c97a38a6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 11:36:57 -0700 Subject: [PATCH 09/20] Decouple module resolver behavior/data --- packages/typescript/src/api/async/api.ts | 116 +++++----- .../typescript/src/api/proto.generated.ts | 40 ++-- packages/typescript/src/api/sync/api.ts | 201 ++++++++---------- packages/typescript/test/async/api.test.ts | 114 +++++++--- .../test/sync/api-generators.test.ts | 12 +- packages/typescript/test/sync/api.test.ts | 114 +++++++--- tsc/internal/api/module_resolution.go | 193 +++++++---------- tsc/internal/api/proto.go | 50 ++--- tsc/internal/api/session.go | 26 +-- .../api/session_module_resolution_test.go | 64 +++--- 10 files changed, 459 insertions(+), 471 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 17c28017d6cb0..8e3108249b761 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -76,7 +76,6 @@ import type { IntrinsicTypeMethod, LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, ModuleResolutionEntry, - ModuleResolutionSource, ModuleResolutionSpec, PackageId, ParsedCommandLine, @@ -243,13 +242,15 @@ export type { }; export interface ModuleResolverOptions { - moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; + moduleResolutions?: ModuleResolutionSpec; resolveModuleName?: ResolveModuleNameCallback; } export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; -export type CreateProgramOptions = Omit & ModuleResolverOptions; +export type CreateProgramOptions = Omit & { + moduleResolver?: ModuleResolver; +}; export type CreateSnapshotProgramParams = Omit & { options: CreateProgramOptions; }; export type ReconfigureSnapshotProgramParams = Omit & { options: CreateProgramOptions; }; export type CreateSnapshotParams = Omit & { @@ -261,19 +262,10 @@ export type LanguageServerSnapshotChanges = Omit void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; - client.registerCallback(name, params => { + const dispose = client.registerCallback(name, params => { const { moduleName, containingDirectory, resolutionMode } = params as { moduleName: string; containingDirectory: string; @@ -281,7 +273,7 @@ function registerModuleResolutionCallback(client: Client, callback: ResolveModul }; return callback(moduleName, containingDirectory, resolutionMode); }); - return name; + return { name, dispose }; } export interface TranspileOptions { @@ -503,11 +495,11 @@ export class API implements FormatDiagnosticsHo private prepareCreateSnapshotParams(params: CreateSnapshotParams | undefined): ProtocolCreateSnapshotParams | undefined { if (!params) return undefined; const prepareOptions = (options: CreateProgramOptions): ProtocolCreateProgramOptions => { - const { moduleResolutions, resolveModuleName, ...rest } = options; + const { moduleResolver, ...rest } = options; + moduleResolver?.ensureNotDisposed(); return { ...rest, - moduleResolutions: toModuleResolutionSource(moduleResolutions), - resolveModuleNameCallback: resolveModuleName ? registerModuleResolutionCallback(this.client, resolveModuleName) : undefined, + moduleResolver: moduleResolver?.id, }; }; return { @@ -598,10 +590,21 @@ export class API implements FormatDiagnosticsHo } } - async createModuleResolutionSet(spec: ModuleResolutionSpec): Promise { + async createModuleResolver(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Promise { await this.ensureInitialized(); - const id = await this.client.apiRequest("createModuleResolutionSet", { spec }); - return new ModuleResolutionSet(id, this.client); + const callback = options?.resolveModuleName ? registerModuleResolutionCallback(this.client, options.resolveModuleName) : undefined; + try { + const id = await this.client.apiRequest("createModuleResolver", { + compilerOptions, + moduleResolutions: options?.moduleResolutions, + resolveModuleNameCallback: callback?.name, + }); + return new ModuleResolver(id, this.client, callback?.dispose); + } + catch (error) { + callback?.dispose(); + throw error; + } } clearSourceFileCache(): void { @@ -837,19 +840,6 @@ export class Snapshot { return this.projectMap.get(data.id); } - async createModuleResolver(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Promise { - this.ensureNotDisposed(); - const id = await this.client.apiRequest("createModuleResolver", { - snapshot: this.id, - compilerOptions, - moduleResolutions: toModuleResolutionSource(options?.moduleResolutions), - resolveModuleNameCallback: options?.resolveModuleName - ? registerModuleResolutionCallback(this.client, options.resolveModuleName) - : undefined, - }); - return new ModuleResolver(id, this.id, this.client, () => this.ensureNotDisposed()); - } - [globalThis.Symbol.dispose](): void { void this.dispose(); } @@ -892,14 +882,34 @@ export class Snapshot { } } -export class ModuleResolutionSet { +export class ModuleResolver { readonly id: number; private readonly client: Client; + private readonly disposeCallback: (() => void) | undefined; private disposed = false; - constructor(id: number, client: Client) { + constructor(id: number, client: Client, disposeCallback: (() => void) | undefined) { this.id = id; this.client = client; + this.disposeCallback = disposeCallback; + } + + async resolveModuleName( + moduleName: string, + containingDirectory: DocumentIdentifier, + options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }, + ): Promise { + this.ensureNotDisposed(); + if (options?.snapshot?.isDisposed()) { + throw new Error("Snapshot is disposed"); + } + return this.client.apiRequest("resolveModuleName", { + snapshot: options?.snapshot?.id, + resolver: this.id, + moduleName, + containingDirectory, + resolutionMode: options?.resolutionMode, + }); } [globalThis.Symbol.asyncDispose](): Promise { // @sync: [globalThis.Symbol.dispose](): void { @@ -908,42 +918,14 @@ export class ModuleResolutionSet { async dispose(): Promise { if (this.disposed) return; - await this.client.apiRequest("releaseModuleResolutionSet", { set: this.id }); + await this.client.apiRequest("releaseModuleResolver", { resolver: this.id }); this.disposed = true; + this.disposeCallback?.(); } /** @internal */ ensureNotDisposed(): void { - if (this.disposed) throw new Error("ModuleResolutionSet is disposed"); - } -} - -export class ModuleResolver { - private readonly id: number; - private readonly snapshotId: number; - private readonly client: Client; - private readonly ensureSnapshotActive: () => void; - - constructor(id: number, snapshotId: number, client: Client, ensureSnapshotActive: () => void) { - this.id = id; - this.snapshotId = snapshotId; - this.client = client; - this.ensureSnapshotActive = ensureSnapshotActive; - } - - async resolveModuleName( - moduleName: string, - containingDirectory: DocumentIdentifier, - resolutionMode?: ResolutionMode, - ): Promise { - this.ensureSnapshotActive(); - return this.client.apiRequest("resolveModuleName", { - snapshot: this.snapshotId, - resolver: this.id, - moduleName, - containingDirectory, - resolutionMode, - }); + if (this.disposed) throw new Error("ModuleResolver is disposed"); } } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 445d9f5293527..ea36bbc9de542 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -26,9 +26,8 @@ export interface APIMethodInfo { createSnapshot: APIMethod; updateSnapshot: APIMethod; getCurrentLanguageServerSnapshot: APIMethod; - createModuleResolutionSet: APIMethod; - releaseModuleResolutionSet: APIMethod; createModuleResolver: APIMethod; + releaseModuleResolver: APIMethod; resolveModuleName: APIMethod; parseCommandLine: APIMethod; readConfigFile: APIMethod; @@ -255,23 +254,18 @@ export interface GetCurrentLanguageServerSnapshotParams { changes?: LanguageServerSnapshotChanges | undefined; } -export interface CreateModuleResolutionSetParams { - spec: ModuleResolutionSpec; -} - -export interface ReleaseModuleResolutionSetParams { - set: number; -} - export interface CreateModuleResolverParams { - snapshot: number; compilerOptions: CompilerOptions; - moduleResolutions?: ModuleResolutionSource | undefined; + moduleResolutions?: ModuleResolutionSpec | undefined; resolveModuleNameCallback?: string | undefined; } +export interface ReleaseModuleResolverParams { + resolver: number; +} + export interface ResolveModuleNameParams { - snapshot: number; + snapshot?: number | undefined; resolver: number; moduleName: string; containingDirectory: DocumentIdentifier; @@ -1034,7 +1028,6 @@ export interface ProfileResult { export interface BatchRequest { method: | "batchRequests" - | "createModuleResolutionSet" | "createModuleResolver" | "createSnapshot" | "createSourceFile" @@ -1180,7 +1173,7 @@ export interface BatchRequest { | "printNode" | "readConfigFile" | "release" - | "releaseModuleResolutionSet" + | "releaseModuleResolver" | "resolveModuleName" | "resolveName" | "saveHeapProfile" @@ -1200,7 +1193,6 @@ export interface BatchRequest { export interface BatchResponse { method: | "batchRequests" - | "createModuleResolutionSet" | "createModuleResolver" | "createSnapshot" | "createSourceFile" @@ -1346,7 +1338,7 @@ export interface BatchResponse { | "printNode" | "readConfigFile" | "release" - | "releaseModuleResolutionSet" + | "releaseModuleResolver" | "resolveModuleName" | "resolveName" | "saveHeapProfile" @@ -1464,11 +1456,6 @@ export interface SnapshotOperationResponse { export interface LanguageServerSnapshotChanges extends SnapshotRequestChangesParams { } -export interface ModuleResolutionSpec { - fallback: "resolve" | "unresolved"; - entries: ModuleResolutionEntry[]; -} - /** CompilerOptions contains the compiler options exposed by the API. */ export interface CompilerOptions { allowJs?: boolean | undefined; @@ -1575,9 +1562,9 @@ export interface CompilerOptions { configFilePath?: string | undefined; } -export interface ModuleResolutionSource { - spec?: ModuleResolutionSpec | undefined; - set?: number | undefined; +export interface ModuleResolutionSpec { + fallback: "resolve" | "unresolved"; + entries: ModuleResolutionEntry[]; } export interface ProjectReference { @@ -1710,8 +1697,7 @@ export interface CreateProgramOptions { compilerOptions: CompilerOptions; projectReferences?: ProjectReference[] | undefined; configFileParsingDiagnostics?: DiagnosticResponse[] | undefined; - moduleResolutions?: ModuleResolutionSource | undefined; - resolveModuleNameCallback?: string | undefined; + moduleResolver?: number | undefined; } export interface ProvidedModuleResolution { diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index b58f20d70ab40..0d72b737dc1b2 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -93,7 +93,6 @@ import type { IntrinsicTypeMethod, LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, ModuleResolutionEntry, - ModuleResolutionSource, ModuleResolutionSpec, PackageId, ParsedCommandLine, @@ -260,13 +259,15 @@ export type { }; export interface ModuleResolverOptions { - moduleResolutions?: ModuleResolutionSpec | ModuleResolutionSet; + moduleResolutions?: ModuleResolutionSpec; resolveModuleName?: ResolveModuleNameCallback; } export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; -export type CreateProgramOptions = Omit & ModuleResolverOptions; +export type CreateProgramOptions = Omit & { + moduleResolver?: ModuleResolver; +}; export type CreateSnapshotProgramParams = Omit & { options: CreateProgramOptions; }; export type ReconfigureSnapshotProgramParams = Omit & { options: CreateProgramOptions; }; export type CreateSnapshotParams = Omit & { @@ -278,19 +279,10 @@ export type LanguageServerSnapshotChanges = Omit void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; - client.registerCallback(name, params => { + const dispose = client.registerCallback(name, params => { const { moduleName, containingDirectory, resolutionMode } = params as { moduleName: string; containingDirectory: string; @@ -298,7 +290,7 @@ function registerModuleResolutionCallback(client: Client, callback: ResolveModul }; return callback(moduleName, containingDirectory, resolutionMode); }); - return name; + return { name, dispose }; } export interface TranspileOptions { @@ -800,11 +792,11 @@ export class API implements FormatDiagnosticsHo private prepareCreateSnapshotParams(params: CreateSnapshotParams | undefined): ProtocolCreateSnapshotParams | undefined { if (!params) return undefined; const prepareOptions = (options: CreateProgramOptions): ProtocolCreateProgramOptions => { - const { moduleResolutions, resolveModuleName, ...rest } = options; + const { moduleResolver, ...rest } = options; + moduleResolver?.ensureNotDisposed(); return { ...rest, - moduleResolutions: toModuleResolutionSource(moduleResolutions), - resolveModuleNameCallback: resolveModuleName ? registerModuleResolutionCallback(this.client, resolveModuleName) : undefined, + moduleResolver: moduleResolver?.id, }; }; return { @@ -960,23 +952,45 @@ export class API implements FormatDiagnosticsHo ); } - get createModuleResolutionSet(): { - (spec: ModuleResolutionSpec): ModuleResolutionSet; - gen(spec: ModuleResolutionSpec): Generator; + get createModuleResolver(): { + (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver; + gen(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator; } { const owner = this; return cacheGeneratorMethod( owner, - "createModuleResolutionSet", - function (spec: ModuleResolutionSpec): ModuleResolutionSet { + "createModuleResolver", + function (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver { owner.ensureInitialized(); - const id = owner.client.apiRequest("createModuleResolutionSet", { spec }); - return new ModuleResolutionSet(id, owner.client); + const callback = options?.resolveModuleName ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) : undefined; + try { + const id = owner.client.apiRequest("createModuleResolver", { + compilerOptions, + moduleResolutions: options?.moduleResolutions, + resolveModuleNameCallback: callback?.name, + }); + return new ModuleResolver(id, owner.client, callback?.dispose); + } + catch (error) { + callback?.dispose(); + throw error; + } }, - function* (spec: ModuleResolutionSpec): Generator { + function* (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator { yield* owner.ensureInitialized.gen(); - const id = yield* apiRequest("createModuleResolutionSet", { spec }); - return new ModuleResolutionSet(id, owner.client); + const callback = options?.resolveModuleName ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) : undefined; + try { + const id = yield* apiRequest("createModuleResolver", { + compilerOptions, + moduleResolutions: options?.moduleResolutions, + resolveModuleNameCallback: callback?.name, + }); + return new ModuleResolver(id, owner.client, callback?.dispose); + } + catch (error) { + callback?.dispose(); + throw error; + } }, ); } @@ -1367,41 +1381,6 @@ export class Snapshot { ); } - get createModuleResolver(): { - (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver; - gen(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "createModuleResolver", - function (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver { - owner.ensureNotDisposed(); - const id = owner.client.apiRequest("createModuleResolver", { - snapshot: owner.id, - compilerOptions, - moduleResolutions: toModuleResolutionSource(options?.moduleResolutions), - resolveModuleNameCallback: options?.resolveModuleName - ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) - : undefined, - }); - return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); - }, - function* (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator { - owner.ensureNotDisposed(); - const id = yield* apiRequest("createModuleResolver", { - snapshot: owner.id, - compilerOptions, - moduleResolutions: toModuleResolutionSource(options?.moduleResolutions), - resolveModuleNameCallback: options?.resolveModuleName - ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) - : undefined, - }); - return new ModuleResolver(id, owner.id, owner.client, () => owner.ensureNotDisposed()); - }, - ); - } - [globalThis.Symbol.dispose](): void { void this.dispose(); } @@ -1482,14 +1461,53 @@ export class Snapshot { } } -export class ModuleResolutionSet { +export class ModuleResolver { readonly id: number; private readonly client: Client; + private readonly disposeCallback: (() => void) | undefined; private disposed = false; - constructor(id: number, client: Client) { + constructor(id: number, client: Client, disposeCallback: (() => void) | undefined) { this.id = id; this.client = client; + this.disposeCallback = disposeCallback; + } + + get resolveModuleName(): { + (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "resolveModuleName", + function (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): ResolveModuleNameResult { + owner.ensureNotDisposed(); + if (options?.snapshot?.isDisposed()) { + throw new Error("Snapshot is disposed"); + } + return owner.client.apiRequest("resolveModuleName", { + snapshot: options?.snapshot?.id, + resolver: owner.id, + moduleName, + containingDirectory, + resolutionMode: options?.resolutionMode, + }); + }, + function* (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): Generator { + owner.ensureNotDisposed(); + if (options?.snapshot?.isDisposed()) { + throw new Error("Snapshot is disposed"); + } + return yield* apiRequest("resolveModuleName", { + snapshot: options?.snapshot?.id, + resolver: owner.id, + moduleName, + containingDirectory, + resolutionMode: options?.resolutionMode, + }); + }, + ); } [globalThis.Symbol.dispose](): void { @@ -1506,65 +1524,22 @@ export class ModuleResolutionSet { "dispose", function (): void { if (owner.disposed) return; - owner.client.apiRequest("releaseModuleResolutionSet", { set: owner.id }); + owner.client.apiRequest("releaseModuleResolver", { resolver: owner.id }); owner.disposed = true; + owner.disposeCallback?.(); }, function* (): Generator { if (owner.disposed) return; - yield* apiRequest("releaseModuleResolutionSet", { set: owner.id }); + yield* apiRequest("releaseModuleResolver", { resolver: owner.id }); owner.disposed = true; + owner.disposeCallback?.(); }, ); } /** @internal */ ensureNotDisposed(): void { - if (this.disposed) throw new Error("ModuleResolutionSet is disposed"); - } -} - -export class ModuleResolver { - private readonly id: number; - private readonly snapshotId: number; - private readonly client: Client; - private readonly ensureSnapshotActive: () => void; - - constructor(id: number, snapshotId: number, client: Client, ensureSnapshotActive: () => void) { - this.id = id; - this.snapshotId = snapshotId; - this.client = client; - this.ensureSnapshotActive = ensureSnapshotActive; - } - - get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): ResolveModuleNameResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): ResolveModuleNameResult { - owner.ensureSnapshotActive(); - return owner.client.apiRequest("resolveModuleName", { - snapshot: owner.snapshotId, - resolver: owner.id, - moduleName, - containingDirectory, - resolutionMode, - }); - }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode): Generator { - owner.ensureSnapshotActive(); - return yield* apiRequest("resolveModuleName", { - snapshot: owner.snapshotId, - resolver: owner.id, - moduleName, - containingDirectory, - resolutionMode, - }); - }, - ); + if (this.disposed) throw new Error("ModuleResolver is disposed"); } } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index edc919568652a..54f7ce613266c 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -72,8 +72,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, - type ModuleResolutionSet, - type ModuleResolverOptions, + type ModuleResolver, type NumberLiteralType, ObjectFlags, type Program, @@ -126,14 +125,13 @@ describe("API", () => { const lsp = undefined! as API; void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); - const resolutionSet = undefined! as ModuleResolutionSet; + const moduleResolver = undefined! as ModuleResolver; void lsp.getCurrentLanguageServerSnapshot({ createPrograms: [{ rootFiles: ["/index.ts"], options: { compilerOptions: {}, - moduleResolutions: resolutionSet, - resolveModuleName: () => ({ resolvedFileName: "/resolved.ts" }), + moduleResolver, }, }], }); @@ -509,34 +507,41 @@ describe("API", () => { fallback: "unresolved" as const, entries: [{ moduleName: "pkg", result: { resolvedFileName } }], }); - const setA = await api.createModuleResolutionSet(spec(providedA)); - const setB = await api.createModuleResolutionSet(spec(providedB)); - const createProgram = (moduleResolutions: ModuleResolverOptions["moduleResolutions"]) => ({ + const resolverA = await api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const resolverB = await api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedB) }); + const inlineResolverA = await api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const inlineResolverB = await api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const createProgram = (moduleResolver?: ModuleResolver) => ({ rootFiles: [root], options: { compilerOptions, - ...(moduleResolutions ? { moduleResolutions } : {}), + ...(moduleResolver ? { moduleResolver } : {}), }, }); - const initial = await api.createSnapshot({ createPrograms: [createProgram(setA)] }); + const initial = await api.createSnapshot({ createPrograms: [createProgram(resolverA)] }); const programId = initial.operation.createdPrograms[0].id; assert.deepEqual([...await initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + assert.equal( + (await resolverA.resolveModuleName("pkg", "/src", { snapshot: initial })).resolvedModule?.resolvedFileName, + providedA, + ); + assert.equal((await resolverA.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, providedA); const sameSet = await initial.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverA) }], }); assert.deepEqual([...await sameSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); fs.writeFile!(root, `import { value } from "pkg"; export const updated = value;`); const sameSetAfterEdit = await sameSet.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverA) }], fileNotifications: { changed: [root] }, }); assert.deepEqual([...await sameSetAfterEdit.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); const changedSet = await sameSetAfterEdit.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setB) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverB) }], }); assert.deepEqual([...await changedSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedB, root]); @@ -546,30 +551,69 @@ describe("API", () => { assert.deepEqual(await removedSet.getProgram(programId)!.getSourceFileNames(), [root]); const inline = await sameSetAfterEdit.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + reconfigurePrograms: [{ id: programId, ...createProgram(inlineResolverA) }], }); const repeatedInline = await inline.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + reconfigurePrograms: [{ id: programId, ...createProgram(inlineResolverB) }], }); assert.deepEqual([...await repeatedInline.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); let callbackCalls = 0; - const callbackOptions = { - compilerOptions, + const callbackResolver = await api.createModuleResolver(compilerOptions, { resolveModuleName: (moduleName: string) => { callbackCalls++; return moduleName === "pkg" ? { resolvedFileName: providedA } : undefined; }, - }; + }); const callbackSnapshot = await api.createSnapshot({ - createPrograms: [{ rootFiles: [root], options: callbackOptions }], + createPrograms: [createProgram(callbackResolver)], }); const callbackProgramId = callbackSnapshot.operation.createdPrograms[0].id; const repeatedCallback = await callbackSnapshot.update({ - reconfigurePrograms: [{ id: callbackProgramId, rootFiles: [root], options: callbackOptions }], + reconfigurePrograms: [{ id: callbackProgramId, ...createProgram(callbackResolver) }], }); assert.deepEqual([...await repeatedCallback.getProgram(callbackProgramId)!.getSourceFileNames()].sort(), [providedA, root]); - assert.equal(callbackCalls, 2); + assert.equal(callbackCalls, 1); + }); + + test("module resolver runs against snapshots or the host filesystem", async () => { + const packageJson = "/node_modules/pkg/package.json"; + const { api: disposableAPI, fs } = spawnAPIWithFS({ + [packageJson]: JSON.stringify({ name: "pkg", version: "1.0.0", types: "a.d.ts" }), + "/node_modules/pkg/a.d.ts": `export declare const value: "a";`, + }); + await using api = disposableAPI; + const resolver = await api.createModuleResolver({ + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }); + const firstSnapshot = await api.createSnapshot(); + assert.equal( + (await resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/a.d.ts", + ); + + fs.writeFile!(packageJson, JSON.stringify({ name: "pkg", version: "1.0.0", types: "b.d.ts" })); + fs.writeFile!("/node_modules/pkg/b.d.ts", `export declare const value: "b";`); + const secondSnapshot = await firstSnapshot.update({ + fileNotifications: { + changed: [packageJson], + created: ["/node_modules/pkg/b.d.ts"], + }, + }); + + assert.equal( + (await resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/a.d.ts", + ); + assert.equal( + (await resolver.resolveModuleName("pkg", "/src", { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/b.d.ts", + ); + assert.equal( + (await resolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, + "/node_modules/pkg/b.d.ts", + ); }); test("provided resolutions do not report native resolution provenance diagnostics", async () => { @@ -577,6 +621,24 @@ describe("API", () => { "/src/index.ts": `import { value } from "./value.ts"; export { value };`, "/value.ts": `export const value = 1;`, }); + const resolver = await api.createModuleResolver( + { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }, + { + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./value.ts", + result: { + resolvedFileName: "/value.ts", + }, + }], + }, + }, + ); const snapshot = await api.createSnapshot({ createPrograms: [{ rootFiles: ["/src/index.ts"], @@ -586,15 +648,7 @@ describe("API", () => { module: ModuleKind.NodeNext, moduleResolution: ModuleResolutionKind.NodeNext, }, - moduleResolutions: { - fallback: "unresolved", - entries: [{ - moduleName: "./value.ts", - result: { - resolvedFileName: "/value.ts", - }, - }], - }, + moduleResolver: resolver, }, }], }); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index c5f1e8892d752..cd73f30e7c494 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1523,13 +1523,11 @@ describe("API - generator batching", () => { fallback: "unresolved" as const, entries: [{ moduleName: "models", result: { resolvedFileName: "/src/models.ts" } }], }; - const moduleResolutionSet = api.batch(api.createModuleResolutionSet.gen(moduleResolutionSpec))[0]; - exercisedMethods.add("API.createModuleResolutionSet"); - const moduleResolver = api.batch(snapshot.createModuleResolver.gen( + const moduleResolver = api.batch(api.createModuleResolver.gen( { moduleResolution: ModuleResolutionKind.NodeNext }, - { moduleResolutions: moduleResolutionSet }, + { moduleResolutions: moduleResolutionSpec }, ))[0]; - exercisedMethods.add("Snapshot.createModuleResolver"); + exercisedMethods.add("API.createModuleResolver"); const cases: ParityCase[] = [ parityCase("API", "parseConfigFile", api.parseConfigFile, assertDeepEquivalent, "/tsconfig.json"), @@ -1764,6 +1762,10 @@ describe("API - generator batching", () => { assert.throws(() => disposableProgram.getSourceFileNames(), /snapshot .* not found/); assert.equal(disposableProgram.dispose(), undefined); exercisedMethods.add("Program.dispose"); + const disposableResolver = destructiveAPI.batch(destructiveAPI.createModuleResolver.gen({}))[0]; + destructiveAPI.batch(disposableResolver.dispose.gen()); + assert.equal(disposableResolver.dispose(), undefined); + exercisedMethods.add("ModuleResolver.dispose"); destructiveAPI.batch(destructiveAPI.close.gen()); assert.equal(destructiveAPI.close(), undefined); exercisedMethods.add("API.close"); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index e991b1ecce79e..b55ff7c430224 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -87,8 +87,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, - type ModuleResolutionSet, - type ModuleResolverOptions, + type ModuleResolver, type NumberLiteralType, ObjectFlags, type Program, @@ -134,14 +133,13 @@ describe("API", () => { const lsp = undefined! as API; void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); - const resolutionSet = undefined! as ModuleResolutionSet; + const moduleResolver = undefined! as ModuleResolver; void lsp.getCurrentLanguageServerSnapshot({ createPrograms: [{ rootFiles: ["/index.ts"], options: { compilerOptions: {}, - moduleResolutions: resolutionSet, - resolveModuleName: () => ({ resolvedFileName: "/resolved.ts" }), + moduleResolver, }, }], }); @@ -498,34 +496,41 @@ describe("API", () => { fallback: "unresolved" as const, entries: [{ moduleName: "pkg", result: { resolvedFileName } }], }); - const setA = api.createModuleResolutionSet(spec(providedA)); - const setB = api.createModuleResolutionSet(spec(providedB)); - const createProgram = (moduleResolutions: ModuleResolverOptions["moduleResolutions"]) => ({ + const resolverA = api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const resolverB = api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedB) }); + const inlineResolverA = api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const inlineResolverB = api.createModuleResolver(compilerOptions, { moduleResolutions: spec(providedA) }); + const createProgram = (moduleResolver?: ModuleResolver) => ({ rootFiles: [root], options: { compilerOptions, - ...(moduleResolutions ? { moduleResolutions } : {}), + ...(moduleResolver ? { moduleResolver } : {}), }, }); - const initial = api.createSnapshot({ createPrograms: [createProgram(setA)] }); + const initial = api.createSnapshot({ createPrograms: [createProgram(resolverA)] }); const programId = initial.operation.createdPrograms[0].id; assert.deepEqual([...initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); + assert.equal( + (resolverA.resolveModuleName("pkg", "/src", { snapshot: initial })).resolvedModule?.resolvedFileName, + providedA, + ); + assert.equal((resolverA.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, providedA); const sameSet = initial.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverA) }], }); assert.deepEqual([...sameSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); fs.writeFile!(root, `import { value } from "pkg"; export const updated = value;`); const sameSetAfterEdit = sameSet.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setA) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverA) }], fileNotifications: { changed: [root] }, }); assert.deepEqual([...sameSetAfterEdit.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); const changedSet = sameSetAfterEdit.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(setB) }], + reconfigurePrograms: [{ id: programId, ...createProgram(resolverB) }], }); assert.deepEqual([...changedSet.getProgram(programId)!.getSourceFileNames()].sort(), [providedB, root]); @@ -535,30 +540,69 @@ describe("API", () => { assert.deepEqual(removedSet.getProgram(programId)!.getSourceFileNames(), [root]); const inline = sameSetAfterEdit.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + reconfigurePrograms: [{ id: programId, ...createProgram(inlineResolverA) }], }); const repeatedInline = inline.update({ - reconfigurePrograms: [{ id: programId, ...createProgram(spec(providedA)) }], + reconfigurePrograms: [{ id: programId, ...createProgram(inlineResolverB) }], }); assert.deepEqual([...repeatedInline.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); let callbackCalls = 0; - const callbackOptions = { - compilerOptions, + const callbackResolver = api.createModuleResolver(compilerOptions, { resolveModuleName: (moduleName: string) => { callbackCalls++; return moduleName === "pkg" ? { resolvedFileName: providedA } : undefined; }, - }; + }); const callbackSnapshot = api.createSnapshot({ - createPrograms: [{ rootFiles: [root], options: callbackOptions }], + createPrograms: [createProgram(callbackResolver)], }); const callbackProgramId = callbackSnapshot.operation.createdPrograms[0].id; const repeatedCallback = callbackSnapshot.update({ - reconfigurePrograms: [{ id: callbackProgramId, rootFiles: [root], options: callbackOptions }], + reconfigurePrograms: [{ id: callbackProgramId, ...createProgram(callbackResolver) }], }); assert.deepEqual([...repeatedCallback.getProgram(callbackProgramId)!.getSourceFileNames()].sort(), [providedA, root]); - assert.equal(callbackCalls, 2); + assert.equal(callbackCalls, 1); + }); + + test("module resolver runs against snapshots or the host filesystem", () => { + const packageJson = "/node_modules/pkg/package.json"; + const { api: disposableAPI, fs } = spawnAPIWithFS({ + [packageJson]: JSON.stringify({ name: "pkg", version: "1.0.0", types: "a.d.ts" }), + "/node_modules/pkg/a.d.ts": `export declare const value: "a";`, + }); + using api = disposableAPI; + const resolver = api.createModuleResolver({ + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }); + const firstSnapshot = api.createSnapshot(); + assert.equal( + (resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/a.d.ts", + ); + + fs.writeFile!(packageJson, JSON.stringify({ name: "pkg", version: "1.0.0", types: "b.d.ts" })); + fs.writeFile!("/node_modules/pkg/b.d.ts", `export declare const value: "b";`); + const secondSnapshot = firstSnapshot.update({ + fileNotifications: { + changed: [packageJson], + created: ["/node_modules/pkg/b.d.ts"], + }, + }); + + assert.equal( + (resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/a.d.ts", + ); + assert.equal( + (resolver.resolveModuleName("pkg", "/src", { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/b.d.ts", + ); + assert.equal( + (resolver.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, + "/node_modules/pkg/b.d.ts", + ); }); test("provided resolutions do not report native resolution provenance diagnostics", () => { @@ -566,6 +610,24 @@ describe("API", () => { "/src/index.ts": `import { value } from "./value.ts"; export { value };`, "/value.ts": `export const value = 1;`, }); + const resolver = api.createModuleResolver( + { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }, + { + moduleResolutions: { + fallback: "unresolved", + entries: [{ + moduleName: "./value.ts", + result: { + resolvedFileName: "/value.ts", + }, + }], + }, + }, + ); const snapshot = api.createSnapshot({ createPrograms: [{ rootFiles: ["/src/index.ts"], @@ -575,15 +637,7 @@ describe("API", () => { module: ModuleKind.NodeNext, moduleResolution: ModuleResolutionKind.NodeNext, }, - moduleResolutions: { - fallback: "unresolved", - entries: [{ - moduleName: "./value.ts", - result: { - resolvedFileName: "/value.ts", - }, - }], - }, + moduleResolver: resolver, }, }], }); diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index f503739bf3ece..77f206806607a 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" ) type moduleResolutionMatchKey struct { @@ -47,7 +48,7 @@ func (p *callbackModuleResolutionProvider) ResolveModuleName( moduleName string, containingDirectory string, resolutionMode core.ResolutionMode, - _ func() *module.ResolvedModule, + fallback func() *module.ResolvedModule, ) (*module.ResolvedModule, error) { if p.base != nil { if result, found := p.base.lookup(moduleName, containingDirectory, resolutionMode); found { @@ -57,6 +58,9 @@ func (p *callbackModuleResolutionProvider) ResolveModuleName( return nil, nil } } + if p.callback == "" { + return fallback(), nil + } params := &ResolveModuleNameCallbackParams{ ModuleName: moduleName, ContainingDirectory: containingDirectory, @@ -202,54 +206,25 @@ func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { }) } -func (s *Session) resolveModuleResolutionSource(source *ModuleResolutionSource) (*providedModuleResolutions, error) { - if source == nil { - return nil, nil - } - - if source.Spec != nil && source.Set != 0 { - return nil, fmt.Errorf("%w: moduleResolutions cannot contain both spec and set", ErrClientError) - } - if source.Spec != nil { - return compileModuleResolutionSpec( - source.Spec, - s.nextModuleResolutionIdentity.Add(1), - s.currentDirectory(), - s.fileSystem().UseCaseSensitiveFileNames(), - ) - } - if source.Set == 0 { - return nil, fmt.Errorf("%w: moduleResolutions must contain spec or set", ErrClientError) - } - s.moduleResolutionSetsMu.RLock() - provider := s.moduleResolutionSets[source.Set] - s.moduleResolutionSetsMu.RUnlock() - if provider == nil { - return nil, fmt.Errorf("%w: module resolution set %d not found", ErrClientError, source.Set) - } - return provider, nil -} - func (s *Session) moduleResolutionProvider(ctx context.Context, options *CreateProgramOptions) (module.ResolutionProvider, error) { - provided, err := s.resolveModuleResolutionSource(options.ModuleResolutions) - if err != nil { - return nil, err + if options.ModuleResolver == 0 { + return nil, nil } - if options.ResolveModuleNameCallback == "" { - if provided == nil { - return nil, nil - } - return provided, nil + s.moduleResolversMu.RLock() + data := s.moduleResolvers[options.ModuleResolver] + s.moduleResolversMu.RUnlock() + if data == nil { + return nil, fmt.Errorf("%w: module resolver %d not found", ErrClientError, options.ModuleResolver) } - if s.conn == nil { + if data.resolveModuleNameCallback != "" && s.conn == nil { return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) } return &callbackModuleResolutionProvider{ - identity: s.nextModuleResolutionIdentity.Add(1), - base: provided, + identity: uint64(options.ModuleResolver), + base: data.provider, conn: s.conn, ctx: ctx, - callback: options.ResolveModuleNameCallback, + callback: data.resolveModuleNameCallback, currentDirectory: s.currentDirectory(), }, nil } @@ -265,9 +240,9 @@ func moduleResolutionError(snapshot *project.Snapshot) error { return nil } -func (s *Session) handleCreateModuleResolutionSet(params *CreateModuleResolutionSetParams) (ModuleResolutionSetID, error) { +func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) (ModuleResolverID, error) { provider, err := compileModuleResolutionSpec( - ¶ms.Spec, + params.ModuleResolutions, s.nextModuleResolutionIdentity.Add(1), s.currentDirectory(), s.fileSystem().UseCaseSensitiveFileNames(), @@ -275,66 +250,53 @@ func (s *Session) handleCreateModuleResolutionSet(params *CreateModuleResolution if err != nil { return 0, err } - id := ModuleResolutionSetID(s.nextModuleResolutionSetID.Add(1)) - s.moduleResolutionSetsMu.Lock() - s.moduleResolutionSets[id] = provider - s.moduleResolutionSetsMu.Unlock() + id := ModuleResolverID(s.nextModuleResolverID.Add(1)) + data := &moduleResolverData{ + compilerOptions: ¶ms.CompilerOptions, + provider: provider, + resolveModuleNameCallback: params.ResolveModuleNameCallback, + } + s.moduleResolversMu.Lock() + s.moduleResolvers[id] = data + s.moduleResolversMu.Unlock() return id, nil } -func (s *Session) handleReleaseModuleResolutionSet(params *ReleaseModuleResolutionSetParams) (any, error) { - if params.Set == 0 { - return nil, fmt.Errorf("%w: empty module resolution set handle", ErrClientError) - } - s.moduleResolutionSetsMu.Lock() - _, ok := s.moduleResolutionSets[params.Set] +func (s *Session) handleReleaseModuleResolver(params *ReleaseModuleResolverParams) (any, error) { + s.moduleResolversMu.Lock() + _, ok := s.moduleResolvers[params.Resolver] if ok { - delete(s.moduleResolutionSets, params.Set) + delete(s.moduleResolvers, params.Resolver) } - s.moduleResolutionSetsMu.Unlock() + s.moduleResolversMu.Unlock() if !ok { - return nil, fmt.Errorf("%w: module resolution set %d not found", ErrClientError, params.Set) + return nil, fmt.Errorf("%w: module resolver %d not found", ErrClientError, params.Resolver) } return nil, nil } -func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) (ModuleResolverID, error) { - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return 0, err - } - provider, err := s.resolveModuleResolutionSource(params.ModuleResolutions) - if err != nil { - return 0, err - } - id := ModuleResolverID(s.nextModuleResolverID.Add(1)) - data := &moduleResolverData{ - resolver: module.NewResolver(sd.snapshot, ¶ms.CompilerOptions, "", "", sd.snapshot.ContentMapperExtensions()), - provider: provider, - resolveModuleNameCallback: params.ResolveModuleNameCallback, - } - sd.moduleResolversMu.Lock() - if sd.moduleResolvers == nil { - sd.moduleResolvers = make(map[ModuleResolverID]*moduleResolverData) - } - sd.moduleResolvers[id] = data - sd.moduleResolversMu.Unlock() - return id, nil +type liveModuleResolutionHost struct { + fs vfs.FS + cwd string +} + +func (h *liveModuleResolutionHost) FS() vfs.FS { + return h.fs +} + +func (h *liveModuleResolutionHost) GetCurrentDirectory() string { + return h.cwd } func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveModuleNameParams) (*ResolveModuleNameResult, error) { if params.ModuleName == "" { return nil, fmt.Errorf("%w: moduleName is empty", ErrClientError) } - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return nil, err - } - sd.moduleResolversMu.RLock() - data := sd.moduleResolvers[params.Resolver] - sd.moduleResolversMu.RUnlock() + s.moduleResolversMu.RLock() + data := s.moduleResolvers[params.Resolver] + s.moduleResolversMu.RUnlock() if data == nil { - return nil, fmt.Errorf("%w: module resolver %d not found in snapshot %d", ErrClientError, params.Resolver, params.Snapshot) + return nil, fmt.Errorf("%w: module resolver %d not found", ErrClientError, params.Resolver) } mode := core.ResolutionModeNone @@ -346,38 +308,35 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo } containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.currentDirectory()), s.currentDirectory()) - var result *module.ResolvedModule - var trace []module.DiagAndArgs - var provided bool - if data.provider != nil { - result, provided = data.provider.lookup(params.ModuleName, containingDirectory, mode) - } - if !provided && (data.provider == nil || data.provider.fallbackToResolution) { - if data.resolveModuleNameCallback != "" { - if s.conn == nil { - return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) - } - callbackParams := &ResolveModuleNameCallbackParams{ - ModuleName: params.ModuleName, - ContainingDirectory: containingDirectory, - } - if params.ResolutionMode != nil { - callbackParams.ResolutionMode = params.ResolutionMode - } - callbackResult, err := s.conn.Call(ctx, data.resolveModuleNameCallback, callbackParams) - if err != nil { - return nil, fmt.Errorf("resolveModuleName callback failed: %w", err) - } - if len(callbackResult) != 0 && string(callbackResult) != "null" { - var providedResolution ProvidedModuleResolution - if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { - return nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) - } - result = providedModuleResolutionToResolvedModule(params.ModuleName, &providedResolution, s.currentDirectory()) - } - } else { - result, trace = data.resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) + var host module.ResolutionHost + var extraExtensions []string + if params.Snapshot != 0 { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err } + host = sd.snapshot + extraExtensions = sd.snapshot.ContentMapperExtensions() + } else { + host = &liveModuleResolutionHost{fs: s.fileSystem(), cwd: s.currentDirectory()} + } + resolver := module.NewResolver(host, data.compilerOptions, "", "", extraExtensions) + var trace []module.DiagAndArgs + behavior := &callbackModuleResolutionProvider{ + identity: uint64(params.Resolver), + base: data.provider, + conn: s.conn, + ctx: ctx, + callback: data.resolveModuleNameCallback, + currentDirectory: s.currentDirectory(), + } + result, err := behavior.ResolveModuleName(params.ModuleName, containingDirectory, mode, func() *module.ResolvedModule { + result, resolutionTrace := resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) + trace = resolutionTrace + return result + }) + if err != nil { + return nil, err } return &ResolveModuleNameResult{ ResolvedModule: newResolvedModuleResponse(result), diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index d310516836a02..0dd5756b93c8e 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -34,15 +34,14 @@ var ( type Method string type ( - SnapshotID uint64 - ProjectID string - SyntheticProjectID string - ModuleResolutionSetID uint64 - ModuleResolverID uint64 - SymbolID uint64 - TypeID uint32 - SignatureID uint64 - NodeHandle string + SnapshotID uint64 + ProjectID string + SyntheticProjectID string + ModuleResolverID uint64 + SymbolID uint64 + TypeID uint32 + SignatureID uint64 + NodeHandle string ) func ProjectHandle(p *project.Project) ProjectID { @@ -78,9 +77,8 @@ const ( MethodCreateSnapshot Method = "createSnapshot" MethodUpdateSnapshot Method = "updateSnapshot" MethodGetCurrentLanguageServerSnapshot Method = "getCurrentLanguageServerSnapshot" - MethodCreateModuleResolutionSet Method = "createModuleResolutionSet" - MethodReleaseModuleResolutionSet Method = "releaseModuleResolutionSet" MethodCreateModuleResolver Method = "createModuleResolver" + MethodReleaseModuleResolver Method = "releaseModuleResolver" MethodResolveModuleName Method = "resolveModuleName" MethodParseCommandLine Method = "parseCommandLine" MethodReadConfigFile Method = "readConfigFile" @@ -455,8 +453,7 @@ type CreateProgramOptions struct { CompilerOptions core.CompilerOptions `json:"compilerOptions"` ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` - ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` - ResolveModuleNameCallback string `json:"resolveModuleNameCallback,omitempty"` + ModuleResolver ModuleResolverID `json:"moduleResolver,omitempty"` } type ( @@ -487,28 +484,18 @@ type ProvidedModuleResolution struct { PackageID *PackageId `json:"packageId,omitempty"` } -type ModuleResolutionSource struct { - Spec *ModuleResolutionSpec `json:"spec,omitempty"` - Set ModuleResolutionSetID `json:"set,omitempty"` -} - -type CreateModuleResolutionSetParams struct { - Spec ModuleResolutionSpec `json:"spec"` -} - -type ReleaseModuleResolutionSetParams struct { - Set ModuleResolutionSetID `json:"set"` +type CreateModuleResolverParams struct { + CompilerOptions core.CompilerOptions `json:"compilerOptions"` + ModuleResolutions *ModuleResolutionSpec `json:"moduleResolutions,omitempty"` + ResolveModuleNameCallback string `json:"resolveModuleNameCallback,omitempty"` } -type CreateModuleResolverParams struct { - Snapshot SnapshotID `json:"snapshot"` - CompilerOptions core.CompilerOptions `json:"compilerOptions"` - ModuleResolutions *ModuleResolutionSource `json:"moduleResolutions,omitempty"` - ResolveModuleNameCallback string `json:"resolveModuleNameCallback,omitempty"` +type ReleaseModuleResolverParams struct { + Resolver ModuleResolverID `json:"resolver"` } type ResolveModuleNameParams struct { - Snapshot SnapshotID `json:"snapshot"` + Snapshot SnapshotID `json:"snapshot,omitempty"` Resolver ModuleResolverID `json:"resolver"` ModuleName string `json:"moduleName"` ContainingDirectory DocumentIdentifier `json:"containingDirectory"` @@ -576,9 +563,8 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodCreateSnapshot: unmarshallerFor[CreateSnapshotParams], MethodUpdateSnapshot: unmarshallerFor[UpdateSnapshotParams], MethodGetCurrentLanguageServerSnapshot: unmarshallerFor[GetCurrentLanguageServerSnapshotParams], - MethodCreateModuleResolutionSet: unmarshallerFor[CreateModuleResolutionSetParams], - MethodReleaseModuleResolutionSet: unmarshallerFor[ReleaseModuleResolutionSetParams], MethodCreateModuleResolver: unmarshallerFor[CreateModuleResolverParams], + MethodReleaseModuleResolver: unmarshallerFor[ReleaseModuleResolverParams], MethodResolveModuleName: unmarshallerFor[ResolveModuleNameParams], MethodParseCommandLine: unmarshallerFor[ParseCommandLineParams], MethodReadConfigFile: unmarshallerFor[ReadConfigFileParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 210b904025791..865db3830d404 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -55,9 +55,6 @@ type snapshotData struct { openProjects collections.Set[tspath.Path] openFiles collections.Set[tspath.Path] - moduleResolvers map[ModuleResolverID]*moduleResolverData - moduleResolversMu sync.RWMutex - // Symbol IDs come from ast.GetSymbolId, a global atomic counter, so the same // *ast.Symbol pointer always has the same unique ID across all projects in the // snapshot. Symbols are registered snapshot-wide to ensure identity semantics: @@ -78,7 +75,7 @@ type snapshotData struct { } type moduleResolverData struct { - resolver *module.Resolver + compilerOptions *core.CompilerOptions provider *providedModuleResolutions resolveModuleNameCallback string } @@ -431,11 +428,10 @@ type Session struct { languageServerUpdateMu sync.Mutex - moduleResolutionSets map[ModuleResolutionSetID]*providedModuleResolutions - moduleResolutionSetsMu sync.RWMutex - nextModuleResolutionSetID atomic.Uint64 nextModuleResolutionIdentity atomic.Uint64 nextModuleResolverID atomic.Uint64 + moduleResolvers map[ModuleResolverID]*moduleResolverData + moduleResolversMu sync.RWMutex conn ipc.Conn cpuProfiler pprof.CPUProfiler @@ -479,11 +475,11 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont withLocale = func(ctx context.Context) context.Context { return ctx } } s := &Session{ - id: formatSessionID(id), - snapshotHost: snapshotHost, - withLocale: withLocale, - snapshots: make(map[SnapshotID]*snapshotData), - moduleResolutionSets: make(map[ModuleResolutionSetID]*providedModuleResolutions), + id: formatSessionID(id), + snapshotHost: snapshotHost, + withLocale: withLocale, + snapshots: make(map[SnapshotID]*snapshotData), + moduleResolvers: make(map[ModuleResolverID]*moduleResolverData), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses @@ -705,12 +701,10 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleUpdateSnapshot(ctx, parsed.(*UpdateSnapshotParams)) case string(MethodGetCurrentLanguageServerSnapshot): return s.handleGetCurrentLanguageServerSnapshot(ctx, parsed.(*GetCurrentLanguageServerSnapshotParams)) - case string(MethodCreateModuleResolutionSet): - return s.handleCreateModuleResolutionSet(parsed.(*CreateModuleResolutionSetParams)) - case string(MethodReleaseModuleResolutionSet): - return s.handleReleaseModuleResolutionSet(parsed.(*ReleaseModuleResolutionSetParams)) case string(MethodCreateModuleResolver): return s.handleCreateModuleResolver(parsed.(*CreateModuleResolverParams)) + case string(MethodReleaseModuleResolver): + return s.handleReleaseModuleResolver(parsed.(*ReleaseModuleResolverParams)) case string(MethodResolveModuleName): return s.handleResolveModuleName(ctx, parsed.(*ResolveModuleNameParams)) case string(MethodParseCommandLine): diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 8bd50aaab0d1c..baf24bbe0104e 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -43,7 +43,6 @@ func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { snapshot, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{}) assert.NilError(t, err) resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ - Snapshot: snapshot.Snapshot, CompilerOptions: core.CompilerOptions{ Module: core.ModuleKindNodeNext, ModuleResolution: core.ModuleResolutionKindNodeNext, @@ -89,12 +88,9 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { providedResolutionEntry("pkg", "/home/projects/p/src", &esm, "/home/projects/p/exact.d.ts"), }, } - setID, err := session.handleCreateModuleResolutionSet(&CreateModuleResolutionSetParams{Spec: spec}) - assert.NilError(t, err) resolverID, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ - Snapshot: snapshot.Snapshot, CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, - ModuleResolutions: &ModuleResolutionSource{Set: setID}, + ModuleResolutions: &spec, }) assert.NilError(t, err) @@ -126,15 +122,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { assert.NilError(t, err) assert.Assert(t, unresolved.ResolvedModule == nil) - _, err = session.handleReleaseModuleResolutionSet(&ReleaseModuleResolutionSetParams{Set: setID}) - assert.NilError(t, err) assertResolution("/home/projects/p/src", core.ModuleKindESNext, "/home/projects/p/exact.d.ts") - _, err = session.handleCreateModuleResolver(&CreateModuleResolverParams{ - Snapshot: snapshot.Snapshot, - CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, - ModuleResolutions: &ModuleResolutionSource{Set: setID}, - }) - assert.ErrorContains(t, err, "not found") } func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { @@ -149,6 +137,20 @@ func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { defer projectSession.Close() session := NewLSPSession(projectSession, nil) defer session.Close() + resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ModuleResolutions: &ModuleResolutionSpec{ + Fallback: ModuleResolutionFallbackUnresolved, + Entries: []*ModuleResolutionEntry{ + providedResolutionEntry("pkg", "", nil, provided), + }, + }, + }) + assert.NilError(t, err) response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ SnapshotRequestChangesParams: SnapshotRequestChangesParams{ //nolint:modernize @@ -160,14 +162,7 @@ func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { Module: core.ModuleKindNodeNext, ModuleResolution: core.ModuleResolutionKindNodeNext, }, - ModuleResolutions: &ModuleResolutionSource{ - Spec: &ModuleResolutionSpec{ - Fallback: ModuleResolutionFallbackUnresolved, - Entries: []*ModuleResolutionEntry{ - providedResolutionEntry("pkg", "", nil, provided), - }, - }, - }, + ModuleResolver: resolver, }, }}, }, @@ -193,23 +188,24 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { snapshot, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{}) assert.NilError(t, err) resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ - Snapshot: snapshot.Snapshot, CompilerOptions: core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, - ModuleResolutions: &ModuleResolutionSource{Spec: &ModuleResolutionSpec{ + ModuleResolutions: &ModuleResolutionSpec{ Fallback: ModuleResolutionFallbackUnresolved, - Entries: []*ModuleResolutionEntry{{ - ModuleName: "pkg", - Result: &ProvidedModuleResolution{ - ResolvedFileName: &DocumentIdentifier{FileName: "/store/pkg/index.d.ts"}, - OriginalPath: &DocumentIdentifier{FileName: "/node_modules/pkg/index.d.ts"}, - PackageID: &PackageId{ - Name: "pkg", - SubModuleName: "", - Version: "1.2.3", + Entries: []*ModuleResolutionEntry{ + { + ModuleName: "pkg", + Result: &ProvidedModuleResolution{ + ResolvedFileName: &DocumentIdentifier{FileName: "/store/pkg/index.d.ts"}, + OriginalPath: &DocumentIdentifier{FileName: "/node_modules/pkg/index.d.ts"}, + PackageID: &PackageId{ + Name: "pkg", + SubModuleName: "", + Version: "1.2.3", + }, }, }, - }}, - }}, + }, + }, }) assert.NilError(t, err) result, err := session.handleResolveModuleName(context.Background(), &ResolveModuleNameParams{ From 9969e793fa17b5fa2b0ed55147720e91bc960a7b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 11:37:10 -0700 Subject: [PATCH 10/20] Allow sync client reentrant calls --- packages/typescript/test/async/api.test.ts | 35 ++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 35 ++++++++++++++++++ tsc/internal/ipc/conn_sync.go | 41 +++++++++++++++------- 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 54f7ce613266c..a643d48d13f78 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -616,6 +616,41 @@ describe("API", () => { ); }); + test("module resolver callbacks can delegate to another resolver", async () => { + await using api = spawnAPI({ + "/src/index.ts": `import "custom"; import "native";`, + "/custom.d.ts": `export {};`, + "/node_modules/native/package.json": JSON.stringify({ name: "native", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/native/index.d.ts": `export {};`, + }); + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = await api.createModuleResolver(compilerOptions); + const customResolver = await api.createModuleResolver(compilerOptions, { + resolveModuleName: async (moduleName, containingDirectory, resolutionMode) => { + if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; + return (await defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode === undefined ? undefined : { resolutionMode }, + )).resolvedModule; + }, + }); + const snapshot = await api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { compilerOptions, moduleResolver: customResolver }, + }], + }); + assert.deepEqual( + [...await snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), + ["/custom.d.ts", "/node_modules/native/index.d.ts", "/src/index.ts"], + ); + }); + test("provided resolutions do not report native resolution provenance diagnostics", async () => { await using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index b55ff7c430224..846f1a4c53f80 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -605,6 +605,41 @@ describe("API", () => { ); }); + test("module resolver callbacks can delegate to another resolver", () => { + using api = spawnAPI({ + "/src/index.ts": `import "custom"; import "native";`, + "/custom.d.ts": `export {};`, + "/node_modules/native/package.json": JSON.stringify({ name: "native", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/native/index.d.ts": `export {};`, + }); + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = api.createModuleResolver(compilerOptions); + const customResolver = api.createModuleResolver(compilerOptions, { + resolveModuleName: (moduleName, containingDirectory, resolutionMode) => { + if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; + return (defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode === undefined ? undefined : { resolutionMode }, + )).resolvedModule; + }, + }); + const snapshot = api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { compilerOptions, moduleResolver: customResolver }, + }], + }); + assert.deepEqual( + [...snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), + ["/custom.d.ts", "/node_modules/native/index.d.ts", "/src/index.ts"], + ); + }); + test("provided resolutions do not report native resolution provenance diagnostics", () => { using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, diff --git a/tsc/internal/ipc/conn_sync.go b/tsc/internal/ipc/conn_sync.go index cc3e001e7ef51..a0b0cecb3c3d1 100644 --- a/tsc/internal/ipc/conn_sync.go +++ b/tsc/internal/ipc/conn_sync.go @@ -186,21 +186,38 @@ func (c *SyncConn) Call(ctx context.Context, method string, params any) (json.Va return nil, ctx.Err() } - // Read the response inline. - msg, err := c.protocol.ReadMessage() - if err != nil { - return nil, err - } + for { + // Read the response inline. + msg, err := c.protocol.ReadMessage() + if err != nil { + return nil, err + } - if msg.IsResponse() && msg.ID != nil && msg.ID.String() == method { - if msg.Error != nil { - return nil, fmt.Errorf("ipc: remote error [%d]: %s", msg.Error.Code, msg.Error.Message) + if msg.IsResponse() && msg.ID != nil && msg.ID.String() == method { + if msg.Error != nil { + return nil, fmt.Errorf("ipc: remote error [%d]: %s", msg.Error.Code, msg.Error.Message) + } + return msg.Result, nil + } + if msg.IsRequest() { + // A synchronous client callback may make a nested API request. Release + // the protocol lock while handling it so nested callbacks can proceed. + c.mu.Unlock() + err := c.handleRequest(ctx, msg) + c.mu.Lock() + if err != nil { + return nil, err + } + continue } - return msg.Result, nil + if msg.IsNotification() { + c.mu.Unlock() + c.handleNotification(ctx, msg) + c.mu.Lock() + continue + } + return nil, fmt.Errorf("ipc: unexpected message while waiting for %q response", method) } - - // Unexpected message while waiting for response - return nil, fmt.Errorf("ipc: unexpected message while waiting for %q response", method) } // Notify sends a notification to the client (no response expected). From a3883d95c5819e3bbe663acb521005d1043cdd53 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 13:34:43 -0700 Subject: [PATCH 11/20] Move resolutionMode to top-level parameter --- packages/typescript/src/api/async/api.ts | 5 +++-- packages/typescript/src/api/sync/api.ts | 12 ++++++------ packages/typescript/test/async/api.test.ts | 10 +++++----- packages/typescript/test/sync/api.test.ts | 10 +++++----- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 8e3108249b761..636ee636a80a3 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -897,7 +897,8 @@ export class ModuleResolver { async resolveModuleName( moduleName: string, containingDirectory: DocumentIdentifier, - options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }, + resolutionMode?: ResolutionMode, + options?: { snapshot?: Snapshot; }, ): Promise { this.ensureNotDisposed(); if (options?.snapshot?.isDisposed()) { @@ -908,7 +909,7 @@ export class ModuleResolver { resolver: this.id, moduleName, containingDirectory, - resolutionMode: options?.resolutionMode, + resolutionMode, }); } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 0d72b737dc1b2..e1fcc1d509585 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1474,14 +1474,14 @@ export class ModuleResolver { } get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): ResolveModuleNameResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): Generator; + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): ResolveModuleNameResult { + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): ResolveModuleNameResult { owner.ensureNotDisposed(); if (options?.snapshot?.isDisposed()) { throw new Error("Snapshot is disposed"); @@ -1491,10 +1491,10 @@ export class ModuleResolver { resolver: owner.id, moduleName, containingDirectory, - resolutionMode: options?.resolutionMode, + resolutionMode, }); }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, options?: { resolutionMode?: ResolutionMode; snapshot?: Snapshot; }): Generator { + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): Generator { owner.ensureNotDisposed(); if (options?.snapshot?.isDisposed()) { throw new Error("Snapshot is disposed"); @@ -1504,7 +1504,7 @@ export class ModuleResolver { resolver: owner.id, moduleName, containingDirectory, - resolutionMode: options?.resolutionMode, + resolutionMode, }); }, ); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index a643d48d13f78..7d6f65de16ba7 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -523,7 +523,7 @@ describe("API", () => { const programId = initial.operation.createdPrograms[0].id; assert.deepEqual([...await initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); assert.equal( - (await resolverA.resolveModuleName("pkg", "/src", { snapshot: initial })).resolvedModule?.resolvedFileName, + (await resolverA.resolveModuleName("pkg", "/src", undefined, { snapshot: initial })).resolvedModule?.resolvedFileName, providedA, ); assert.equal((await resolverA.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, providedA); @@ -589,7 +589,7 @@ describe("API", () => { }); const firstSnapshot = await api.createSnapshot(); assert.equal( - (await resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + (await resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/a.d.ts", ); @@ -603,11 +603,11 @@ describe("API", () => { }); assert.equal( - (await resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + (await resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/a.d.ts", ); assert.equal( - (await resolver.resolveModuleName("pkg", "/src", { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, + (await resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/b.d.ts", ); assert.equal( @@ -635,7 +635,7 @@ describe("API", () => { return (await defaultResolver.resolveModuleName( moduleName, containingDirectory, - resolutionMode === undefined ? undefined : { resolutionMode }, + resolutionMode, )).resolvedModule; }, }); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 846f1a4c53f80..6178774688829 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -512,7 +512,7 @@ describe("API", () => { const programId = initial.operation.createdPrograms[0].id; assert.deepEqual([...initial.getProgram(programId)!.getSourceFileNames()].sort(), [providedA, root]); assert.equal( - (resolverA.resolveModuleName("pkg", "/src", { snapshot: initial })).resolvedModule?.resolvedFileName, + (resolverA.resolveModuleName("pkg", "/src", undefined, { snapshot: initial })).resolvedModule?.resolvedFileName, providedA, ); assert.equal((resolverA.resolveModuleName("pkg", "/src")).resolvedModule?.resolvedFileName, providedA); @@ -578,7 +578,7 @@ describe("API", () => { }); const firstSnapshot = api.createSnapshot(); assert.equal( - (resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + (resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/a.d.ts", ); @@ -592,11 +592,11 @@ describe("API", () => { }); assert.equal( - (resolver.resolveModuleName("pkg", "/src", { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, + (resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: firstSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/a.d.ts", ); assert.equal( - (resolver.resolveModuleName("pkg", "/src", { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, + (resolver.resolveModuleName("pkg", "/src", undefined, { snapshot: secondSnapshot })).resolvedModule?.resolvedFileName, "/node_modules/pkg/b.d.ts", ); assert.equal( @@ -624,7 +624,7 @@ describe("API", () => { return (defaultResolver.resolveModuleName( moduleName, containingDirectory, - resolutionMode === undefined ? undefined : { resolutionMode }, + resolutionMode, )).resolvedModule; }, }); From f8fd1917a840c8f9ea66db9e78dbf7dd85395afa Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 15:41:37 -0700 Subject: [PATCH 12/20] Refine --- packages/typescript/src/api/async/api.ts | 41 +++++- .../typescript/src/api/proto.generated.ts | 1 + packages/typescript/src/api/sync/api.ts | 52 +++++-- packages/typescript/test/async/api.test.ts | 43 +++++- packages/typescript/test/sync/api.test.ts | 43 +++++- tsc/internal/api/module_resolution.go | 134 +++++++++++------- tsc/internal/api/proto.go | 2 + tsc/internal/api/session.go | 22 +-- .../api/session_module_resolution_test.go | 15 +- tsc/internal/compiler/fileloader.go | 14 +- tsc/internal/compiler/program.go | 10 +- tsc/internal/module/resolver.go | 4 + tsc/internal/module/types.go | 9 +- tsc/internal/project/project.go | 12 +- .../project/projectcollectionbuilder.go | 12 +- tsc/internal/project/snapshot.go | 2 +- 16 files changed, 304 insertions(+), 112 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 636ee636a80a3..f70c721998813 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -246,7 +246,11 @@ export interface ModuleResolverOptions { resolveModuleName?: ResolveModuleNameCallback; } -export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; +export interface ResolveModuleNameCallbackOptions { + snapshot: InProgressSnapshot; +} + +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { moduleResolver?: ModuleResolver; @@ -265,13 +269,25 @@ export type LanguageServerSnapshotChanges = Omit void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; + const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { - const { moduleName, containingDirectory, resolutionMode } = params as { + const { moduleName, containingDirectory, resolutionMode, inProgressSnapshot } = params as { moduleName: string; containingDirectory: string; resolutionMode?: ResolutionMode; + inProgressSnapshot: number; }; - return callback(moduleName, containingDirectory, resolutionMode); + let snapshot = inProgressSnapshots.get(inProgressSnapshot); + if (snapshot === undefined) { + snapshot = new InProgressSnapshot(inProgressSnapshot); + inProgressSnapshots.set(inProgressSnapshot, snapshot); + } + return callback( + moduleName, + containingDirectory, + resolutionMode, + { snapshot }, + ); }); return { name, dispose }; } @@ -898,14 +914,15 @@ export class ModuleResolver { moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, - options?: { snapshot?: Snapshot; }, + options?: { snapshot?: Snapshot | InProgressSnapshot; }, ): Promise { this.ensureNotDisposed(); - if (options?.snapshot?.isDisposed()) { + if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { throw new Error("Snapshot is disposed"); } return this.client.apiRequest("resolveModuleName", { - snapshot: options?.snapshot?.id, + snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, + inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, resolver: this.id, moduleName, containingDirectory, @@ -930,6 +947,18 @@ export class ModuleResolver { } } +export class InProgressSnapshot { + private readonly _inProgressSnapshotBrand = undefined; + + /** @internal */ + readonly id: number; + + /** @internal */ + constructor(id: number) { + this.id = id; + } +} + class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index ea36bbc9de542..ef697ac2b16ee 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -266,6 +266,7 @@ export interface ReleaseModuleResolverParams { export interface ResolveModuleNameParams { snapshot?: number | undefined; + inProgressSnapshot?: number | undefined; resolver: number; moduleName: string; containingDirectory: DocumentIdentifier; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index e1fcc1d509585..2aca46af612fc 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -263,7 +263,11 @@ export interface ModuleResolverOptions { resolveModuleName?: ResolveModuleNameCallback; } -export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined) => ProvidedModuleResolution | undefined; +export interface ResolveModuleNameCallbackOptions { + snapshot: InProgressSnapshot; +} + +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { moduleResolver?: ModuleResolver; @@ -282,13 +286,25 @@ export type LanguageServerSnapshotChanges = Omit void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; + const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { - const { moduleName, containingDirectory, resolutionMode } = params as { + const { moduleName, containingDirectory, resolutionMode, inProgressSnapshot } = params as { moduleName: string; containingDirectory: string; resolutionMode?: ResolutionMode; + inProgressSnapshot: number; }; - return callback(moduleName, containingDirectory, resolutionMode); + let snapshot = inProgressSnapshots.get(inProgressSnapshot); + if (snapshot === undefined) { + snapshot = new InProgressSnapshot(inProgressSnapshot); + inProgressSnapshots.set(inProgressSnapshot, snapshot); + } + return callback( + moduleName, + containingDirectory, + resolutionMode, + { snapshot }, + ); }); return { name, dispose }; } @@ -1474,33 +1490,35 @@ export class ModuleResolver { } get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): ResolveModuleNameResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): Generator; + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): ResolveModuleNameResult { + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): ResolveModuleNameResult { owner.ensureNotDisposed(); - if (options?.snapshot?.isDisposed()) { + if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { throw new Error("Snapshot is disposed"); } return owner.client.apiRequest("resolveModuleName", { - snapshot: options?.snapshot?.id, + snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, + inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, resolver: owner.id, moduleName, containingDirectory, resolutionMode, }); }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot; }): Generator { + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): Generator { owner.ensureNotDisposed(); - if (options?.snapshot?.isDisposed()) { + if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { throw new Error("Snapshot is disposed"); } return yield* apiRequest("resolveModuleName", { - snapshot: options?.snapshot?.id, + snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, + inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, resolver: owner.id, moduleName, containingDirectory, @@ -1543,6 +1561,18 @@ export class ModuleResolver { } } +export class InProgressSnapshot { + private readonly _inProgressSnapshotBrand = undefined; + + /** @internal */ + readonly id: number; + + /** @internal */ + constructor(id: number) { + this.id = id; + } +} + class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 7d6f65de16ba7..a7d54f26dde95 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -629,13 +629,16 @@ describe("API", () => { moduleResolution: ModuleResolutionKind.NodeNext, }; const defaultResolver = await api.createModuleResolver(compilerOptions); + const callbackSnapshots: object[] = []; const customResolver = await api.createModuleResolver(compilerOptions, { - resolveModuleName: async (moduleName, containingDirectory, resolutionMode) => { + resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + callbackSnapshots.push(snapshot); if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; return (await defaultResolver.resolveModuleName( moduleName, containingDirectory, resolutionMode, + { snapshot }, )).resolvedModule; }, }); @@ -649,6 +652,44 @@ describe("API", () => { [...await snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), ["/custom.d.ts", "/node_modules/native/index.d.ts", "/src/index.ts"], ); + assert.equal(callbackSnapshots.length, 2); + assert.equal(callbackSnapshots[0], callbackSnapshots[1]); + }); + + test("module resolver callbacks can resolve against the in-progress snapshot filesystem", async () => { + await using api = spawnAPI({ + "/src/index.ts": `import "layered";`, + }); + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = await api.createModuleResolver(compilerOptions); + const customResolver = await api.createModuleResolver(compilerOptions, { + resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + return (await defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode, + { snapshot }, + )).resolvedModule; + }, + }); + const snapshot = await api.createSnapshot({ + fileSystem: createFileSystemLayer([ + ["/node_modules/layered/package.json", JSON.stringify({ name: "layered", version: "1.0.0", types: "index.d.ts" })], + ["/node_modules/layered/index.d.ts", `export {};`], + ]), + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { compilerOptions, moduleResolver: customResolver }, + }], + }); + assert.deepEqual( + [...await snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), + ["/node_modules/layered/index.d.ts", "/src/index.ts"], + ); }); test("provided resolutions do not report native resolution provenance diagnostics", async () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6178774688829..2401b86548e4e 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -618,13 +618,16 @@ describe("API", () => { moduleResolution: ModuleResolutionKind.NodeNext, }; const defaultResolver = api.createModuleResolver(compilerOptions); + const callbackSnapshots: object[] = []; const customResolver = api.createModuleResolver(compilerOptions, { - resolveModuleName: (moduleName, containingDirectory, resolutionMode) => { + resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + callbackSnapshots.push(snapshot); if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; return (defaultResolver.resolveModuleName( moduleName, containingDirectory, resolutionMode, + { snapshot }, )).resolvedModule; }, }); @@ -638,6 +641,44 @@ describe("API", () => { [...snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), ["/custom.d.ts", "/node_modules/native/index.d.ts", "/src/index.ts"], ); + assert.equal(callbackSnapshots.length, 2); + assert.equal(callbackSnapshots[0], callbackSnapshots[1]); + }); + + test("module resolver callbacks can resolve against the in-progress snapshot filesystem", () => { + using api = spawnAPI({ + "/src/index.ts": `import "layered";`, + }); + const compilerOptions = { + noLib: true, + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = api.createModuleResolver(compilerOptions); + const customResolver = api.createModuleResolver(compilerOptions, { + resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + return (defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode, + { snapshot }, + )).resolvedModule; + }, + }); + const snapshot = api.createSnapshot({ + fileSystem: createFileSystemLayer([ + ["/node_modules/layered/package.json", JSON.stringify({ name: "layered", version: "1.0.0", types: "index.d.ts" })], + ["/node_modules/layered/index.d.ts", `export {};`], + ]), + createPrograms: [{ + rootFiles: ["/src/index.ts"], + options: { compilerOptions, moduleResolver: customResolver }, + }], + }); + assert.deepEqual( + [...snapshot.operation.createdPrograms[0].getSourceFileNames()].sort(), + ["/node_modules/layered/index.d.ts", "/src/index.ts"], + ); }); test("provided resolutions do not report native resolution provenance diagnostics", () => { diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 77f206806607a..d43ae0abf869e 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -31,58 +31,76 @@ type providedModuleResolutions struct { useCaseSensitive bool } -type callbackModuleResolutionProvider struct { +type moduleResolutionProviderFactory struct { identity uint64 base *providedModuleResolutions + session *Session conn ipc.Conn ctx context.Context callback string currentDirectory string } -func (p *callbackModuleResolutionProvider) Identity() uint64 { - return p.identity +func (f *moduleResolutionProviderFactory) Identity() uint64 { + return f.identity } -func (p *callbackModuleResolutionProvider) ResolveModuleName( +type moduleResolutionProvider struct { + factory *moduleResolutionProviderFactory + inProgressSnapshot uint64 + fallbackResolver *module.Resolver +} + +func (f *moduleResolutionProviderFactory) NewProvider(fallback *module.Resolver) (module.ResolutionProvider, func()) { + inProgressSnapshot := f.session.registerInProgressSnapshot(fallback) + provider := &moduleResolutionProvider{ + factory: f, + inProgressSnapshot: inProgressSnapshot, + fallbackResolver: fallback, + } + return provider, func() { + f.session.releaseInProgressSnapshot(inProgressSnapshot) + } +} + +func (p *moduleResolutionProvider) ResolveModuleName( moduleName string, containingDirectory string, resolutionMode core.ResolutionMode, - fallback func() *module.ResolvedModule, -) (*module.ResolvedModule, error) { - if p.base != nil { - if result, found := p.base.lookup(moduleName, containingDirectory, resolutionMode); found { - return result, nil +) (*module.ResolvedModule, []module.DiagAndArgs, error) { + f := p.factory + if f.base != nil { + if result, found := f.base.lookup(moduleName, containingDirectory, resolutionMode); found { + return result, nil, nil } - if !p.base.fallbackToResolution { - return nil, nil + if !f.base.fallbackToResolution { + return nil, nil, nil } } - if p.callback == "" { - return fallback(), nil + if f.callback == "" { + result, trace := p.fallbackResolver.ResolveModuleNameFromDirectory(moduleName, containingDirectory, resolutionMode) + return result, trace, nil } params := &ResolveModuleNameCallbackParams{ ModuleName: moduleName, ContainingDirectory: containingDirectory, } + params.InProgressSnapshot = p.inProgressSnapshot mode := ResolutionMode(resolutionMode) params.ResolutionMode = &mode - callbackResult, err := p.conn.Call(p.ctx, p.callback, params) + callbackResult, err := f.conn.Call(f.ctx, f.callback, params) if err != nil { - return nil, fmt.Errorf("resolveModuleName callback failed: %w", err) + return nil, nil, fmt.Errorf("resolveModuleName callback failed: %w", err) } + if len(callbackResult) == 0 || string(callbackResult) == "null" { - return nil, nil + return nil, nil, nil } var providedResolution ProvidedModuleResolution if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { - return nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) + return nil, nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) } - return providedModuleResolutionToResolvedModule(moduleName, &providedResolution, p.currentDirectory), nil -} - -func (p *providedModuleResolutions) Identity() uint64 { - return p.identity + return providedModuleResolutionToResolvedModule(&providedResolution, f.currentDirectory), nil, nil } func (p *providedModuleResolutions) lookup(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { @@ -101,21 +119,6 @@ func (p *providedModuleResolutions) lookup(moduleName string, containingDirector return nil, false } -func (p *providedModuleResolutions) ResolveModuleName( - moduleName string, - containingDirectory string, - resolutionMode core.ResolutionMode, - fallback func() *module.ResolvedModule, -) (*module.ResolvedModule, error) { - if result, found := p.lookup(moduleName, containingDirectory, resolutionMode); found { - return result, nil - } - if p.fallbackToResolution { - return fallback(), nil - } - return nil, nil -} - func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { if spec == nil { return nil, nil @@ -166,14 +169,14 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu return nil, fmt.Errorf("%w: duplicate module resolution entry for %q", ErrClientError, entry.ModuleName) } - provided := providedModuleResolutionToResolvedModule(entry.ModuleName, entry.Result, currentDirectory) + provided := providedModuleResolutionToResolvedModule(entry.Result, currentDirectory) provider.entries[key] = provided } return provider, nil } -func providedModuleResolutionToResolvedModule(moduleName string, provided *ProvidedModuleResolution, currentDirectory string) *module.ResolvedModule { +func providedModuleResolutionToResolvedModule(provided *ProvidedModuleResolution, currentDirectory string) *module.ResolvedModule { if provided == nil || provided.ResolvedFileName == nil { return nil } @@ -206,7 +209,7 @@ func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { }) } -func (s *Session) moduleResolutionProvider(ctx context.Context, options *CreateProgramOptions) (module.ResolutionProvider, error) { +func (s *Session) moduleResolutionProviderFactory(ctx context.Context, options *CreateProgramOptions) (module.ResolutionProviderFactory, error) { if options.ModuleResolver == 0 { return nil, nil } @@ -219,9 +222,10 @@ func (s *Session) moduleResolutionProvider(ctx context.Context, options *CreateP if data.resolveModuleNameCallback != "" && s.conn == nil { return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) } - return &callbackModuleResolutionProvider{ + return &moduleResolutionProviderFactory{ identity: uint64(options.ModuleResolver), base: data.provider, + session: s, conn: s.conn, ctx: ctx, callback: data.resolveModuleNameCallback, @@ -229,6 +233,20 @@ func (s *Session) moduleResolutionProvider(ctx context.Context, options *CreateP }, nil } +func (s *Session) registerInProgressSnapshot(resolver *module.Resolver) uint64 { + id := s.nextInProgressSnapshotHandle.Add(1) + s.inProgressSnapshotsMu.Lock() + s.inProgressSnapshots[id] = resolver + s.inProgressSnapshotsMu.Unlock() + return id +} + +func (s *Session) releaseInProgressSnapshot(id uint64) { + s.inProgressSnapshotsMu.Lock() + delete(s.inProgressSnapshots, id) + s.inProgressSnapshotsMu.Unlock() +} + func moduleResolutionError(snapshot *project.Snapshot) error { for _, project := range snapshot.ProjectCollection.Projects() { if project.Program != nil { @@ -308,33 +326,39 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo } containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.currentDirectory()), s.currentDirectory()) - var host module.ResolutionHost - var extraExtensions []string - if params.Snapshot != 0 { + var resolver *module.Resolver + if params.Snapshot != 0 && params.InProgressSnapshot != 0 { + return nil, fmt.Errorf("%w: snapshot and inProgressSnapshot are mutually exclusive", ErrClientError) + } + if params.InProgressSnapshot != 0 { + s.inProgressSnapshotsMu.RLock() + inProgressResolver := s.inProgressSnapshots[params.InProgressSnapshot] + s.inProgressSnapshotsMu.RUnlock() + if inProgressResolver == nil { + return nil, fmt.Errorf("%w: in-progress snapshot %d not found", ErrClientError, params.InProgressSnapshot) + } + resolver = inProgressResolver.NewResolverForCompilerOptions(data.compilerOptions) + } else if params.Snapshot != 0 { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err } - host = sd.snapshot - extraExtensions = sd.snapshot.ContentMapperExtensions() + resolver = module.NewResolver(sd.snapshot, data.compilerOptions, "", "", sd.snapshot.ContentMapperExtensions()) } else { - host = &liveModuleResolutionHost{fs: s.fileSystem(), cwd: s.currentDirectory()} + resolver = module.NewResolver(&liveModuleResolutionHost{fs: s.fileSystem(), cwd: s.currentDirectory()}, data.compilerOptions, "", "", nil) } - resolver := module.NewResolver(host, data.compilerOptions, "", "", extraExtensions) - var trace []module.DiagAndArgs - behavior := &callbackModuleResolutionProvider{ + factory := &moduleResolutionProviderFactory{ identity: uint64(params.Resolver), base: data.provider, + session: s, conn: s.conn, ctx: ctx, callback: data.resolveModuleNameCallback, currentDirectory: s.currentDirectory(), } - result, err := behavior.ResolveModuleName(params.ModuleName, containingDirectory, mode, func() *module.ResolvedModule { - result, resolutionTrace := resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) - trace = resolutionTrace - return result - }) + provider, cleanup := factory.NewProvider(resolver) + defer cleanup() + result, trace, err := provider.ResolveModuleName(params.ModuleName, containingDirectory, mode) if err != nil { return nil, err } diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 0dd5756b93c8e..c6273fcf63561 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -496,6 +496,7 @@ type ReleaseModuleResolverParams struct { type ResolveModuleNameParams struct { Snapshot SnapshotID `json:"snapshot,omitempty"` + InProgressSnapshot uint64 `json:"inProgressSnapshot,omitempty"` Resolver ModuleResolverID `json:"resolver"` ModuleName string `json:"moduleName"` ContainingDirectory DocumentIdentifier `json:"containingDirectory"` @@ -506,6 +507,7 @@ type ResolveModuleNameCallbackParams struct { ModuleName string `json:"moduleName"` ContainingDirectory string `json:"containingDirectory"` ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` + InProgressSnapshot uint64 `json:"inProgressSnapshot"` } type ResolveModuleNameResult struct { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 865db3830d404..6800263278ab9 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -432,6 +432,9 @@ type Session struct { nextModuleResolverID atomic.Uint64 moduleResolvers map[ModuleResolverID]*moduleResolverData moduleResolversMu sync.RWMutex + nextInProgressSnapshotHandle atomic.Uint64 + inProgressSnapshots map[uint64]*module.Resolver + inProgressSnapshotsMu sync.RWMutex conn ipc.Conn cpuProfiler pprof.CPUProfiler @@ -475,11 +478,12 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont withLocale = func(ctx context.Context) context.Context { return ctx } } s := &Session{ - id: formatSessionID(id), - snapshotHost: snapshotHost, - withLocale: withLocale, - snapshots: make(map[SnapshotID]*snapshotData), - moduleResolvers: make(map[ModuleResolverID]*moduleResolverData), + id: formatSessionID(id), + snapshotHost: snapshotHost, + withLocale: withLocale, + snapshots: make(map[SnapshotID]*snapshotData), + moduleResolvers: make(map[ModuleResolverID]*moduleResolverData), + inProgressSnapshots: make(map[uint64]*module.Resolver), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses @@ -1320,11 +1324,11 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq ProjectReferences: programParams.Options.ProjectReferences, ConfigFileParsingDiagnostics: core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), } - provider, err := s.moduleResolutionProvider(ctx, &programParams.Options) + factory, err := s.moduleResolutionProviderFactory(ctx, &programParams.Options) if err != nil { return nil, err } - apiRequest.CreatePrograms[i].ModuleResolutionProvider = provider + apiRequest.CreatePrograms[i].ResolutionProviderFactory = factory } apiRequest.ReconfigurePrograms = make([]*project.APIReconfigureProgramRequest, len(changes.ReconfigurePrograms)) reconfiguredProgramIDs := collections.Set[int]{} @@ -1351,11 +1355,11 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq ProjectReferences: programParams.Options.ProjectReferences, ConfigFileParsingDiagnostics: core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), } - provider, err := s.moduleResolutionProvider(ctx, &programParams.Options) + factory, err := s.moduleResolutionProviderFactory(ctx, &programParams.Options) if err != nil { return nil, err } - apiRequest.ReconfigurePrograms[i].ModuleResolutionProvider = provider + apiRequest.ReconfigurePrograms[i].ResolutionProviderFactory = factory } if len(changes.RemovePrograms) > 0 { apiRequest.RemovePrograms = collections.NewSetWithSizeHint[int](len(changes.RemovePrograms)) diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index baf24bbe0104e..b0e8e3e6b7032 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) @@ -224,19 +225,29 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { t.Parallel() + projectSession, _ := projecttestutil.Setup(map[string]any{}) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() conn := &failingModuleResolutionConn{} - provider := &callbackModuleResolutionProvider{ + factory := &moduleResolutionProviderFactory{ identity: 1, + session: session, conn: conn, ctx: context.Background(), callback: "resolveModuleName/1", currentDirectory: "/", } + host := &liveModuleResolutionHost{fs: projectSession.FS(), cwd: "/"} + provider, cleanup := factory.NewProvider(module.NewResolver(host, core.EmptyCompilerOptions, "", "", nil)) for range 2 { - _, err := provider.ResolveModuleName("pkg", "/src", core.ResolutionModeESM, nil) + _, _, err := provider.ResolveModuleName("pkg", "/src", core.ResolutionModeESM) assert.ErrorContains(t, err, "callback error") } assert.Equal(t, conn.calls, 2) + assert.Equal(t, len(session.inProgressSnapshots), 1) + cleanup() + assert.Equal(t, len(session.inProgressSnapshots), 0) } func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 21bf05d5331bb..387b2d9903ce6 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -47,6 +47,7 @@ type sourceFileFromReferenceDiagnostic struct { type fileLoader struct { opts ProgramOptions resolver *module.Resolver + resolutionProvider module.ResolutionProvider defaultLibraryPath string comparePathsOptions tspath.ComparePathsOptions supportedExtensions [][]string @@ -179,6 +180,11 @@ func processAllProgramFiles( } loader.addProjectReferenceTasks(singleThreaded) loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, compilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) + if opts.ResolutionProviderFactory != nil { + var cleanup func() + loader.resolutionProvider, cleanup = opts.ResolutionProviderFactory.NewProvider(loader.resolver) + defer cleanup() + } if opts.Tracing != nil { defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)() } @@ -877,16 +883,12 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { mode := getModeForUsageLocation(file.FileName(), meta, entry, optionsForFile) var resolvedModule *module.ResolvedModule var trace []module.DiagAndArgs - if p.opts.ModuleResolutionProvider != nil { + if p.resolutionProvider != nil { var err error - resolvedModule, err = p.opts.ModuleResolutionProvider.ResolveModuleName( + resolvedModule, trace, err = p.resolutionProvider.ResolveModuleName( moduleName, tspath.GetDirectoryPath(fileName), mode, - func() *module.ResolvedModule { - resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) - return resolvedModule - }, ) if err != nil { p.moduleResolutionErrorOnce.Do(func() { diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index bee40420150a9..5bd4cf45fe455 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -43,7 +43,7 @@ type ProgramOptions struct { TypingsLocation string ProjectName string Tracing *tracing.Tracing - ModuleResolutionProvider module.ResolutionProvider + ResolutionProviderFactory module.ResolutionProviderFactory // SkipModuleResolution avoids all module and type reference resolution while // still collecting import metadata needed for emit. SkipModuleResolution bool @@ -327,7 +327,7 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost if createCheckerPool != nil { newOpts.CreateCheckerPool = createCheckerPool } - if resolutionProviderIdentity(p.opts.ModuleResolutionProvider) != resolutionProviderIdentity(newOpts.ModuleResolutionProvider) { + if resolutionProviderFactoryIdentity(p.opts.ResolutionProviderFactory) != resolutionProviderFactoryIdentity(newOpts.ResolutionProviderFactory) { return nil, nil, false } @@ -420,11 +420,11 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost return result, newFile, true } -func resolutionProviderIdentity(provider module.ResolutionProvider) uint64 { - if provider == nil { +func resolutionProviderFactoryIdentity(factory module.ResolutionProviderFactory) uint64 { + if factory == nil { return 0 } - return provider.Identity() + return factory.Identity() } func (p *Program) initCheckerPool() { diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 4a73978f2dc6a..cf5c4661c18b3 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -177,6 +177,10 @@ func NewResolver( } } +func (r *Resolver) NewResolverForCompilerOptions(options *core.CompilerOptions) *Resolver { + return NewResolver(r.host, options, "", "", r.extraExtensions) +} + func NewResolverWithOptions( host ResolutionHost, compilerOptions *core.CompilerOptions, diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 89de51d92444e..c5fe2dc250d4f 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -16,14 +16,17 @@ type ResolutionHost interface { GetCurrentDirectory() string } -type ResolutionProvider interface { +type ResolutionProviderFactory interface { Identity() uint64 + NewProvider(fallback *Resolver) (ResolutionProvider, func()) +} + +type ResolutionProvider interface { ResolveModuleName( moduleName string, containingDirectory string, resolutionMode core.ResolutionMode, - fallback func() *ResolvedModule, - ) (*ResolvedModule, error) + ) (*ResolvedModule, []DiagAndArgs, error) } type ModeAwareCacheKey struct { diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 51dbcde90012c..a433971396667 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -100,7 +100,7 @@ type Project struct { checkerPool *checkerPool - moduleResolutionProvider module.ResolutionProvider + resolutionProviderFactory module.ResolutionProviderFactory // installedTypingsInfo is the value of `project.ComputeTypingsInfo()` that was // used during the most recently completed typings installation. @@ -111,11 +111,11 @@ type Project struct { var _ ls.Project = (*Project)(nil) -func resolutionProviderIdentity(provider module.ResolutionProvider) uint64 { - if provider == nil { +func resolutionProviderFactoryIdentity(factory module.ResolutionProviderFactory) uint64 { + if factory == nil { return 0 } - return provider.Identity() + return factory.Identity() } func NewConfiguredProject( @@ -349,7 +349,7 @@ func (p *Project) Clone() *Project { checkerPool: p.checkerPool, - moduleResolutionProvider: p.moduleResolutionProvider, + resolutionProviderFactory: p.resolutionProviderFactory, installedTypingsInfo: p.installedTypingsInfo, typingsFiles: p.typingsFiles, @@ -492,7 +492,7 @@ func (p *Project) CreateProgram() CreateProgramResult { UseSourceOfProjectReference: true, TypingsLocation: typingsLocation, CreateCheckerPool: createCheckerPool, - ModuleResolutionProvider: p.moduleResolutionProvider, + ResolutionProviderFactory: p.resolutionProviderFactory, }, ) } diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index 3de19abee35d4..d0709ee44f2ed 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -317,7 +317,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.CompilerOptions, request.ProjectReferences, request.ConfigFileParsingDiagnostics, - request.ModuleResolutionProvider, + request.ResolutionProviderFactory, b.inferredContentMappers, logger, ) @@ -332,7 +332,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.CompilerOptions, request.ProjectReferences, request.ConfigFileParsingDiagnostics, - request.ModuleResolutionProvider, + request.ResolutionProviderFactory, b.inferredContentMappers, logger, ) @@ -1259,7 +1259,7 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, - moduleResolutionProvider module.ResolutionProvider, + resolutionProviderFactory module.ResolutionProviderFactory, contentMappers []*contentmapper.Mapper, logger *logging.LogTree, ) *dirty.SyncMapEntry[tspath.Path, *Project] { @@ -1268,7 +1268,7 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( if !loaded { syntheticProject := newSyntheticProject(name, b.sessionOptions.CurrentDirectory, compilerOptions, rootFileNames, projectReferences, contentMappers, b, logger) syntheticProject.CommandLine.Errors = configFileParsingDiagnostics - syntheticProject.moduleResolutionProvider = moduleResolutionProvider + syntheticProject.resolutionProviderFactory = resolutionProviderFactory project, _ = b.syntheticProjects.LoadOrStore(projectPath, syntheticProject) return project } @@ -1289,14 +1289,14 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( !projectReferencesEqual(p.CommandLine.ProjectReferences(), projectReferences) || !reflect.DeepEqual(p.CommandLine.Errors, configFileParsingDiagnostics) || !slices.Equal(p.CommandLine.ContentMappers(), newCommandLine.ContentMappers()) || - resolutionProviderIdentity(p.moduleResolutionProvider) != resolutionProviderIdentity(moduleResolutionProvider) + resolutionProviderFactoryIdentity(p.resolutionProviderFactory) != resolutionProviderFactoryIdentity(resolutionProviderFactory) }, func(p *Project) { if logger != nil { logger.Log(fmt.Sprintf("Updating synthetic project config with %d root files", len(rootFileNames))) } p.SetCommandLine(newCommandLine) - p.moduleResolutionProvider = moduleResolutionProvider + p.resolutionProviderFactory = resolutionProviderFactory }, ) return project diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 7b66d07b832b6..4c06aa3d2455c 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -324,7 +324,7 @@ type APICreateProgramRequest struct { CompilerOptions *core.CompilerOptions ProjectReferences []*core.ProjectReference ConfigFileParsingDiagnostics []*ast.Diagnostic - ModuleResolutionProvider module.ResolutionProvider + ResolutionProviderFactory module.ResolutionProviderFactory } type APIReconfigureProgramRequest struct { From 6c4f7100292ff019991c7cfccc7d4022fd75825b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 16:58:26 -0700 Subject: [PATCH 13/20] Fix multiple things --- packages/typescript/src/api/async/api.ts | 49 +++++--- packages/typescript/src/api/sync/api.ts | 73 ++++++----- packages/typescript/test/async/api.test.ts | 76 ++++++++++- packages/typescript/test/sync/api.test.ts | 76 ++++++++++- tsc/internal/api/module_resolution.go | 118 +++++++++--------- tsc/internal/api/proto.go | 3 +- tsc/internal/api/session.go | 62 ++++----- .../api/session_module_resolution_test.go | 10 +- tsc/internal/compiler/fileloader.go | 6 +- tsc/internal/module/resolver.go | 4 - tsc/internal/module/types.go | 1 + 11 files changed, 322 insertions(+), 156 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ca312e8947ddb..6619fc9219863 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -247,7 +247,7 @@ export interface ModuleResolverOptions { } export interface ResolveModuleNameCallbackOptions { - snapshot: InProgressSnapshot; + snapshot: Snapshot | InProgressSnapshot | undefined; } export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; @@ -267,20 +267,27 @@ export type LanguageServerSnapshotChanges = Omit void; } { +function registerModuleResolutionCallback(client: Client, callback: ResolveModuleNameCallback, getSnapshot: (id: number) => Snapshot | undefined): { name: string; dispose: () => void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { - const { moduleName, containingDirectory, resolutionMode, inProgressSnapshot } = params as { + const { moduleName, containingDirectory, resolutionMode, snapshot: snapshotId, inProgressSnapshot } = params as { moduleName: string; containingDirectory: string; resolutionMode?: ResolutionMode; - inProgressSnapshot: number; + snapshot?: number; + inProgressSnapshot?: number; }; - let snapshot = inProgressSnapshots.get(inProgressSnapshot); - if (snapshot === undefined) { - snapshot = new InProgressSnapshot(inProgressSnapshot); - inProgressSnapshots.set(inProgressSnapshot, snapshot); + let snapshot: Snapshot | InProgressSnapshot | undefined = snapshotId === undefined ? undefined : getSnapshot(snapshotId); + if (snapshotId !== undefined && snapshot === undefined) { + throw new Error(`Snapshot ${snapshotId} is inactive`); + } + if (inProgressSnapshot !== undefined) { + snapshot = inProgressSnapshots.get(inProgressSnapshot); + if (snapshot === undefined) { + snapshot = new InProgressSnapshot(inProgressSnapshot); + inProgressSnapshots.set(inProgressSnapshot, snapshot); + } } return callback( moduleName, @@ -319,7 +326,7 @@ export class API implements FormatDiagnosticsHo private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; private initialized: boolean = false; private initializing: Promise | undefined; - private activeSnapshots: Set = new Set(); + private activeSnapshots: Map = new Map(); readonly printer: Printer; readonly internal: InternalAPI; @@ -471,20 +478,20 @@ export class API implements FormatDiagnosticsHo this.toPath!, this, () => { - this.activeSnapshots.delete(snapshot); + this.activeSnapshots.delete(snapshot.id); this.sourceFileCache.releaseSnapshot(snapshot.id); }, this.createSnapshotUpdater(() => snapshot), undefined, ); - this.activeSnapshots.add(snapshot); + this.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } private async updateSnapshot(baseSnapshot: Snapshot, params: CreateSnapshotParams): Promise { await this.ensureInitialized(); - if (!this.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (this.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot update an inactive snapshot"); } @@ -500,13 +507,13 @@ export class API implements FormatDiagnosticsHo this.toPath!, this, () => { - this.activeSnapshots.delete(snapshot); + this.activeSnapshots.delete(snapshot.id); this.sourceFileCache.releaseSnapshot(snapshot.id); }, this.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - this.activeSnapshots.add(snapshot); + this.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } @@ -564,7 +571,7 @@ export class API implements FormatDiagnosticsHo const changes = args[0] as LanguageServerSnapshotChanges | undefined; const baseSnapshot = args[1] as Snapshot | undefined; - if (baseSnapshot && (!this.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + if (baseSnapshot && (this.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed())) { throw new Error("Cannot use an inactive snapshot as a response base"); } const data = await this.client.apiRequest("getCurrentLanguageServerSnapshot", { @@ -581,13 +588,13 @@ export class API implements FormatDiagnosticsHo this.toPath!, this, () => { - this.activeSnapshots.delete(snapshot); + this.activeSnapshots.delete(snapshot.id); this.sourceFileCache.releaseSnapshot(snapshot.id); }, this.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - this.activeSnapshots.add(snapshot); + this.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } @@ -599,7 +606,7 @@ export class API implements FormatDiagnosticsHo await this.initializing?.catch(() => {}); // @sync-skip // Dispose all active snapshots try { - for (const snapshot of [...this.activeSnapshots]) { + for (const snapshot of [...this.activeSnapshots.values()]) { await snapshot.dispose(); } this.sourceFileCache.clear(); @@ -611,7 +618,9 @@ export class API implements FormatDiagnosticsHo async createModuleResolver(compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Promise { await this.ensureInitialized(); - const callback = options?.resolveModuleName ? registerModuleResolutionCallback(this.client, options.resolveModuleName) : undefined; + const callback = options?.resolveModuleName + ? registerModuleResolutionCallback(this.client, options.resolveModuleName, id => this.activeSnapshots.get(id)) + : undefined; try { const id = await this.client.apiRequest("createModuleResolver", { compilerOptions, @@ -633,7 +642,7 @@ export class API implements FormatDiagnosticsHo async runWithTemporaryFileUpdate(baseSnapshot: Snapshot, file: DocumentIdentifier, newText: string, cb: (newSnapshot: Snapshot) => void | Promise): Promise { await this.ensureInitialized(); - if (!this.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (this.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot run a temporary file update on an inactive snapshot"); } const snapshot = await baseSnapshot.update({ diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 18642145ca58d..f8a495b382536 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -264,7 +264,7 @@ export interface ModuleResolverOptions { } export interface ResolveModuleNameCallbackOptions { - snapshot: InProgressSnapshot; + snapshot: Snapshot | InProgressSnapshot | undefined; } export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; @@ -284,20 +284,27 @@ export type LanguageServerSnapshotChanges = Omit void; } { +function registerModuleResolutionCallback(client: Client, callback: ResolveModuleNameCallback, getSnapshot: (id: number) => Snapshot | undefined): { name: string; dispose: () => void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { - const { moduleName, containingDirectory, resolutionMode, inProgressSnapshot } = params as { + const { moduleName, containingDirectory, resolutionMode, snapshot: snapshotId, inProgressSnapshot } = params as { moduleName: string; containingDirectory: string; resolutionMode?: ResolutionMode; - inProgressSnapshot: number; + snapshot?: number; + inProgressSnapshot?: number; }; - let snapshot = inProgressSnapshots.get(inProgressSnapshot); - if (snapshot === undefined) { - snapshot = new InProgressSnapshot(inProgressSnapshot); - inProgressSnapshots.set(inProgressSnapshot, snapshot); + let snapshot: Snapshot | InProgressSnapshot | undefined = snapshotId === undefined ? undefined : getSnapshot(snapshotId); + if (snapshotId !== undefined && snapshot === undefined) { + throw new Error(`Snapshot ${snapshotId} is inactive`); + } + if (inProgressSnapshot !== undefined) { + snapshot = inProgressSnapshots.get(inProgressSnapshot); + if (snapshot === undefined) { + snapshot = new InProgressSnapshot(inProgressSnapshot); + inProgressSnapshots.set(inProgressSnapshot, snapshot); + } } return callback( moduleName, @@ -338,7 +345,7 @@ export class API implements FormatDiagnosticsHo private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; private initialized: boolean = false; private initializing: void | undefined; - private activeSnapshots: Set = new Set(); + private activeSnapshots: Map = new Map(); readonly printer: Printer; readonly internal: InternalAPI; @@ -704,13 +711,13 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), undefined, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } @@ -729,13 +736,13 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), undefined, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } @@ -752,7 +759,7 @@ export class API implements FormatDiagnosticsHo "updateSnapshot", function (baseSnapshot: Snapshot, params: CreateSnapshotParams): Snapshot { owner.ensureInitialized(); - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot update an inactive snapshot"); } @@ -768,18 +775,18 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; }, function* (baseSnapshot: Snapshot, params: CreateSnapshotParams): Generator { yield* owner.ensureInitialized.gen(); - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot update an inactive snapshot"); } @@ -795,13 +802,13 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; }, ); @@ -867,7 +874,7 @@ export class API implements FormatDiagnosticsHo const changes = args[0] as LanguageServerSnapshotChanges | undefined; const baseSnapshot = args[1] as Snapshot | undefined; - if (baseSnapshot && (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + if (baseSnapshot && (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed())) { throw new Error("Cannot use an inactive snapshot as a response base"); } const data = owner.client.apiRequest("getCurrentLanguageServerSnapshot", { @@ -884,13 +891,13 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } function gen( @@ -903,7 +910,7 @@ export class API implements FormatDiagnosticsHo const changes = args[0] as LanguageServerSnapshotChanges | undefined; const baseSnapshot = args[1] as Snapshot | undefined; - if (baseSnapshot && (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + if (baseSnapshot && (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed())) { throw new Error("Cannot use an inactive snapshot as a response base"); } const data = yield* apiRequest("getCurrentLanguageServerSnapshot", { @@ -920,13 +927,13 @@ export class API implements FormatDiagnosticsHo owner.toPath!, owner, () => { - owner.activeSnapshots.delete(snapshot); + owner.activeSnapshots.delete(snapshot.id); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, owner.createSnapshotUpdater(() => snapshot), baseSnapshot, ); - owner.activeSnapshots.add(snapshot); + owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } return cacheGeneratorMethod(owner, "getCurrentLanguageServerSnapshot", getCurrentLanguageServerSnapshot, gen); @@ -947,7 +954,7 @@ export class API implements FormatDiagnosticsHo function (): void { // Dispose all active snapshots try { - for (const snapshot of [...owner.activeSnapshots]) { + for (const snapshot of [...owner.activeSnapshots.values()]) { snapshot.dispose(); } owner.sourceFileCache.clear(); @@ -959,7 +966,7 @@ export class API implements FormatDiagnosticsHo function* (): Generator { // Dispose all active snapshots try { - for (const snapshot of [...owner.activeSnapshots]) { + for (const snapshot of [...owner.activeSnapshots.values()]) { yield* snapshot.dispose.gen(); } owner.sourceFileCache.clear(); @@ -981,7 +988,9 @@ export class API implements FormatDiagnosticsHo "createModuleResolver", function (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): ModuleResolver { owner.ensureInitialized(); - const callback = options?.resolveModuleName ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) : undefined; + const callback = options?.resolveModuleName + ? registerModuleResolutionCallback(owner.client, options.resolveModuleName, id => owner.activeSnapshots.get(id)) + : undefined; try { const id = owner.client.apiRequest("createModuleResolver", { compilerOptions, @@ -997,7 +1006,9 @@ export class API implements FormatDiagnosticsHo }, function* (compilerOptions: CompilerOptions, options?: ModuleResolverOptions): Generator { yield* owner.ensureInitialized.gen(); - const callback = options?.resolveModuleName ? registerModuleResolutionCallback(owner.client, options.resolveModuleName) : undefined; + const callback = options?.resolveModuleName + ? registerModuleResolutionCallback(owner.client, options.resolveModuleName, id => owner.activeSnapshots.get(id)) + : undefined; try { const id = yield* apiRequest("createModuleResolver", { compilerOptions, @@ -1029,7 +1040,7 @@ export class API implements FormatDiagnosticsHo function (baseSnapshot: Snapshot, file: DocumentIdentifier, newText: string, cb: (newSnapshot: Snapshot) => void): void { owner.ensureInitialized(); - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot run a temporary file update on an inactive snapshot"); } const snapshot = baseSnapshot.update({ @@ -1050,7 +1061,7 @@ export class API implements FormatDiagnosticsHo function* (baseSnapshot: Snapshot, file: DocumentIdentifier, newText: string, cb: (newSnapshot: Snapshot) => void | Generator): Generator { yield* owner.ensureInitialized.gen(); - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + if (owner.activeSnapshots.get(baseSnapshot.id) !== baseSnapshot || baseSnapshot.isDisposed()) { throw new Error("Cannot run a temporary file update on an inactive snapshot"); } const snapshot = yield* baseSnapshot.update.gen({ diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index c0e66beabbd6c..c013544dfbdd1 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -64,6 +64,7 @@ import { IndexKind, type IndexType, type InferredProjectId, + type InProgressSnapshot, type InterfaceType, type IntrinsicType, isErrorType, @@ -628,11 +629,12 @@ describe("API", () => { moduleResolution: ModuleResolutionKind.NodeNext, }; const defaultResolver = await api.createModuleResolver(compilerOptions); - const callbackSnapshots: object[] = []; + const callbackSnapshots: (Snapshot | InProgressSnapshot | undefined)[] = []; const customResolver = await api.createModuleResolver(compilerOptions, { resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { callbackSnapshots.push(snapshot); if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; + assert.ok(snapshot); return (await defaultResolver.resolveModuleName( moduleName, containingDirectory, @@ -668,6 +670,7 @@ describe("API", () => { const defaultResolver = await api.createModuleResolver(compilerOptions); const customResolver = await api.createModuleResolver(compilerOptions, { resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + assert.ok(snapshot); return (await defaultResolver.resolveModuleName( moduleName, containingDirectory, @@ -693,6 +696,77 @@ describe("API", () => { ); }); + test("program module resolution uses the resolver compiler options", async () => { + await using api = spawnAPI({ + "/src/index.ts": `import "pkg/feature";`, + "/node_modules/pkg/package.json": JSON.stringify({ + name: "pkg", + version: "1.0.0", + exports: { "./feature": { resolver: "./dist/feature.d.ts" } }, + }), + "/node_modules/pkg/dist/feature.d.ts": `export {};`, + }); + const resolver = await api.createModuleResolver({ + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + customConditions: ["resolver"], + }); + const snapshot = await api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { + noLib: true, + module: ModuleKind.Node16, + moduleResolution: ModuleResolutionKind.Node16, + }, + options: { moduleResolver: resolver }, + }], + }); + + assert.deepEqual( + [...await snapshot.operation.createdPrograms![0].getSourceFileNames()].sort(), + ["/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], + ); + }); + + test("module resolver callbacks preserve retained and live filesystem context", async () => { + await using api = spawnAPI({ + "/src/index.ts": `export {};`, + "/node_modules/pkg/package.json": JSON.stringify({ name: "pkg", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/pkg/index.d.ts": `export {};`, + }); + const compilerOptions = { + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = await api.createModuleResolver(compilerOptions); + const callbackSnapshots: (Snapshot | InProgressSnapshot | undefined)[] = []; + const passthroughResolver = await api.createModuleResolver(compilerOptions, { + resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + callbackSnapshots.push(snapshot); + return (await defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode, + snapshot === undefined ? undefined : { snapshot }, + )).resolvedModule; + }, + }); + const snapshot = await api.createSnapshot(); + + assert.equal( + (await passthroughResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext, { snapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + assert.equal(callbackSnapshots[0], snapshot); + + assert.equal( + (await passthroughResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + assert.equal(callbackSnapshots[1], undefined); + }); + test("provided resolutions do not report native resolution provenance diagnostics", async () => { await using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6d5abfae76db5..d7a6a45b02531 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -79,6 +79,7 @@ import { IndexKind, type IndexType, type InferredProjectId, + type InProgressSnapshot, type InterfaceType, type IntrinsicType, isErrorType, @@ -617,11 +618,12 @@ describe("API", () => { moduleResolution: ModuleResolutionKind.NodeNext, }; const defaultResolver = api.createModuleResolver(compilerOptions); - const callbackSnapshots: object[] = []; + const callbackSnapshots: (Snapshot | InProgressSnapshot | undefined)[] = []; const customResolver = api.createModuleResolver(compilerOptions, { resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { callbackSnapshots.push(snapshot); if (moduleName === "custom") return { resolvedFileName: "/custom.d.ts" }; + assert.ok(snapshot); return (defaultResolver.resolveModuleName( moduleName, containingDirectory, @@ -657,6 +659,7 @@ describe("API", () => { const defaultResolver = api.createModuleResolver(compilerOptions); const customResolver = api.createModuleResolver(compilerOptions, { resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + assert.ok(snapshot); return (defaultResolver.resolveModuleName( moduleName, containingDirectory, @@ -682,6 +685,77 @@ describe("API", () => { ); }); + test("program module resolution uses the resolver compiler options", () => { + using api = spawnAPI({ + "/src/index.ts": `import "pkg/feature";`, + "/node_modules/pkg/package.json": JSON.stringify({ + name: "pkg", + version: "1.0.0", + exports: { "./feature": { resolver: "./dist/feature.d.ts" } }, + }), + "/node_modules/pkg/dist/feature.d.ts": `export {};`, + }); + const resolver = api.createModuleResolver({ + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + customConditions: ["resolver"], + }); + const snapshot = api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { + noLib: true, + module: ModuleKind.Node16, + moduleResolution: ModuleResolutionKind.Node16, + }, + options: { moduleResolver: resolver }, + }], + }); + + assert.deepEqual( + [...snapshot.operation.createdPrograms![0].getSourceFileNames()].sort(), + ["/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], + ); + }); + + test("module resolver callbacks preserve retained and live filesystem context", () => { + using api = spawnAPI({ + "/src/index.ts": `export {};`, + "/node_modules/pkg/package.json": JSON.stringify({ name: "pkg", version: "1.0.0", types: "index.d.ts" }), + "/node_modules/pkg/index.d.ts": `export {};`, + }); + const compilerOptions = { + module: ModuleKind.NodeNext, + moduleResolution: ModuleResolutionKind.NodeNext, + }; + const defaultResolver = api.createModuleResolver(compilerOptions); + const callbackSnapshots: (Snapshot | InProgressSnapshot | undefined)[] = []; + const passthroughResolver = api.createModuleResolver(compilerOptions, { + resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { + callbackSnapshots.push(snapshot); + return (defaultResolver.resolveModuleName( + moduleName, + containingDirectory, + resolutionMode, + snapshot === undefined ? undefined : { snapshot }, + )).resolvedModule; + }, + }); + const snapshot = api.createSnapshot(); + + assert.equal( + (passthroughResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext, { snapshot })).resolvedModule?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + assert.equal(callbackSnapshots[0], snapshot); + + assert.equal( + (passthroughResolver.resolveModuleName("pkg", "/src", ModuleKind.ESNext)).resolvedModule?.resolvedFileName, + "/node_modules/pkg/index.d.ts", + ); + assert.equal(callbackSnapshots[1], undefined); + }); + test("provided resolutions do not report native resolution provenance diagnostics", () => { using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index d43ae0abf869e..3fec8efb1e180 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -12,7 +12,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/tspath" - "github.com/microsoft/TypeScript/tsc/internal/vfs" ) type moduleResolutionMatchKey struct { @@ -24,29 +23,34 @@ type moduleResolutionMatchKey struct { } type providedModuleResolutions struct { - identity uint64 - fallbackToResolution bool - entries map[moduleResolutionMatchKey]*module.ResolvedModule - currentDirectory string - useCaseSensitive bool + useCaseSensitiveFileNames bool + fallbackToResolution bool + entries map[moduleResolutionMatchKey]*module.ResolvedModule + currentDirectory string } type moduleResolutionProviderFactory struct { - identity uint64 - base *providedModuleResolutions + registration *moduleResolverRegistration session *Session conn ipc.Conn ctx context.Context - callback string currentDirectory string } func (f *moduleResolutionProviderFactory) Identity() uint64 { - return f.identity + return uint64(f.registration.id) +} + +func (f *moduleResolutionProviderFactory) CompilerOptions() *core.CompilerOptions { + return f.registration.compilerOptions } type moduleResolutionProvider struct { - factory *moduleResolutionProviderFactory + registration *moduleResolverRegistration + conn ipc.Conn + ctx context.Context + currentDirectory string + snapshot SnapshotID inProgressSnapshot uint64 fallbackResolver *module.Resolver } @@ -54,7 +58,10 @@ type moduleResolutionProvider struct { func (f *moduleResolutionProviderFactory) NewProvider(fallback *module.Resolver) (module.ResolutionProvider, func()) { inProgressSnapshot := f.session.registerInProgressSnapshot(fallback) provider := &moduleResolutionProvider{ - factory: f, + registration: f.registration, + conn: f.conn, + ctx: f.ctx, + currentDirectory: f.currentDirectory, inProgressSnapshot: inProgressSnapshot, fallbackResolver: fallback, } @@ -68,16 +75,16 @@ func (p *moduleResolutionProvider) ResolveModuleName( containingDirectory string, resolutionMode core.ResolutionMode, ) (*module.ResolvedModule, []module.DiagAndArgs, error) { - f := p.factory - if f.base != nil { - if result, found := f.base.lookup(moduleName, containingDirectory, resolutionMode); found { + registration := p.registration + if registration.resolutions != nil { + if result, found := registration.resolutions.lookup(moduleName, containingDirectory, resolutionMode); found { return result, nil, nil } - if !f.base.fallbackToResolution { + if !registration.resolutions.fallbackToResolution { return nil, nil, nil } } - if f.callback == "" { + if registration.resolveModuleNameCallback == "" { result, trace := p.fallbackResolver.ResolveModuleNameFromDirectory(moduleName, containingDirectory, resolutionMode) return result, trace, nil } @@ -85,10 +92,15 @@ func (p *moduleResolutionProvider) ResolveModuleName( ModuleName: moduleName, ContainingDirectory: containingDirectory, } - params.InProgressSnapshot = p.inProgressSnapshot + if p.snapshot != 0 { + params.Snapshot = &p.snapshot + } + if p.inProgressSnapshot != 0 { + params.InProgressSnapshot = &p.inProgressSnapshot + } mode := ResolutionMode(resolutionMode) params.ResolutionMode = &mode - callbackResult, err := f.conn.Call(f.ctx, f.callback, params) + callbackResult, err := p.conn.Call(p.ctx, registration.resolveModuleNameCallback, params) if err != nil { return nil, nil, fmt.Errorf("resolveModuleName callback failed: %w", err) } @@ -100,11 +112,11 @@ func (p *moduleResolutionProvider) ResolveModuleName( if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { return nil, nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) } - return providedModuleResolutionToResolvedModule(&providedResolution, f.currentDirectory), nil, nil + return providedModuleResolutionToResolvedModule(&providedResolution, p.currentDirectory), nil, nil } func (p *providedModuleResolutions) lookup(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { - directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitive) + directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitiveFileNames) keys := [...]moduleResolutionMatchKey{ {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, {moduleName: moduleName, directory: directory, hasDirectory: true}, @@ -119,7 +131,7 @@ func (p *providedModuleResolutions) lookup(moduleName string, containingDirector return nil, false } -func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { +func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { if spec == nil { return nil, nil } @@ -134,11 +146,10 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, identity uint64, cu } provider := &providedModuleResolutions{ - identity: identity, - fallbackToResolution: fallbackToResolution, - entries: make(map[moduleResolutionMatchKey]*module.ResolvedModule, len(spec.Entries)), - currentDirectory: currentDirectory, - useCaseSensitive: useCaseSensitive, + fallbackToResolution: fallbackToResolution, + entries: make(map[moduleResolutionMatchKey]*module.ResolvedModule, len(spec.Entries)), + currentDirectory: currentDirectory, + useCaseSensitiveFileNames: useCaseSensitive, } for i, entry := range spec.Entries { if entry == nil { @@ -223,13 +234,11 @@ func (s *Session) moduleResolutionProviderFactory(ctx context.Context, options * return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) } return &moduleResolutionProviderFactory{ - identity: uint64(options.ModuleResolver), - base: data.provider, + registration: data, session: s, conn: s.conn, ctx: ctx, - callback: data.resolveModuleNameCallback, - currentDirectory: s.currentDirectory(), + currentDirectory: s.GetCurrentDirectory(), }, nil } @@ -261,17 +270,17 @@ func moduleResolutionError(snapshot *project.Snapshot) error { func (s *Session) handleCreateModuleResolver(params *CreateModuleResolverParams) (ModuleResolverID, error) { provider, err := compileModuleResolutionSpec( params.ModuleResolutions, - s.nextModuleResolutionIdentity.Add(1), - s.currentDirectory(), - s.fileSystem().UseCaseSensitiveFileNames(), + s.GetCurrentDirectory(), + s.FS().UseCaseSensitiveFileNames(), ) if err != nil { return 0, err } id := ModuleResolverID(s.nextModuleResolverID.Add(1)) - data := &moduleResolverData{ + data := &moduleResolverRegistration{ + id: id, compilerOptions: ¶ms.CompilerOptions, - provider: provider, + resolutions: provider, resolveModuleNameCallback: params.ResolveModuleNameCallback, } s.moduleResolversMu.Lock() @@ -293,19 +302,6 @@ func (s *Session) handleReleaseModuleResolver(params *ReleaseModuleResolverParam return nil, nil } -type liveModuleResolutionHost struct { - fs vfs.FS - cwd string -} - -func (h *liveModuleResolutionHost) FS() vfs.FS { - return h.fs -} - -func (h *liveModuleResolutionHost) GetCurrentDirectory() string { - return h.cwd -} - func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveModuleNameParams) (*ResolveModuleNameResult, error) { if params.ModuleName == "" { return nil, fmt.Errorf("%w: moduleName is empty", ErrClientError) @@ -324,7 +320,7 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo return nil, fmt.Errorf("%w: invalid resolutionMode %s", ErrClientError, mode.String()) } } - containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.currentDirectory()), s.currentDirectory()) + containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.GetCurrentDirectory()), s.GetCurrentDirectory()) var resolver *module.Resolver if params.Snapshot != 0 && params.InProgressSnapshot != 0 { @@ -337,7 +333,7 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo if inProgressResolver == nil { return nil, fmt.Errorf("%w: in-progress snapshot %d not found", ErrClientError, params.InProgressSnapshot) } - resolver = inProgressResolver.NewResolverForCompilerOptions(data.compilerOptions) + resolver = inProgressResolver } else if params.Snapshot != 0 { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { @@ -345,19 +341,17 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo } resolver = module.NewResolver(sd.snapshot, data.compilerOptions, "", "", sd.snapshot.ContentMapperExtensions()) } else { - resolver = module.NewResolver(&liveModuleResolutionHost{fs: s.fileSystem(), cwd: s.currentDirectory()}, data.compilerOptions, "", "", nil) + resolver = module.NewResolver(s, data.compilerOptions, "", "", nil) } - factory := &moduleResolutionProviderFactory{ - identity: uint64(params.Resolver), - base: data.provider, - session: s, - conn: s.conn, - ctx: ctx, - callback: data.resolveModuleNameCallback, - currentDirectory: s.currentDirectory(), + provider := &moduleResolutionProvider{ + registration: data, + conn: s.conn, + ctx: ctx, + currentDirectory: s.GetCurrentDirectory(), + snapshot: params.Snapshot, + inProgressSnapshot: params.InProgressSnapshot, + fallbackResolver: resolver, } - provider, cleanup := factory.NewProvider(resolver) - defer cleanup() result, trace, err := provider.ResolveModuleName(params.ModuleName, containingDirectory, mode) if err != nil { return nil, err diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index a3fb03eae40d9..cc7ebd0d05d58 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -494,7 +494,8 @@ type ResolveModuleNameCallbackParams struct { ModuleName string `json:"moduleName"` ContainingDirectory string `json:"containingDirectory"` ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` - InProgressSnapshot uint64 `json:"inProgressSnapshot"` + Snapshot *SnapshotID `json:"snapshot,omitempty"` + InProgressSnapshot *uint64 `json:"inProgressSnapshot,omitempty"` } type ResolveModuleNameResult struct { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 4d5a8c823651a..5da5bf179406e 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -74,9 +74,10 @@ type snapshotData struct { projectRegistriesMu sync.RWMutex } -type moduleResolverData struct { +type moduleResolverRegistration struct { + id ModuleResolverID compilerOptions *core.CompilerOptions - provider *providedModuleResolutions + resolutions *providedModuleResolutions resolveModuleNameCallback string } @@ -427,9 +428,8 @@ type Session struct { languageServerUpdateMu sync.Mutex - nextModuleResolutionIdentity atomic.Uint64 nextModuleResolverID atomic.Uint64 - moduleResolvers map[ModuleResolverID]*moduleResolverData + moduleResolvers map[ModuleResolverID]*moduleResolverRegistration moduleResolversMu sync.RWMutex nextInProgressSnapshotHandle atomic.Uint64 inProgressSnapshots map[uint64]*module.Resolver @@ -481,7 +481,7 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont snapshotHost: snapshotHost, withLocale: withLocale, snapshots: make(map[SnapshotID]*snapshotData), - moduleResolvers: make(map[ModuleResolverID]*moduleResolverData), + moduleResolvers: make(map[ModuleResolverID]*moduleResolverRegistration), inProgressSnapshots: make(map[uint64]*module.Resolver), } if options != nil { @@ -499,11 +499,11 @@ func (s *Session) SetConnection(conn ipc.Conn) { s.conn = conn } -func (s *Session) currentDirectory() string { +func (s *Session) GetCurrentDirectory() string { return s.snapshotHost.GetCurrentDirectory() } -func (s *Session) fileSystem() vfs.FS { +func (s *Session) FS() vfs.FS { if s.projectSession != nil { return s.projectSession.FS() } @@ -1176,7 +1176,7 @@ func (s *Session) HandleNotification(ctx context.Context, method string, params func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, error) { return &InitializeResponse{ UseCaseSensitiveFileNames: s.useCaseSensitiveFileNames(), - CurrentDirectory: s.currentDirectory(), + CurrentDirectory: s.GetCurrentDirectory(), }, nil } @@ -1191,7 +1191,7 @@ func (s *Session) handleCreateSnapshot(ctx context.Context, params *CreateSnapsh fileChanges := s.toFileChangeSummary(params.FileNotifications) var snapshotFileSystem vfs.FS if params.FileSystem != nil { - fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(params.FileSystem, s.fileSystem(), s.currentDirectory(), &fileChanges) + fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(params.FileSystem, s.FS(), s.GetCurrentDirectory(), &fileChanges) if fileSystemErr != nil { return nil, fmt.Errorf("%w: %w", ErrClientError, fileSystemErr) } @@ -1237,9 +1237,9 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh if changes.FileSystem != nil { baseFileSystem := snapshotFileSystem if baseFileSystem == nil { - baseFileSystem = s.fileSystem() + baseFileSystem = s.FS() } - fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(changes.FileSystem, baseFileSystem, s.currentDirectory(), &fileChanges) + fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(changes.FileSystem, baseFileSystem, s.GetCurrentDirectory(), &fileChanges) if fileSystemErr != nil { return nil, fmt.Errorf("%w: %w", ErrClientError, fileSystemErr) } @@ -1268,7 +1268,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq apiRequest := &project.APISnapshotRequest{} for _, p := range changes.OpenProjects { - configFileName := p.ToAbsoluteFileName(s.currentDirectory()) + configFileName := p.ToAbsoluteFileName(s.GetCurrentDirectory()) configuredProjectID, ok := project.ParseConfiguredProjectID(s.toPath(configFileName)) if !ok { return nil, fmt.Errorf("%w: invalid configured project ID: %s", ErrClientError, configFileName) @@ -1284,7 +1284,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq } for _, p := range changes.CloseProjects { - configPath := s.toPath(p.ToAbsoluteFileName(s.currentDirectory())) + configPath := s.toPath(p.ToAbsoluteFileName(s.GetCurrentDirectory())) if apiRequest.CloseProjects == nil { apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.Path](len(changes.CloseProjects)) } @@ -1292,7 +1292,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq } for _, f := range changes.OpenFiles { - uri := f.ToURI(s.currentDirectory()) + uri := f.ToURI(s.GetCurrentDirectory()) if apiRequest.EnsureFiles == nil { apiRequest.EnsureFiles = collections.NewSetWithSizeHint[lsproto.DocumentUri](len(changes.OpenFiles)) } @@ -1304,7 +1304,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq } for _, f := range changes.CloseFiles { - path := s.toPath(f.ToURI(s.currentDirectory()).FileName()) + path := s.toPath(f.ToURI(s.GetCurrentDirectory()).FileName()) if apiRequest.CloseFiles == nil { apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.Path](len(changes.CloseFiles)) } @@ -1318,7 +1318,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq } rootFileNames := make([]string, len(programParams.RootFiles)) for j, rootFile := range programParams.RootFiles { - rootFileNames[j] = rootFile.ToAbsoluteFileName(s.currentDirectory()) + rootFileNames[j] = rootFile.ToAbsoluteFileName(s.GetCurrentDirectory()) } request := &project.APICreateProgramRequest{ RootFileNames: rootFileNames, @@ -1351,7 +1351,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq reconfiguredProgramIDs.Add(programID) rootFileNames := make([]string, len(programParams.RootFiles)) for j, rootFile := range programParams.RootFiles { - rootFileNames[j] = rootFile.ToAbsoluteFileName(s.currentDirectory()) + rootFileNames[j] = rootFile.ToAbsoluteFileName(s.GetCurrentDirectory()) } request := &project.APIReconfigureProgramRequest{ ProgramID: programID, @@ -1565,7 +1565,7 @@ func (s *Session) handleGetDefaultProjectForFile(ctx context.Context, params *Ge return nil, err } - uri := params.File.ToURI(s.currentDirectory()) + uri := params.File.ToURI(s.GetCurrentDirectory()) proj := sd.snapshot.GetDefaultProject(uri) if proj == nil { return nil, nil @@ -1581,7 +1581,7 @@ func (s *Session) handleParseCommandLine(ctx context.Context, params *ParseComma // handleReadConfigFile reads and parses a JSON configuration file. func (s *Session) handleReadConfigFile(ctx context.Context, params *ReadConfigFileParams) (*ReadConfigFileResponse, error) { - configFileName := params.File.ToAbsoluteFileName(s.currentDirectory()) + configFileName := params.File.ToAbsoluteFileName(s.GetCurrentDirectory()) configFileContent, ok := s.snapshotHost.FS().ReadFile(configFileName) if !ok { return &ReadConfigFileResponse{ @@ -1611,9 +1611,9 @@ func (s *Session) handleParseJsonConfigFileContent(ctx context.Context, params * var basePath string var configFileName string if params.ConfigDirectory != nil { - basePath = tspath.GetNormalizedAbsolutePath(*params.ConfigDirectory, s.currentDirectory()) + basePath = tspath.GetNormalizedAbsolutePath(*params.ConfigDirectory, s.GetCurrentDirectory()) } else { - configFileName = params.ConfigFileName.ToAbsoluteFileName(s.currentDirectory()) + configFileName = params.ConfigFileName.ToAbsoluteFileName(s.GetCurrentDirectory()) basePath = tspath.GetDirectoryPath(configFileName) } @@ -1631,7 +1631,7 @@ func (s *Session) handleParseJsonConfigFileContent(ctx context.Context, params * // handleParseConfigFile parses a tsconfig.json file and returns its contents. func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfigFileParams) (*ConfigFileResponse, error) { - configFileName := params.File.ToAbsoluteFileName(s.currentDirectory()) + configFileName := params.File.ToAbsoluteFileName(s.GetCurrentDirectory()) configFileContent, ok := s.snapshotHost.FS().ReadFile(configFileName) if !ok { return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, configFileName) @@ -1671,7 +1671,7 @@ func (s *Session) handleCreateSourceFile(ctx context.Context, params *CreateSour // @gen-proto-result: SourceFileResponse func (s *Session) handleCreateSourceFileFromFile(ctx context.Context, params *CreateSourceFileFromFileParams) (any, error) { - fileName := tspath.GetNormalizedAbsolutePath(params.FileName, s.currentDirectory()) + fileName := tspath.GetNormalizedAbsolutePath(params.FileName, s.GetCurrentDirectory()) sourceText, ok := s.snapshotHost.FS().ReadFile(fileName) if !ok { return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, fileName) @@ -1691,7 +1691,7 @@ func (s *Session) createSourceFile(fileName string, sourceText string, options C if !isValidCreateSourceFileScriptKind(scriptKind) { return nil, fmt.Errorf("%w: invalid scriptKind %d", ErrClientError, scriptKind) } - fileName = tspath.GetNormalizedAbsolutePath(fileName, s.currentDirectory()) + fileName = tspath.GetNormalizedAbsolutePath(fileName, s.GetCurrentDirectory()) return parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, Path: s.toPath(fileName), @@ -1708,7 +1708,7 @@ func isValidCreateSourceFileScriptKind(scriptKind core.ScriptKind) bool { } func (s *Session) handleTranspileFromFile(ctx context.Context, params *TranspileFromFileParams, declaration bool) (*TranspileOutputResponse, error) { - fileName := tspath.GetNormalizedAbsolutePath(params.FileName, s.currentDirectory()) + fileName := tspath.GetNormalizedAbsolutePath(params.FileName, s.GetCurrentDirectory()) input, ok := s.snapshotHost.FS().ReadFile(fileName) if !ok { return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, fileName) @@ -2618,7 +2618,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo userPreferences := workingSnapshot.UserPreferences() if registry := workingSnapshot.AutoImportRegistry(); registry == nil || !registry.IsPreparedForImportingFile(sourceFile.FileName(), projectID, userPreferences) { - preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, workingSnapshot, params.File.ToURI(s.currentDirectory()), nil) + preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, workingSnapshot, params.File.ToURI(s.GetCurrentDirectory()), nil) if s.projectSession != nil { s.projectSession.TryAdoptSnapshotInBackground(workingSnapshot, preparedSnapshot) } @@ -4435,9 +4435,9 @@ func (s *Session) createSnapshotOperationResponse(snapshot *project.Snapshot, re if request.OpenFiles != nil { results := make([]*OpenedFileOperationResult, len(request.OpenFiles)) for i, file := range request.OpenFiles { - project := snapshot.GetDefaultProject(file.ToURI(s.currentDirectory())) + project := snapshot.GetDefaultProject(file.ToURI(s.GetCurrentDirectory())) if project == nil { - panic("no project found for opened file " + file.ToAbsoluteFileName(s.currentDirectory())) + panic("no project found for opened file " + file.ToAbsoluteFileName(s.GetCurrentDirectory())) } results[i] = &OpenedFileOperationResult{Project: project.ID()} } @@ -4510,7 +4510,7 @@ func formatSessionID(id uint64) string { // toPath converts a file name to a normalized path. func (s *Session) toPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, s.currentDirectory(), s.useCaseSensitiveFileNames()) + return tspath.ToPath(fileName, s.GetCurrentDirectory(), s.useCaseSensitiveFileNames()) } // toFileChangeSummary converts API file changes to a project.FileChangeSummary. @@ -4524,7 +4524,7 @@ func (s *Session) toFileChangeSummary(changes *FileNotifications) project.FileCh summary.IncludesWatchChangeOutsideNodeModules = true return summary } - cwd := s.currentDirectory() + cwd := s.GetCurrentDirectory() for _, doc := range changes.Changed { uri := doc.ToURI(cwd) summary.Changed.Add(uri) @@ -4778,7 +4778,7 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge } result, err := run(sd.snapshot, program) if errors.Is(err, ls.ErrNeedsAutoImports) { - preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, sd.snapshot, params.File.ToURI(s.currentDirectory()), nil) + preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, sd.snapshot, params.File.ToURI(s.GetCurrentDirectory()), nil) if s.projectSession != nil { s.projectSession.TryAdoptSnapshotInBackground(sd.snapshot, preparedSnapshot) } diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index d2c4847bb0e00..9684a3029f125 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -230,16 +230,18 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() conn := &failingModuleResolutionConn{} + registration := &moduleResolverRegistration{ + id: 1, + resolveModuleNameCallback: "resolveModuleName/1", + } factory := &moduleResolutionProviderFactory{ - identity: 1, + registration: registration, session: session, conn: conn, ctx: context.Background(), - callback: "resolveModuleName/1", currentDirectory: "/", } - host := &liveModuleResolutionHost{fs: projectSession.FS(), cwd: "/"} - provider, cleanup := factory.NewProvider(module.NewResolver(host, core.EmptyCompilerOptions, "", "", nil)) + provider, cleanup := factory.NewProvider(module.NewResolver(session, core.EmptyCompilerOptions, "", "", nil)) for range 2 { _, _, err := provider.ResolveModuleName("pkg", "/src", core.ResolutionModeESM) assert.ErrorContains(t, err, "callback error") diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 387b2d9903ce6..4c89586bad97b 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -179,7 +179,11 @@ func processAllProgramFiles( contentMapperExtensions: opts.Config.ContentMapperExtensions(), } loader.addProjectReferenceTasks(singleThreaded) - loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, compilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) + resolverCompilerOptions := compilerOptions + if opts.ResolutionProviderFactory != nil { + resolverCompilerOptions = opts.ResolutionProviderFactory.CompilerOptions() + } + loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, resolverCompilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) if opts.ResolutionProviderFactory != nil { var cleanup func() loader.resolutionProvider, cleanup = opts.ResolutionProviderFactory.NewProvider(loader.resolver) diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index cf5c4661c18b3..4a73978f2dc6a 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -177,10 +177,6 @@ func NewResolver( } } -func (r *Resolver) NewResolverForCompilerOptions(options *core.CompilerOptions) *Resolver { - return NewResolver(r.host, options, "", "", r.extraExtensions) -} - func NewResolverWithOptions( host ResolutionHost, compilerOptions *core.CompilerOptions, diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index c5fe2dc250d4f..795ccc409219a 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -18,6 +18,7 @@ type ResolutionHost interface { type ResolutionProviderFactory interface { Identity() uint64 + CompilerOptions() *core.CompilerOptions NewProvider(fallback *Resolver) (ResolutionProvider, func()) } From 65ca1dd876341a7ba8da743758730d77cccf9c90 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 17:07:18 -0700 Subject: [PATCH 14/20] Remove InProgressSnapshot class --- packages/typescript/src/api/async/api.ts | 24 +++++--------------- packages/typescript/src/api/sync/api.ts | 26 +++++----------------- packages/typescript/test/async/api.test.ts | 2 ++ packages/typescript/test/sync/api.test.ts | 2 ++ 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6619fc9219863..77feb404beeba 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -250,6 +250,9 @@ export interface ResolveModuleNameCallbackOptions { snapshot: Snapshot | InProgressSnapshot | undefined; } +declare const inProgressSnapshotBrand: unique symbol; +export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: never; }; + export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { @@ -269,7 +272,6 @@ export type LanguageServerSnapshotChanges = Omit Snapshot | undefined): { name: string; dispose: () => void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; - const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { const { moduleName, containingDirectory, resolutionMode, snapshot: snapshotId, inProgressSnapshot } = params as { moduleName: string; @@ -283,11 +285,7 @@ function registerModuleResolutionCallback(client: Client, callback: ResolveModul throw new Error(`Snapshot ${snapshotId} is inactive`); } if (inProgressSnapshot !== undefined) { - snapshot = inProgressSnapshots.get(inProgressSnapshot); - if (snapshot === undefined) { - snapshot = new InProgressSnapshot(inProgressSnapshot); - inProgressSnapshots.set(inProgressSnapshot, snapshot); - } + snapshot = -inProgressSnapshot as InProgressSnapshot; } return callback( moduleName, @@ -956,7 +954,7 @@ export class ModuleResolver { } return this.client.apiRequest("resolveModuleName", { snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, - inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, + inProgressSnapshot: typeof options?.snapshot === "number" ? -options.snapshot : undefined, resolver: this.id, moduleName, containingDirectory, @@ -981,18 +979,6 @@ export class ModuleResolver { } } -export class InProgressSnapshot { - private readonly _inProgressSnapshotBrand = undefined; - - /** @internal */ - readonly id: number; - - /** @internal */ - constructor(id: number) { - this.id = id; - } -} - class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index f8a495b382536..d5d1defe64ded 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -267,6 +267,9 @@ export interface ResolveModuleNameCallbackOptions { snapshot: Snapshot | InProgressSnapshot | undefined; } +declare const inProgressSnapshotBrand: unique symbol; +export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: never; }; + export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { @@ -286,7 +289,6 @@ export type LanguageServerSnapshotChanges = Omit Snapshot | undefined): { name: string; dispose: () => void; } { const name = `resolveModuleName/${++nextModuleResolutionCallbackId}`; - const inProgressSnapshots = new Map(); const dispose = client.registerCallback(name, params => { const { moduleName, containingDirectory, resolutionMode, snapshot: snapshotId, inProgressSnapshot } = params as { moduleName: string; @@ -300,11 +302,7 @@ function registerModuleResolutionCallback(client: Client, callback: ResolveModul throw new Error(`Snapshot ${snapshotId} is inactive`); } if (inProgressSnapshot !== undefined) { - snapshot = inProgressSnapshots.get(inProgressSnapshot); - if (snapshot === undefined) { - snapshot = new InProgressSnapshot(inProgressSnapshot); - inProgressSnapshots.set(inProgressSnapshot, snapshot); - } + snapshot = -inProgressSnapshot as InProgressSnapshot; } return callback( moduleName, @@ -1539,7 +1537,7 @@ export class ModuleResolver { } return owner.client.apiRequest("resolveModuleName", { snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, - inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, + inProgressSnapshot: typeof options?.snapshot === "number" ? -options.snapshot : undefined, resolver: owner.id, moduleName, containingDirectory, @@ -1553,7 +1551,7 @@ export class ModuleResolver { } return yield* apiRequest("resolveModuleName", { snapshot: options?.snapshot instanceof Snapshot ? options.snapshot.id : undefined, - inProgressSnapshot: options?.snapshot instanceof InProgressSnapshot ? options.snapshot.id : undefined, + inProgressSnapshot: typeof options?.snapshot === "number" ? -options.snapshot : undefined, resolver: owner.id, moduleName, containingDirectory, @@ -1596,18 +1594,6 @@ export class ModuleResolver { } } -export class InProgressSnapshot { - private readonly _inProgressSnapshotBrand = undefined; - - /** @internal */ - readonly id: number; - - /** @internal */ - constructor(id: number) { - this.id = id; - } -} - class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index c013544dfbdd1..23d7c6cf2312d 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -656,6 +656,8 @@ describe("API", () => { ); assert.equal(callbackSnapshots.length, 2); assert.equal(callbackSnapshots[0], callbackSnapshots[1]); + assert.ok(typeof callbackSnapshots[0] === "number"); + assert.ok(callbackSnapshots[0] < 0); }); test("module resolver callbacks can resolve against the in-progress snapshot filesystem", async () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index d7a6a45b02531..f523560445a7e 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -645,6 +645,8 @@ describe("API", () => { ); assert.equal(callbackSnapshots.length, 2); assert.equal(callbackSnapshots[0], callbackSnapshots[1]); + assert.ok(typeof callbackSnapshots[0] === "number"); + assert.ok(callbackSnapshots[0] < 0); }); test("module resolver callbacks can resolve against the in-progress snapshot filesystem", () => { From 4f09ae9ed3b21802df044a0ed48fb5b657bd8f64 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 17:08:34 -0700 Subject: [PATCH 15/20] Fix EOP types --- packages/typescript/src/api/async/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 77feb404beeba..6964497123fdb 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -242,8 +242,8 @@ export type { }; export interface ModuleResolverOptions { - moduleResolutions?: ModuleResolutionSpec; - resolveModuleName?: ResolveModuleNameCallback; + moduleResolutions?: ModuleResolutionSpec | undefined; + resolveModuleName?: ResolveModuleNameCallback | undefined; } export interface ResolveModuleNameCallbackOptions { @@ -256,7 +256,7 @@ export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { - moduleResolver?: ModuleResolver; + moduleResolver?: ModuleResolver | undefined; }; export type CreateSnapshotProgramParams = Omit & { options?: CreateProgramOptions | undefined; }; export type ReconfigureSnapshotProgramParams = Omit & { options?: CreateProgramOptions | undefined; }; @@ -946,7 +946,7 @@ export class ModuleResolver { moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, - options?: { snapshot?: Snapshot | InProgressSnapshot; }, + options?: { snapshot?: Snapshot | InProgressSnapshot | undefined; }, ): Promise { this.ensureNotDisposed(); if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { From f299c1f1eafecc7694940b527a61d6eb265a440a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 17:12:11 -0700 Subject: [PATCH 16/20] Generate --- packages/typescript/src/api/sync/api.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index d5d1defe64ded..8ef1bd1513b92 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -259,8 +259,8 @@ export type { }; export interface ModuleResolverOptions { - moduleResolutions?: ModuleResolutionSpec; - resolveModuleName?: ResolveModuleNameCallback; + moduleResolutions?: ModuleResolutionSpec | undefined; + resolveModuleName?: ResolveModuleNameCallback | undefined; } export interface ResolveModuleNameCallbackOptions { @@ -273,7 +273,7 @@ export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; export type CreateProgramOptions = Omit & { - moduleResolver?: ModuleResolver; + moduleResolver?: ModuleResolver | undefined; }; export type CreateSnapshotProgramParams = Omit & { options?: CreateProgramOptions | undefined; }; export type ReconfigureSnapshotProgramParams = Omit & { options?: CreateProgramOptions | undefined; }; @@ -1523,14 +1523,14 @@ export class ModuleResolver { } get resolveModuleName(): { - (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): ResolveModuleNameResult; - gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): Generator; + (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot | undefined; }): ResolveModuleNameResult; + gen(moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot | undefined; }): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "resolveModuleName", - function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): ResolveModuleNameResult { + function (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot | undefined; }): ResolveModuleNameResult { owner.ensureNotDisposed(); if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { throw new Error("Snapshot is disposed"); @@ -1544,7 +1544,7 @@ export class ModuleResolver { resolutionMode, }); }, - function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot; }): Generator { + function* (moduleName: string, containingDirectory: DocumentIdentifier, resolutionMode?: ResolutionMode, options?: { snapshot?: Snapshot | InProgressSnapshot | undefined; }): Generator { owner.ensureNotDisposed(); if (options?.snapshot instanceof Snapshot && options.snapshot.isDisposed()) { throw new Error("Snapshot is disposed"); From 5b7335d0331c14cadd59d931435f878aed1a58cd Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 18 Sep 2026 17:25:09 -0700 Subject: [PATCH 17/20] Return resolveModuleName callback errors as API errors --- .../api/session_module_resolution_test.go | 39 +++++++++++++++++++ .../project/projectcollectionbuilder.go | 10 ++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 9684a3029f125..660a87e2228fb 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -252,6 +252,45 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { assert.Equal(t, len(session.inProgressSnapshots), 0) } +func TestModuleResolutionCallbackErrorRejectsLanguageServerUpdate(t *testing.T) { + t.Parallel() + + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/src/index.ts": `import "pkg";`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + session.conn = &failingModuleResolutionConn{} + resolver, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + ResolveModuleNameCallback: "resolveModuleName/1", + }) + assert.NilError(t, err) + baseSnapshot := projectSession.Snapshot() + + _, err = session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: "/src/index.ts"}}, + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + Module: core.ModuleKindNodeNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + }, + Options: &CreateProgramOptions{ModuleResolver: resolver}, + }}, + }}, + }) + assert.ErrorContains(t, err, "callback error") + assert.Assert(t, projectSession.Snapshot() == baseSnapshot) + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) +} + func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { entry := &ModuleResolutionEntry{ ModuleName: moduleName, diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index 692734a8de0f6..ac9a6660350d4 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -365,7 +365,15 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque return true }) } - return nil + var moduleResolutionError error + b.forEachProject(func(entry dirty.Value[*Project]) bool { + project := entry.Value() + if project.Program != nil { + moduleResolutionError = project.Program.ModuleResolutionError() + } + return moduleResolutionError == nil + }) + return moduleResolutionError } func (b *ProjectCollectionBuilder) nextSyntheticProjectID() SyntheticProjectID { From ef0ee304b7e31c48389790e057a655d3eb7b1c5a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 21 Sep 2026 15:17:55 -0700 Subject: [PATCH 18/20] Make static resolver accessible via public Go API --- packages/typescript/src/api/async/api.ts | 6 +- .../typescript/src/api/proto.generated.ts | 4 +- packages/typescript/src/api/sync/api.ts | 6 +- packages/typescript/test/async/api.test.ts | 8 +- packages/typescript/test/sync/api.test.ts | 8 +- tsc/internal/api/module_resolution.go | 206 +++++++++--------- tsc/internal/api/proto.go | 10 +- tsc/internal/api/session.go | 16 +- .../api/session_module_resolution_test.go | 28 +-- tsc/internal/compiler/emitHost.go | 2 +- tsc/internal/compiler/fileloader.go | 44 ++-- tsc/internal/compiler/program.go | 52 +++-- tsc/internal/execute/watcher.go | 2 +- tsc/internal/ls/autoimport/aliasresolver.go | 6 +- tsc/internal/ls/autoimport/extract.go | 6 +- tsc/internal/ls/autoimport/util.go | 2 +- tsc/internal/ls/sourcedefinition.go | 4 +- tsc/internal/module/resolver.go | 47 ++-- tsc/internal/module/resolver_test.go | 8 +- tsc/internal/module/staticresolver.go | 155 +++++++++++++ tsc/internal/module/staticresolver_test.go | 67 ++++++ tsc/internal/module/types.go | 29 ++- tsc/internal/project/ata/ata.go | 4 +- tsc/internal/project/project.go | 46 ++-- .../project/projectcollectionbuilder.go | 18 +- tsc/internal/project/snapshot.go | 8 +- 26 files changed, 530 insertions(+), 262 deletions(-) create mode 100644 tsc/internal/module/staticresolver.go create mode 100644 tsc/internal/module/staticresolver_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6964497123fdb..c253df77af594 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -82,7 +82,6 @@ import type { ProjectId, ProjectReference, ProjectResponse, - ProvidedModuleResolution, ReadConfigFileResponse, ReconfigureSnapshotProgramParams as ProtocolReconfigureSnapshotProgramParams, ResolutionMode, @@ -92,6 +91,7 @@ import type { SignaturePropertyMethod, SignatureResponse, SourceFileMetadata, + StaticModuleResolution, SymbolPropertyMethod, SymbolResponse, SymbolsPropertyMethod, @@ -211,7 +211,6 @@ export type { ParsedCommandLine, ProjectId, ProjectReference, - ProvidedModuleResolution, ReadConfigFileResponse, RequestTiming, ResolutionMode, @@ -219,6 +218,7 @@ export type { ResolvedTypeReferenceDirective, ResolveModuleNameResult, SourceFileMetadata, + StaticModuleResolution, StringLiteralType, StringMappingType, StructuredType, @@ -253,7 +253,7 @@ export interface ResolveModuleNameCallbackOptions { declare const inProgressSnapshotBrand: unique symbol; export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: never; }; -export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => StaticModuleResolution | undefined | Promise; // @sync: export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => StaticModuleResolution | undefined; export type CreateProgramOptions = Omit & { moduleResolver?: ModuleResolver | undefined; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index cab7a00ccca1d..b815dfbf6c090 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -1687,7 +1687,7 @@ export interface ModuleResolutionEntry { moduleName: string; containingDirectory?: DocumentIdentifier | undefined; resolutionMode?: ResolutionMode | undefined; - result: ProvidedModuleResolution; + result: StaticModuleResolution; } /** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */ @@ -1702,7 +1702,7 @@ export interface CreateProgramOptions { moduleResolver?: number | undefined; } -export interface ProvidedModuleResolution { +export interface StaticModuleResolution { resolvedFileName?: DocumentIdentifier | undefined; originalPath?: DocumentIdentifier | undefined; packageId?: PackageId | undefined; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 8ef1bd1513b92..4d09791af294d 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -99,7 +99,6 @@ import type { ProjectId, ProjectReference, ProjectResponse, - ProvidedModuleResolution, ReadConfigFileResponse, ReconfigureSnapshotProgramParams as ProtocolReconfigureSnapshotProgramParams, ResolutionMode, @@ -109,6 +108,7 @@ import type { SignaturePropertyMethod, SignatureResponse, SourceFileMetadata, + StaticModuleResolution, SymbolPropertyMethod, SymbolResponse, SymbolsPropertyMethod, @@ -228,7 +228,6 @@ export type { ParsedCommandLine, ProjectId, ProjectReference, - ProvidedModuleResolution, ReadConfigFileResponse, RequestTiming, ResolutionMode, @@ -236,6 +235,7 @@ export type { ResolvedTypeReferenceDirective, ResolveModuleNameResult, SourceFileMetadata, + StaticModuleResolution, StringLiteralType, StringMappingType, StructuredType, @@ -270,7 +270,7 @@ export interface ResolveModuleNameCallbackOptions { declare const inProgressSnapshotBrand: unique symbol; export type InProgressSnapshot = number & { readonly [inProgressSnapshotBrand]: never; }; -export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => ProvidedModuleResolution | undefined; +export type ResolveModuleNameCallback = (moduleName: string, containingDirectory: string, resolutionMode: ResolutionMode | undefined, options: ResolveModuleNameCallbackOptions) => StaticModuleResolution | undefined; export type CreateProgramOptions = Omit & { moduleResolver?: ModuleResolver | undefined; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 23d7c6cf2312d..2b6c53aa6c2d0 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -700,13 +700,15 @@ describe("API", () => { test("program module resolution uses the resolver compiler options", async () => { await using api = spawnAPI({ - "/src/index.ts": `import "pkg/feature";`, + "/src/index.ts": `/// +import "pkg/feature";`, "/node_modules/pkg/package.json": JSON.stringify({ name: "pkg", version: "1.0.0", exports: { "./feature": { resolver: "./dist/feature.d.ts" } }, }), "/node_modules/pkg/dist/feature.d.ts": `export {};`, + "/node_modules/@types/resolver-types/index.d.ts": `export {};`, }); const resolver = await api.createModuleResolver({ module: ModuleKind.ESNext, @@ -727,7 +729,7 @@ describe("API", () => { assert.deepEqual( [...await snapshot.operation.createdPrograms![0].getSourceFileNames()].sort(), - ["/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], + ["/node_modules/@types/resolver-types/index.d.ts", "/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], ); }); @@ -769,7 +771,7 @@ describe("API", () => { assert.equal(callbackSnapshots[1], undefined); }); - test("provided resolutions do not report native resolution provenance diagnostics", async () => { + test("static resolutions do not report native resolution provenance diagnostics", async () => { await using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, "/value.ts": `export const value = 1;`, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f523560445a7e..e6f9a252273ab 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -689,13 +689,15 @@ describe("API", () => { test("program module resolution uses the resolver compiler options", () => { using api = spawnAPI({ - "/src/index.ts": `import "pkg/feature";`, + "/src/index.ts": `/// +import "pkg/feature";`, "/node_modules/pkg/package.json": JSON.stringify({ name: "pkg", version: "1.0.0", exports: { "./feature": { resolver: "./dist/feature.d.ts" } }, }), "/node_modules/pkg/dist/feature.d.ts": `export {};`, + "/node_modules/@types/resolver-types/index.d.ts": `export {};`, }); const resolver = api.createModuleResolver({ module: ModuleKind.ESNext, @@ -716,7 +718,7 @@ describe("API", () => { assert.deepEqual( [...snapshot.operation.createdPrograms![0].getSourceFileNames()].sort(), - ["/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], + ["/node_modules/@types/resolver-types/index.d.ts", "/node_modules/pkg/dist/feature.d.ts", "/src/index.ts"], ); }); @@ -758,7 +760,7 @@ describe("API", () => { assert.equal(callbackSnapshots[1], undefined); }); - test("provided resolutions do not report native resolution provenance diagnostics", () => { + test("static resolutions do not report native resolution provenance diagnostics", () => { using api = spawnAPI({ "/src/index.ts": `import { value } from "./value.ts"; export { value };`, "/value.ts": `export const value = 1;`, diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 3fec8efb1e180..15fb62213c9af 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -10,26 +10,12 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -type moduleResolutionMatchKey struct { - moduleName string - directory tspath.Path - mode core.ResolutionMode - hasDirectory bool - hasMode bool -} - -type providedModuleResolutions struct { - useCaseSensitiveFileNames bool - fallbackToResolution bool - entries map[moduleResolutionMatchKey]*module.ResolvedModule - currentDirectory string -} - -type moduleResolutionProviderFactory struct { +type moduleResolverFactory struct { registration *moduleResolverRegistration session *Session conn ipc.Conn @@ -37,27 +23,29 @@ type moduleResolutionProviderFactory struct { currentDirectory string } -func (f *moduleResolutionProviderFactory) Identity() uint64 { - return uint64(f.registration.id) -} - -func (f *moduleResolutionProviderFactory) CompilerOptions() *core.CompilerOptions { +func (f *moduleResolverFactory) CompilerOptions() *core.CompilerOptions { return f.registration.compilerOptions } -type moduleResolutionProvider struct { +type callbackModuleResolver struct { registration *moduleResolverRegistration conn ipc.Conn ctx context.Context currentDirectory string snapshot SnapshotID inProgressSnapshot uint64 - fallbackResolver *module.Resolver + fallbackResolver module.Resolver } -func (f *moduleResolutionProviderFactory) NewProvider(fallback *module.Resolver) (module.ResolutionProvider, func()) { +func (f *moduleResolverFactory) NewResolver(fallback module.Resolver) (module.Resolver, func()) { + if f.registration.resolveModuleNameCallback == "" { + if f.registration.resolutions != nil { + fallback = module.NewStaticResolver(fallback, f.registration.resolutions) + } + return fallback, func() {} + } inProgressSnapshot := f.session.registerInProgressSnapshot(fallback) - provider := &moduleResolutionProvider{ + var resolver module.Resolver = &callbackModuleResolver{ registration: f.registration, conn: f.conn, ctx: f.ctx, @@ -65,29 +53,38 @@ func (f *moduleResolutionProviderFactory) NewProvider(fallback *module.Resolver) inProgressSnapshot: inProgressSnapshot, fallbackResolver: fallback, } - return provider, func() { + if f.registration.resolutions != nil { + resolver = module.NewStaticResolver(resolver, f.registration.resolutions) + } + return resolver, func() { f.session.releaseInProgressSnapshot(inProgressSnapshot) } } -func (p *moduleResolutionProvider) ResolveModuleName( +func (p *callbackModuleResolver) ResolveModuleName( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference module.ResolvedProjectReference, +) (*module.ResolvedModule, []module.DiagAndArgs, error) { + return p.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) +} + +func (p *callbackModuleResolver) ResolveModuleNameFromDirectory( moduleName string, containingDirectory string, resolutionMode core.ResolutionMode, ) (*module.ResolvedModule, []module.DiagAndArgs, error) { - registration := p.registration - if registration.resolutions != nil { - if result, found := registration.resolutions.lookup(moduleName, containingDirectory, resolutionMode); found { - return result, nil, nil - } - if !registration.resolutions.fallbackToResolution { - return nil, nil, nil - } - } - if registration.resolveModuleNameCallback == "" { - result, trace := p.fallbackResolver.ResolveModuleNameFromDirectory(moduleName, containingDirectory, resolutionMode) - return result, trace, nil - } + return p.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) +} + +func (p *callbackModuleResolver) resolveModuleName( + moduleName string, + containingFile string, + containingDirectory string, + resolutionMode core.ResolutionMode, + redirectedReference module.ResolvedProjectReference, +) (*module.ResolvedModule, []module.DiagAndArgs, error) { params := &ResolveModuleNameCallbackParams{ ModuleName: moduleName, ContainingDirectory: containingDirectory, @@ -100,7 +97,7 @@ func (p *moduleResolutionProvider) ResolveModuleName( } mode := ResolutionMode(resolutionMode) params.ResolutionMode = &mode - callbackResult, err := p.conn.Call(p.ctx, registration.resolveModuleNameCallback, params) + callbackResult, err := p.conn.Call(p.ctx, p.registration.resolveModuleNameCallback, params) if err != nil { return nil, nil, fmt.Errorf("resolveModuleName callback failed: %w", err) } @@ -108,30 +105,40 @@ func (p *moduleResolutionProvider) ResolveModuleName( if len(callbackResult) == 0 || string(callbackResult) == "null" { return nil, nil, nil } - var providedResolution ProvidedModuleResolution - if err := json.Unmarshal(callbackResult, &providedResolution); err != nil { + var staticResolution StaticModuleResolution + if err := json.Unmarshal(callbackResult, &staticResolution); err != nil { return nil, nil, fmt.Errorf("invalid resolveModuleName callback result: %w", err) } - return providedModuleResolutionToResolvedModule(&providedResolution, p.currentDirectory), nil, nil + return staticModuleResolutionToResolvedModule(&staticResolution, p.currentDirectory), nil, nil } -func (p *providedModuleResolutions) lookup(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*module.ResolvedModule, bool) { - directory := tspath.ToPath(containingDirectory, p.currentDirectory, p.useCaseSensitiveFileNames) - keys := [...]moduleResolutionMatchKey{ - {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, - {moduleName: moduleName, directory: directory, hasDirectory: true}, - {moduleName: moduleName, mode: resolutionMode, hasMode: true}, - {moduleName: moduleName}, - } - for _, key := range keys { - if result, ok := p.entries[key]; ok { - return result, true - } - } - return nil, false +func (p *callbackModuleResolver) ResolveTypeReferenceDirective( + typeReferenceDirectiveName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference module.ResolvedProjectReference, +) (*module.ResolvedTypeReferenceDirective, []module.DiagAndArgs) { + return p.fallbackResolver.ResolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, resolutionMode, redirectedReference) } -func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory string, useCaseSensitive bool) (*providedModuleResolutions, error) { +func (p *callbackModuleResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { + return p.fallbackResolver.GetPackageScopeForPath(directory) +} + +func (p *callbackModuleResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { + p.fallbackResolver.PackageJsonCacheEntries(f) +} + +func (p *callbackModuleResolver) ResolvePackageDirectory( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference module.ResolvedProjectReference, +) *module.ResolvedModule { + return p.fallbackResolver.ResolvePackageDirectory(moduleName, containingFile, resolutionMode, redirectedReference) +} + +func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory string, useCaseSensitive bool) (*module.StaticResolutions, error) { if spec == nil { return nil, nil } @@ -145,12 +152,7 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory st return nil, fmt.Errorf("%w: invalid module resolution fallback %q", ErrClientError, spec.Fallback) } - provider := &providedModuleResolutions{ - fallbackToResolution: fallbackToResolution, - entries: make(map[moduleResolutionMatchKey]*module.ResolvedModule, len(spec.Entries)), - currentDirectory: currentDirectory, - useCaseSensitiveFileNames: useCaseSensitive, - } + entries := make([]module.StaticResolutionEntry, 0, len(spec.Entries)) for i, entry := range spec.Entries { if entry == nil { return nil, fmt.Errorf("%w: module resolution entry %d is null", ErrClientError, i) @@ -162,47 +164,44 @@ func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory st return nil, fmt.Errorf("%w: module resolution entry %d has no result", ErrClientError, i) } - key := moduleResolutionMatchKey{moduleName: entry.ModuleName} + staticEntry := module.StaticResolutionEntry{ModuleName: entry.ModuleName} if entry.ContainingDirectory != nil { - directory := tspath.GetNormalizedAbsolutePath(entry.ContainingDirectory.ToAbsoluteFileName(currentDirectory), currentDirectory) - key.directory = tspath.ToPath(directory, currentDirectory, useCaseSensitive) - key.hasDirectory = true + staticEntry.ContainingDirectory = tspath.GetNormalizedAbsolutePath(entry.ContainingDirectory.ToAbsoluteFileName(currentDirectory), currentDirectory) } if entry.ResolutionMode != nil { mode := core.ModuleKind(*entry.ResolutionMode) if mode != core.ModuleKindNone && mode != core.ModuleKindCommonJS && mode != core.ModuleKindESNext { return nil, fmt.Errorf("%w: module resolution entry %d has invalid resolutionMode %s", ErrClientError, i, mode.String()) } - key.mode = mode - key.hasMode = true - } - if _, exists := provider.entries[key]; exists { - return nil, fmt.Errorf("%w: duplicate module resolution entry for %q", ErrClientError, entry.ModuleName) + staticEntry.ResolutionMode = &mode } - - provided := providedModuleResolutionToResolvedModule(entry.Result, currentDirectory) - provider.entries[key] = provided + staticEntry.Result = staticModuleResolutionToResolvedModule(entry.Result, currentDirectory) + entries = append(entries, staticEntry) } - return provider, nil + resolutions, err := module.NewStaticResolutions(entries, fallbackToResolution, currentDirectory, useCaseSensitive) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrClientError, err) + } + return resolutions, nil } -func providedModuleResolutionToResolvedModule(provided *ProvidedModuleResolution, currentDirectory string) *module.ResolvedModule { - if provided == nil || provided.ResolvedFileName == nil { +func staticModuleResolutionToResolvedModule(staticResolution *StaticModuleResolution, currentDirectory string) *module.ResolvedModule { + if staticResolution == nil || staticResolution.ResolvedFileName == nil { return nil } result := &module.ResolvedModule{ - ResolvedFileName: tspath.GetNormalizedAbsolutePath(provided.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), + ResolvedFileName: tspath.GetNormalizedAbsolutePath(staticResolution.ResolvedFileName.ToAbsoluteFileName(currentDirectory), currentDirectory), } - if provided.OriginalPath != nil { - result.OriginalPath = tspath.GetNormalizedAbsolutePath(provided.OriginalPath.ToAbsoluteFileName(currentDirectory), currentDirectory) + if staticResolution.OriginalPath != nil { + result.OriginalPath = tspath.GetNormalizedAbsolutePath(staticResolution.OriginalPath.ToAbsoluteFileName(currentDirectory), currentDirectory) } - if provided.PackageID != nil { + if staticResolution.PackageID != nil { result.PackageId = module.PackageId{ - Name: provided.PackageID.Name, - SubModuleName: provided.PackageID.SubModuleName, - Version: provided.PackageID.Version, - PeerDependencies: provided.PackageID.PeerDependencies, + Name: staticResolution.PackageID.Name, + SubModuleName: staticResolution.PackageID.SubModuleName, + Version: staticResolution.PackageID.Version, + PeerDependencies: staticResolution.PackageID.PeerDependencies, } } originalPath := result.ResolvedFileName @@ -220,7 +219,7 @@ func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { }) } -func (s *Session) moduleResolutionProviderFactory(ctx context.Context, options *CreateProgramOptions) (module.ResolutionProviderFactory, error) { +func (s *Session) moduleResolverFactory(ctx context.Context, options *CreateProgramOptions) (project.ModuleResolverFactory, error) { if options.ModuleResolver == 0 { return nil, nil } @@ -233,7 +232,7 @@ func (s *Session) moduleResolutionProviderFactory(ctx context.Context, options * if data.resolveModuleNameCallback != "" && s.conn == nil { return nil, fmt.Errorf("%w: API connection is not initialized", ErrClientError) } - return &moduleResolutionProviderFactory{ + return &moduleResolverFactory{ registration: data, session: s, conn: s.conn, @@ -242,7 +241,7 @@ func (s *Session) moduleResolutionProviderFactory(ctx context.Context, options * }, nil } -func (s *Session) registerInProgressSnapshot(resolver *module.Resolver) uint64 { +func (s *Session) registerInProgressSnapshot(resolver module.Resolver) uint64 { id := s.nextInProgressSnapshotHandle.Add(1) s.inProgressSnapshotsMu.Lock() s.inProgressSnapshots[id] = resolver @@ -322,7 +321,7 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo } containingDirectory := tspath.GetNormalizedAbsolutePath(params.ContainingDirectory.ToAbsoluteFileName(s.GetCurrentDirectory()), s.GetCurrentDirectory()) - var resolver *module.Resolver + var resolver module.Resolver if params.Snapshot != 0 && params.InProgressSnapshot != 0 { return nil, fmt.Errorf("%w: snapshot and inProgressSnapshot are mutually exclusive", ErrClientError) } @@ -343,16 +342,21 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo } else { resolver = module.NewResolver(s, data.compilerOptions, "", "", nil) } - provider := &moduleResolutionProvider{ - registration: data, - conn: s.conn, - ctx: ctx, - currentDirectory: s.GetCurrentDirectory(), - snapshot: params.Snapshot, - inProgressSnapshot: params.InProgressSnapshot, - fallbackResolver: resolver, + if data.resolveModuleNameCallback != "" { + resolver = &callbackModuleResolver{ + registration: data, + conn: s.conn, + ctx: ctx, + currentDirectory: s.GetCurrentDirectory(), + snapshot: params.Snapshot, + inProgressSnapshot: params.InProgressSnapshot, + fallbackResolver: resolver, + } + } + if data.resolutions != nil { + resolver = module.NewStaticResolver(resolver, data.resolutions) } - result, trace, err := provider.ResolveModuleName(params.ModuleName, containingDirectory, mode) + result, trace, err := resolver.ResolveModuleNameFromDirectory(params.ModuleName, containingDirectory, mode) if err != nil { return nil, err } diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index cc7ebd0d05d58..8a0e3a9354703 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -459,13 +459,13 @@ type ModuleResolutionSpec struct { } type ModuleResolutionEntry struct { - ModuleName string `json:"moduleName"` - ContainingDirectory *DocumentIdentifier `json:"containingDirectory,omitempty"` - ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` - Result *ProvidedModuleResolution `json:"result" nonnil:"true"` + ModuleName string `json:"moduleName"` + ContainingDirectory *DocumentIdentifier `json:"containingDirectory,omitempty"` + ResolutionMode *ResolutionMode `json:"resolutionMode,omitempty"` + Result *StaticModuleResolution `json:"result" nonnil:"true"` } -type ProvidedModuleResolution struct { +type StaticModuleResolution struct { ResolvedFileName *DocumentIdentifier `json:"resolvedFileName,omitempty"` OriginalPath *DocumentIdentifier `json:"originalPath,omitempty"` PackageID *PackageId `json:"packageId,omitempty"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 5da5bf179406e..ee5586c81d485 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -77,7 +77,7 @@ type snapshotData struct { type moduleResolverRegistration struct { id ModuleResolverID compilerOptions *core.CompilerOptions - resolutions *providedModuleResolutions + resolutions *module.StaticResolutions resolveModuleNameCallback string } @@ -432,7 +432,7 @@ type Session struct { moduleResolvers map[ModuleResolverID]*moduleResolverRegistration moduleResolversMu sync.RWMutex nextInProgressSnapshotHandle atomic.Uint64 - inProgressSnapshots map[uint64]*module.Resolver + inProgressSnapshots map[uint64]module.Resolver inProgressSnapshotsMu sync.RWMutex conn ipc.Conn @@ -482,7 +482,7 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont withLocale: withLocale, snapshots: make(map[SnapshotID]*snapshotData), moduleResolvers: make(map[ModuleResolverID]*moduleResolverRegistration), - inProgressSnapshots: make(map[uint64]*module.Resolver), + inProgressSnapshots: make(map[uint64]module.Resolver), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses @@ -1327,11 +1327,12 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq if programParams.Options != nil { request.ProjectReferences = programParams.Options.ProjectReferences request.ConfigFileParsingDiagnostics = core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }) - factory, err := s.moduleResolutionProviderFactory(ctx, programParams.Options) + factory, err := s.moduleResolverFactory(ctx, programParams.Options) if err != nil { return nil, err } - request.ResolutionProviderFactory = factory + request.ModuleResolverFactory = factory + request.ModuleResolverID = uint64(programParams.Options.ModuleResolver) } apiRequest.CreatePrograms[i] = request } @@ -1361,11 +1362,12 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq if programParams.Options != nil { request.ProjectReferences = programParams.Options.ProjectReferences request.ConfigFileParsingDiagnostics = core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }) - factory, err := s.moduleResolutionProviderFactory(ctx, programParams.Options) + factory, err := s.moduleResolverFactory(ctx, programParams.Options) if err != nil { return nil, err } - request.ResolutionProviderFactory = factory + request.ModuleResolverFactory = factory + request.ModuleResolverID = uint64(programParams.Options.ModuleResolver) } apiRequest.ReconfigurePrograms[i] = request } diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 660a87e2228fb..1d7d648d0ca81 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -63,7 +63,7 @@ func TestModuleResolverUsesSnapshotFileSystem(t *testing.T) { assert.Assert(t, len(result.Trace) > 0) } -func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { +func TestStaticModuleResolutionSpecificityAndLifetime(t *testing.T) { t.Parallel() projectSession, _ := projecttestutil.Setup(map[string]any{ @@ -83,10 +83,10 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { spec := ModuleResolutionSpec{ Fallback: ModuleResolutionFallbackUnresolved, Entries: []*ModuleResolutionEntry{ - providedResolutionEntry("pkg", "", nil, "/home/projects/p/global.d.ts"), - providedResolutionEntry("pkg", "", &esm, "/home/projects/p/mode.d.ts"), - providedResolutionEntry("pkg", "/home/projects/p/src", nil, "/home/projects/p/dir.d.ts"), - providedResolutionEntry("pkg", "/home/projects/p/src", &esm, "/home/projects/p/exact.d.ts"), + staticResolutionEntry("pkg", "", nil, "/home/projects/p/global.d.ts"), + staticResolutionEntry("pkg", "", &esm, "/home/projects/p/mode.d.ts"), + staticResolutionEntry("pkg", "/home/projects/p/src", nil, "/home/projects/p/dir.d.ts"), + staticResolutionEntry("pkg", "/home/projects/p/src", &esm, "/home/projects/p/exact.d.ts"), }, } resolverID, err := session.handleCreateModuleResolver(&CreateModuleResolverParams{ @@ -126,7 +126,7 @@ func TestProvidedModuleResolutionSpecificityAndLifetime(t *testing.T) { assertResolution("/home/projects/p/src", core.ModuleKindESNext, "/home/projects/p/exact.d.ts") } -func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { +func TestCreateProgramUsesStaticModuleResolutions(t *testing.T) { t.Parallel() const root = "/home/projects/p/src/index.ts" @@ -147,7 +147,7 @@ func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { ModuleResolutions: &ModuleResolutionSpec{ Fallback: ModuleResolutionFallbackUnresolved, Entries: []*ModuleResolutionEntry{ - providedResolutionEntry("pkg", "", nil, provided), + staticResolutionEntry("pkg", "", nil, provided), }, }, }) @@ -178,7 +178,7 @@ func TestCreateProgramUsesProvidedModuleResolutions(t *testing.T) { assert.DeepEqual(t, fileNames, []string{provided, root}) } -func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { +func TestStaticModuleResolutionPreservesStaticIdentity(t *testing.T) { t.Parallel() projectSession, _ := projecttestutil.Setup(map[string]any{}) @@ -195,7 +195,7 @@ func TestProvidedModuleResolutionPreservesStaticIdentity(t *testing.T) { Entries: []*ModuleResolutionEntry{ { ModuleName: "pkg", - Result: &ProvidedModuleResolution{ + Result: &StaticModuleResolution{ ResolvedFileName: &DocumentIdentifier{FileName: "/store/pkg/index.d.ts"}, OriginalPath: &DocumentIdentifier{FileName: "/node_modules/pkg/index.d.ts"}, PackageID: &PackageId{ @@ -234,16 +234,16 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { id: 1, resolveModuleNameCallback: "resolveModuleName/1", } - factory := &moduleResolutionProviderFactory{ + factory := &moduleResolverFactory{ registration: registration, session: session, conn: conn, ctx: context.Background(), currentDirectory: "/", } - provider, cleanup := factory.NewProvider(module.NewResolver(session, core.EmptyCompilerOptions, "", "", nil)) + provider, cleanup := factory.NewResolver(module.NewResolver(session, core.EmptyCompilerOptions, "", "", nil)) for range 2 { - _, _, err := provider.ResolveModuleName("pkg", "/src", core.ResolutionModeESM) + _, _, err := provider.ResolveModuleNameFromDirectory("pkg", "/src", core.ResolutionModeESM) assert.ErrorContains(t, err, "callback error") } assert.Equal(t, conn.calls, 2) @@ -291,10 +291,10 @@ func TestModuleResolutionCallbackErrorRejectsLanguageServerUpdate(t *testing.T) assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) } -func providedResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { +func staticResolutionEntry(moduleName string, directory string, mode *core.ModuleKind, fileName string) *ModuleResolutionEntry { entry := &ModuleResolutionEntry{ ModuleName: moduleName, - Result: &ProvidedModuleResolution{ + Result: &StaticModuleResolution{ ResolvedFileName: &DocumentIdentifier{FileName: fileName}, }, } diff --git a/tsc/internal/compiler/emitHost.go b/tsc/internal/compiler/emitHost.go index e6d4e1ce153d0..de6c2dde62742 100644 --- a/tsc/internal/compiler/emitHost.go +++ b/tsc/internal/compiler/emitHost.go @@ -138,6 +138,6 @@ func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { } func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { - resolved, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) + resolved, _, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) return resolved } diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 4c89586bad97b..6dd4c9b891f21 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -46,8 +46,7 @@ type sourceFileFromReferenceDiagnostic struct { type fileLoader struct { opts ProgramOptions - resolver *module.Resolver - resolutionProvider module.ResolutionProvider + resolver module.Resolver defaultLibraryPath string comparePathsOptions tspath.ComparePathsOptions supportedExtensions [][]string @@ -112,7 +111,7 @@ func (r *redirectsFile) Path() tspath.Path { } type processedFiles struct { - resolver *module.Resolver + resolver module.Resolver files []*ast.SourceFile // duplicateSourceFiles tracks parsed files loaded during program construction // that were later dropped from the final program, such as losing filename @@ -180,14 +179,12 @@ func processAllProgramFiles( } loader.addProjectReferenceTasks(singleThreaded) resolverCompilerOptions := compilerOptions - if opts.ResolutionProviderFactory != nil { - resolverCompilerOptions = opts.ResolutionProviderFactory.CompilerOptions() + if opts.ModuleResolverCompilerOptions != nil { + resolverCompilerOptions = opts.ModuleResolverCompilerOptions } loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, resolverCompilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) - if opts.ResolutionProviderFactory != nil { - var cleanup func() - loader.resolutionProvider, cleanup = opts.ResolutionProviderFactory.NewProvider(loader.resolver) - defer cleanup() + if opts.CreateModuleResolver != nil { + loader.resolver = opts.CreateModuleResolver(loader.resolver) } if opts.Tracing != nil { defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)() @@ -887,23 +884,15 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { mode := getModeForUsageLocation(file.FileName(), meta, entry, optionsForFile) var resolvedModule *module.ResolvedModule var trace []module.DiagAndArgs - if p.resolutionProvider != nil { - var err error - resolvedModule, trace, err = p.resolutionProvider.ResolveModuleName( - moduleName, - tspath.GetDirectoryPath(fileName), - mode, - ) - if err != nil { - p.moduleResolutionErrorOnce.Do(func() { - p.moduleResolutionError = err - }) - } - if resolvedModule == nil { - resolvedModule = &module.ResolvedModule{} - } - } else { - resolvedModule, trace = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + var err error + resolvedModule, trace, err = p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect) + if err != nil { + p.moduleResolutionErrorOnce.Do(func() { + p.moduleResolutionError = err + }) + } + if resolvedModule == nil { + resolvedModule = &module.ResolvedModule{} } resolutionsInFile[module.ModeAwareCacheKey{Name: moduleName, Mode: mode}] = resolvedModule resolutionsTrace = append(resolutionsTrace, trace...) @@ -995,7 +984,8 @@ func (p *fileLoader) resolveLibrary(libraryName, resolveFrom string) (*module.Re if tr := p.opts.Tracing; tr != nil { defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom}, false)() } - return p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) + resolved, trace, _ := p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) + return resolved, trace } func getLibraryNameFromLibFileName(libFileName string) string { diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 5bd4cf45fe455..86f985049b487 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -35,15 +35,16 @@ import ( ) type ProgramOptions struct { - Host CompilerHost - Config *tsoptions.ParsedCommandLine - UseSourceOfProjectReference bool - SingleThreaded core.Tristate - CreateCheckerPool func(*Program) CheckerPool - TypingsLocation string - ProjectName string - Tracing *tracing.Tracing - ResolutionProviderFactory module.ResolutionProviderFactory + Host CompilerHost + Config *tsoptions.ParsedCommandLine + UseSourceOfProjectReference bool + SingleThreaded core.Tristate + CreateCheckerPool func(*Program) CheckerPool + TypingsLocation string + ProjectName string + Tracing *tracing.Tracing + CreateModuleResolver func(fallback module.Resolver) module.Resolver + ModuleResolverCompilerOptions *core.CompilerOptions // SkipModuleResolution avoids all module and type reference resolution while // still collecting import metadata needed for emit. SkipModuleResolution bool @@ -302,8 +303,13 @@ func NewProgram(opts ProgramOptions) *Program { // only if the host cannot locate the file (e.g. it was deleted). Callers that manage // host-side parse caches must release this exact pointer when the old program could not be // reused, since it was acquired speculatively before that decision was made. -func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { - if result, newFile, reused := p.ReuseProgram(changedFilePath, newHost, createCheckerPool); reused { +func (p *Program) UpdateProgram( + changedFilePath tspath.Path, + newHost CompilerHost, + createCheckerPool func(*Program) CheckerPool, + createModuleResolver func(module.Resolver) module.Resolver, +) (*Program, *ast.SourceFile, bool) { + if result, newFile, reused := p.ReuseProgram(changedFilePath, newHost, createCheckerPool, createModuleResolver); reused { return result, newFile, true } else { newOpts := p.opts @@ -311,6 +317,9 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos if createCheckerPool != nil { newOpts.CreateCheckerPool = createCheckerPool } + if createModuleResolver != nil { + newOpts.CreateModuleResolver = createModuleResolver + } return NewProgram(newOpts), newFile, false } } @@ -321,16 +330,20 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos // file cannot be replaced in place. Unlike UpdateProgram, it never constructs a // full fallback program, so callers that build their own fallback (e.g. with a // different host) do not pay for a discarded program build. -func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { +func (p *Program) ReuseProgram( + changedFilePath tspath.Path, + newHost CompilerHost, + createCheckerPool func(*Program) CheckerPool, + createModuleResolver func(module.Resolver) module.Resolver, +) (*Program, *ast.SourceFile, bool) { newOpts := p.opts newOpts.Host = newHost if createCheckerPool != nil { newOpts.CreateCheckerPool = createCheckerPool } - if resolutionProviderFactoryIdentity(p.opts.ResolutionProviderFactory) != resolutionProviderFactoryIdentity(newOpts.ResolutionProviderFactory) { - return nil, nil, false + if createModuleResolver != nil { + newOpts.CreateModuleResolver = createModuleResolver } - oldFile := p.filesByPath[changedFilePath] var newFile *ast.SourceFile var oldSupplementalFiles []*ast.SourceFile @@ -420,13 +433,6 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost return result, newFile, true } -func resolutionProviderFactoryIdentity(factory module.ResolutionProviderFactory) uint64 { - if factory == nil { - return 0 - } - return factory.Identity() -} - func (p *Program) initCheckerPool() { if !p.finishedProcessing { panic("Program must finish processing files before initializing checker pool") @@ -2309,7 +2315,7 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { } func (p *Program) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { - resolved, _ := p.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) + resolved, _, _ := p.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) return resolved } diff --git a/tsc/internal/execute/watcher.go b/tsc/internal/execute/watcher.go index 3a3b9405340c9..55907e5d8e7ce 100644 --- a/tsc/internal/execute/watcher.go +++ b/tsc/internal/execute/watcher.go @@ -562,7 +562,7 @@ func (w *Watcher) tryUpdateProgram(host *watchCompilerHost) bool { } } - newProgram, _, reused := oldProgram.ReuseProgram(changedPath, host, nil) + newProgram, _, reused := oldProgram.ReuseProgram(changedPath, host, nil, nil) if reused { w.program = incremental.NewProgram(newProgram, w.program, nil, w.sys.Now, w.testing != nil) } diff --git a/tsc/internal/ls/autoimport/aliasresolver.go b/tsc/internal/ls/autoimport/aliasresolver.go index 52955e92c3d09..19ebbde69c1e0 100644 --- a/tsc/internal/ls/autoimport/aliasresolver.go +++ b/tsc/internal/ls/autoimport/aliasresolver.go @@ -21,7 +21,7 @@ type pathAndFileName struct { type aliasResolver struct { toPath func(fileName string) tspath.Path host RegistryCloneHost - moduleResolver *module.Resolver + moduleResolver *module.DynamicResolver rootFiles []*ast.SourceFile // symlinks maps from realpath to symlinked path and file name @@ -34,7 +34,7 @@ func newAliasResolver( rootFiles []*ast.SourceFile, symlinks map[tspath.Path]pathAndFileName, host RegistryCloneHost, - moduleResolver *module.Resolver, + moduleResolver *module.DynamicResolver, toPath func(fileName string) tspath.Path, onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string), ) *aliasResolver { @@ -118,7 +118,7 @@ func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, mod if resolved, ok := cache.Load(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}); ok { return resolved } - resolved, _ := r.moduleResolver.ResolveModuleName(moduleReference, currentSourceFile.FileName(), mode, nil) + resolved, _, _ := r.moduleResolver.ResolveModuleName(moduleReference, currentSourceFile.FileName(), mode, nil) resolved, _ = cache.LoadOrStore(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}, resolved) if !resolved.IsResolved() && !tspath.PathIsRelative(moduleReference) { r.onFailedAmbientModuleLookup(currentSourceFile, moduleReference) diff --git a/tsc/internal/ls/autoimport/extract.go b/tsc/internal/ls/autoimport/extract.go index c1a57f1af1749..bc489101192da 100644 --- a/tsc/internal/ls/autoimport/extract.go +++ b/tsc/internal/ls/autoimport/extract.go @@ -28,7 +28,7 @@ type symbolExtractor struct { type exportExtractor struct { *symbolExtractor - moduleResolver *module.Resolver + moduleResolver *module.DynamicResolver } type extractorStats struct { @@ -70,7 +70,7 @@ func newSymbolExtractor(packageName string, checker *checker.Checker, toPath fun } } -func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor { +func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.DynamicResolver, realpath func(string) string) *exportExtractor { return &exportExtractor{ symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath), moduleResolver: moduleResolver, @@ -156,7 +156,7 @@ func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export { moduleID := ModuleID(name) var moduleFileName string if tspath.IsExternalModuleNameRelative(name) { - if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() { + if resolved, _, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() { moduleFileName = resolved.ResolvedFileName moduleID = ModuleID(e.toPath(moduleFileName)) } else { diff --git a/tsc/internal/ls/autoimport/util.go b/tsc/internal/ls/autoimport/util.go index d1768a9c343c4..f1dda9c3e9f14 100644 --- a/tsc/internal/ls/autoimport/util.go +++ b/tsc/internal/ls/autoimport/util.go @@ -314,7 +314,7 @@ func (rh *resolutionHost) FS() vfs.FS { return rh.fs } -func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver { +func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.DynamicResolver { rh := &resolutionHost{ fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}), currentDirectory: host.GetCurrentDirectory(), diff --git a/tsc/internal/ls/sourcedefinition.go b/tsc/internal/ls/sourcedefinition.go index bea42cddfe45a..1aa626639f5e5 100644 --- a/tsc/internal/ls/sourcedefinition.go +++ b/tsc/internal/ls/sourcedefinition.go @@ -135,7 +135,7 @@ type sourceDefResolver struct { options *core.CompilerOptions getSourceFile func(string) *ast.SourceFile resolveFrom string - resolver *module.Resolver + resolver *module.DynamicResolver parsedFiles map[string]*ast.SourceFile } @@ -422,7 +422,7 @@ func (r *sourceDefResolver) resolveImplementationFrom( } for _, mode := range modes { - resolved, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil) + resolved, _, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil) if resolved != nil && resolved.IsResolved() && !tspath.IsDeclarationFileName(resolved.ResolvedFileName) { return resolved.ResolvedFileName } diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 4a73978f2dc6a..d316cafefde38 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -66,7 +66,7 @@ func (t *tracer) getTraces() []DiagAndArgs { } type resolutionState struct { - resolver *Resolver + resolver *DynamicResolver tracer *tracer // request fields @@ -97,8 +97,7 @@ func newResolutionState( resolutionMode core.ResolutionMode, compilerOptions *core.CompilerOptions, redirectedReference ResolvedProjectReference, - resolver *Resolver, - traceBuilder *tracer, + resolver *DynamicResolver, traceBuilder *tracer, ) *resolutionState { state := &resolutionState{ name: name, @@ -146,7 +145,7 @@ func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redir return compilerOptions } -type Resolver struct { +type DynamicResolver struct { caches host ResolutionHost compilerOptions *core.CompilerOptions @@ -166,8 +165,8 @@ func NewResolver( typingsLocation string, projectName string, extraExtensions []string, -) *Resolver { - return &Resolver{ +) *DynamicResolver { + return &DynamicResolver{ host: host, caches: newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options), compilerOptions: options, @@ -183,8 +182,8 @@ func NewResolverWithOptions( typingsLocation string, projectName string, opts ResolverOptions, -) *Resolver { - r := &Resolver{ +) *DynamicResolver { + r := &DynamicResolver{ host: host, compilerOptions: compilerOptions, typingsLocation: typingsLocation, @@ -198,18 +197,18 @@ func NewResolverWithOptions( return r } -func (r *Resolver) newTraceBuilder() *tracer { +func (r *DynamicResolver) newTraceBuilder() *tracer { if r.compilerOptions.TraceResolution == core.TSTrue { return &tracer{} } return nil } -func (r *Resolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { +func (r *DynamicResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory) } -func (r *Resolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { +func (r *DynamicResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { r.caches.packageJsonInfoCache.Range(f) } @@ -219,7 +218,7 @@ func (r *tracer) traceResolutionUsingProjectReference(redirectedReference Resolv } } -func (r *Resolver) ResolveTypeReferenceDirective( +func (r *DynamicResolver) ResolveTypeReferenceDirective( typeReferenceDirectiveName string, containingFile string, resolutionMode core.ResolutionMode, @@ -264,15 +263,17 @@ func (r *Resolver) ResolveTypeReferenceDirective( return result, traceBuilder.getTraces() } -func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { - return r.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) +func (r *DynamicResolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs, error) { + result, trace := r.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) + return result, trace, nil } -func (r *Resolver) ResolveModuleNameFromDirectory(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, []DiagAndArgs) { - return r.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) +func (r *DynamicResolver) ResolveModuleNameFromDirectory(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, []DiagAndArgs, error) { + result, trace := r.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) + return result, trace, nil } -func (r *Resolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { +func (r *DynamicResolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { traceBuilder := r.newTraceBuilder() cacheKey := moduleResolutionCacheKey{ @@ -332,7 +333,7 @@ func (r *Resolver) resolveModuleName(moduleName string, containingFile string, c return finalResult, traceBuilder.getTraces() } -func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { +func (r *DynamicResolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil) @@ -343,7 +344,7 @@ func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile str return nil } -func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { +func (r *DynamicResolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { if r.typingsLocation == "" || tspath.IsExternalModuleNameRelative(moduleName) || (originalResult.ResolvedFileName != "" && tspath.ExtensionIsOneOf(originalResult.Extension, tspath.SupportedTSExtensionsWithJsonFlat)) { @@ -372,7 +373,7 @@ func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDi return result } -func (r *Resolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule { +func (r *DynamicResolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule { containingDirectory := tspath.GetDirectoryPath(containingFile) state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, core.ModuleKindCommonJS, r.compilerOptions, nil, r, nil) state.isConfigLookup = true @@ -1995,7 +1996,7 @@ type ParsedPatterns struct { patterns []core.Pattern } -func (r *Resolver) getParsedPatternsForPaths(compilerOptions *core.CompilerOptions) *ParsedPatterns { +func (r *DynamicResolver) getParsedPatternsForPaths(compilerOptions *core.CompilerOptions) *ParsedPatterns { return r.parsedPatternsForPaths.Get(compilerOptions.Paths) } @@ -2172,7 +2173,7 @@ func (e *ResolvedEntrypoint) SymlinkOrRealpath() string { return e.ResolvedFileName } -func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string, enableDirectorySearch bool) []*ResolvedEntrypoint { +func (r *DynamicResolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string, enableDirectorySearch bool) []*ResolvedEntrypoint { extensions := extensionsTypeScript | extensionsDeclaration features := NodeResolutionFeaturesAll state := &resolutionState{resolver: r, extensions: extensions, features: features, compilerOptions: r.compilerOptions} @@ -2231,7 +2232,7 @@ func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.In return nil } -func (r *Resolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { +func (r *DynamicResolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { var originalFileName string resolvedFileName := fileName if realPath := r.host.FS().Realpath(fileName); realPath != fileName { diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 1d75c85c4b47a..1b099aff5e80a 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -42,7 +42,7 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { resolver := module.NewResolver(host, opts, "", "", nil) for _, name := range []string{"pkg", "pkg/"} { - r, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil) + r, _, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil) if !r.IsResolved() { t.Errorf("%q failed to resolve", name) } @@ -182,7 +182,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { containingFile = "/repo/src/b/file.ts" } wg.Go(func() { - r, _ := resolver.ResolveModuleName(name, containingFile, core.ModuleKindESNext, nil) + r, _, _ := resolver.ResolveModuleName(name, containingFile, core.ModuleKindESNext, nil) results <- resolutionResult{name, r.IsResolved()} }) } @@ -260,7 +260,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { } results <- resolutionResult{containingFile: containingFile, resolved: resolved} }() - r, _ := resolver.ResolveModuleName("pkg/sub", containingFile, core.ModuleKindESNext, nil) + r, _, _ := resolver.ResolveModuleName("pkg/sub", containingFile, core.ModuleKindESNext, nil) resolved = r.IsResolved() }) } @@ -381,7 +381,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { } results <- resolutionResult{containingFile: containingFile, resolved: resolved} }() - r, _ := resolver.ResolveModuleName("pkg", containingFile, core.ModuleKindESNext, nil) + r, _, _ := resolver.ResolveModuleName("pkg", containingFile, core.ModuleKindESNext, nil) resolved = r.IsResolved() }) } diff --git a/tsc/internal/module/staticresolver.go b/tsc/internal/module/staticresolver.go new file mode 100644 index 0000000000000..34a55ba2c9f00 --- /dev/null +++ b/tsc/internal/module/staticresolver.go @@ -0,0 +1,155 @@ +package module + +import ( + "errors" + "fmt" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +type StaticResolutionEntry struct { + ModuleName string + ContainingDirectory string + ResolutionMode *core.ResolutionMode + Result *ResolvedModule +} + +type staticResolutionKey struct { + moduleName string + directory tspath.Path + mode core.ResolutionMode + hasDirectory bool + hasMode bool +} + +type StaticResolutions struct { + fallbackToResolver bool + entries map[staticResolutionKey]*ResolvedModule + currentDirectory string + useCaseSensitiveFileNames bool +} + +func NewStaticResolutions( + entries []StaticResolutionEntry, + fallbackToResolver bool, + currentDirectory string, + useCaseSensitiveFileNames bool, +) (*StaticResolutions, error) { + resolutions := &StaticResolutions{ + fallbackToResolver: fallbackToResolver, + entries: make(map[staticResolutionKey]*ResolvedModule, len(entries)), + currentDirectory: currentDirectory, + useCaseSensitiveFileNames: useCaseSensitiveFileNames, + } + for _, entry := range entries { + if entry.ModuleName == "" { + return nil, errors.New("module name is empty") + } + key := staticResolutionKey{moduleName: entry.ModuleName} + if entry.ContainingDirectory != "" { + key.directory = tspath.ToPath(entry.ContainingDirectory, currentDirectory, useCaseSensitiveFileNames) + key.hasDirectory = true + } + if entry.ResolutionMode != nil { + key.mode = *entry.ResolutionMode + key.hasMode = true + } + if _, exists := resolutions.entries[key]; exists { + return nil, fmt.Errorf("duplicate static module resolution for %q", entry.ModuleName) + } + resolutions.entries[key] = entry.Result + } + return resolutions, nil +} + +func (r *StaticResolutions) lookup(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, bool) { + directory := tspath.ToPath(containingDirectory, r.currentDirectory, r.useCaseSensitiveFileNames) + keys := [...]staticResolutionKey{ + {moduleName: moduleName, directory: directory, mode: resolutionMode, hasDirectory: true, hasMode: true}, + {moduleName: moduleName, directory: directory, hasDirectory: true}, + {moduleName: moduleName, mode: resolutionMode, hasMode: true}, + {moduleName: moduleName}, + } + for _, key := range keys { + if result, ok := r.entries[key]; ok { + return result, true + } + } + return nil, false +} + +type StaticResolver struct { + fallback Resolver + resolutions *StaticResolutions +} + +func NewStaticResolver(fallback Resolver, resolutions *StaticResolutions) *StaticResolver { + return &StaticResolver{fallback: fallback, resolutions: resolutions} +} + +func (r *StaticResolver) ResolveModuleName( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, +) (*ResolvedModule, []DiagAndArgs, error) { + return r.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) +} + +func (r *StaticResolver) ResolveModuleNameFromDirectory( + moduleName string, + containingDirectory string, + resolutionMode core.ResolutionMode, +) (*ResolvedModule, []DiagAndArgs, error) { + if result, found := r.resolutions.lookup(moduleName, containingDirectory, resolutionMode); found { + return result, nil, nil + } + if !r.resolutions.fallbackToResolver { + return nil, nil, nil + } + return r.fallback.ResolveModuleNameFromDirectory(moduleName, containingDirectory, resolutionMode) +} + +func (r *StaticResolver) resolveModuleName( + moduleName string, + containingFile string, + containingDirectory string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, +) (*ResolvedModule, []DiagAndArgs, error) { + if result, found := r.resolutions.lookup(moduleName, containingDirectory, resolutionMode); found { + return result, nil, nil + } + if !r.resolutions.fallbackToResolver { + return nil, nil, nil + } + return r.fallback.ResolveModuleName(moduleName, containingFile, resolutionMode, redirectedReference) +} + +func (r *StaticResolver) ResolveTypeReferenceDirective( + typeReferenceDirectiveName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, +) (*ResolvedTypeReferenceDirective, []DiagAndArgs) { + return r.fallback.ResolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, resolutionMode, redirectedReference) +} + +func (r *StaticResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { + return r.fallback.GetPackageScopeForPath(directory) +} + +func (r *StaticResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { + r.fallback.PackageJsonCacheEntries(f) +} + +func (r *StaticResolver) ResolvePackageDirectory( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, +) *ResolvedModule { + return r.fallback.ResolvePackageDirectory(moduleName, containingFile, resolutionMode, redirectedReference) +} diff --git a/tsc/internal/module/staticresolver_test.go b/tsc/internal/module/staticresolver_test.go new file mode 100644 index 0000000000000..a0d51ffc44c27 --- /dev/null +++ b/tsc/internal/module/staticresolver_test.go @@ -0,0 +1,67 @@ +package module_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func TestStaticResolver(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/repo/node_modules/fallback/package.json": `{"name":"fallback","types":"index.d.ts"}`, + "/repo/node_modules/fallback/index.d.ts": "export {};", + }, true) + fallback := module.NewResolver( + &resolutionHostStub{fs: fs, cwd: "/repo"}, + &core.CompilerOptions{ + Module: core.ModuleKindESNext, + ModuleResolution: core.ModuleResolutionKindBundler, + }, + "", + "", + nil, + ) + esm := core.ResolutionModeESM + resolutions, err := module.NewStaticResolutions( + []module.StaticResolutionEntry{ + {ModuleName: "provided", Result: &module.ResolvedModule{ResolvedFileName: "/global.d.ts"}}, + {ModuleName: "provided", ContainingDirectory: "/repo/src", Result: &module.ResolvedModule{ResolvedFileName: "/directory.d.ts"}}, + {ModuleName: "provided", ResolutionMode: &esm, Result: &module.ResolvedModule{ResolvedFileName: "/esm.d.ts"}}, + {ModuleName: "provided", ContainingDirectory: "/repo/src", ResolutionMode: &esm, Result: &module.ResolvedModule{ResolvedFileName: "/directory-esm.d.ts"}}, + {ModuleName: "unresolved"}, + }, + true, + "/repo", + true, + ) + assert.NilError(t, err) + resolver := module.NewStaticResolver(fallback, resolutions) + + tests := []struct { + name string + containingFile string + mode core.ResolutionMode + resolvedFileName string + }{ + {name: "provided", containingFile: "/repo/src/index.ts", mode: core.ResolutionModeESM, resolvedFileName: "/directory-esm.d.ts"}, + {name: "provided", containingFile: "/repo/src/index.ts", mode: core.ResolutionModeCommonJS, resolvedFileName: "/directory.d.ts"}, + {name: "provided", containingFile: "/repo/other/index.ts", mode: core.ResolutionModeESM, resolvedFileName: "/esm.d.ts"}, + {name: "provided", containingFile: "/repo/other/index.ts", mode: core.ResolutionModeCommonJS, resolvedFileName: "/global.d.ts"}, + {name: "fallback", containingFile: "/repo/src/index.ts", mode: core.ResolutionModeESM, resolvedFileName: "/repo/node_modules/fallback/index.d.ts"}, + {name: "unresolved", containingFile: "/repo/src/index.ts", mode: core.ResolutionModeESM}, + } + for _, test := range tests { + result, _, err := resolver.ResolveModuleName(test.name, test.containingFile, test.mode, nil) + assert.NilError(t, err) + if test.resolvedFileName == "" { + assert.Assert(t, result == nil) + } else { + assert.Equal(t, result.ResolvedFileName, test.resolvedFileName) + } + } +} diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 795ccc409219a..00d9f0f3b786b 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -16,18 +17,32 @@ type ResolutionHost interface { GetCurrentDirectory() string } -type ResolutionProviderFactory interface { - Identity() uint64 - CompilerOptions() *core.CompilerOptions - NewProvider(fallback *Resolver) (ResolutionProvider, func()) -} - -type ResolutionProvider interface { +type Resolver interface { ResolveModuleName( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, + ) (*ResolvedModule, []DiagAndArgs, error) + ResolveModuleNameFromDirectory( moduleName string, containingDirectory string, resolutionMode core.ResolutionMode, ) (*ResolvedModule, []DiagAndArgs, error) + ResolveTypeReferenceDirective( + typeReferenceDirectiveName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, + ) (*ResolvedTypeReferenceDirective, []DiagAndArgs) + GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry + PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) + ResolvePackageDirectory( + moduleName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, + ) *ResolvedModule } type ModeAwareCacheKey struct { diff --git a/tsc/internal/project/ata/ata.go b/tsc/internal/project/ata/ata.go index 16b3b4b05bbc2..66cf484ef7794 100644 --- a/tsc/internal/project/ata/ata.go +++ b/tsc/internal/project/ata/ata.go @@ -476,8 +476,8 @@ func (ti *TypingsInstaller) ensureTypingsLocationExists(fs vfs.FS, logger loggin } } -func (ti *TypingsInstaller) typingToFileName(resolver *module.Resolver, packageName string) string { - result, _ := resolver.ResolveModuleName(packageName, tspath.CombinePaths(ti.typingsLocation, "index.d.ts"), core.ModuleKindNone, nil) +func (ti *TypingsInstaller) typingToFileName(resolver *module.DynamicResolver, packageName string) string { + result, _, _ := resolver.ResolveModuleName(packageName, tspath.CombinePaths(ti.typingsLocation, "index.d.ts"), core.ModuleKindNone, nil) return result.ResolvedFileName } diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index b925b9e9188f8..288ee5acaf6d5 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -166,7 +166,8 @@ type Project struct { checkerPool *checkerPool - resolutionProviderFactory module.ResolutionProviderFactory + moduleResolverFactory ModuleResolverFactory + moduleResolverID uint64 // installedTypingsInfo is the value of `project.ComputeTypingsInfo()` that was // used during the most recently completed typings installation. @@ -177,13 +178,6 @@ type Project struct { var _ ls.Project = (*Project)(nil) -func resolutionProviderFactoryIdentity(factory module.ResolutionProviderFactory) uint64 { - if factory == nil { - return 0 - } - return factory.Identity() -} - func NewConfiguredProject( configFileName string, configFilePath tspath.Path, @@ -422,7 +416,8 @@ func (p *Project) Clone() *Project { checkerPool: p.checkerPool, - resolutionProviderFactory: p.resolutionProviderFactory, + moduleResolverFactory: p.moduleResolverFactory, + moduleResolverID: p.moduleResolverID, installedTypingsInfo: p.installedTypingsInfo, typingsFiles: p.typingsFiles, @@ -514,12 +509,26 @@ func (p *Project) CreateProgram() CreateProgramResult { createCheckerPool := func(program *compiler.Program) compiler.CheckerPool { return newCheckerPool(p.host.sessionOptions.CheckerPoolOptions, program, p.log) } + var cleanupModuleResolver func() + createModuleResolver := func(fallback module.Resolver) module.Resolver { + if p.moduleResolverFactory == nil { + return fallback + } + resolver, cleanup := p.moduleResolverFactory.NewResolver(fallback) + cleanupModuleResolver = cleanup + return resolver + } + defer func() { + if cleanupModuleResolver != nil { + cleanupModuleResolver() + } + }() // Create the command line, potentially augmented with typing files commandLine := p.getCommandLineWithTypingsFiles() if p.dirtyFilePath != "" && p.Program != nil && p.Program.CommandLine() == commandLine { var dirtyFile *ast.SourceFile - newProgram, dirtyFile, programCloned = p.Program.UpdateProgram(p.dirtyFilePath, p.host, createCheckerPool) + newProgram, dirtyFile, programCloned = p.Program.UpdateProgram(p.dirtyFilePath, p.host, createCheckerPool, createModuleResolver) if programCloned { updateKind = ProgramUpdateKindCloned for _, file := range newProgram.SourceFiles() { @@ -558,14 +567,19 @@ func (p *Project) CreateProgram() CreateProgramResult { if p.GetTypeAcquisition().Enable.IsTrue() { typingsLocation = p.host.sessionOptions.TypingsLocation } + var moduleResolverCompilerOptions *core.CompilerOptions + if p.moduleResolverFactory != nil { + moduleResolverCompilerOptions = p.moduleResolverFactory.CompilerOptions() + } newProgram = compiler.NewProgram( compiler.ProgramOptions{ - Host: p.host, - Config: commandLine, - UseSourceOfProjectReference: true, - TypingsLocation: typingsLocation, - CreateCheckerPool: createCheckerPool, - ResolutionProviderFactory: p.resolutionProviderFactory, + Host: p.host, + Config: commandLine, + UseSourceOfProjectReference: true, + TypingsLocation: typingsLocation, + CreateCheckerPool: createCheckerPool, + CreateModuleResolver: createModuleResolver, + ModuleResolverCompilerOptions: moduleResolverCompilerOptions, }, ) } diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index ac9a6660350d4..b7199b09354a3 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -17,7 +17,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" - "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/project/dirty" "github.com/microsoft/TypeScript/tsc/internal/project/logging" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" @@ -316,7 +315,8 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.CompilerOptions, request.ProjectReferences, request.ConfigFileParsingDiagnostics, - request.ResolutionProviderFactory, + request.ModuleResolverFactory, + request.ModuleResolverID, b.inferredContentMappers, logger, ) @@ -330,7 +330,8 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.CompilerOptions, request.ProjectReferences, request.ConfigFileParsingDiagnostics, - request.ResolutionProviderFactory, + request.ModuleResolverFactory, + request.ModuleResolverID, b.inferredContentMappers, logger, ) @@ -1285,7 +1286,8 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, - resolutionProviderFactory module.ResolutionProviderFactory, + moduleResolverFactory ModuleResolverFactory, + moduleResolverID uint64, contentMappers []*contentmapper.Mapper, logger *logging.LogTree, ) *dirty.SyncMapEntry[SyntheticProjectID, *Project] { @@ -1293,7 +1295,8 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( if !loaded { syntheticProject := newSyntheticProject(projectID, b.sessionOptions.CurrentDirectory, compilerOptions, rootFileNames, projectReferences, contentMappers, b, logger) syntheticProject.CommandLine.Errors = configFileParsingDiagnostics - syntheticProject.resolutionProviderFactory = resolutionProviderFactory + syntheticProject.moduleResolverFactory = moduleResolverFactory + syntheticProject.moduleResolverID = moduleResolverID project, _ = b.syntheticProjects.LoadOrStore(projectID, syntheticProject) return project } @@ -1314,14 +1317,15 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( !projectReferencesEqual(p.CommandLine.ProjectReferences(), projectReferences) || !reflect.DeepEqual(p.CommandLine.Errors, configFileParsingDiagnostics) || !slices.Equal(p.CommandLine.ContentMappers(), newCommandLine.ContentMappers()) || - resolutionProviderFactoryIdentity(p.resolutionProviderFactory) != resolutionProviderFactoryIdentity(resolutionProviderFactory) + p.moduleResolverID != moduleResolverID }, func(p *Project) { if logger != nil { logger.Log(fmt.Sprintf("Updating synthetic project config with %d root files", len(rootFileNames))) } p.SetCommandLine(newCommandLine) - p.resolutionProviderFactory = resolutionProviderFactory + p.moduleResolverFactory = moduleResolverFactory + p.moduleResolverID = moduleResolverID }, ) return project diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index cff12f3dc6915..2ab7c6b35bda4 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -324,7 +324,13 @@ type APICreateProgramRequest struct { CompilerOptions *core.CompilerOptions ProjectReferences []*core.ProjectReference ConfigFileParsingDiagnostics []*ast.Diagnostic - ResolutionProviderFactory module.ResolutionProviderFactory + ModuleResolverFactory ModuleResolverFactory + ModuleResolverID uint64 +} + +type ModuleResolverFactory interface { + CompilerOptions() *core.CompilerOptions + NewResolver(fallback module.Resolver) (module.Resolver, func()) } type APIReconfigureProgramRequest struct { From f16e60ff3d738627f308753b58ba55515eb25b42 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 22 Sep 2026 15:02:59 -0700 Subject: [PATCH 19/20] Further renaming and cleaning up --- packages/typescript/test/async/api.test.ts | 40 +++++- packages/typescript/test/sync/api.test.ts | 40 +++++- tsc/internal/api/module_resolution.go | 120 +++++++++++------- tsc/internal/api/session.go | 26 ++-- .../api/session_module_resolution_test.go | 9 +- tsc/internal/compiler/fileloader.go | 21 ++- tsc/internal/compiler/program.go | 23 ++-- tsc/internal/ls/autoimport/aliasresolver.go | 4 +- .../ls/autoimport/aliasresolver_crash_test.go | 2 +- tsc/internal/ls/autoimport/extract.go | 4 +- tsc/internal/ls/autoimport/registry.go | 10 +- tsc/internal/ls/autoimport/util.go | 6 +- tsc/internal/ls/sourcedefinition.go | 9 +- tsc/internal/module/resolver.go | 80 +++++------- tsc/internal/module/resolver_test.go | 8 +- tsc/internal/module/staticresolver_test.go | 11 +- tsc/internal/project/ata/ata.go | 12 +- tsc/internal/project/project.go | 23 ++-- tsc/internal/project/snapshot.go | 3 +- tsc/internal/tsoptions/contentmappers.go | 5 +- 20 files changed, 286 insertions(+), 170 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 2b6c53aa6c2d0..c0728bdaf0519 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -669,7 +669,10 @@ describe("API", () => { module: ModuleKind.NodeNext, moduleResolution: ModuleResolutionKind.NodeNext, }; - const defaultResolver = await api.createModuleResolver(compilerOptions); + const defaultResolver = await api.createModuleResolver({ + ...compilerOptions, + customConditions: ["delegated"], + }); const customResolver = await api.createModuleResolver(compilerOptions, { resolveModuleName: async (moduleName, containingDirectory, resolutionMode, { snapshot }) => { assert.ok(snapshot); @@ -683,7 +686,14 @@ describe("API", () => { }); const snapshot = await api.createSnapshot({ fileSystem: createFileSystemLayer([ - ["/node_modules/layered/package.json", JSON.stringify({ name: "layered", version: "1.0.0", types: "index.d.ts" })], + [ + "/node_modules/layered/package.json", + JSON.stringify({ + name: "layered", + version: "1.0.0", + exports: { ".": { delegated: "./index.d.ts" } }, + }), + ], ["/node_modules/layered/index.d.ts", `export {};`], ]), createPrograms: [{ @@ -698,6 +708,32 @@ describe("API", () => { ); }); + test("module resolver callback errors reject lib replacement", async () => { + await using api = spawnAPI({ + "/src/index.ts": `export {};`, + }); + const resolver = await api.createModuleResolver( + { moduleResolution: ModuleResolutionKind.Bundler }, + { + resolveModuleName: () => { + throw new Error("lib replacement callback failed"); + }, + }, + ); + + await assert.rejects( // @sync: assert.throws( + () => + api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { libReplacement: true }, + options: { moduleResolver: resolver }, + }], + }), + /lib replacement callback failed/, + ); + }); + test("program module resolution uses the resolver compiler options", async () => { await using api = spawnAPI({ "/src/index.ts": `/// diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index e6f9a252273ab..b03a6759fea69 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -658,7 +658,10 @@ describe("API", () => { module: ModuleKind.NodeNext, moduleResolution: ModuleResolutionKind.NodeNext, }; - const defaultResolver = api.createModuleResolver(compilerOptions); + const defaultResolver = api.createModuleResolver({ + ...compilerOptions, + customConditions: ["delegated"], + }); const customResolver = api.createModuleResolver(compilerOptions, { resolveModuleName: (moduleName, containingDirectory, resolutionMode, { snapshot }) => { assert.ok(snapshot); @@ -672,7 +675,14 @@ describe("API", () => { }); const snapshot = api.createSnapshot({ fileSystem: createFileSystemLayer([ - ["/node_modules/layered/package.json", JSON.stringify({ name: "layered", version: "1.0.0", types: "index.d.ts" })], + [ + "/node_modules/layered/package.json", + JSON.stringify({ + name: "layered", + version: "1.0.0", + exports: { ".": { delegated: "./index.d.ts" } }, + }), + ], ["/node_modules/layered/index.d.ts", `export {};`], ]), createPrograms: [{ @@ -687,6 +697,32 @@ describe("API", () => { ); }); + test("module resolver callback errors reject lib replacement", () => { + using api = spawnAPI({ + "/src/index.ts": `export {};`, + }); + const resolver = api.createModuleResolver( + { moduleResolution: ModuleResolutionKind.Bundler }, + { + resolveModuleName: () => { + throw new Error("lib replacement callback failed"); + }, + }, + ); + + assert.throws( + () => + api.createSnapshot({ + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { libReplacement: true }, + options: { moduleResolver: resolver }, + }], + }), + /lib replacement callback failed/, + ); + }); + test("program module resolution uses the resolver compiler options", () => { using api = spawnAPI({ "/src/index.ts": `/// diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 15fb62213c9af..7df844f5e3875 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/ipc" @@ -23,41 +24,47 @@ type moduleResolverFactory struct { currentDirectory string } -func (f *moduleResolverFactory) CompilerOptions() *core.CompilerOptions { - return f.registration.compilerOptions +type programResolutionContext struct { + options module.ResolverOptions + resolvers map[ModuleResolverID]module.Resolver + mu sync.Mutex } type callbackModuleResolver struct { - registration *moduleResolverRegistration - conn ipc.Conn - ctx context.Context - currentDirectory string - snapshot SnapshotID - inProgressSnapshot uint64 - fallbackResolver module.Resolver + registration *moduleResolverRegistration + conn ipc.Conn + ctx context.Context + currentDirectory string + snapshot SnapshotID + programResolutionContextID uint64 + fallbackResolver module.Resolver } -func (f *moduleResolverFactory) NewResolver(fallback module.Resolver) (module.Resolver, func()) { +func (f *moduleResolverFactory) NewResolver( + options module.ResolverOptions, +) (module.Resolver, func()) { + options.CompilerOptions = f.registration.compilerOptions + var fallback module.Resolver = module.NewResolver(options) if f.registration.resolveModuleNameCallback == "" { if f.registration.resolutions != nil { fallback = module.NewStaticResolver(fallback, f.registration.resolutions) } return fallback, func() {} } - inProgressSnapshot := f.session.registerInProgressSnapshot(fallback) + contextID := f.session.registerProgramResolutionContext(fallback, options, f.registration) var resolver module.Resolver = &callbackModuleResolver{ - registration: f.registration, - conn: f.conn, - ctx: f.ctx, - currentDirectory: f.currentDirectory, - inProgressSnapshot: inProgressSnapshot, - fallbackResolver: fallback, + registration: f.registration, + conn: f.conn, + ctx: f.ctx, + currentDirectory: f.currentDirectory, + programResolutionContextID: contextID, + fallbackResolver: fallback, } if f.registration.resolutions != nil { resolver = module.NewStaticResolver(resolver, f.registration.resolutions) } return resolver, func() { - f.session.releaseInProgressSnapshot(inProgressSnapshot) + f.session.releaseProgramResolutionContext(contextID) } } @@ -92,8 +99,8 @@ func (p *callbackModuleResolver) resolveModuleName( if p.snapshot != 0 { params.Snapshot = &p.snapshot } - if p.inProgressSnapshot != 0 { - params.InProgressSnapshot = &p.inProgressSnapshot + if p.programResolutionContextID != 0 { + params.InProgressSnapshot = &p.programResolutionContextID } mode := ResolutionMode(resolutionMode) params.ResolutionMode = &mode @@ -241,18 +248,38 @@ func (s *Session) moduleResolverFactory(ctx context.Context, options *CreateProg }, nil } -func (s *Session) registerInProgressSnapshot(resolver module.Resolver) uint64 { - id := s.nextInProgressSnapshotHandle.Add(1) - s.inProgressSnapshotsMu.Lock() - s.inProgressSnapshots[id] = resolver - s.inProgressSnapshotsMu.Unlock() +func (s *Session) registerProgramResolutionContext( + resolver module.Resolver, + options module.ResolverOptions, + registration *moduleResolverRegistration, +) uint64 { + id := s.nextProgramResolutionContextID.Add(1) + s.programResolutionContextsMu.Lock() + s.programResolutionContexts[id] = &programResolutionContext{ + options: options, + resolvers: map[ModuleResolverID]module.Resolver{registration.id: resolver}, + } + s.programResolutionContextsMu.Unlock() return id } -func (s *Session) releaseInProgressSnapshot(id uint64) { - s.inProgressSnapshotsMu.Lock() - delete(s.inProgressSnapshots, id) - s.inProgressSnapshotsMu.Unlock() +func (s *Session) releaseProgramResolutionContext(id uint64) { + s.programResolutionContextsMu.Lock() + delete(s.programResolutionContexts, id) + s.programResolutionContextsMu.Unlock() +} + +func (c *programResolutionContext) resolverFor(registration *moduleResolverRegistration) module.Resolver { + c.mu.Lock() + defer c.mu.Unlock() + if resolver := c.resolvers[registration.id]; resolver != nil { + return resolver + } + options := c.options + options.CompilerOptions = registration.compilerOptions + resolver := module.NewResolver(options) + c.resolvers[registration.id] = resolver + return resolver } func moduleResolutionError(snapshot *project.Snapshot) error { @@ -326,31 +353,38 @@ func (s *Session) handleResolveModuleName(ctx context.Context, params *ResolveMo return nil, fmt.Errorf("%w: snapshot and inProgressSnapshot are mutually exclusive", ErrClientError) } if params.InProgressSnapshot != 0 { - s.inProgressSnapshotsMu.RLock() - inProgressResolver := s.inProgressSnapshots[params.InProgressSnapshot] - s.inProgressSnapshotsMu.RUnlock() - if inProgressResolver == nil { + s.programResolutionContextsMu.RLock() + resolutionContext := s.programResolutionContexts[params.InProgressSnapshot] + s.programResolutionContextsMu.RUnlock() + if resolutionContext == nil { return nil, fmt.Errorf("%w: in-progress snapshot %d not found", ErrClientError, params.InProgressSnapshot) } - resolver = inProgressResolver + resolver = resolutionContext.resolverFor(data) } else if params.Snapshot != 0 { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err } - resolver = module.NewResolver(sd.snapshot, data.compilerOptions, "", "", sd.snapshot.ContentMapperExtensions()) + resolver = module.NewResolver(module.ResolverOptions{ + Host: sd.snapshot, + CompilerOptions: data.compilerOptions, + ExtraExtensions: sd.snapshot.ContentMapperExtensions(), + }) } else { - resolver = module.NewResolver(s, data.compilerOptions, "", "", nil) + resolver = module.NewResolver(module.ResolverOptions{ + Host: s, + CompilerOptions: data.compilerOptions, + }) } if data.resolveModuleNameCallback != "" { resolver = &callbackModuleResolver{ - registration: data, - conn: s.conn, - ctx: ctx, - currentDirectory: s.GetCurrentDirectory(), - snapshot: params.Snapshot, - inProgressSnapshot: params.InProgressSnapshot, - fallbackResolver: resolver, + registration: data, + conn: s.conn, + ctx: ctx, + currentDirectory: s.GetCurrentDirectory(), + snapshot: params.Snapshot, + programResolutionContextID: params.InProgressSnapshot, + fallbackResolver: resolver, } } if data.resolutions != nil { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index ee5586c81d485..f8e8d1369a1c1 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -428,13 +428,13 @@ type Session struct { languageServerUpdateMu sync.Mutex - nextModuleResolverID atomic.Uint64 - moduleResolvers map[ModuleResolverID]*moduleResolverRegistration - moduleResolversMu sync.RWMutex - nextInProgressSnapshotHandle atomic.Uint64 - inProgressSnapshots map[uint64]module.Resolver - inProgressSnapshotsMu sync.RWMutex - conn ipc.Conn + nextModuleResolverID atomic.Uint64 + moduleResolvers map[ModuleResolverID]*moduleResolverRegistration + moduleResolversMu sync.RWMutex + nextProgramResolutionContextID atomic.Uint64 + programResolutionContexts map[uint64]*programResolutionContext + programResolutionContextsMu sync.RWMutex + conn ipc.Conn cpuProfiler pprof.CPUProfiler } @@ -477,12 +477,12 @@ func newSession(snapshotHost *project.SnapshotHost, withLocale func(context.Cont withLocale = func(ctx context.Context) context.Context { return ctx } } s := &Session{ - id: formatSessionID(id), - snapshotHost: snapshotHost, - withLocale: withLocale, - snapshots: make(map[SnapshotID]*snapshotData), - moduleResolvers: make(map[ModuleResolverID]*moduleResolverRegistration), - inProgressSnapshots: make(map[uint64]module.Resolver), + id: formatSessionID(id), + snapshotHost: snapshotHost, + withLocale: withLocale, + snapshots: make(map[SnapshotID]*snapshotData), + moduleResolvers: make(map[ModuleResolverID]*moduleResolverRegistration), + programResolutionContexts: make(map[uint64]*programResolutionContext), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 1d7d648d0ca81..215d8b926b90e 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -241,15 +241,18 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { ctx: context.Background(), currentDirectory: "/", } - provider, cleanup := factory.NewResolver(module.NewResolver(session, core.EmptyCompilerOptions, "", "", nil)) + provider, cleanup := factory.NewResolver(module.ResolverOptions{ + Host: session, + CompilerOptions: core.EmptyCompilerOptions, + }) for range 2 { _, _, err := provider.ResolveModuleNameFromDirectory("pkg", "/src", core.ResolutionModeESM) assert.ErrorContains(t, err, "callback error") } assert.Equal(t, conn.calls, 2) - assert.Equal(t, len(session.inProgressSnapshots), 1) + assert.Equal(t, len(session.programResolutionContexts), 1) cleanup() - assert.Equal(t, len(session.inProgressSnapshots), 0) + assert.Equal(t, len(session.programResolutionContexts), 0) } func TestModuleResolutionCallbackErrorRejectsLanguageServerUpdate(t *testing.T) { diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 6dd4c9b891f21..f8bba578a534a 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -178,13 +178,17 @@ func processAllProgramFiles( contentMapperExtensions: opts.Config.ContentMapperExtensions(), } loader.addProjectReferenceTasks(singleThreaded) - resolverCompilerOptions := compilerOptions - if opts.ModuleResolverCompilerOptions != nil { - resolverCompilerOptions = opts.ModuleResolverCompilerOptions + resolverOptions := module.ResolverOptions{ + Host: loader.projectReferenceFileMapper.host, + CompilerOptions: compilerOptions, + TypingsLocation: opts.TypingsLocation, + ProjectName: opts.ProjectName, + ExtraExtensions: opts.Config.ContentMapperExtensions(), } - loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, resolverCompilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) if opts.CreateModuleResolver != nil { - loader.resolver = opts.CreateModuleResolver(loader.resolver) + loader.resolver = opts.CreateModuleResolver(resolverOptions) + } else { + loader.resolver = module.NewResolver(resolverOptions) } if opts.Tracing != nil { defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)() @@ -984,7 +988,12 @@ func (p *fileLoader) resolveLibrary(libraryName, resolveFrom string) (*module.Re if tr := p.opts.Tracing; tr != nil { defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom}, false)() } - resolved, trace, _ := p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) + resolved, trace, err := p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) + if err != nil { + p.moduleResolutionErrorOnce.Do(func() { + p.moduleResolutionError = err + }) + } return resolved, trace } diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 86f985049b487..2aa270bd11302 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -35,16 +35,15 @@ import ( ) type ProgramOptions struct { - Host CompilerHost - Config *tsoptions.ParsedCommandLine - UseSourceOfProjectReference bool - SingleThreaded core.Tristate - CreateCheckerPool func(*Program) CheckerPool - TypingsLocation string - ProjectName string - Tracing *tracing.Tracing - CreateModuleResolver func(fallback module.Resolver) module.Resolver - ModuleResolverCompilerOptions *core.CompilerOptions + Host CompilerHost + Config *tsoptions.ParsedCommandLine + UseSourceOfProjectReference bool + SingleThreaded core.Tristate + CreateCheckerPool func(*Program) CheckerPool + TypingsLocation string + ProjectName string + Tracing *tracing.Tracing + CreateModuleResolver func(options module.ResolverOptions) module.Resolver // SkipModuleResolution avoids all module and type reference resolution while // still collecting import metadata needed for emit. SkipModuleResolution bool @@ -307,7 +306,7 @@ func (p *Program) UpdateProgram( changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool, - createModuleResolver func(module.Resolver) module.Resolver, + createModuleResolver func(module.ResolverOptions) module.Resolver, ) (*Program, *ast.SourceFile, bool) { if result, newFile, reused := p.ReuseProgram(changedFilePath, newHost, createCheckerPool, createModuleResolver); reused { return result, newFile, true @@ -334,7 +333,7 @@ func (p *Program) ReuseProgram( changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool, - createModuleResolver func(module.Resolver) module.Resolver, + createModuleResolver func(module.ResolverOptions) module.Resolver, ) (*Program, *ast.SourceFile, bool) { newOpts := p.opts newOpts.Host = newHost diff --git a/tsc/internal/ls/autoimport/aliasresolver.go b/tsc/internal/ls/autoimport/aliasresolver.go index 19ebbde69c1e0..62882eef2e5f7 100644 --- a/tsc/internal/ls/autoimport/aliasresolver.go +++ b/tsc/internal/ls/autoimport/aliasresolver.go @@ -21,7 +21,7 @@ type pathAndFileName struct { type aliasResolver struct { toPath func(fileName string) tspath.Path host RegistryCloneHost - moduleResolver *module.DynamicResolver + moduleResolver *module.DefaultResolver rootFiles []*ast.SourceFile // symlinks maps from realpath to symlinked path and file name @@ -34,7 +34,7 @@ func newAliasResolver( rootFiles []*ast.SourceFile, symlinks map[tspath.Path]pathAndFileName, host RegistryCloneHost, - moduleResolver *module.DynamicResolver, + moduleResolver *module.DefaultResolver, toPath func(fileName string) tspath.Path, onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string), ) *aliasResolver { diff --git a/tsc/internal/ls/autoimport/aliasresolver_crash_test.go b/tsc/internal/ls/autoimport/aliasresolver_crash_test.go index b502c35ffab8c..1043d9a6f8111 100644 --- a/tsc/internal/ls/autoimport/aliasresolver_crash_test.go +++ b/tsc/internal/ls/autoimport/aliasresolver_crash_test.go @@ -58,7 +58,7 @@ func TestAliasResolverGetDiagnosticsDoesNotPanic(t *testing.T) { }, text, core.ScriptKindTS) binder.BindSourceFile(sourceFile) - resolver := module.NewResolver(host, core.EmptyCompilerOptions, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: core.EmptyCompilerOptions}) r := newAliasResolver( []*ast.SourceFile{sourceFile}, nil, diff --git a/tsc/internal/ls/autoimport/extract.go b/tsc/internal/ls/autoimport/extract.go index bc489101192da..f21f585ae90a9 100644 --- a/tsc/internal/ls/autoimport/extract.go +++ b/tsc/internal/ls/autoimport/extract.go @@ -28,7 +28,7 @@ type symbolExtractor struct { type exportExtractor struct { *symbolExtractor - moduleResolver *module.DynamicResolver + moduleResolver *module.DefaultResolver } type extractorStats struct { @@ -70,7 +70,7 @@ func newSymbolExtractor(packageName string, checker *checker.Checker, toPath fun } } -func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.DynamicResolver, realpath func(string) string) *exportExtractor { +func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.DefaultResolver, realpath func(string) string) *exportExtractor { return &exportExtractor{ symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath), moduleResolver: moduleResolver, diff --git a/tsc/internal/ls/autoimport/registry.go b/tsc/internal/ls/autoimport/registry.go index 1e8cefcd6c59a..a6c2f256afba3 100644 --- a/tsc/internal/ls/autoimport/registry.go +++ b/tsc/internal/ls/autoimport/registry.go @@ -1099,7 +1099,10 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan } } if len(rootFiles) > 0 { - moduleResolver := module.NewResolverWithOptions(b.host, core.EmptyCompilerOptions, "", "", b.resolverOptions) + resolverOptions := b.resolverOptions + resolverOptions.Host = b.host + resolverOptions.CompilerOptions = core.EmptyCompilerOptions + moduleResolver := module.NewResolver(resolverOptions) aliasResolver := newAliasResolver( slices.Collect(maps.Values(rootFiles)), nil, @@ -1244,7 +1247,10 @@ func (b *registryBuilder) buildProjectBucket( var mu sync.Mutex fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().UseCaseSensitiveFileNames()) result.bucket = &RegistryBucket{} - moduleResolver := module.NewResolverWithOptions(b.host, core.EmptyCompilerOptions, "", "", b.resolverOptions) + resolverOptions := b.resolverOptions + resolverOptions.Host = b.host + resolverOptions.CompilerOptions = core.EmptyCompilerOptions + moduleResolver := module.NewResolver(resolverOptions) program := b.host.GetProgramForProject(projectID) projectRootPath := b.base.toPath(program.GetCurrentDirectory()) symlinkCache := program.GetSymlinkCache() diff --git a/tsc/internal/ls/autoimport/util.go b/tsc/internal/ls/autoimport/util.go index f1dda9c3e9f14..e2cd1f175b440 100644 --- a/tsc/internal/ls/autoimport/util.go +++ b/tsc/internal/ls/autoimport/util.go @@ -314,10 +314,12 @@ func (rh *resolutionHost) FS() vfs.FS { return rh.fs } -func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.DynamicResolver { +func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.DefaultResolver { rh := &resolutionHost{ fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}), currentDirectory: host.GetCurrentDirectory(), } - return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts) + opts.Host = rh + opts.CompilerOptions = core.EmptyCompilerOptions + return module.NewResolver(opts) } diff --git a/tsc/internal/ls/sourcedefinition.go b/tsc/internal/ls/sourcedefinition.go index 1aa626639f5e5..0a8024aa644f2 100644 --- a/tsc/internal/ls/sourcedefinition.go +++ b/tsc/internal/ls/sourcedefinition.go @@ -135,7 +135,7 @@ type sourceDefResolver struct { options *core.CompilerOptions getSourceFile func(string) *ast.SourceFile resolveFrom string - resolver *module.DynamicResolver + resolver *module.DefaultResolver parsedFiles map[string]*ast.SourceFile } @@ -152,7 +152,12 @@ func (l *LanguageService) newSourceDefResolver( options: options, getSourceFile: program.GetSourceFile, resolveFrom: resolveFrom, - resolver: module.NewResolver(program.Host(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), "", program.CommandLine().ContentMapperExtensions()), + resolver: module.NewResolver(module.ResolverOptions{ + Host: program.Host(), + CompilerOptions: noDtsOptions, + TypingsLocation: program.GetGlobalTypingsCacheLocation(), + ExtraExtensions: program.CommandLine().ContentMapperExtensions(), + }), } } diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index d316cafefde38..367884c90698a 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -66,7 +66,7 @@ func (t *tracer) getTraces() []DiagAndArgs { } type resolutionState struct { - resolver *DynamicResolver + resolver *DefaultResolver tracer *tracer // request fields @@ -97,7 +97,7 @@ func newResolutionState( resolutionMode core.ResolutionMode, compilerOptions *core.CompilerOptions, redirectedReference ResolvedProjectReference, - resolver *DynamicResolver, traceBuilder *tracer, + resolver *DefaultResolver, traceBuilder *tracer, ) *resolutionState { state := &resolutionState{ name: name, @@ -145,7 +145,7 @@ func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redir return compilerOptions } -type DynamicResolver struct { +type DefaultResolver struct { caches host ResolutionHost compilerOptions *core.CompilerOptions @@ -156,59 +156,42 @@ type DynamicResolver struct { } type ResolverOptions struct { + Host ResolutionHost + CompilerOptions *core.CompilerOptions + TypingsLocation string + ProjectName string + ExtraExtensions []string PackageJsonCache *packagejson.InfoCache } -func NewResolver( - host ResolutionHost, - options *core.CompilerOptions, - typingsLocation string, - projectName string, - extraExtensions []string, -) *DynamicResolver { - return &DynamicResolver{ - host: host, - caches: newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options), - compilerOptions: options, - typingsLocation: typingsLocation, - projectName: projectName, - extraExtensions: extraExtensions, - } -} - -func NewResolverWithOptions( - host ResolutionHost, - compilerOptions *core.CompilerOptions, - typingsLocation string, - projectName string, - opts ResolverOptions, -) *DynamicResolver { - r := &DynamicResolver{ - host: host, - compilerOptions: compilerOptions, - typingsLocation: typingsLocation, - projectName: projectName, +func NewResolver(opts ResolverOptions) *DefaultResolver { + r := &DefaultResolver{ + host: opts.Host, + compilerOptions: opts.CompilerOptions, + typingsLocation: opts.TypingsLocation, + projectName: opts.ProjectName, + extraExtensions: opts.ExtraExtensions, } if opts.PackageJsonCache != nil { r.packageJsonInfoCache = opts.PackageJsonCache } else { - r.caches = newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), compilerOptions) + r.caches = newCaches(opts.Host.GetCurrentDirectory(), opts.Host.FS().UseCaseSensitiveFileNames(), opts.CompilerOptions) } return r } -func (r *DynamicResolver) newTraceBuilder() *tracer { +func (r *DefaultResolver) newTraceBuilder() *tracer { if r.compilerOptions.TraceResolution == core.TSTrue { return &tracer{} } return nil } -func (r *DynamicResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { +func (r *DefaultResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory) } -func (r *DynamicResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { +func (r *DefaultResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { r.caches.packageJsonInfoCache.Range(f) } @@ -218,7 +201,7 @@ func (r *tracer) traceResolutionUsingProjectReference(redirectedReference Resolv } } -func (r *DynamicResolver) ResolveTypeReferenceDirective( +func (r *DefaultResolver) ResolveTypeReferenceDirective( typeReferenceDirectiveName string, containingFile string, resolutionMode core.ResolutionMode, @@ -263,17 +246,17 @@ func (r *DynamicResolver) ResolveTypeReferenceDirective( return result, traceBuilder.getTraces() } -func (r *DynamicResolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs, error) { +func (r *DefaultResolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs, error) { result, trace := r.resolveModuleName(moduleName, containingFile, tspath.GetDirectoryPath(containingFile), resolutionMode, redirectedReference) return result, trace, nil } -func (r *DynamicResolver) ResolveModuleNameFromDirectory(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, []DiagAndArgs, error) { +func (r *DefaultResolver) ResolveModuleNameFromDirectory(moduleName string, containingDirectory string, resolutionMode core.ResolutionMode) (*ResolvedModule, []DiagAndArgs, error) { result, trace := r.resolveModuleName(moduleName, containingDirectory, containingDirectory, resolutionMode, nil) return result, trace, nil } -func (r *DynamicResolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { +func (r *DefaultResolver) resolveModuleName(moduleName string, containingFile string, containingDirectory string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { traceBuilder := r.newTraceBuilder() cacheKey := moduleResolutionCacheKey{ @@ -333,7 +316,7 @@ func (r *DynamicResolver) resolveModuleName(moduleName string, containingFile st return finalResult, traceBuilder.getTraces() } -func (r *DynamicResolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { +func (r *DefaultResolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil) @@ -344,7 +327,7 @@ func (r *DynamicResolver) ResolvePackageDirectory(moduleName string, containingF return nil } -func (r *DynamicResolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { +func (r *DefaultResolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { if r.typingsLocation == "" || tspath.IsExternalModuleNameRelative(moduleName) || (originalResult.ResolvedFileName != "" && tspath.ExtensionIsOneOf(originalResult.Extension, tspath.SupportedTSExtensionsWithJsonFlat)) { @@ -373,7 +356,7 @@ func (r *DynamicResolver) tryResolveFromTypingsLocation(moduleName string, conta return result } -func (r *DynamicResolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule { +func (r *DefaultResolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule { containingDirectory := tspath.GetDirectoryPath(containingFile) state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, core.ModuleKindCommonJS, r.compilerOptions, nil, r, nil) state.isConfigLookup = true @@ -1996,7 +1979,7 @@ type ParsedPatterns struct { patterns []core.Pattern } -func (r *DynamicResolver) getParsedPatternsForPaths(compilerOptions *core.CompilerOptions) *ParsedPatterns { +func (r *DefaultResolver) getParsedPatternsForPaths(compilerOptions *core.CompilerOptions) *ParsedPatterns { return r.parsedPatternsForPaths.Get(compilerOptions.Paths) } @@ -2087,7 +2070,10 @@ func extensionIsOk(extensions extensions, extension string) bool { } func ResolveConfig(moduleName string, containingFile string, host ResolutionHost) *ResolvedModule { - resolver := NewResolver(host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) + resolver := NewResolver(ResolverOptions{ + Host: host, + CompilerOptions: &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + }) return resolver.resolveConfig(moduleName, containingFile) } @@ -2173,7 +2159,7 @@ func (e *ResolvedEntrypoint) SymlinkOrRealpath() string { return e.ResolvedFileName } -func (r *DynamicResolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string, enableDirectorySearch bool) []*ResolvedEntrypoint { +func (r *DefaultResolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string, enableDirectorySearch bool) []*ResolvedEntrypoint { extensions := extensionsTypeScript | extensionsDeclaration features := NodeResolutionFeaturesAll state := &resolutionState{resolver: r, extensions: extensions, features: features, compilerOptions: r.compilerOptions} @@ -2232,7 +2218,7 @@ func (r *DynamicResolver) GetEntrypointsFromPackageJsonInfo(packageJson *package return nil } -func (r *DynamicResolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { +func (r *DefaultResolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { var originalFileName string resolvedFileName := fileName if realPath := r.host.FS().Realpath(fileName); realPath != fileName { diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 1b099aff5e80a..9de8cf9c8c223 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -39,7 +39,7 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) for _, name := range []string{"pkg", "pkg/"} { r, _, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil) @@ -168,7 +168,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) type resolutionResult struct { name string @@ -240,7 +240,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) var panicked atomic.Bool type resolutionResult struct { @@ -363,7 +363,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) var panicked atomic.Bool type resolutionResult struct { diff --git a/tsc/internal/module/staticresolver_test.go b/tsc/internal/module/staticresolver_test.go index a0d51ffc44c27..a93c711158389 100644 --- a/tsc/internal/module/staticresolver_test.go +++ b/tsc/internal/module/staticresolver_test.go @@ -16,16 +16,13 @@ func TestStaticResolver(t *testing.T) { "/repo/node_modules/fallback/package.json": `{"name":"fallback","types":"index.d.ts"}`, "/repo/node_modules/fallback/index.d.ts": "export {};", }, true) - fallback := module.NewResolver( - &resolutionHostStub{fs: fs, cwd: "/repo"}, - &core.CompilerOptions{ + fallback := module.NewResolver(module.ResolverOptions{ + Host: &resolutionHostStub{fs: fs, cwd: "/repo"}, + CompilerOptions: &core.CompilerOptions{ Module: core.ModuleKindESNext, ModuleResolution: core.ModuleResolutionKindBundler, }, - "", - "", - nil, - ) + }) esm := core.ResolutionModeESM resolutions, err := module.NewStaticResolutions( []module.StaticResolutionEntry{ diff --git a/tsc/internal/project/ata/ata.go b/tsc/internal/project/ata/ata.go index 66cf484ef7794..e6f0b1c7504db 100644 --- a/tsc/internal/project/ata/ata.go +++ b/tsc/internal/project/ata/ata.go @@ -188,7 +188,10 @@ func (ti *TypingsInstaller) installTypings( if packageNames, ok := ti.installWorker(requestID, scopedTypings, logger); ok { logger.Log(fmt.Sprintf("ATA:: Installed typings %v", packageNames)) var installedTypingFiles []string - resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{ + Host: ti.host, + CompilerOptions: &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + }) for _, packageName := range filteredTypings { typingFile := ti.typingToFileName(resolver, packageName) if typingFile == "" { @@ -416,7 +419,10 @@ func (ti *TypingsInstaller) processCacheLocation(projectID string, fs vfs.FS, lo logger.Log("ATA:: Loaded content of " + packageLockJson + ": " + npmLockContents) // !!! sheetal strada uses Node10 - resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{ + Host: ti.host, + CompilerOptions: &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, + }) if npmConfig.DevDependencies != nil && (npmLock.Packages != nil || npmLock.Dependencies != nil) { for key := range npmConfig.DevDependencies { npmLockValue, npmLockValueExists := npmLock.Packages["node_modules/"+key] @@ -476,7 +482,7 @@ func (ti *TypingsInstaller) ensureTypingsLocationExists(fs vfs.FS, logger loggin } } -func (ti *TypingsInstaller) typingToFileName(resolver *module.DynamicResolver, packageName string) string { +func (ti *TypingsInstaller) typingToFileName(resolver *module.DefaultResolver, packageName string) string { result, _, _ := resolver.ResolveModuleName(packageName, tspath.CombinePaths(ti.typingsLocation, "index.d.ts"), core.ModuleKindNone, nil) return result.ResolvedFileName } diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 288ee5acaf6d5..1b94c3dbf60a4 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -510,11 +510,11 @@ func (p *Project) CreateProgram() CreateProgramResult { return newCheckerPool(p.host.sessionOptions.CheckerPoolOptions, program, p.log) } var cleanupModuleResolver func() - createModuleResolver := func(fallback module.Resolver) module.Resolver { + createModuleResolver := func(options module.ResolverOptions) module.Resolver { if p.moduleResolverFactory == nil { - return fallback + return module.NewResolver(options) } - resolver, cleanup := p.moduleResolverFactory.NewResolver(fallback) + resolver, cleanup := p.moduleResolverFactory.NewResolver(options) cleanupModuleResolver = cleanup return resolver } @@ -567,19 +567,14 @@ func (p *Project) CreateProgram() CreateProgramResult { if p.GetTypeAcquisition().Enable.IsTrue() { typingsLocation = p.host.sessionOptions.TypingsLocation } - var moduleResolverCompilerOptions *core.CompilerOptions - if p.moduleResolverFactory != nil { - moduleResolverCompilerOptions = p.moduleResolverFactory.CompilerOptions() - } newProgram = compiler.NewProgram( compiler.ProgramOptions{ - Host: p.host, - Config: commandLine, - UseSourceOfProjectReference: true, - TypingsLocation: typingsLocation, - CreateCheckerPool: createCheckerPool, - CreateModuleResolver: createModuleResolver, - ModuleResolverCompilerOptions: moduleResolverCompilerOptions, + Host: p.host, + Config: commandLine, + UseSourceOfProjectReference: true, + TypingsLocation: typingsLocation, + CreateCheckerPool: createCheckerPool, + CreateModuleResolver: createModuleResolver, }, ) } diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 2ab7c6b35bda4..7a5234165b1fc 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -329,8 +329,7 @@ type APICreateProgramRequest struct { } type ModuleResolverFactory interface { - CompilerOptions() *core.CompilerOptions - NewResolver(fallback module.Resolver) (module.Resolver, func()) + NewResolver(options module.ResolverOptions) (module.Resolver, func()) } type APIReconfigureProgramRequest struct { diff --git a/tsc/internal/tsoptions/contentmappers.go b/tsc/internal/tsoptions/contentmappers.go index 124e34a1d4bb3..10e31596d94bf 100644 --- a/tsc/internal/tsoptions/contentmappers.go +++ b/tsc/internal/tsoptions/contentmappers.go @@ -15,7 +15,10 @@ import ( // and package directory. It never executes the package. On failure it returns a diagnostic describing why // the mapper could not be resolved; on success the diagnostic is nil. func resolveContentMapperManifest(host ParseConfigHost, containingFile string, packageName string) (contentmapper.Manifest, string, *ast.Diagnostic) { - resolver := module.NewResolver(host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindBundler}, "", "", nil) + resolver := module.NewResolver(module.ResolverOptions{ + Host: host, + CompilerOptions: &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindBundler}, + }) resolved := resolver.ResolvePackageDirectory(packageName, containingFile, core.ResolutionModeNone, nil) if resolved == nil || resolved.ResolvedFileName == "" { return contentmapper.Manifest{}, "", ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_could_not_be_resolved, packageName) From 13e887bb0a8bfb4d5f3712de0b15287983f486d9 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 22 Sep 2026 17:22:37 -0700 Subject: [PATCH 20/20] Delete atrocity --- tsc/internal/compiler/program.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 2aa270bd11302..4521c7f318d21 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -2313,11 +2313,6 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { }) } -func (p *Program) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { - resolved, _, _ := p.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) - return resolved -} - func (p *Program) ForEachResolvedModule(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { forEachResolution(p.resolvedModules, callback, file) }