diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ef6d7fb7ef10b..5f5c5d5baf9dc 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -317,9 +317,9 @@ export class API implements FormatDiagnosticsHo return this.client.apiRequest("transpileModule", { input, options }); } - async transpileModuleFromFile(fileName: string, options: TranspileOptions = {}): Promise { + async transpileModuleFromFile(file: DocumentIdentifier, options: TranspileOptions = {}): Promise { await this.ensureInitialized(); - return this.client.apiRequest("transpileModuleFromFile", { fileName, options }); + return this.client.apiRequest("transpileModuleFromFile", { fileName: resolveFileName(file), options }); } async transpileDeclaration(input: string, options: TranspileOptions = {}): Promise { @@ -327,9 +327,9 @@ export class API implements FormatDiagnosticsHo return this.client.apiRequest("transpileDeclaration", { input, options }); } - async transpileDeclarationFromFile(fileName: string, options: TranspileOptions = {}): Promise { + async transpileDeclarationFromFile(file: DocumentIdentifier, options: TranspileOptions = {}): Promise { await this.ensureInitialized(); - return this.client.apiRequest("transpileDeclarationFromFile", { fileName, options }); + return this.client.apiRequest("transpileDeclarationFromFile", { fileName: resolveFileName(file), options }); } async updateSnapshot(params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Promise { @@ -1086,8 +1086,8 @@ export class Program implements FormatDiagnosticsHost { * is not part of the program. Metadata is fetched lazily per file and cached on this * `Program` instance. */ - getSourceFileMetadata(fileName: string): Promise { - return this.getSourceFileMetadataByPath(this.toPath(fileName)); + getSourceFileMetadata(file: DocumentIdentifier): Promise { + return this.getSourceFileMetadataByPath(this.toPath(resolveFileName(file))); } /** diff --git a/packages/typescript/src/api/proto.ts b/packages/typescript/src/api/proto.ts index 6c29b4c3a4a13..0be9ac250a756 100644 --- a/packages/typescript/src/api/proto.ts +++ b/packages/typescript/src/api/proto.ts @@ -60,6 +60,12 @@ export function resolveFileName(identifier: DocumentIdentifier): string { if (typeof identifier === "string") { return identifier; } + if (typeof identifier !== "object" || identifier === null || typeof identifier.uri !== "string") { + const received = typeof identifier === "object" && identifier !== null + ? `an object with keys: ${Object.keys(identifier).join(", ")}` + : String(identifier); + throw new TypeError(`Expected a string or { uri } for the document, received ${received}`); + } return documentURIToFileName(identifier.uri); } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index b92a3fa26fb72..f9c9a84620fa2 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -451,20 +451,20 @@ export class API implements FormatDiagnosticsHo } get transpileModuleFromFile(): { - (fileName: string, options?: TranspileOptions): TranspileOutput; - gen(fileName: string, options?: TranspileOptions): Generator; + (file: DocumentIdentifier, options?: TranspileOptions): TranspileOutput; + gen(file: DocumentIdentifier, options?: TranspileOptions): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "transpileModuleFromFile", - function (fileName: string, options: TranspileOptions = {}): TranspileOutput { + function (file: DocumentIdentifier, options: TranspileOptions = {}): TranspileOutput { owner.ensureInitialized(); - return owner.client.apiRequest("transpileModuleFromFile", { fileName, options }); + return owner.client.apiRequest("transpileModuleFromFile", { fileName: resolveFileName(file), options }); }, - function* (fileName: string, options: TranspileOptions = {}): Generator { + function* (file: DocumentIdentifier, options: TranspileOptions = {}): Generator { yield* owner.ensureInitialized.gen(); - return yield* apiRequest("transpileModuleFromFile", { fileName, options }); + return yield* apiRequest("transpileModuleFromFile", { fileName: resolveFileName(file), options }); }, ); } @@ -489,20 +489,20 @@ export class API implements FormatDiagnosticsHo } get transpileDeclarationFromFile(): { - (fileName: string, options?: TranspileOptions): TranspileOutput; - gen(fileName: string, options?: TranspileOptions): Generator; + (file: DocumentIdentifier, options?: TranspileOptions): TranspileOutput; + gen(file: DocumentIdentifier, options?: TranspileOptions): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "transpileDeclarationFromFile", - function (fileName: string, options: TranspileOptions = {}): TranspileOutput { + function (file: DocumentIdentifier, options: TranspileOptions = {}): TranspileOutput { owner.ensureInitialized(); - return owner.client.apiRequest("transpileDeclarationFromFile", { fileName, options }); + return owner.client.apiRequest("transpileDeclarationFromFile", { fileName: resolveFileName(file), options }); }, - function* (fileName: string, options: TranspileOptions = {}): Generator { + function* (file: DocumentIdentifier, options: TranspileOptions = {}): Generator { yield* owner.ensureInitialized.gen(); - return yield* apiRequest("transpileDeclarationFromFile", { fileName, options }); + return yield* apiRequest("transpileDeclarationFromFile", { fileName: resolveFileName(file), options }); }, ); } @@ -2008,18 +2008,18 @@ export class Program implements FormatDiagnosticsHost { * `Program` instance. */ get getSourceFileMetadata(): { - (fileName: string): SourceFileMetadata | undefined; - gen(fileName: string): Generator; + (file: DocumentIdentifier): SourceFileMetadata | undefined; + gen(file: DocumentIdentifier): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getSourceFileMetadata", - function (fileName: string): SourceFileMetadata | undefined { - return owner.getSourceFileMetadataByPath(owner.toPath(fileName)); + function (file: DocumentIdentifier): SourceFileMetadata | undefined { + return owner.getSourceFileMetadataByPath(owner.toPath(resolveFileName(file))); }, - function* (fileName: string): Generator { - return yield* owner.getSourceFileMetadataByPath.gen(owner.toPath(fileName)); + function* (file: DocumentIdentifier): Generator { + return yield* owner.getSourceFileMetadataByPath.gen(owner.toPath(resolveFileName(file))); }, ); } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b40091c90d6c9..0e636de995ebf 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -52,6 +52,7 @@ import { CheckFlags, type ConditionalType, DiagnosticCategory, + type DocumentIdentifier, EmitOnly, type FreshableType, type ImportAdderAction, @@ -287,7 +288,7 @@ describe("API", () => { }); assert.equal(isolatedDeclarationModuleOutput.diagnostics?.length ?? 0, 0); - const moduleFileOutput = await api.transpileModuleFromFile("/input.ts", { + const moduleFileOutput = await api.transpileModuleFromFile({ uri: "file:///input.ts" }, { compilerOptions: { module: ModuleKind.CommonJS }, }); assert.match(moduleFileOutput.outputText, /exports\.x = 1/); @@ -300,7 +301,7 @@ describe("API", () => { }); assert.equal(windowsDeclarationOutput.outputText, "export declare const x: number;\n"); - const declarationFileOutput = await api.transpileDeclarationFromFile("/input.ts"); + const declarationFileOutput = await api.transpileDeclarationFromFile({ uri: "file:///input.ts" }); assert.equal(declarationFileOutput.outputText, "export declare const x: number;\n"); } finally { @@ -1087,6 +1088,26 @@ describe("Checker - getMemberInModuleExports", () => { }); describe("SourceFile", () => { + test("getSourceFile rejects invalid document identifiers", async () => { + const api = spawnAPI(); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getProject("/tsconfig.json")!.program; + const document = { fileName: "/src/index.ts" } as unknown as DocumentIdentifier; + + await assert.rejects( // @sync: assert.throws( + () => program.getSourceFile(document), + { + name: "TypeError", + message: "Expected a string or { uri } for the document, received an object with keys: fileName", + }, + ); + } + finally { + await api.close(); + } + }); + test("getSourceFileNames returns all program files, not just root files", async () => { const api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -1173,7 +1194,7 @@ describe("SourceFile", () => { const mts = await program.getSourceFile("/src/esm.mts"); assert.ok(mts); - assert.equal((await program.getSourceFileMetadata(mts.fileName))?.impliedNodeFormat, ModuleKind.ESNext); + assert.equal((await program.getSourceFileMetadata({ uri: "file:///src/esm.mts" }))?.impliedNodeFormat, ModuleKind.ESNext); assert.equal((await program.getSourceFileMetadataByPath(mts.path))?.impliedNodeFormat, ModuleKind.ESNext); const cts = await program.getSourceFile("/src/cjs.cts"); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index d534bc20a6e4e..e945d6479f628 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -62,6 +62,7 @@ import { CheckFlags, type ConditionalType, DiagnosticCategory, + type DocumentIdentifier, EmitOnly, type FreshableType, type ImportAdderAction, @@ -295,7 +296,7 @@ describe("API", () => { }); assert.equal(isolatedDeclarationModuleOutput.diagnostics?.length ?? 0, 0); - const moduleFileOutput = api.transpileModuleFromFile("/input.ts", { + const moduleFileOutput = api.transpileModuleFromFile({ uri: "file:///input.ts" }, { compilerOptions: { module: ModuleKind.CommonJS }, }); assert.match(moduleFileOutput.outputText, /exports\.x = 1/); @@ -308,7 +309,7 @@ describe("API", () => { }); assert.equal(windowsDeclarationOutput.outputText, "export declare const x: number;\n"); - const declarationFileOutput = api.transpileDeclarationFromFile("/input.ts"); + const declarationFileOutput = api.transpileDeclarationFromFile({ uri: "file:///input.ts" }); assert.equal(declarationFileOutput.outputText, "export declare const x: number;\n"); } finally { @@ -1003,6 +1004,26 @@ describe("Checker - getMemberInModuleExports", () => { }); describe("SourceFile", () => { + test("getSourceFile rejects invalid document identifiers", () => { + const api = spawnAPI(); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getProject("/tsconfig.json")!.program; + const document = { fileName: "/src/index.ts" } as unknown as DocumentIdentifier; + + assert.throws( + () => program.getSourceFile(document), + { + name: "TypeError", + message: "Expected a string or { uri } for the document, received an object with keys: fileName", + }, + ); + } + finally { + api.close(); + } + }); + test("getSourceFileNames returns all program files, not just root files", () => { const api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -1089,7 +1110,7 @@ describe("SourceFile", () => { const mts = program.getSourceFile("/src/esm.mts"); assert.ok(mts); - assert.equal((program.getSourceFileMetadata(mts.fileName))?.impliedNodeFormat, ModuleKind.ESNext); + assert.equal((program.getSourceFileMetadata({ uri: "file:///src/esm.mts" }))?.impliedNodeFormat, ModuleKind.ESNext); assert.equal((program.getSourceFileMetadataByPath(mts.path))?.impliedNodeFormat, ModuleKind.ESNext); const cts = program.getSourceFile("/src/cjs.cts");