From 41d1ce62cd4fa4b1088218f6afbc8f4eb4721027 Mon Sep 17 00:00:00 2001 From: TypeScript Bot Date: Wed, 27 Nov 2019 13:47:36 -0800 Subject: [PATCH] Cherry-pick PR #35335 into release-3.7 (#35367) Component commits: c44de23315 Fix compileOnSaveEmit when using source of project reference redirect with --out Fixes #35226 522efb4798 Fix typo --- src/compiler/emitter.ts | 5 +- src/compiler/program.ts | 11 +-- src/compiler/types.ts | 12 ++- src/compiler/utilities.ts | 30 +++---- .../tsserver/projectReferenceCompileOnSave.ts | 79 +++++++++++++++++++ 5 files changed, 110 insertions(+), 27 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index cbff1fb2512f7..707d278eea0fe 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -23,7 +23,7 @@ namespace ts { forceDtsEmit = false, onlyBuildInfo?: boolean, includeBuildInfo?: boolean) { - const sourceFiles = isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile); + const sourceFiles = isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile, forceDtsEmit); const options = host.getCompilerOptions(); if (options.outFile || options.out) { const prepends = host.getPrependNodes(); @@ -274,7 +274,7 @@ namespace ts { forEachEmittedFile( host, emitSourceFileOrBundle, - getSourceFilesToEmit(host, targetSourceFile), + getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit), forceDtsEmit, onlyBuildInfo, !targetSourceFile @@ -754,6 +754,7 @@ namespace ts { getLibFileFromReference: notImplemented, isSourceFileFromExternalLibrary: returnFalse, getResolvedProjectReferenceToRedirect: returnUndefined, + isSourceOfProjectReferenceRedirect: returnFalse, writeFile: (name, text, writeByteOrderMark) => { switch (name) { case jsFilePath: diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 989e7fa28cb23..4fd4d864138d7 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1010,15 +1010,9 @@ namespace ts { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } - function isValidSourceFileForEmit(file: SourceFile) { - // source file is allowed to be emitted and its not source of project reference redirect - return sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect) && - !isSourceOfProjectReferenceRedirect(file.fileName); - } - function getCommonSourceDirectory() { if (commonSourceDirectory === undefined) { - const emittedFiles = filter(files, file => isValidSourceFileForEmit(file)); + const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { // If a rootDir is specified use it as the commonSourceDirectory commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); @@ -1477,6 +1471,7 @@ namespace ts { getLibFileFromReference: program.getLibFileFromReference, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect, + isSourceOfProjectReferenceRedirect, getProbableSymlinks, writeFile: writeFileCallback || ( (fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)), @@ -2945,7 +2940,7 @@ namespace ts { const rootPaths = arrayToSet(rootNames, toPath); for (const file of files) { // Ignore file that is not emitted - if (isValidSourceFileForEmit(file) && !rootPaths.has(file.path)) { + if (sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { addProgramDiagnosticAtRefPath( file, rootPaths, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 08cc7343ccaea..3d1b525859b94 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5714,13 +5714,19 @@ namespace ts { } /* @internal */ - export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost { + export interface SourceFileMayBeEmittedHost { + getCompilerOptions(): CompilerOptions; + isSourceFileFromExternalLibrary(file: SourceFile): boolean; + getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined; + isSourceOfProjectReferenceRedirect(fileName: string): boolean; + } + + /* @internal */ + export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost, SourceFileMayBeEmittedHost { getSourceFiles(): readonly SourceFile[]; useCaseSensitiveFileNames(): boolean; getCurrentDirectory(): string; - isSourceFileFromExternalLibrary(file: SourceFile): boolean; - getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined; getLibFileFromReference(ref: FileReference): SourceFile | undefined; getCommonSourceDirectory(): string; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 30271cedc24da..daef302d6e676 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3659,34 +3659,36 @@ namespace ts { * @param host An EmitHost. * @param targetSourceFile An optional target source file to emit. */ - export function getSourceFilesToEmit(host: EmitHost, targetSourceFile?: SourceFile): readonly SourceFile[] { + export function getSourceFilesToEmit(host: EmitHost, targetSourceFile?: SourceFile, forceDtsEmit?: boolean): readonly SourceFile[] { const options = host.getCompilerOptions(); - const isSourceFileFromExternalLibrary = (file: SourceFile) => host.isSourceFileFromExternalLibrary(file); - const getResolvedProjectReferenceToRedirect = (fileName: string) => host.getResolvedProjectReferenceToRedirect(fileName); if (options.outFile || options.out) { const moduleKind = getEmitModuleKind(options); const moduleEmitEnabled = options.emitDeclarationOnly || moduleKind === ModuleKind.AMD || moduleKind === ModuleKind.System; // Can emit only sources that are not declaration file and are either non module code or module with --module or --target es6 specified - return filter(host.getSourceFiles(), sourceFile => - (moduleEmitEnabled || !isExternalModule(sourceFile)) && sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect)); + return filter( + host.getSourceFiles(), + sourceFile => + (moduleEmitEnabled || !isExternalModule(sourceFile)) && + sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit) + ); } else { const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile]; - return filter(sourceFiles, sourceFile => sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect)); + return filter( + sourceFiles, + sourceFile => sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit) + ); } } /** Don't call this for `--outFile`, just for `--outDir` or plain emit. `--outFile` needs additional checks. */ - export function sourceFileMayBeEmitted( - sourceFile: SourceFile, - options: CompilerOptions, - isSourceFileFromExternalLibrary: (file: SourceFile) => boolean, - getResolvedProjectReferenceToRedirect: (fileName: string) => ResolvedProjectReference | undefined - ) { + export function sourceFileMayBeEmitted(sourceFile: SourceFile, host: SourceFileMayBeEmittedHost, forceDtsEmit?: boolean) { + const options = host.getCompilerOptions(); return !(options.noEmitForJsFiles && isSourceFileJS(sourceFile)) && !sourceFile.isDeclarationFile && - !isSourceFileFromExternalLibrary(sourceFile) && - !(isJsonSourceFile(sourceFile) && getResolvedProjectReferenceToRedirect(sourceFile.fileName)); + !host.isSourceFileFromExternalLibrary(sourceFile) && + !(isJsonSourceFile(sourceFile) && host.getResolvedProjectReferenceToRedirect(sourceFile.fileName)) && + (forceDtsEmit || !host.isSourceOfProjectReferenceRedirect(sourceFile.fileName)); } export function getSourceFilePathInNewDir(fileName: string, host: EmitHost, newDirPath: string): string { diff --git a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts index 7399078f1ef9d..301854a02a296 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts @@ -406,4 +406,83 @@ ${appendDts}` }); }); }); + + describe("unittests:: tsserver:: with project references and compile on save with external projects", () => { + it("compile on save emits same output as project build", () => { + const tsbaseJson: File = { + path: `${projectRoot}/tsbase.json`, + content: JSON.stringify({ + compileOnSave: true, + compilerOptions: { + module: "none", + composite: true + } + }) + }; + const buttonClass = `${projectRoot}/buttonClass`; + const buttonConfig: File = { + path: `${buttonClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const buttonSource: File = { + path: `${buttonClass}/Source.ts`, + content: `module Hmi { + export class Button { + public static myStaticFunction() { + } + } +}` + }; + + const siblingClass = `${projectRoot}/SiblingClass`; + const siblingConfig: File = { + path: `${siblingClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + references: [{ + path: "../buttonClass/" + }], + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const siblingSource: File = { + path: `${siblingClass}/Source.ts`, + content: `module Hmi { + export class Sibling { + public mySiblingFunction() { + } + } +}` + }; + const host = createServerHost([libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); + + // ts build should succeed + const solutionBuilder = tscWatch.createSolutionBuilder(host, [siblingConfig.path], {}); + solutionBuilder.build(); + assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); + const sourceJs = changeExtension(siblingSource.path, ".js"); + const expectedSiblingJs = host.readFile(sourceJs); + + const session = createSession(host); + openFilesForSession([siblingSource], session); + + session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { + file: siblingSource.path, + projectFileName: siblingConfig.path + } + }); + assert.equal(host.readFile(sourceJs), expectedSiblingJs); + }); + }); }