Source of project reference behave as if those files cannot be emitted.

This commit is contained in:
Sheetal Nandi
2019-07-08 15:55:35 -07:00
parent b1fa2ebff5
commit 824c22c460
9 changed files with 468 additions and 35 deletions

View File

@@ -472,6 +472,33 @@ namespace ts {
}
}
function recursiveCreateDirectory(directoryPath: string, sys: System) {
const basePath = getDirectoryPath(directoryPath);
const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath);
if (shouldCreateParent) {
recursiveCreateDirectory(basePath, sys);
}
if (shouldCreateParent || !sys.directoryExists(directoryPath)) {
sys.createDirectory(directoryPath);
}
}
/**
* patch writefile to create folder before writing the file
*/
/*@internal*/
export function patchWriteFileEnsuringDirectory(sys: System) {
// patch writefile to create folder before writing the file
const originalWriteFile = sys.writeFile;
sys.writeFile = (path, data, writeBom) => {
const directoryPath = getDirectoryPath(normalizeSlashes(path));
if (directoryPath && !sys.directoryExists(directoryPath)) {
recursiveCreateDirectory(directoryPath, sys);
}
originalWriteFile.call(sys, path, data, writeBom);
};
}
/*@internal*/
interface NodeBuffer extends Uint8Array {
write(str: string, offset?: number, length?: number, encoding?: string): number;
@@ -1259,17 +1286,6 @@ namespace ts {
};
}
function recursiveCreateDirectory(directoryPath: string, sys: System) {
const basePath = getDirectoryPath(directoryPath);
const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath);
if (shouldCreateParent) {
recursiveCreateDirectory(basePath, sys);
}
if (shouldCreateParent || !sys.directoryExists(directoryPath)) {
sys.createDirectory(directoryPath);
}
}
let sys: System | undefined;
if (typeof ChakraHost !== "undefined") {
sys = getChakraSystem();
@@ -1281,14 +1297,7 @@ namespace ts {
}
if (sys) {
// patch writefile to create folder before writing the file
const originalWriteFile = sys.writeFile;
sys.writeFile = (path, data, writeBom) => {
const directoryPath = getDirectoryPath(normalizeSlashes(path));
if (directoryPath && !sys!.directoryExists(directoryPath)) {
recursiveCreateDirectory(directoryPath, sys!);
}
originalWriteFile.call(sys, path, data, writeBom);
};
patchWriteFileEnsuringDirectory(sys);
}
return sys!;
})();

View File

@@ -66,6 +66,8 @@ interface Array<T> {}`
params.newLine,
params.useWindowsStylePaths,
params.environmentVariables);
// Just like sys, patch the host to use writeFile
patchWriteFileEnsuringDirectory(host);
return host;
}
@@ -990,6 +992,19 @@ interface Array<T> {}`
}
}
export type TestServerHostTrackingWrittenFiles = TestServerHost & { writtenFiles: Map<true>; };
export function changeToHostTrackingWrittenFiles(inputHost: TestServerHost) {
const host = inputHost as TestServerHostTrackingWrittenFiles;
const originalWriteFile = host.writeFile;
host.writtenFiles = createMap<true>();
host.writeFile = (fileName, content) => {
originalWriteFile.call(host, fileName, content);
const path = host.toFullPath(fileName);
host.writtenFiles.set(path, true);
};
return host;
}
export const tsbuildProjectsLocation = "/user/username/projects";
export function getTsBuildProjectFilePath(project: string, file: string) {
return `${tsbuildProjectsLocation}/${project}/${file}`;

View File

@@ -537,8 +537,11 @@ namespace ts.server {
return this.projectService.getSourceFileLike(fileName, this);
}
private shouldEmitFile(scriptInfo: ScriptInfo) {
return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent();
/*@internal*/
shouldEmitFile(scriptInfo: ScriptInfo | undefined) {
return scriptInfo &&
!scriptInfo.isDynamicOrHasMixedContent() &&
!this.program!.isSourceOfProjectReferenceRedirect(scriptInfo.path);
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
@@ -548,7 +551,7 @@ namespace ts.server {
updateProjectIfDirty(this);
this.builderState = BuilderState.create(this.program!, this.projectService.toCanonicalFileName, this.builderState);
return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program!, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash!(data)), // TODO: GH#18217
sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)!) ? sourceFile.fileName : undefined);
sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined);
}
/**

View File

@@ -1030,7 +1030,9 @@ namespace ts.server {
private getEmitOutput(args: protocol.FileRequestArgs): EmitOutput {
const { file, project } = this.getFileAndProject(args);
return project.getLanguageService().getEmitOutput(file);
return project.shouldEmitFile(project.getScriptInfo(file)) ?
project.getLanguageService().getEmitOutput(file) :
{ emitSkipped: true, outputFiles: [] };
}
private mapDefinitionInfo(definitions: ReadonlyArray<DefinitionInfo>, project: Project): ReadonlyArray<protocol.FileSpanWithContext> {

View File

@@ -137,6 +137,7 @@
"unittests/tsserver/occurences.ts",
"unittests/tsserver/openFile.ts",
"unittests/tsserver/projectErrors.ts",
"unittests/tsserver/projectReferenceCompileOnSave.ts",
"unittests/tsserver/projectReferenceErrors.ts",
"unittests/tsserver/projectReferences.ts",
"unittests/tsserver/projects.ts",

View File

@@ -2,18 +2,12 @@ namespace ts.tscWatch {
import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation;
import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath;
import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile;
type TsBuildWatchSystem = WatchedSystem & { writtenFiles: Map<true>; };
type TsBuildWatchSystem = TestFSWithWatch.TestServerHostTrackingWrittenFiles;
function createTsBuildWatchSystem(fileOrFolderList: ReadonlyArray<TestFSWithWatch.FileOrFolderOrSymLink>, params?: TestFSWithWatch.TestServerHostCreationParameters) {
const host = createWatchedSystem(fileOrFolderList, params) as TsBuildWatchSystem;
const originalWriteFile = host.writeFile;
host.writtenFiles = createMap<true>();
host.writeFile = (fileName, content) => {
originalWriteFile.call(host, fileName, content);
const path = host.toFullPath(fileName);
host.writtenFiles.set(path, true);
};
return host;
return TestFSWithWatch.changeToHostTrackingWrittenFiles(
createWatchedSystem(fileOrFolderList, params)
);
}
export function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray<string>, defaultOptions?: BuildOptions) {

View File

@@ -498,7 +498,7 @@ namespace ts.projectSystem {
return protocolToLocation(str)(start);
}
function protocolToLocation(text: string): (pos: number) => protocol.Location {
export function protocolToLocation(text: string): (pos: number) => protocol.Location {
const lineStarts = computeLineStarts(text);
return pos => {
const x = computeLineAndCharacterOfPosition(lineStarts, pos);

View File

@@ -0,0 +1,410 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: with project references and compile on save", () => {
const projectLocation = "/user/username/projects/myproject";
const dependecyLocation = `${projectLocation}/dependency`;
const usageLocation = `${projectLocation}/usage`;
const dependencyTs: File = {
path: `${dependecyLocation}/fns.ts`,
content: `export function fn1() { }
export function fn2() { }
`
};
const dependencyConfig: File = {
path: `${dependecyLocation}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: { composite: true, declarationDir: "../decls" },
compileOnSave: true
})
};
const usageTs: File = {
path: `${usageLocation}/usage.ts`,
content: `import {
fn1,
fn2,
} from '../decls/fns'
fn1();
fn2();
`
};
const usageConfig: File = {
path: `${usageLocation}/tsconfig.json`,
content: JSON.stringify({
compileOnSave: true,
references: [{ path: "../dependency" }]
})
};
interface VerifySingleScenarioWorker extends VerifySingleScenario {
withProject: boolean;
}
function verifySingleScenarioWorker({
withProject, scenario, openFiles, requestArgs, change, expectedResult
}: VerifySingleScenarioWorker) {
it(scenario, () => {
const host = TestFSWithWatch.changeToHostTrackingWrittenFiles(
createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, libFile])
);
const session = createSession(host);
openFilesForSession(openFiles(), session);
const reqArgs = requestArgs();
const {
expectedAffected,
expectedEmit: { expectedEmitSuccess, expectedFiles },
expectedEmitOutput
} = expectedResult(withProject);
if (change) {
session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
arguments: { file: dependencyTs.path }
});
const { file, insertString } = change();
if (session.getProjectService().openFiles.has(file.path)) {
const toLocation = protocolToLocation(file.content);
const location = toLocation(file.content.length);
session.executeCommandSeq<protocol.ChangeRequest>({
command: protocol.CommandTypes.Change,
arguments: {
file: file.path,
...location,
endLine: location.line,
endOffset: location.offset,
insertString
}
});
}
else {
host.writeFile(file.path, `${file.content}${insertString}`);
}
host.writtenFiles.clear();
}
const args = withProject ? reqArgs : { file: reqArgs.file };
// Verify CompileOnSaveAffectedFileList
const actualAffectedFiles = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
arguments: args
}).response as protocol.CompileOnSaveAffectedFileListSingleProject[];
assert.deepEqual(actualAffectedFiles, expectedAffected, "Affected files");
// Verify CompileOnSaveEmit
const actualEmit = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
command: protocol.CommandTypes.CompileOnSaveEmitFile,
arguments: args
}).response;
assert.deepEqual(actualEmit, expectedEmitSuccess, "Emit files");
assert.equal(host.writtenFiles.size, expectedFiles.length);
for (const file of expectedFiles) {
assert.equal(host.readFile(file.path), file.content, `Expected to write ${file.path}`);
assert.isTrue(host.writtenFiles.has(file.path), `${file.path} is newly written`);
}
// Verify EmitOutput
const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq<protocol.EmitOutputRequest>({
command: protocol.CommandTypes.EmitOutput,
arguments: args
}).response as EmitOutput;
assert.deepEqual(actualEmitOutput, expectedEmitOutput, "Emit output");
});
}
interface VerifySingleScenario {
scenario: string;
openFiles: () => readonly File[];
requestArgs: () => protocol.FileRequestArgs;
skipWithoutProject?: boolean;
change?: () => SingleScenarioChange;
expectedResult: GetSingleScenarioResult;
}
function verifySingleScenario(scenario: VerifySingleScenario) {
if (!scenario.skipWithoutProject) {
describe("without specifying project file", () => {
verifySingleScenarioWorker({
withProject: false,
...scenario
});
});
}
describe("with specifying project file", () => {
verifySingleScenarioWorker({
withProject: true,
...scenario
});
});
}
interface SingleScenarioExpectedEmit {
expectedEmitSuccess: boolean;
expectedFiles: readonly File[];
}
interface SingleScenarioResult {
expectedAffected: protocol.CompileOnSaveAffectedFileListSingleProject[];
expectedEmit: SingleScenarioExpectedEmit;
expectedEmitOutput: EmitOutput;
}
type GetSingleScenarioResult = (withProject: boolean) => SingleScenarioResult;
interface SingleScenarioChange {
file: File;
insertString: string;
}
interface ScenarioDetails {
scenarioName: string;
requestArgs: () => protocol.FileRequestArgs;
skipWithoutProject?: boolean;
initial: GetSingleScenarioResult;
localChangeToDependency: GetSingleScenarioResult;
localChangeToUsage: GetSingleScenarioResult;
changeToDependency: GetSingleScenarioResult;
changeToUsage: GetSingleScenarioResult;
}
interface VerifyScenario {
openFiles: () => readonly File[];
scenarios: readonly ScenarioDetails[];
}
const localChange = "function fn3() { }";
const change = `export ${localChange}`;
const changeJs = `function fn3() { }
exports.fn3 = fn3;`;
const changeDts = "export declare function fn3(): void;";
function verifyScenario({ openFiles, scenarios }: VerifyScenario) {
for (const {
scenarioName, requestArgs, skipWithoutProject, initial,
localChangeToDependency, localChangeToUsage,
changeToDependency, changeToUsage
} of scenarios) {
describe(scenarioName, () => {
verifySingleScenario({
scenario: "with initial file open",
openFiles,
requestArgs,
skipWithoutProject,
expectedResult: initial
});
verifySingleScenario({
scenario: "with local change to dependency",
openFiles,
requestArgs,
skipWithoutProject,
change: () => ({ file: dependencyTs, insertString: localChange }),
expectedResult: localChangeToDependency
});
verifySingleScenario({
scenario: "with local change to usage",
openFiles,
requestArgs,
skipWithoutProject,
change: () => ({ file: usageTs, insertString: localChange }),
expectedResult: localChangeToUsage
});
verifySingleScenario({
scenario: "with change to dependency",
openFiles,
requestArgs,
skipWithoutProject,
change: () => ({ file: dependencyTs, insertString: change }),
expectedResult: changeToDependency
});
verifySingleScenario({
scenario: "with change to usage",
openFiles,
requestArgs,
skipWithoutProject,
change: () => ({ file: usageTs, insertString: change }),
expectedResult: changeToUsage
});
});
}
}
function expectedAffectedFiles(config: File, fileNames: File[]): protocol.CompileOnSaveAffectedFileListSingleProject {
return {
projectFileName: config.path,
fileNames: fileNames.map(f => f.path),
projectUsesOutFile: false
};
}
function expectedUsageEmit(appendJsText?: string): SingleScenarioExpectedEmit {
const appendJs = appendJsText ? `${appendJsText}
` : "";
return {
expectedEmitSuccess: true,
expectedFiles: [{
path: `${usageLocation}/usage.js`,
content: `"use strict";
exports.__esModule = true;
var fns_1 = require("../decls/fns");
fns_1.fn1();
fns_1.fn2();
${appendJs}`
}]
};
}
function expectedEmitOutput({ expectedFiles }: SingleScenarioExpectedEmit): EmitOutput {
return {
outputFiles: expectedFiles.map(({ path, content }) => ({
name: path,
text: content,
writeByteOrderMark: false
})),
emitSkipped: false
};
}
function expectedUsageEmitOutput(appendJsText?: string): EmitOutput {
return expectedEmitOutput(expectedUsageEmit(appendJsText));
}
function noEmit(): SingleScenarioExpectedEmit {
return {
expectedEmitSuccess: false,
expectedFiles: emptyArray
};
}
function noEmitOutput(): EmitOutput {
return {
emitSkipped: true,
outputFiles: []
};
}
function expectedDependencyEmit(appendJsText?: string, appendDtsText?: string): SingleScenarioExpectedEmit {
const appendJs = appendJsText ? `${appendJsText}
` : "";
const appendDts = appendDtsText ? `${appendDtsText}
` : "";
return {
expectedEmitSuccess: true,
expectedFiles: [
{
path: `${dependecyLocation}/fns.js`,
content: `"use strict";
exports.__esModule = true;
function fn1() { }
exports.fn1 = fn1;
function fn2() { }
exports.fn2 = fn2;
${appendJs}`
},
{
path: `${projectLocation}/decls/fns.d.ts`,
content: `export declare function fn1(): void;
export declare function fn2(): void;
${appendDts}`
}
]
};
}
function expectedDependencyEmitOutput(appendJsText?: string, appendDtsText?: string): EmitOutput {
return expectedEmitOutput(expectedDependencyEmit(appendJsText, appendDtsText));
}
function scenarioDetailsOfUsage(isDependencyOpen?: boolean): ScenarioDetails[] {
return [
{
scenarioName: "Of usageTs",
requestArgs: () => ({ file: usageTs.path, projectFileName: usageConfig.path }),
initial: () => initialUsageTs(),
// no change to usage so same as initial only usage file
localChangeToDependency: () => initialUsageTs(),
localChangeToUsage: () => initialUsageTs(localChange),
changeToDependency: () => initialUsageTs(),
changeToUsage: () => initialUsageTs(changeJs)
},
{
scenarioName: "Of dependencyTs in usage project",
requestArgs: () => ({ file: dependencyTs.path, projectFileName: usageConfig.path }),
skipWithoutProject: !!isDependencyOpen,
initial: () => initialDependencyTs(),
localChangeToDependency: () => initialDependencyTs(/*noUsageFiles*/ true),
localChangeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true),
changeToDependency: () => initialDependencyTs(),
changeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true)
}
];
function initialUsageTs(jsText?: string) {
return {
expectedAffected: [
expectedAffectedFiles(usageConfig, [usageTs])
],
expectedEmit: expectedUsageEmit(jsText),
expectedEmitOutput: expectedUsageEmitOutput(jsText)
};
}
function initialDependencyTs(noUsageFiles?: true) {
return {
expectedAffected: [
expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs])
],
expectedEmit: noEmit(),
expectedEmitOutput: noEmitOutput()
};
}
}
function scenarioDetailsOfDependencyWhenOpen(): ScenarioDetails {
return {
scenarioName: "Of dependencyTs",
requestArgs: () => ({ file: dependencyTs.path, projectFileName: dependencyConfig.path }),
initial,
localChangeToDependency: withProject => ({
expectedAffected: withProject ?
[
expectedAffectedFiles(dependencyConfig, [dependencyTs])
] :
[
expectedAffectedFiles(usageConfig, []),
expectedAffectedFiles(dependencyConfig, [dependencyTs])
],
expectedEmit: expectedDependencyEmit(localChange),
expectedEmitOutput: expectedDependencyEmitOutput(localChange)
}),
localChangeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true),
changeToDependency: withProject => initial(withProject, /*noUsageFiles*/ undefined, changeJs, changeDts),
changeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true)
};
function initial(withProject: boolean, noUsageFiles?: true, appendJs?: string, appendDts?: string): SingleScenarioResult {
return {
expectedAffected: withProject ?
[
expectedAffectedFiles(dependencyConfig, [dependencyTs])
] :
[
expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]),
expectedAffectedFiles(dependencyConfig, [dependencyTs])
],
expectedEmit: expectedDependencyEmit(appendJs, appendDts),
expectedEmitOutput: expectedDependencyEmitOutput(appendJs, appendDts)
};
}
}
describe("when dependency project is not open", () => {
verifyScenario({
openFiles: () => [usageTs],
scenarios: scenarioDetailsOfUsage()
});
});
describe("when the depedency file is open", () => {
verifyScenario({
openFiles: () => [usageTs, dependencyTs],
scenarios: [
...scenarioDetailsOfUsage(/*isDependencyOpen*/ true),
scenarioDetailsOfDependencyWhenOpen(),
]
});
});
});
}

View File

@@ -8463,7 +8463,6 @@ declare namespace ts.server {
getGlobalProjectErrors(): ReadonlyArray<Diagnostic>;
getAllProjectErrors(): ReadonlyArray<Diagnostic>;
getLanguageService(ensureSynchronized?: boolean): LanguageService;
private shouldEmitFile;
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[];
/**
* Returns true if emit was conducted