diff --git a/src/compiler/program.ts b/src/compiler/program.ts index bbc0fa09780..30c61f50dff 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -874,12 +874,12 @@ namespace ts { return oldProgram.structureIsReused = StructureIsReused.Completely; } - function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { + function getEmitHost(writeFileCallback?: WriteFileCallback, getCurrentDirectoryCallback?: GetCurrentDirectoryCallback): EmitHost { return { getCanonicalFileName, getCommonSourceDirectory: program.getCommonSourceDirectory, getCompilerOptions: program.getCompilerOptions, - getCurrentDirectory: () => currentDirectory, + getCurrentDirectory: getCurrentDirectoryCallback || (() => currentDirectory), getNewLine: () => host.getNewLine(), getSourceFile: program.getSourceFile, getSourceFileByPath: program.getSourceFileByPath, @@ -907,15 +907,15 @@ namespace ts { return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false)); } - function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: CustomTransformers): EmitResult { - return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers)); + function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: CustomTransformers, getCurrentDirectoryCallback?: GetCurrentDirectoryCallback): EmitResult { + return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers, getCurrentDirectoryCallback)); } function isEmitBlocked(emitFileName: string): boolean { return hasEmitBlockingDiagnostics.contains(toPath(emitFileName, currentDirectory, getCanonicalFileName)); } - function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { + function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers, getCurrentDirectoryCallback?: GetCurrentDirectoryCallback): EmitResult { let declarationDiagnostics: Diagnostic[] = []; if (options.noEmit) { @@ -960,7 +960,7 @@ namespace ts { const transformers = emitOnlyDtsFiles ? [] : getTransformers(options, customTransformers); const emitResult = emitFiles( emitResolver, - getEmitHost(writeFileCallback), + getEmitHost(writeFileCallback, getCurrentDirectoryCallback), sourceFile, emitOnlyDtsFiles, transformers); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ac64624e1a6..ac5de153226 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2405,6 +2405,10 @@ namespace ts { (fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: SourceFile[]): void; } + export interface GetCurrentDirectoryCallback { + (): string; + } + export class OperationCanceledException { } export interface CancellationToken { @@ -2436,7 +2440,7 @@ namespace ts { * used for writing the JavaScript and declaration files. Otherwise, the writeFile parameter * will be invoked when writing the JavaScript and declaration files. */ - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers, getCurrentDirectoryCallback?: GetCurrentDirectoryCallback): EmitResult; getOptionsDiagnostics(cancellationToken?: CancellationToken): Diagnostic[]; getGlobalDiagnostics(cancellationToken?: CancellationToken): Diagnostic[]; diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 7e262a1b257..3183ffd71b0 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -600,5 +600,48 @@ namespace ts.projectSystem { assert.isTrue(outFileContent.indexOf(file2.content) === -1); assert.isTrue(outFileContent.indexOf(file3.content) === -1); }); + + it("should use project root as current directory so that compile on save results in correct file mapping", () => { + const inputFileName = "Foo.ts"; + const file1 = { + path: `/root/TypeScriptProject3/TypeScriptProject3/${inputFileName}`, + content: "consonle.log('file1');" + }; + const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + + const outFileName = "bar.js"; + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path]), + options: { + outFile: outFileName, + sourceMap: true, + compileOnSave: true + }, + projectFileName: externalProjectName + }); + + const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); + + // Verify js file + const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; + assert.isTrue(host.fileExists(expectedOutFileName)); + const outFileContent = host.readFile(expectedOutFileName); + verifyContentHasString(outFileContent, file1.content); + verifyContentHasString(outFileContent, `//# sourceMappingURL=${outFileName}.map`); + + // Verify map file + const expectedMapFileName = expectedOutFileName + ".map"; + assert.isTrue(host.fileExists(expectedMapFileName)); + const mapFileContent = host.readFile(expectedMapFileName); + verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); + + function verifyContentHasString(content: string, string: string) { + assert.isTrue(content.indexOf(string) !== -1, `Expected "${content}" to have "${string}"`); + } + }); }); -} \ No newline at end of file +} diff --git a/src/server/builder.ts b/src/server/builder.ts index 895732ebece..711045d0ae6 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -148,9 +148,9 @@ namespace ts.server { const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false); if (!emitSkipped) { - const projectRootPath = this.project.getProjectRootPath(); + const currentDirectoryForEmit = this.project.getCurrentDirectoryForScriptInfoEmit(scriptInfo); for (const outputFile of outputFiles) { - const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); + const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, currentDirectoryForEmit); writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); } } diff --git a/src/server/project.ts b/src/server/project.ts index ac040a77ace..a7d8605315b 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -367,7 +367,16 @@ namespace ts.server { if (!this.languageServiceEnabled) { return undefined; } - return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles); + + const getCurrentDirectoryCallback = memoize( + () => this.getCurrentDirectoryForScriptInfoEmit(info) + ); + return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles, getCurrentDirectoryCallback); + } + + getCurrentDirectoryForScriptInfoEmit(info: ScriptInfo) { + const projectRootPath = this.getProjectRootPath(); + return projectRootPath || getDirectoryPath(info.fileName); } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { diff --git a/src/services/services.ts b/src/services/services.ts index b508285b182..11061181ee6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1528,7 +1528,7 @@ namespace ts { return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); } - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput { + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, getCurrentDirectoryCallback?: GetCurrentDirectoryCallback): EmitOutput { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); @@ -1543,7 +1543,7 @@ namespace ts { } const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, getCurrentDirectoryCallback); return { outputFiles, diff --git a/src/services/transpile.ts b/src/services/transpile.ts index 561c188c6cd..5ba393a90c9 100644 --- a/src/services/transpile.ts +++ b/src/services/transpile.ts @@ -104,7 +104,7 @@ namespace ts { addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); } // Emit - program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); + program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers, /*getCurrentDirectoryCallback*/ undefined); Debug.assert(outputText !== undefined, "Output generation failed"); diff --git a/src/services/types.ts b/src/services/types.ts index 2d47da2fd1d..07aaaeeb4b4 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -269,7 +269,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, getCurrentDirectoryCallBack?: GetCurrentDirectoryCallback): EmitOutput; getProgram(): Program;