Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix compileOnSaveEmit when using source of project reference redirect with --out #35335

Merged
merged 2 commits into from Nov 27, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/compiler/emitter.ts
Expand Up @@ -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();
Expand Down Expand Up @@ -274,7 +274,7 @@ namespace ts {
forEachEmittedFile(
host,
emitSourceFileOrBundle,
getSourceFilesToEmit(host, targetSourceFile),
getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit),
forceDtsEmit,
onlyBuildInfo,
!targetSourceFile
Expand Down Expand Up @@ -754,6 +754,7 @@ namespace ts {
getLibFileFromReference: notImplemented,
isSourceFileFromExternalLibrary: returnFalse,
getResolvedProjectReferenceToRedirect: returnUndefined,
isSourceOfProjectReferenceRedirect: returnFalse,
writeFile: (name, text, writeByteOrderMark) => {
switch (name) {
case jsFilePath:
Expand Down
11 changes: 3 additions & 8 deletions src/compiler/program.ts
Expand Up @@ -1004,15 +1004,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);
Expand Down Expand Up @@ -1471,6 +1465,7 @@ namespace ts {
getLibFileFromReference: program.getLibFileFromReference,
isSourceFileFromExternalLibrary,
getResolvedProjectReferenceToRedirect,
isSourceOfProjectReferenceRedirect,
getProbableSymlinks,
writeFile: writeFileCallback || (
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
Expand Down Expand Up @@ -2953,7 +2948,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,
Expand Down
12 changes: 9 additions & 3 deletions src/compiler/types.ts
Expand Up @@ -5719,13 +5719,19 @@ namespace ts {
}

/* @internal */
export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost {
export interface SourceFileMapBeEmittedHost {
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
getCompilerOptions(): CompilerOptions;
isSourceFileFromExternalLibrary(file: SourceFile): boolean;
getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined;
isSourceOfProjectReferenceRedirect(fileName: string): boolean;
}

/* @internal */
export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost, SourceFileMapBeEmittedHost {
getSourceFiles(): readonly SourceFile[];
useCaseSensitiveFileNames(): boolean;
getCurrentDirectory(): string;

isSourceFileFromExternalLibrary(file: SourceFile): boolean;
getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined;
getLibFileFromReference(ref: FileReference): SourceFile | undefined;

getCommonSourceDirectory(): string;
Expand Down
30 changes: 16 additions & 14 deletions src/compiler/utilities.ts
Expand Up @@ -3656,34 +3656,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: SourceFileMapBeEmittedHost, 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 {
Expand Down
79 changes: 79 additions & 0 deletions src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts
Expand Up @@ -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<protocol.CompileOnSaveEmitFileRequest>({
command: protocol.CommandTypes.CompileOnSaveEmitFile,
arguments: {
file: siblingSource.path,
projectFileName: siblingConfig.path
}
});
assert.equal(host.readFile(sourceJs), expectedSiblingJs);
});
});
}