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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃 Cherry-pick PR #35335 into release-3.7 #35367

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -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);
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions src/compiler/types.ts
Expand Up @@ -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;
Expand Down
30 changes: 16 additions & 14 deletions src/compiler/utilities.ts
Expand Up @@ -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 {
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);
});
});
}