Merge pull request #28902 from Microsoft/unitTestsSplit

Unittests refactoring
This commit is contained in:
Sheetal Nandi
2018-12-21 12:52:29 -08:00
committed by GitHub
106 changed files with 15109 additions and 15019 deletions
+77 -33
View File
@@ -36,64 +36,108 @@
"runner.ts",
"unittests/extractTestHelpers.ts",
"unittests/tsserverProjectSystem.ts",
"unittests/typingsInstaller.ts",
"unittests/services/extract/helpers.ts",
"unittests/tscWatch/helpers.ts",
"unittests/tsserver/helpers.ts",
"unittests/asserts.ts",
"unittests/base64.ts",
"unittests/builder.ts",
"unittests/cancellableLanguageServiceOperations.ts",
"unittests/commandLineParsing.ts",
"unittests/compileOnSave.ts",
"unittests/compilerCore.ts",
"unittests/configurationExtension.ts",
"unittests/convertCompilerOptionsFromJson.ts",
"unittests/convertToAsyncFunction.ts",
"unittests/convertToBase64.ts",
"unittests/convertTypeAcquisitionFromJson.ts",
"unittests/customTransforms.ts",
"unittests/extractConstants.ts",
"unittests/extractFunctions.ts",
"unittests/extractRanges.ts",
"unittests/factory.ts",
"unittests/hostNewLineSupport.ts",
"unittests/incrementalParser.ts",
"unittests/initializeTSConfig.ts",
"unittests/jsDocParsing.ts",
"unittests/languageService.ts",
"unittests/matchFiles.ts",
"unittests/moduleResolution.ts",
"unittests/organizeImports.ts",
"unittests/parsePseudoBigInt.ts",
"unittests/paths.ts",
"unittests/printer.ts",
"unittests/programMissingFiles.ts",
"unittests/programNoParseFalsyFileNames.ts",
"unittests/projectErrors.ts",
"unittests/projectReferences.ts",
"unittests/programApi.ts",
"unittests/publicApi.ts",
"unittests/reuseProgramStructure.ts",
"unittests/session.ts",
"unittests/semver.ts",
"unittests/showConfig.ts",
"unittests/symbolWalker.ts",
"unittests/telemetry.ts",
"unittests/textChanges.ts",
"unittests/textStorage.ts",
"unittests/transform.ts",
"unittests/transpile.ts",
"unittests/tsbuild.ts",
"unittests/tsbuildWatchMode.ts",
"unittests/tsconfigParsing.ts",
"unittests/tscWatchMode.ts",
"unittests/versionCache.ts",
"unittests/config/commandLineParsing.ts",
"unittests/config/configurationExtension.ts",
"unittests/config/convertCompilerOptionsFromJson.ts",
"unittests/config/convertTypeAcquisitionFromJson.ts",
"unittests/config/initializeTSConfig.ts",
"unittests/config/matchFiles.ts",
"unittests/config/projectReferences.ts",
"unittests/config/showConfig.ts",
"unittests/config/tsconfigParsing.ts",
"unittests/evaluation/asyncArrow.ts",
"unittests/evaluation/asyncGenerator.ts",
"unittests/evaluation/forAwaitOf.ts",
"unittests/services/cancellableLanguageServiceOperations.ts",
"unittests/services/colorization.ts",
"unittests/services/convertToAsyncFunction.ts",
"unittests/services/documentRegistry.ts",
"unittests/services/extract/constants.ts",
"unittests/services/extract/functions.ts",
"unittests/services/extract/symbolWalker.ts",
"unittests/services/extract/ranges.ts",
"unittests/services/hostNewLineSupport.ts",
"unittests/services/languageService.ts",
"unittests/services/organizeImports.ts",
"unittests/services/patternMatcher.ts",
"unittests/services/preProcessFile.ts"
"unittests/services/preProcessFile.ts",
"unittests/services/textChanges.ts",
"unittests/services/transpile.ts",
"unittests/tscWatch/consoleClearing.ts",
"unittests/tscWatch/emit.ts",
"unittests/tscWatch/programUpdates.ts",
"unittests/tscWatch/resolutionCache.ts",
"unittests/tscWatch/watchEnvironment.ts",
"unittests/tscWatch/watchApi.ts",
"unittests/tsserver/cachingFileSystemInformation.ts",
"unittests/tsserver/cancellationToken.ts",
"unittests/tsserver/compileOnSave.ts",
"unittests/tsserver/completions.ts",
"unittests/tsserver/configFileSearch.ts",
"unittests/tsserver/configuredProjects.ts",
"unittests/tsserver/declarationFileMaps.ts",
"unittests/tsserver/documentRegistry.ts",
"unittests/tsserver/duplicatePackages.ts",
"unittests/tsserver/events/largeFileReferenced.ts",
"unittests/tsserver/events/projectLanguageServiceState.ts",
"unittests/tsserver/events/projectLoading.ts",
"unittests/tsserver/events/projectUpdatedInBackground.ts",
"unittests/tsserver/events/surveyReady.ts",
"unittests/tsserver/externalProjects.ts",
"unittests/tsserver/forceConsistentCasingInFileNames.ts",
"unittests/tsserver/formatSettings.ts",
"unittests/tsserver/getApplicableRefactors.ts",
"unittests/tsserver/getEditsForFileRename.ts",
"unittests/tsserver/importHelpers.ts",
"unittests/tsserver/inferredProjects.ts",
"unittests/tsserver/languageService.ts",
"unittests/tsserver/maxNodeModuleJsDepth.ts",
"unittests/tsserver/metadataInResponse.ts",
"unittests/tsserver/navTo.ts",
"unittests/tsserver/occurences.ts",
"unittests/tsserver/openFile.ts",
"unittests/tsserver/projectErrors.ts",
"unittests/tsserver/projectReferences.ts",
"unittests/tsserver/projects.ts",
"unittests/tsserver/refactors.ts",
"unittests/tsserver/reload.ts",
"unittests/tsserver/rename.ts",
"unittests/tsserver/resolutionCache.ts",
"unittests/tsserver/session.ts",
"unittests/tsserver/skipLibCheck.ts",
"unittests/tsserver/symLinks.ts",
"unittests/tsserver/syntaxOperations.ts",
"unittests/tsserver/textStorage.ts",
"unittests/tsserver/telemetry.ts",
"unittests/tsserver/typeAquisition.ts",
"unittests/tsserver/typeReferenceDirectives.ts",
"unittests/tsserver/typingsInstaller.ts",
"unittests/tsserver/untitledFiles.ts",
"unittests/tsserver/versionCache.ts",
"unittests/tsserver/watchEnvironment.ts"
]
}
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("assert", () => {
describe("unittests:: assert", () => {
it("deepEqual", () => {
assert.throws(() => assert.deepEqual(createNodeArray([createIdentifier("A")]), createNodeArray([createIdentifier("B")])));
assert.throws(() => assert.deepEqual(createNodeArray([], /*hasTrailingComma*/ true), createNodeArray([], /*hasTrailingComma*/ false)));
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("base64", () => {
describe("unittests:: base64", () => {
describe("base64decode", () => {
it("can decode input strings correctly without needing a host implementation", () => {
const tests = [
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("builder", () => {
describe("unittests:: builder", () => {
it("emits dependent files", () => {
const files: NamedSourceText[] = [
{ name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") },
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("compilerCore", () => {
describe("unittests:: compilerCore", () => {
describe("equalOwnProperties", () => {
it("correctly equates objects", () => {
assert.isTrue(equalOwnProperties({}, {}));
@@ -1,5 +1,5 @@
namespace ts {
describe("parseCommandLine", () => {
describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => {
function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) {
const parsed = parseCommandLine(commandLine);
@@ -367,7 +367,7 @@ namespace ts {
});
});
describe("parseBuildOptions", () => {
describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => {
function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) {
const parsed = parseBuildCommand(commandLine);
const parsedBuildOptions = JSON.stringify(parsed.buildOptions);
@@ -208,7 +208,7 @@ namespace ts {
}
}
describe("configurationExtension", () => {
describe("unittests:: config:: configurationExtension", () => {
forEach<[string, string, fakes.ParseConfigHost], void>([
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]
@@ -1,5 +1,5 @@
namespace ts {
describe("convertCompilerOptionsFromJson", () => {
describe("unittests:: config:: convertCompilerOptionsFromJson", () => {
const formatDiagnosticHost: FormatDiagnosticsHost = {
getCurrentDirectory: () => "/apath/",
getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true),
@@ -1,6 +1,6 @@
namespace ts {
interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; }
describe("convertTypeAcquisitionFromJson", () => {
describe("unittests:: config:: convertTypeAcquisitionFromJson", () => {
function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) {
assertTypeAcquisitionWithJson(json, configFileName, expectedResult);
assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult);
@@ -1,5 +1,5 @@
namespace ts {
describe("initTSConfig", () => {
describe("unittests:: config:: initTSConfig", () => {
function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) {
describe(name, () => {
const commandLine = parseCommandLine(commandLinesArgs);
@@ -30,4 +30,4 @@ namespace ts {
initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]);
});
}
}
@@ -143,7 +143,7 @@ namespace ts {
return createFileDiagnostic(file, start, length, diagnosticMessage, arg0);
}
describe("matchFiles", () => {
describe("unittests:: config:: matchFiles", () => {
it("with defaults", () => {
const json = {};
const expected: ParsedCommandLine = {
@@ -96,7 +96,7 @@ namespace ts {
checkResult(prog, host);
}
describe("project-references meta check", () => {
describe("unittests:: config:: project-references meta check", () => {
it("default setup was created correctly", () => {
const spec: TestSpecification = {
"/primary": {
@@ -118,7 +118,7 @@ namespace ts {
/**
* Validate that we enforce the basic settings constraints for referenced projects
*/
describe("project-references constraint checking for settings", () => {
describe("unittests:: config:: project-references constraint checking for settings", () => {
it("errors when declaration = false", () => {
const spec: TestSpecification = {
"/primary": {
@@ -248,7 +248,7 @@ namespace ts {
/**
* Path mapping behavior
*/
describe("project-references path mapping", () => {
describe("unittests:: config:: project-references path mapping", () => {
it("redirects to the output .d.ts file", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -268,7 +268,7 @@ namespace ts {
});
});
describe("project-references nice-behavior", () => {
describe("unittests:: config:: project-references nice-behavior", () => {
it("issues a nice error when the input file is missing", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -289,7 +289,7 @@ namespace ts {
/**
* 'composite' behavior
*/
describe("project-references behavior changes under composite: true", () => {
describe("unittests:: config:: project-references behavior changes under composite: true", () => {
it("doesn't infer the rootDir from source paths", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -308,7 +308,7 @@ namespace ts {
});
});
describe("errors when a file in a composite project occurs outside the root", () => {
describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => {
it("Errors when a file is outside the rootdir", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -1,5 +1,5 @@
namespace ts {
describe("showConfig", () => {
describe("unittests:: config:: showConfig", () => {
function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) {
describe(name, () => {
const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`;
@@ -1,5 +1,5 @@
namespace ts {
describe("parseConfigFileTextToJson", () => {
describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => {
function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) {
const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText);
assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject));
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("convertToBase64", () => {
describe("unittests:: convertToBase64", () => {
function runTest(input: string): void {
const actual = convertToBase64(input);
const expected = sys.base64encode!(input);
+2 -2
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("customTransforms", () => {
describe("unittests:: customTransforms", () => {
function emitsCorrectly(name: string, sources: { file: string, text: string }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) {
it(name, () => {
const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015));
@@ -97,4 +97,4 @@ namespace ts {
experimentalDecorators: true
});
});
}
}
@@ -1,4 +1,4 @@
describe("asyncArrowEvaluation", () => {
describe("unittests:: evaluation:: asyncArrowEvaluation", () => {
// https://github.com/Microsoft/TypeScript/issues/24722
it("this capture (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
@@ -15,4 +15,4 @@ describe("asyncArrowEvaluation", () => {
await result.main();
assert.instanceOf(result.output[0].a(), result.A);
});
});
});
@@ -1,4 +1,4 @@
describe("asyncGeneratorEvaluation", () => {
describe("unittests:: evaluation:: asyncGeneratorEvaluation", () => {
it("return (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
async function * g() {
@@ -27,4 +27,4 @@ describe("asyncGeneratorEvaluation", () => {
{ value: 0, done: true }
]);
});
});
});
@@ -1,4 +1,4 @@
describe("forAwaitOfEvaluation", () => {
describe("unittests:: evaluation:: forAwaitOfEvaluation", () => {
it("sync (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
let i = 0;
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("FactoryAPI", () => {
describe("unittests:: FactoryAPI", () => {
function assertSyntaxKind(node: Node, expected: SyntaxKind) {
assert.strictEqual(node.kind, expected, `Actual: ${Debug.showSyntaxKind(node)} Expected: ${(ts as any).SyntaxKind[expected]}`);
}
@@ -120,7 +120,7 @@ namespace ts {
}
}
describe("Incremental", () => {
describe("unittests:: Incremental Parser", () => {
it("Inserting into method", () => {
const source = "class C {\r\n" +
" public foo1() { }\r\n" +
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("JSDocParsing", () => {
describe("unittests:: JSDocParsing", () => {
describe("TypeExpressions", () => {
function parsesCorrectly(name: string, content: string) {
it(name, () => {
+7 -7
View File
@@ -80,7 +80,7 @@ namespace ts {
}
}
describe("Node module resolution - relative paths", () => {
describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => {
function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void {
for (const ext of supportedTSExtensions) {
@@ -200,7 +200,7 @@ namespace ts {
});
});
describe("Node module resolution - non-relative paths", () => {
describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => {
it("computes correct commonPrefix for moduleName cache", () => {
const resolutionCache = createModuleResolutionCache("/", (f) => f);
let cache = resolutionCache.getOrCreateCacheForModuleName("a");
@@ -457,7 +457,7 @@ namespace ts {
});
});
describe("Module resolution - relative imports", () => {
describe("unittests:: moduleResolution:: Relative imports", () => {
function test(files: Map<string>, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) {
const options: CompilerOptions = { module: ModuleKind.CommonJS };
const host: CompilerHost = {
@@ -530,7 +530,7 @@ export = C;
});
});
describe("Files with different casing", () => {
describe("unittests:: moduleResolution:: Files with different casing", () => {
let library: SourceFile;
function test(files: Map<string>, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], diagnosticCodes: number[]): void {
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
@@ -651,7 +651,7 @@ import b = require("./moduleB");
});
});
describe("baseUrl augmented module resolution", () => {
describe("unittests:: moduleResolution:: baseUrl augmented module resolution", () => {
it("module resolution without path mappings/rootDirs", () => {
test(/*hasDirectoryExists*/ false);
@@ -1098,7 +1098,7 @@ import b = require("./moduleB");
});
});
describe("ModuleResolutionHost.directoryExists", () => {
describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => {
it("No 'fileExists' calls if containing directory is missing", () => {
const host: ModuleResolutionHost = {
readFile: notImplemented,
@@ -1111,7 +1111,7 @@ import b = require("./moduleB");
});
});
describe("Type reference directive resolution: ", () => {
describe("unittests:: moduleResolution:: Type reference directive resolution: ", () => {
function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) {
const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles));
const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host);
@@ -1,5 +1,5 @@
namespace ts {
describe("BigInt literal base conversions", () => {
describe("unittests:: BigInt literal base conversions", () => {
describe("parsePseudoBigInt", () => {
const testNumbers: number[] = [];
for (let i = 0; i < 1e3; i++) testNumbers.push(i);
@@ -68,4 +68,4 @@ namespace ts {
});
});
});
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
describe("core paths", () => {
describe("unittests:: core paths", () => {
it("normalizeSlashes", () => {
assert.strictEqual(ts.normalizeSlashes("a"), "a");
assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b");
@@ -289,4 +289,4 @@ describe("core paths", () => {
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), "..");
assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/");
});
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("PrinterAPI", () => {
describe("unittests:: PrinterAPI", () => {
function makePrintsCorrectly(prefix: string) {
return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) {
it(name, () => {
@@ -11,7 +11,7 @@ namespace ts {
assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`);
}
describe("Program.getMissingFilePaths", () => {
describe("unittests:: Program.getMissingFilePaths", () => {
const options: CompilerOptions = {
noLib: true,
@@ -97,9 +97,37 @@ namespace ts {
"d:/pretend/nonexistent4.tsx"
]);
});
it("should not have missing file paths", () => {
const testSource = `
class Foo extends HTMLElement {
bar: string = 'baz';
}`;
const host: CompilerHost = {
getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => {
return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined;
},
getDefaultLibFileName: () => "",
writeFile: (_fileName, _content) => { throw new Error("unsupported"); },
getCurrentDirectory: () => sys.getCurrentDirectory(),
getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getNewLine: () => sys.newLine,
useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames,
fileExists: fileName => fileName === "test.ts",
readFile: fileName => fileName === "test.ts" ? testSource : undefined,
resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); },
getDirectories: _path => { throw new Error("unsupported"); },
};
const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host);
assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1");
assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0");
assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0");
});
});
describe("Program.isSourceFileFromExternalLibrary", () => {
describe("unittests:: Program.isSourceFileFromExternalLibrary", () => {
it("works on redirect files", () => {
// In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'.
const a = new documents.TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";');
@@ -1,36 +0,0 @@
namespace ts {
describe("programNoParseFalsyFileNames", () => {
let program: Program;
beforeEach(() => {
const testSource = `
class Foo extends HTMLElement {
bar: string = 'baz';
}`;
const host: CompilerHost = {
getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => {
return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined;
},
getDefaultLibFileName: () => "",
writeFile: (_fileName, _content) => { throw new Error("unsupported"); },
getCurrentDirectory: () => sys.getCurrentDirectory(),
getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getNewLine: () => sys.newLine,
useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames,
fileExists: fileName => fileName === "test.ts",
readFile: fileName => fileName === "test.ts" ? testSource : undefined,
resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); },
getDirectories: _path => { throw new Error("unsupported"); },
};
program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host);
});
it("should not have missing file paths", () => {
assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1");
assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0");
assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0");
});
});
}
-202
View File
@@ -1,202 +0,0 @@
namespace ts.projectSystem {
describe("Project errors", () => {
function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray<string>): void {
assert.isTrue(projectFiles !== undefined, "missing project files");
checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors);
}
function checkProjectErrorsWorker(errors: ReadonlyArray<Diagnostic>, expectedErrors: ReadonlyArray<string>): void {
assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
if (expectedErrors.length) {
for (let i = 0; i < errors.length; i++) {
const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n");
const expectedMessage = expectedErrors[i];
assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
}
}
}
function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) {
assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
if (expectedErrors.length) {
zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => {
assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
});
}
}
it("external project - diagnostics for missing files", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/applib.ts",
content: ""
};
const host = createServerHost([file1, libFile]);
const session = createSession(host);
const projectService = session.getProjectService();
const projectFileName = "/a/b/test.csproj";
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName }
};
{
projectService.openExternalProject({
projectFileName,
options: {},
rootFiles: toExternalFiles([file1.path, file2.path])
});
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
// only file1 exists - expect error
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
}
host.reloadFS([file2, libFile]);
{
// only file2 exists - expect error
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]);
}
host.reloadFS([file1, file2, libFile]);
{
// both files exist - expect no errors
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, []);
}
});
it("configured projects - diagnostics for missing files", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/applib.ts",
content: ""
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const host = createServerHost([file1, config, libFile]);
const session = createSession(host);
const projectService = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = configuredProjectAt(projectService, 0);
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName: project.getProjectName() }
};
let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
host.reloadFS([file1, file2, config, libFile]);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, []);
});
it("configured projects - diagnostics for corrupted config 1", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/lib.ts",
content: ""
};
const correctConfig = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const corruptedConfig = {
path: correctConfig.path,
content: correctConfig.content.substr(1)
};
const host = createServerHost([file1, file2, corruptedConfig]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, [
"'{' expected."
]);
assert.isNotNull(projectErrors[0].file);
assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path);
}
// fix config and trigger watcher
host.reloadFS([file1, file2, correctConfig]);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, []);
}
});
it("configured projects - diagnostics for corrupted config 2", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/lib.ts",
content: ""
};
const correctConfig = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const corruptedConfig = {
path: correctConfig.path,
content: correctConfig.content.substr(1)
};
const host = createServerHost([file1, file2, correctConfig]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, []);
}
// break config and trigger watcher
host.reloadFS([file1, file2, corruptedConfig]);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, [
"'{' expected."
]);
assert.isNotNull(projectErrors[0].file);
assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path);
}
});
});
}
@@ -210,7 +210,7 @@ namespace ts {
checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective);
}
describe("Reuse program structure", () => {
describe("unittests:: Reuse program structure:: General", () => {
const target = ScriptTarget.Latest;
const files: NamedSourceText[] = [
{
@@ -895,7 +895,7 @@ namespace ts {
});
});
describe("host is optional", () => {
describe("unittests:: Reuse program structure:: host is optional", () => {
it("should work if host is not provided", () => {
createProgram([], {});
});
@@ -905,7 +905,7 @@ namespace ts {
import createTestSystem = TestFSWithWatch.createWatchedSystem;
import libFile = TestFSWithWatch.libFile;
describe("isProgramUptoDate should return true when there is no change in compiler options and", () => {
describe("unittests:: Reuse program structure:: isProgramUptoDate should return true when there is no change in compiler options and", () => {
function verifyProgramIsUptoDate(
program: Program,
newRootFileNames: string[],
+2 -2
View File
@@ -1,6 +1,6 @@
namespace ts {
import theory = utils.theory;
describe("semver", () => {
describe("unittests:: semver", () => {
describe("Version", () => {
function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) {
assert.strictEqual(version.major, major);
@@ -225,4 +225,4 @@ namespace ts {
]);
});
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("cancellableLanguageServiceOperations", () => {
describe("unittests:: services:: cancellableLanguageServiceOperations", () => {
const file = `
function foo(): void;
function foo<T>(x: T): T;
@@ -6,7 +6,7 @@ interface ClassificationEntry {
position?: number;
}
describe("Colorization", () => {
describe("unittests:: services:: Colorization", () => {
// Use the shim adapter to ensure test coverage of the shim layer for the classifier
const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false);
const classifier = languageServiceAdapter.getClassifier();
@@ -343,7 +343,7 @@ interface Array<T> {}`
}
}
describe("convertToAsyncFunctions", () => {
describe("unittests:: services:: convertToAsyncFunctions", () => {
_testConvertToAsyncFunction("convertToAsyncFunction_basic", `
function [#|f|](): Promise<void>{
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
@@ -1,4 +1,4 @@
describe("DocumentRegistry", () => {
describe("unittests:: services:: DocumentRegistry", () => {
it("documents are shared between projects", () => {
const documentRegistry = ts.createDocumentRegistry();
const defaultCompilerOptions = ts.getDefaultCompilerOptions();
@@ -1,5 +1,5 @@
namespace ts {
describe("extractConstants", () => {
describe("unittests:: services:: extract:: extractConstants", () => {
testExtractConstant("extractConstant_TopLevel",
`let x = [#|1|];`);
@@ -1,5 +1,5 @@
namespace ts {
describe("extractFunctions", () => {
describe("unittests:: services:: extract:: extractFunctions", () => {
testExtractFunction("extractFunction1",
`namespace A {
let x = 1;
@@ -42,7 +42,7 @@ namespace ts {
}
}
describe("extractRanges", () => {
describe("unittests:: services:: extract:: extractRanges", () => {
it("get extract range from selection", () => {
testExtractRange(`
[#|
@@ -418,4 +418,4 @@ switch (x) {
testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]);
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("Symbol Walker", () => {
describe("unittests:: services:: extract:: Symbol Walker", () => {
function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) {
it(description, () => {
const result = Harness.Compiler.compileFiles([{
@@ -42,4 +42,4 @@ export default function foo(a: number, b: Bar): void {}`, (file, checker) => {
assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history
});
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("hostNewLineSupport", () => {
describe("unittests:: services:: hostNewLineSupport", () => {
function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) {
function snapFor(path: string): IScriptSnapshot | undefined {
if (path === "lib.d.ts") {
@@ -46,4 +46,4 @@ namespace ts {
`);
});
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("languageService", () => {
describe("unittests:: services:: languageService", () => {
const files: {[index: string]: string} = {
"foo.ts": `import Vue from "./vue";
import Component from "./vue-class-component";
@@ -43,4 +43,4 @@ export function Component(x: Config): any;`
expect(definitions).to.exist; // tslint:disable-line no-unused-expression
});
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("Organize imports", () => {
describe("unittests:: services:: Organize imports", () => {
describe("Sort imports", () => {
it("Sort - non-relative vs non-relative", () => {
assertSortsBefore(
@@ -1,4 +1,4 @@
describe("PatternMatcher", () => {
describe("unittests:: services:: PatternMatcher", () => {
describe("BreakIntoCharacterSpans", () => {
it("EmptyIdentifier", () => {
verifyBreakIntoCharacterSpans("");
@@ -1,4 +1,4 @@
describe("PreProcessFile:", () => {
describe("unittests:: services:: PreProcessFile:", () => {
function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void {
const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports);
@@ -2,7 +2,7 @@
// tslint:disable trim-trailing-whitespace
namespace ts {
describe("textChanges", () => {
describe("unittests:: services:: textChanges", () => {
function findChild(name: string, n: Node) {
return find(n)!;
@@ -753,4 +753,4 @@ let x = foo
});
}
});
}
}
@@ -1,5 +1,5 @@
namespace ts {
describe("Transpile", () => {
describe("unittests:: services:: Transpile", () => {
interface TranspileTestSettings {
options?: TranspileOptions;
+1 -1
View File
@@ -1,5 +1,5 @@
namespace ts {
describe("TransformAPI", () => {
describe("unittests:: TransformAPI", () => {
function replaceUndefinedWithVoid0(context: TransformationContext) {
const previousOnSubstituteNode = context.onSubstituteNode;
context.enableSubstitution(SyntaxKind.Identifier);
+15 -15
View File
@@ -8,7 +8,7 @@ namespace ts {
"/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map",
"/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"];
describe("tsbuild - sanity check of clean build of 'sample1' project", () => {
describe("unittests:: tsbuild - sanity check of clean build of 'sample1' project", () => {
it("can build the sample project 'sample1' without error", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -61,7 +61,7 @@ namespace ts {
});
});
describe("tsbuild - dry builds", () => {
describe("unittests:: tsbuild - dry builds", () => {
it("doesn't write any files in a dry build", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -90,7 +90,7 @@ namespace ts {
});
});
describe("tsbuild - clean builds", () => {
describe("unittests:: tsbuild - clean builds", () => {
it("removes all files it built", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -111,7 +111,7 @@ namespace ts {
});
});
describe("tsbuild - force builds", () => {
describe("unittests:: tsbuild - force builds", () => {
it("always builds under --force", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -137,7 +137,7 @@ namespace ts {
});
});
describe("tsbuild - can detect when and what to rebuild", () => {
describe("unittests:: tsbuild - can detect when and what to rebuild", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true });
@@ -200,7 +200,7 @@ namespace ts {
});
});
describe("tsbuild - downstream-blocked compilations", () => {
describe("unittests:: tsbuild - downstream-blocked compilations", () => {
it("won't build downstream projects if upstream projects have errors", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -222,7 +222,7 @@ namespace ts {
});
});
describe("tsbuild - project invalidation", () => {
describe("unittests:: tsbuild - project invalidation", () => {
it("invalidates projects correctly", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -270,7 +270,7 @@ export class cNew {}`);
});
});
describe("tsbuild - with resolveJsonModule option", () => {
describe("unittests:: tsbuild - with resolveJsonModule option", () => {
const projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite");
const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/hello.json"];
@@ -320,7 +320,7 @@ export default hello.hello`);
});
});
describe("tsbuild - lists files", () => {
describe("unittests:: tsbuild - lists files", () => {
it("listFiles", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -369,7 +369,7 @@ export default hello.hello`);
});
});
describe("tsbuild - with rootDir of project reference in parentDirectory", () => {
describe("unittests:: tsbuild - with rootDir of project reference in parentDirectory", () => {
const projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent");
const allExpectedOutputs = [
"/src/dist/other/other.js", "/src/dist/other/other.d.ts",
@@ -388,7 +388,7 @@ export default hello.hello`);
});
});
describe("tsbuild - when project reference is referenced transitively", () => {
describe("unittests:: tsbuild - when project reference is referenced transitively", () => {
const projFs = loadProjectFromDisk("tests/projects/transitiveReferences");
const allExpectedOutputs = [
"/src/a.js", "/src/a.d.ts",
@@ -460,7 +460,7 @@ export const b = new A();`);
export namespace OutFile {
const outFileFs = loadProjectFromDisk("tests/projects/outfile-concat");
describe("tsbuild - baseline sectioned sourcemaps", () => {
describe("unittests:: tsbuild - baseline sectioned sourcemaps", () => {
let fs: vfs.FileSystem | undefined;
before(() => {
fs = outFileFs.shadow();
@@ -480,7 +480,7 @@ export const b = new A();`);
});
});
describe("tsbuild - downstream prepend projects always get rebuilt", () => {
describe("unittests:: tsbuild - downstream prepend projects always get rebuilt", () => {
it("", () => {
const fs = outFileFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -508,7 +508,7 @@ export const b = new A();`);
"/src/core/index.d.ts.map",
];
describe("tsbuild - empty files option in tsconfig", () => {
describe("unittests:: tsbuild - empty files option in tsconfig", () => {
it("has empty files diagnostic when files is empty and no references are provided", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
@@ -541,7 +541,7 @@ export const b = new A();`);
});
}
describe("tsbuild - graph-ordering", () => {
describe("unittests:: tsbuild - graph-ordering", () => {
let host: fakes.SolutionBuilderHost | undefined;
const deps: [string, string][] = [
["A", "B"],
+1 -2
View File
@@ -1,5 +1,4 @@
namespace ts.tscWatch {
export import libFile = TestFSWithWatch.libFile;
import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation;
import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath;
import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile;
@@ -15,7 +14,7 @@ namespace ts.tscWatch {
return solutionBuilder;
}
describe("tsbuild-watch program updates", () => {
describe("unittests:: tsbuild-watch program updates", () => {
const project = "sample1";
const enum SubProject {
core = "core",
@@ -0,0 +1,97 @@
namespace ts.tscWatch {
describe("unittests:: tsc-watch:: console clearing", () => {
const currentDirectoryLog = "Current directory: / CaseSensitiveFileNames: false\n";
const fileWatcherAddedLog = [
"FileWatcher:: Added:: WatchInfo: /f.ts 250 Source file\n",
"FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 Source file\n"
];
const file: File = {
path: "/f.ts",
content: ""
};
function getProgramSynchronizingLog(options: CompilerOptions) {
return [
"Synchronizing program\n",
"CreatingProgramWith::\n",
" roots: [\"/f.ts\"]\n",
` options: ${JSON.stringify(options)}\n`
];
}
function isConsoleClearDisabled(options: CompilerOptions) {
return options.diagnostics || options.extendedDiagnostics || options.preserveWatchOutput;
}
function verifyCompilation(host: WatchedSystem, options: CompilerOptions, initialDisableOptions?: CompilerOptions) {
const disableConsoleClear = isConsoleClearDisabled(options);
const hasLog = options.extendedDiagnostics || options.diagnostics;
checkOutputErrorsInitial(host, emptyArray, initialDisableOptions ? isConsoleClearDisabled(initialDisableOptions) : disableConsoleClear, hasLog ? [
currentDirectoryLog,
...getProgramSynchronizingLog(options),
...(options.extendedDiagnostics ? fileWatcherAddedLog : emptyArray)
] : undefined);
host.modifyFile(file.path, "//");
host.runQueuedTimeoutCallbacks();
checkOutputErrorsIncremental(host, emptyArray, disableConsoleClear, hasLog ? [
"FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n",
"Scheduling update\n",
"Elapsed:: 0ms FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n"
] : undefined, hasLog ? getProgramSynchronizingLog(options) : undefined);
}
function checkConsoleClearingUsingCommandLineOptions(options: CompilerOptions = {}) {
const files = [file, libFile];
const host = createWatchedSystem(files);
createWatchOfFilesAndCompilerOptions([file.path], host, options);
verifyCompilation(host, options);
}
it("without --diagnostics or --extendedDiagnostics", () => {
checkConsoleClearingUsingCommandLineOptions();
});
it("with --diagnostics", () => {
checkConsoleClearingUsingCommandLineOptions({
diagnostics: true,
});
});
it("with --extendedDiagnostics", () => {
checkConsoleClearingUsingCommandLineOptions({
extendedDiagnostics: true,
});
});
it("with --preserveWatchOutput", () => {
checkConsoleClearingUsingCommandLineOptions({
preserveWatchOutput: true,
});
});
describe("when preserveWatchOutput is true in config file", () => {
const compilerOptions: CompilerOptions = {
preserveWatchOutput: true
};
const configFile: File = {
path: "/tsconfig.json",
content: JSON.stringify({ compilerOptions })
};
const files = [file, configFile, libFile];
it("using createWatchOfConfigFile ", () => {
const host = createWatchedSystem(files);
createWatchOfConfigFile(configFile.path, host);
// Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed
verifyCompilation(host, compilerOptions, {});
});
it("when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", () => {
const host = createWatchedSystem(files);
const reportDiagnostic = createDiagnosticReporter(host);
const optionsToExtend: CompilerOptions = {};
const configParseResult = parseConfigFileWithSystem(configFile.path, optionsToExtend, host, reportDiagnostic)!;
const watchCompilerHost = createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath!, optionsToExtend, host, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(host));
watchCompilerHost.configFileParsingResult = configParseResult;
createWatchProgram(watchCompilerHost);
verifyCompilation(host, compilerOptions);
});
});
});
}
+718
View File
@@ -0,0 +1,718 @@
namespace ts.tscWatch {
function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) {
return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`;
}
function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) {
return `TSFILE: ${filename}${host.newLine}`;
}
interface FileOrFolderEmit extends File {
output?: string;
}
function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit {
const result = file as FileOrFolderEmit;
if (getOutput) {
result.output = getOutput(file);
}
return result;
}
function getEmittedLines(files: FileOrFolderEmit[]) {
const seen = createMap<true>();
const result: string[] = [];
for (const { output } of files) {
if (output && !seen.has(output)) {
seen.set(output, true);
result.push(output);
}
}
return result;
}
function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) {
const expectedAffectedFiles = getEmittedLines(affectedFiles);
const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line);
checkOutputContains(host, expectedAffectedFiles);
checkOutputDoesNotContain(host, expectedNonAffectedFiles);
}
describe("unittests:: tsc-watch:: emit with outFile or out setting", () => {
function createWatchForOut(out?: string, outFile?: string) {
const host = createWatchedSystem([]);
const config: FileOrFolderEmit = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: { listEmittedFiles: true }
})
};
let getOutput: (file: File) => string;
if (out) {
config.content = JSON.stringify({
compilerOptions: { listEmittedFiles: true, out }
});
getOutput = __ => getEmittedLineForSingleFileOutput(out, host);
}
else if (outFile) {
config.content = JSON.stringify({
compilerOptions: { listEmittedFiles: true, outFile }
});
getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host);
}
else {
getOutput = file => getEmittedLineForMultiFileOutput(file, host);
}
const f1 = getFileOrFolderEmit({
path: "/a/a.ts",
content: "let x = 1"
}, getOutput);
const f2 = getFileOrFolderEmit({
path: "/a/b.ts",
content: "let y = 1"
}, getOutput);
const files = [f1, f2, config, libFile];
host.reloadFS(files);
createWatchOfConfigFile(config.path, host);
const allEmittedLines = getEmittedLines(files);
checkOutputContains(host, allEmittedLines);
host.clearOutput();
f1.content = "let x = 11";
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
checkAffectedLines(host, [f1], allEmittedLines);
}
it("projectUsesOutFile should not be returned if not set", () => {
createWatchForOut();
});
it("projectUsesOutFile should be true if out is set", () => {
const outJs = "/a/out.js";
createWatchForOut(outJs);
});
it("projectUsesOutFile should be true if outFile is set", () => {
const outJs = "/a/out.js";
createWatchForOut(/*out*/ undefined, outJs);
});
function verifyFilesEmittedOnce(useOutFile: boolean) {
const file1: File = {
path: "/a/b/output/AnotherDependency/file1.d.ts",
content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }"
};
const file2: File = {
path: "/a/b/dependencies/file2.d.ts",
content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }"
};
const file3: File = {
path: "/a/b/project/src/main.ts",
content: "namespace Main { export function fooBar() {} }"
};
const file4: File = {
path: "/a/b/project/src/main2.ts",
content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }"
};
const configFile: File = {
path: "/a/b/project/tsconfig.json",
content: JSON.stringify({
compilerOptions: useOutFile ?
{ outFile: "../output/common.js", target: "es5" } :
{ outDir: "../output", target: "es5" },
files: [file1.path, file2.path, file3.path, file4.path]
})
};
const files = [file1, file2, file3, file4];
const allfiles = files.concat(configFile);
const host = createWatchedSystem(allfiles);
const originalWriteFile = host.writeFile.bind(host);
const mapOfFilesWritten = createMap<number>();
host.writeFile = (p: string, content: string) => {
const count = mapOfFilesWritten.get(p);
mapOfFilesWritten.set(p, count ? count + 1 : 1);
return originalWriteFile(p, content);
};
createWatchOfConfigFile(configFile.path, host);
if (useOutFile) {
// Only out file
assert.equal(mapOfFilesWritten.size, 1);
}
else {
// main.js and main2.js
assert.equal(mapOfFilesWritten.size, 2);
}
mapOfFilesWritten.forEach((value, key) => {
assert.equal(value, 1, "Key: " + key);
});
}
it("with --outFile and multiple declaration files in the program", () => {
verifyFilesEmittedOnce(/*useOutFile*/ true);
});
it("without --outFile and multiple declaration files in the program", () => {
verifyFilesEmittedOnce(/*useOutFile*/ false);
});
});
describe("unittests:: tsc-watch:: emit for configured projects", () => {
const file1Consumer1Path = "/a/b/file1Consumer1.ts";
const moduleFile1Path = "/a/b/moduleFile1.ts";
const configFilePath = "/a/b/tsconfig.json";
interface InitialStateParams {
/** custom config file options */
configObj?: any;
/** list of the files that will be emitted for first compilation */
firstCompilationEmitFiles?: string[];
/** get the emit file for file - default is multi file emit line */
getEmitLine?(file: File, host: WatchedSystem): string;
/** Additional files and folders to add */
getAdditionalFileOrFolder?(): File[];
/** initial list of files to emit if not the default list */
firstReloadFileList?: string[];
}
function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) {
const host = createWatchedSystem([]);
const getOutputName = getEmitLine ? (file: File) => getEmitLine(file, host) :
(file: File) => getEmittedLineForMultiFileOutput(file, host);
const moduleFile1 = getFileOrFolderEmit({
path: moduleFile1Path,
content: "export function Foo() { };",
}, getOutputName);
const file1Consumer1 = getFileOrFolderEmit({
path: file1Consumer1Path,
content: `import {Foo} from "./moduleFile1"; export var y = 10;`,
}, getOutputName);
const file1Consumer2 = getFileOrFolderEmit({
path: "/a/b/file1Consumer2.ts",
content: `import {Foo} from "./moduleFile1"; let z = 10;`,
}, getOutputName);
const moduleFile2 = getFileOrFolderEmit({
path: "/a/b/moduleFile2.ts",
content: `export var Foo4 = 10;`,
}, getOutputName);
const globalFile3 = getFileOrFolderEmit({
path: "/a/b/globalFile3.ts",
content: `interface GlobalFoo { age: number }`
});
const additionalFiles = getAdditionalFileOrFolder ?
map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) :
[];
(configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true;
const configFile = getFileOrFolderEmit({
path: configFilePath,
content: JSON.stringify(configObj)
});
const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles];
let allEmittedFiles = getEmittedLines(files);
host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files);
// Initial compile
createWatchOfConfigFile(configFile.path, host);
if (firstCompilationEmitFiles) {
checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles);
}
else {
checkOutputContains(host, allEmittedFiles);
}
host.clearOutput();
return {
moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile,
files,
getFile,
verifyAffectedFiles,
verifyAffectedAllFiles,
getOutputName
};
function getFiles(filelist: string[]) {
return map(filelist, getFile);
}
function getFile(fileName: string) {
return find(files, file => file.path === fileName)!;
}
function verifyAffectedAllFiles() {
host.reloadFS(files);
host.checkTimeoutQueueLengthAndRun(1);
checkOutputContains(host, allEmittedFiles);
host.clearOutput();
}
function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) {
if (!filesToReload) {
filesToReload = files;
}
else if (filesToReload.length > files.length) {
allEmittedFiles = getEmittedLines(filesToReload);
}
host.reloadFS(filesToReload);
host.checkTimeoutQueueLengthAndRun(1);
checkAffectedLines(host, expected, allEmittedFiles);
host.clearOutput();
}
}
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
const {
moduleFile1, file1Consumer1, file1Consumer2,
verifyAffectedFiles
} = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]);
// Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`
moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`;
verifyAffectedFiles([moduleFile1]);
});
it("should be up-to-date with the reference map changes", () => {
const {
moduleFile1, file1Consumer1, file1Consumer2,
verifyAffectedFiles
} = getInitialState();
// Change file1Consumer1 content to `export let y = Foo();`
file1Consumer1.content = `export let y = Foo();`;
verifyAffectedFiles([file1Consumer1]);
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer2]);
// Add the import statements back to file1Consumer1
file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`;
verifyAffectedFiles([file1Consumer1]);
// Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`
moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]);
// Multiple file edits in one go:
// Change file1Consumer1 content to `export let y = Foo();`
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
file1Consumer1.content = `export let y = Foo();`;
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]);
});
it("should be up-to-date with deleted files", () => {
const {
moduleFile1, file1Consumer1, file1Consumer2,
files,
verifyAffectedFiles
} = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
// Delete file1Consumer2
const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file);
verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad);
});
it("should be up-to-date with newly created files", () => {
const {
moduleFile1, file1Consumer1, file1Consumer2,
files,
verifyAffectedFiles,
getOutputName
} = getInitialState();
const file1Consumer3 = getFileOrFolderEmit({
path: "/a/b/file1Consumer3.ts",
content: `import {Foo} from "./moduleFile1"; let y = Foo();`
}, getOutputName);
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3));
});
it("should detect changes in non-root files", () => {
const {
moduleFile1, file1Consumer1,
verifyAffectedFiles
} = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] });
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1]);
// change file1 internal, and verify only file1 is affected
moduleFile1.content += "var T1: number;";
verifyAffectedFiles([moduleFile1]);
});
it("should return all files if a global file changed shape", () => {
const {
globalFile3, verifyAffectedAllFiles
} = getInitialState();
globalFile3.content += "var T2: string;";
verifyAffectedAllFiles();
});
it("should always return the file itself if '--isolatedModules' is specified", () => {
const {
moduleFile1, verifyAffectedFiles
} = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } });
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1]);
});
it("should always return the file itself if '--out' or '--outFile' is specified", () => {
const outFilePath = "/a/b/out.js";
const {
moduleFile1, verifyAffectedFiles
} = getInitialState({
configObj: { compilerOptions: { module: "system", outFile: outFilePath } },
getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host)
});
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1]);
});
it("should return cascaded affected file list", () => {
const file1Consumer1Consumer1: File = {
path: "/a/b/file1Consumer1Consumer1.ts",
content: `import {y} from "./file1Consumer1";`
};
const {
moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile
} = getInitialState({
getAdditionalFileOrFolder: () => [file1Consumer1Consumer1]
});
const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path);
file1Consumer1.content += "export var T: number;";
verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]);
// Doesnt change the shape of file1Consumer1
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]);
// Change both files before the timeout
file1Consumer1.content += "export var T2: number;";
moduleFile1.content = `export var T2: number;export function Foo() { };`;
verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]);
});
it("should work fine for files with circular references", () => {
// TODO: do not exit on such errors? Just continue to watch the files for update in watch mode
const file1: File = {
path: "/a/b/file1.ts",
content: `
/// <reference path="./file2.ts" />
export var t1 = 10;`
};
const file2: File = {
path: "/a/b/file2.ts",
content: `
/// <reference path="./file1.ts" />
export var t2 = 10;`
};
const {
configFile,
getFile,
verifyAffectedFiles
} = getInitialState({
firstCompilationEmitFiles: [file1.path, file2.path],
getAdditionalFileOrFolder: () => [file1, file2],
firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath]
});
const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path);
file1Emit.content += "export var t3 = 10;";
verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]);
});
it("should detect removed code file", () => {
const referenceFile1: File = {
path: "/a/b/referenceFile1.ts",
content: `
/// <reference path="./moduleFile1.ts" />
export var x = Foo();`
};
const {
configFile,
getFile,
verifyAffectedFiles
} = getInitialState({
firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path],
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath]
});
const referenceFile1Emit = getFile(referenceFile1.path);
verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]);
});
it("should detect non-existing code file", () => {
const referenceFile1: File = {
path: "/a/b/referenceFile1.ts",
content: `
/// <reference path="./moduleFile2.ts" />
export var x = Foo();`
};
const {
configFile,
moduleFile2,
getFile,
verifyAffectedFiles
} = getInitialState({
firstCompilationEmitFiles: [referenceFile1.path],
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath]
});
const referenceFile1Emit = getFile(referenceFile1.path);
referenceFile1Emit.content += "export var yy = Foo();";
verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]);
// Create module File2 and see both files are saved
verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]);
});
});
describe("unittests:: tsc-watch:: emit file content", () => {
interface EmittedFile extends File {
shouldBeWritten: boolean;
}
function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] {
return map(contents, (content, index) => {
return {
content,
path: changeExtension(files[index].path, Extension.Js),
shouldBeWritten: true
};
}
);
}
function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) {
for (const { path, content, shouldBeWritten } of emittedFiles) {
if (shouldBeWritten) {
assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`);
assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`);
}
else {
assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`);
}
}
}
function verifyEmittedFileContents(newLine: string, inputFiles: File[], initialEmittedFileContents: string[],
modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: File) {
const host = createWatchedSystem([], { newLine });
const files = concatenate(
map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))),
configFile ? [libFile, configFile] : [libFile]
);
const allEmittedFiles = getEmittedLines(files);
host.reloadFS(files);
// Initial compile
if (configFile) {
createWatchOfConfigFile(configFile.path, host);
}
else {
// First file as the root
createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true });
}
checkOutputContains(host, allEmittedFiles);
const emittedFiles = getEmittedFiles(files, initialEmittedFileContents);
verifyEmittedFiles(host, emittedFiles);
host.clearOutput();
const affectedFiles = modifyFiles(files, emittedFiles);
host.reloadFS(files);
host.checkTimeoutQueueLengthAndRun(1);
checkAffectedLines(host, affectedFiles, allEmittedFiles);
verifyEmittedFiles(host, emittedFiles);
}
function verifyNewLine(newLine: string) {
const lines = ["var x = 1;", "var y = 2;"];
const fileContent = lines.join(newLine);
const f = {
path: "/a/app.ts",
content: fileContent
};
verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles);
function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) {
files[0].content = fileContent + newLine + "var z = 3;";
emittedFiles[0].content = files[0].content + newLine;
return [files[0]];
}
}
it("handles new lines: \\n", () => {
verifyNewLine("\n");
});
it("handles new lines: \\r\\n", () => {
verifyNewLine("\r\n");
});
it("should emit specified file", () => {
const file1 = {
path: "/a/b/f1.ts",
content: `export function Foo() { return 10; }`
};
const file2 = {
path: "/a/b/f2.ts",
content: `import {Foo} from "./f1"; export let y = Foo();`
};
const file3 = {
path: "/a/b/f3.ts",
content: `import {y} from "./f2"; let x = y;`
};
const configFile = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } })
};
verifyEmittedFileContents("\r\n", [file1, file2, file3], [
`"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`,
`"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`,
`"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n`
], modifyFiles, configFile);
function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) {
files[0].content += `export function foo2() { return 2; }`;
emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`;
emittedFiles[2].shouldBeWritten = false;
return files.slice(0, 2);
}
});
it("Elides const enums correctly in incremental compilation", () => {
const currentDirectory = "/user/someone/projects/myproject";
const file1: File = {
path: `${currentDirectory}/file1.ts`,
content: "export const enum E1 { V = 1 }"
};
const file2: File = {
path: `${currentDirectory}/file2.ts`,
content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }`
};
const file3: File = {
path: `${currentDirectory}/file3.ts`,
content: `import { E2 } from "./file2"; const v: E2 = E2.V;`
};
const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`;
verifyEmittedFileContents("\n", [file3, file2, file1], [
`${strictAndEsModule}var v = 1 /* V */;\n`,
strictAndEsModule,
strictAndEsModule
], modifyFiles);
function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) {
files[0].content += `function foo2() { return 2; }`;
emittedFiles[0].content += `function foo2() { return 2; }\n`;
emittedFiles[1].shouldBeWritten = false;
emittedFiles[2].shouldBeWritten = false;
return [files[0]];
}
});
it("file is deleted and created as part of change", () => {
const projectLocation = "/home/username/project";
const file: File = {
path: `${projectLocation}/app/file.ts`,
content: "var a = 10;"
};
const fileJs = `${projectLocation}/app/file.js`;
const configFile: File = {
path: `${projectLocation}/tsconfig.json`,
content: JSON.stringify({
include: [
"app/**/*.ts"
]
})
};
const files = [file, configFile, libFile];
const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true });
createWatchOfConfigFile("tsconfig.json", host);
verifyProgram();
file.content += "\nvar b = 10;";
host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true });
host.runQueuedTimeoutCallbacks();
verifyProgram();
function verifyProgram() {
assert.isTrue(host.fileExists(fileJs));
assert.equal(host.readFile(fileJs), file.content + "\n");
}
});
});
describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => {
it("when instead of filechanged recursive directory watcher is invoked", () => {
const configFile: File = {
path: "/a/rootFolder/project/tsconfig.json",
content: JSON.stringify({
compilerOptions: {
module: "none",
allowJs: true,
outDir: "Static/scripts/"
},
include: [
"Scripts/**/*"
],
})
};
const outputFolder = "/a/rootFolder/project/Static/scripts/";
const file1: File = {
path: "/a/rootFolder/project/Scripts/TypeScript.ts",
content: "var z = 10;"
};
const file2: File = {
path: "/a/rootFolder/project/Scripts/Javascript.js",
content: "var zz = 10;"
};
const files = [configFile, file1, file2, libFile];
const host = createWatchedSystem(files);
const watch = createWatchOfConfigFile(configFile.path, host);
checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path));
file1.content = "var zz30 = 100;";
host.reloadFS(files, { invokeDirectoryWatcherInsteadOfFileChanged: true });
host.runQueuedTimeoutCallbacks();
checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path));
const outputFile1 = changeExtension((outputFolder + getBaseFileName(file1.path)), ".js");
assert.isTrue(host.fileExists(outputFile1));
assert.equal(host.readFile(outputFile1), file1.content + host.newLine);
});
});
}
@@ -0,0 +1,207 @@
namespace ts.tscWatch {
export import WatchedSystem = TestFSWithWatch.TestServerHost;
export type File = TestFSWithWatch.File;
export type SymLink = TestFSWithWatch.SymLink;
export import libFile = TestFSWithWatch.libFile;
export import createWatchedSystem = TestFSWithWatch.createWatchedSystem;
export import checkArray = TestFSWithWatch.checkArray;
export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles;
export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed;
export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
export import checkOutputContains = TestFSWithWatch.checkOutputContains;
export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain;
export const commonFile1: File = {
path: "/a/b/commonFile1.ts",
content: "let x = 1"
};
export const commonFile2: File = {
path: "/a/b/commonFile2.ts",
content: "let y = 1"
};
export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray<string>) {
checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
}
export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray<string>) {
checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles);
}
export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) {
const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host);
compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation;
const watch = createWatchProgram(compilerHost);
return () => watch.getCurrentProgram();
}
export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) {
const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host);
compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation;
const watch = createWatchProgram(compilerHost);
return () => watch.getCurrentProgram().getProgram();
}
export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) {
const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host);
compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation;
const watch = createWatchProgram(compilerHost);
return () => watch.getCurrentProgram().getProgram();
}
const elapsedRegex = /^Elapsed:: [0-9]+ms/;
function checkOutputErrors(
host: WatchedSystem,
logsBeforeWatchDiagnostic: string[] | undefined,
preErrorsWatchDiagnostic: Diagnostic,
logsBeforeErrors: string[] | undefined,
errors: ReadonlyArray<Diagnostic> | ReadonlyArray<string>,
disableConsoleClears?: boolean | undefined,
...postErrorsWatchDiagnostics: Diagnostic[]
) {
let screenClears = 0;
const outputs = host.getOutput();
const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length +
(logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0);
assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs));
let index = 0;
forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log));
assertWatchDiagnostic(preErrorsWatchDiagnostic);
forEach(logsBeforeErrors, log => assertLog("logBeforeError", log));
// Verify errors
forEach(errors, assertDiagnostic);
forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic);
assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears");
host.clearOutput();
function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic {
return !!(diagnostic as Diagnostic).messageText;
}
function assertDiagnostic(diagnostic: Diagnostic | string) {
const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic;
assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected));
index++;
}
function assertLog(caption: string, expected: string) {
const actual = outputs[index];
assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected));
index++;
}
function assertWatchDiagnostic(diagnostic: Diagnostic) {
const expected = getWatchDiagnosticWithoutDate(diagnostic);
if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) {
assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`);
screenClears++;
}
assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected));
index++;
}
function getOutputAtFailedMessage(caption: string, expectedOutput: string) {
return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`;
}
function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) {
const newLines = contains(screenStartingMessageCodes, diagnostic.code)
? `${host.newLine}${host.newLine}`
: host.newLine;
return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`;
}
}
function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray<Diagnostic> | ReadonlyArray<string>) {
return errors.length === 1
? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes)
: createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length);
}
export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray<Diagnostic> | ReadonlyArray<string>, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) {
checkOutputErrors(
host,
/*logsBeforeWatchDiagnostic*/ undefined,
createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode),
logsBeforeErrors,
errors,
disableConsoleClears,
createErrorsFoundCompilerDiagnostic(errors));
}
export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray<Diagnostic> | ReadonlyArray<string>, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) {
checkOutputErrors(
host,
logsBeforeWatchDiagnostic,
createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation),
logsBeforeErrors,
errors,
disableConsoleClears,
createErrorsFoundCompilerDiagnostic(errors));
}
export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray<Diagnostic> | ReadonlyArray<string>, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) {
checkOutputErrors(
host,
logsBeforeWatchDiagnostic,
createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation),
logsBeforeErrors,
errors,
disableConsoleClears);
assert.equal(host.exitCode, expectedExitCode);
}
export function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic {
return {
file,
start,
length,
messageText: text,
category: message.category,
code: message.code,
};
}
export function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic {
let text = getLocaleSpecificMessage(message);
if (arguments.length > 1) {
text = formatStringFromArgs(text, arguments, 1);
}
return getDiagnosticOfFileFrom(/*file*/ undefined, text, /*start*/ undefined, /*length*/ undefined, message);
}
export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic {
let text = getLocaleSpecificMessage(message);
if (arguments.length > 4) {
text = formatStringFromArgs(text, arguments, 4);
}
return getDiagnosticOfFileFrom(file, text, start, length, message);
}
export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic {
let text = getLocaleSpecificMessage(message);
if (arguments.length > 5) {
text = formatStringFromArgs(text, arguments, 5);
}
return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!,
text, start, length, message);
}
export function getUnknownCompilerOption(program: Program, configFile: File, option: string) {
const quotedOption = `"${option}"`;
return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option);
}
export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) {
const quotedModuleName = `"${moduleName}"`;
return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,448 @@
namespace ts.tscWatch {
describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => {
it("works", () => {
const root = {
path: "/a/d/f0.ts",
content: `import {x} from "f1"`
};
const imported = {
path: "/a/f1.ts",
content: `foo()`
};
const files = [root, imported, libFile];
const host = createWatchedSystem(files);
const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD });
const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path);
const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo");
// ensure that imported file was found
checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]);
const originalFileExists = host.fileExists;
{
const newContent = `import {x} from "f1"
var x: string = 1;`;
root.content = newContent;
host.reloadFS(files);
// patch fileExists to make sure that disk is not touched
host.fileExists = notImplemented;
// trigger synchronization to make sure that import will be fetched from the cache
host.runQueuedTimeoutCallbacks();
// ensure file has correct number of errors after edit
checkOutputErrorsIncremental(host, [
f1IsNotModule,
getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"),
cannotFindFoo
]);
}
{
let fileExistsIsCalled = false;
host.fileExists = (fileName): boolean => {
if (fileName === "lib.d.ts") {
return false;
}
fileExistsIsCalled = true;
assert.isTrue(fileName.indexOf("/f2.") !== -1);
return originalFileExists.call(host, fileName);
};
root.content = `import {x} from "f2"`;
host.reloadFS(files);
// trigger synchronization to make sure that LSHost will try to find 'f2' module on disk
host.runQueuedTimeoutCallbacks();
// ensure file has correct number of errors after edit
checkOutputErrorsIncremental(host, [
getDiagnosticModuleNotFoundOfFile(watch(), root, "f2")
]);
assert.isTrue(fileExistsIsCalled);
}
{
let fileExistsCalled = false;
host.fileExists = (fileName): boolean => {
if (fileName === "lib.d.ts") {
return false;
}
fileExistsCalled = true;
assert.isTrue(fileName.indexOf("/f1.") !== -1);
return originalFileExists.call(host, fileName);
};
const newContent = `import {x} from "f1"`;
root.content = newContent;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]);
assert.isTrue(fileExistsCalled);
}
});
it("loads missing files from disk", () => {
const root = {
path: `/a/foo.ts`,
content: `import {x} from "bar"`
};
const imported = {
path: `/a/bar.d.ts`,
content: `export const y = 1;`
};
const files = [root, libFile];
const host = createWatchedSystem(files);
const originalFileExists = host.fileExists;
let fileExistsCalledForBar = false;
host.fileExists = fileName => {
if (fileName === "lib.d.ts") {
return false;
}
if (!fileExistsCalledForBar) {
fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1;
}
return originalFileExists.call(host, fileName);
};
const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD });
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
checkOutputErrorsInitial(host, [
getDiagnosticModuleNotFoundOfFile(watch(), root, "bar")
]);
fileExistsCalledForBar = false;
root.content = `import {y} from "bar"`;
host.reloadFS(files.concat(imported));
host.runQueuedTimeoutCallbacks();
checkOutputErrorsIncremental(host, emptyArray);
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called.");
});
it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => {
const root = {
path: `/a/foo.ts`,
content: `import {x} from "bar"`
};
const imported = {
path: `/a/bar.d.ts`,
content: `export const y = 1;export const x = 10;`
};
const files = [root, libFile];
const filesWithImported = files.concat(imported);
const host = createWatchedSystem(filesWithImported);
const originalFileExists = host.fileExists;
let fileExistsCalledForBar = false;
host.fileExists = fileName => {
if (fileName === "lib.d.ts") {
return false;
}
if (!fileExistsCalledForBar) {
fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1;
}
return originalFileExists.call(host, fileName);
};
const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD });
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
checkOutputErrorsInitial(host, emptyArray);
fileExistsCalledForBar = false;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called.");
checkOutputErrorsIncremental(host, [
getDiagnosticModuleNotFoundOfFile(watch(), root, "bar")
]);
fileExistsCalledForBar = false;
host.reloadFS(filesWithImported);
host.checkTimeoutQueueLengthAndRun(1);
checkOutputErrorsIncremental(host, emptyArray);
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called.");
});
it("works when module resolution changes to ambient module", () => {
const root = {
path: "/a/b/foo.ts",
content: `import * as fs from "fs";`
};
const packageJson = {
path: "/a/b/node_modules/@types/node/package.json",
content: `
{
"main": ""
}
`
};
const nodeType = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: `
declare module "fs" {
export interface Stats {
isFile(): boolean;
}
}`
};
const files = [root, libFile];
const filesWithNodeType = files.concat(packageJson, nodeType);
const host = createWatchedSystem(files, { currentDirectory: "/a/b" });
const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { });
checkOutputErrorsInitial(host, [
getDiagnosticModuleNotFoundOfFile(watch(), root, "fs")
]);
host.reloadFS(filesWithNodeType);
host.runQueuedTimeoutCallbacks();
checkOutputErrorsIncremental(host, emptyArray);
});
it("works when included file with ambient module changes", () => {
const root = {
path: "/a/b/foo.ts",
content: `
import * as fs from "fs";
import * as u from "url";
`
};
const file = {
path: "/a/b/bar.d.ts",
content: `
declare module "url" {
export interface Url {
href?: string;
}
}
`
};
const fileContentWithFS = `
declare module "fs" {
export interface Stats {
isFile(): boolean;
}
}
`;
const files = [root, file, libFile];
const host = createWatchedSystem(files, { currentDirectory: "/a/b" });
const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {});
checkOutputErrorsInitial(host, [
getDiagnosticModuleNotFoundOfFile(watch(), root, "fs")
]);
file.content += fileContentWithFS;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
checkOutputErrorsIncremental(host, emptyArray);
});
it("works when reusing program with files from external library", () => {
interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; }
const configDir = "/a/b/projects/myProject/src/";
const file1: File = {
path: configDir + "file1.ts",
content: 'import module1 = require("module1");\nmodule1("hello");'
};
const file2: File = {
path: configDir + "file2.ts",
content: 'import module11 = require("module1");\nmodule11("hello");'
};
const module1: File = {
path: "/a/b/projects/myProject/node_modules/module1/index.js",
content: "module.exports = options => { return options.toString(); }"
};
const configFile: File = {
path: configDir + "tsconfig.json",
content: JSON.stringify({
compilerOptions: {
allowJs: true,
rootDir: ".",
outDir: "../dist",
moduleResolution: "node",
maxNodeModuleJsDepth: 1
}
})
};
const outDirFolder = "/a/b/projects/myProject/dist/";
const programFiles = [file1, file2, module1, libFile];
const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" });
const watch = createWatchOfConfigFile(configFile.path, host);
checkProgramActualFiles(watch(), programFiles.map(f => f.path));
checkOutputErrorsInitial(host, emptyArray);
const expectedFiles: ExpectedFile[] = [
createExpectedEmittedFile(file1),
createExpectedEmittedFile(file2),
createExpectedToNotEmitFile("index.js"),
createExpectedToNotEmitFile("src/index.js"),
createExpectedToNotEmitFile("src/file1.js"),
createExpectedToNotEmitFile("src/file2.js"),
createExpectedToNotEmitFile("lib.js"),
createExpectedToNotEmitFile("lib.d.ts")
];
verifyExpectedFiles(expectedFiles);
file1.content += "\n;";
expectedFiles[0].content += ";\n"; // Only emit file1 with this change
expectedFiles[1].isExpectedToEmit = false;
host.reloadFS(programFiles.concat(configFile));
host.runQueuedTimeoutCallbacks();
checkProgramActualFiles(watch(), programFiles.map(f => f.path));
checkOutputErrorsIncremental(host, emptyArray);
verifyExpectedFiles(expectedFiles);
function verifyExpectedFiles(expectedFiles: ExpectedFile[]) {
forEach(expectedFiles, f => {
assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit"));
if (f.isExpectedToEmit) {
assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path);
}
});
}
function createExpectedToNotEmitFile(fileName: string): ExpectedFile {
return {
path: outDirFolder + fileName,
isExpectedToEmit: false
};
}
function createExpectedEmittedFile(file: File): ExpectedFile {
return {
path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js,
isExpectedToEmit: true,
content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n"
};
}
});
it("works when renaming node_modules folder that already contains @types folder", () => {
const currentDirectory = "/user/username/projects/myproject";
const file: File = {
path: `${currentDirectory}/a.ts`,
content: `import * as q from "qqq";`
};
const module: File = {
path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`,
content: "export {}"
};
const files = [file, module, libFile];
const host = createWatchedSystem(files, { currentDirectory });
const watch = createWatchOfFilesAndCompilerOptions([file.path], host);
checkProgramActualFiles(watch(), [file.path, libFile.path]);
checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true);
host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`);
host.runQueuedTimeoutCallbacks();
checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]);
checkOutputErrorsIncremental(host, emptyArray);
});
describe("ignores files/folder changes in node_modules that start with '.'", () => {
const projectPath = "/user/username/projects/project";
const npmCacheFile: File = {
path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`,
content: JSON.stringify({ something: 10 })
};
const file1: File = {
path: `${projectPath}/test.ts`,
content: `import { x } from "somemodule";`
};
const file2: File = {
path: `${projectPath}/node_modules/somemodule/index.d.ts`,
content: `export const x = 10;`
};
const files = [libFile, file1, file2];
const expectedFiles = files.map(f => f.path);
it("when watching node_modules in inferred project for failed lookup", () => {
const host = createWatchedSystem(files);
const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1);
checkProgramActualFiles(watch(), expectedFiles);
host.checkTimeoutQueueLength(0);
host.ensureFileOrFolder(npmCacheFile);
host.checkTimeoutQueueLength(0);
});
it("when watching node_modules as part of wild card directories in config project", () => {
const config: File = {
path: `${projectPath}/tsconfig.json`,
content: "{}"
};
const host = createWatchedSystem(files.concat(config));
const watch = createWatchOfConfigFile(config.path, host);
checkProgramActualFiles(watch(), expectedFiles);
host.checkTimeoutQueueLength(0);
host.ensureFileOrFolder(npmCacheFile);
host.checkTimeoutQueueLength(0);
});
});
});
describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => {
const projectRoot = "/user/username/projects/project";
const mainPackageRoot = `${projectRoot}/main`;
const linkedPackageRoot = `${projectRoot}/linked-package`;
const mainFile: File = {
path: `${mainPackageRoot}/index.ts`,
content: "import { Foo } from '@scoped/linked-package'"
};
const config: File = {
path: `${mainPackageRoot}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." },
files: ["index.ts"]
})
};
const linkedPackageInMain: SymLink = {
path: `${mainPackageRoot}/node_modules/@scoped/linked-package`,
symLink: `${linkedPackageRoot}`
};
const linkedPackageJson: File = {
path: `${linkedPackageRoot}/package.json`,
content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" })
};
const linkedPackageIndex: File = {
path: `${linkedPackageRoot}/dist/index.d.ts`,
content: "export * from './other';"
};
const linkedPackageOther: File = {
path: `${linkedPackageRoot}/dist/other.d.ts`,
content: 'export declare const Foo = "BAR";'
};
it("verify watched directories", () => {
const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther];
const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot });
createWatchOfConfigFile("tsconfig.json", host);
checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true);
});
});
}
@@ -0,0 +1,40 @@
namespace ts.tscWatch {
describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => {
const projectRoot = "/user/username/projects/project";
const configFileJson: any = {
compilerOptions: { module: "commonjs", resolveJsonModule: true },
files: ["index.ts"]
};
const mainFile: File = {
path: `${projectRoot}/index.ts`,
content: "import settings from './settings.json';"
};
const config: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify(configFileJson)
};
const settingsJson: File = {
path: `${projectRoot}/settings.json`,
content: JSON.stringify({ content: "Print this" })
};
it("verify that module resolution with json extension works when returned without extension", () => {
const files = [libFile, mainFile, config, settingsJson];
const host = createWatchedSystem(files, { currentDirectory: projectRoot });
const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host);
const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path);
compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => {
const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost);
const resolvedModule = result.resolvedModule!;
return {
resolvedFileName: resolvedModule.resolvedFileName,
isExternalLibraryImport: resolvedModule.isExternalLibraryImport,
originalFileName: resolvedModule.originalPath,
};
});
const watch = createWatchProgram(compilerHost);
const program = watch.getCurrentProgram().getProgram();
checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]);
});
});
}
@@ -0,0 +1,178 @@
namespace ts.tscWatch {
import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory;
describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => {
it("watchFile using dynamic priority polling", () => {
const projectFolder = "/a/username/project";
const file1: File = {
path: `${projectFolder}/typescript.ts`,
content: "var z = 10;"
};
const files = [file1, libFile];
const environmentVariables = createMap<string>();
environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling");
const host = createWatchedSystem(files, { environmentVariables });
const watch = createWatchOfFilesAndCompilerOptions([file1.path], host);
const initialProgram = watch();
verifyProgram();
const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium];
for (let index = 0; index < mediumPollingIntervalThreshold; index++) {
// Transition libFile and file1 to low priority queue
host.checkTimeoutQueueLengthAndRun(1);
assert.deepEqual(watch(), initialProgram);
}
// Make a change to file
file1.content = "var zz30 = 100;";
host.reloadFS(files);
// This should detect change in the file
host.checkTimeoutQueueLengthAndRun(1);
assert.deepEqual(watch(), initialProgram);
// Callbacks: medium priority + high priority queue and scheduled program update
host.checkTimeoutQueueLengthAndRun(3);
// During this timeout the file would be detected as unchanged
let fileUnchangeDetected = 1;
const newProgram = watch();
assert.notStrictEqual(newProgram, initialProgram);
verifyProgram();
const outputFile1 = changeExtension(file1.path, ".js");
assert.isTrue(host.fileExists(outputFile1));
assert.equal(host.readFile(outputFile1), file1.content + host.newLine);
const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold;
for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) {
// For high + Medium/low polling interval
host.checkTimeoutQueueLengthAndRun(2);
assert.deepEqual(watch(), newProgram);
}
// Everything goes in high polling interval queue
host.checkTimeoutQueueLengthAndRun(1);
assert.deepEqual(watch(), newProgram);
function verifyProgram() {
checkProgramActualFiles(watch(), files.map(f => f.path));
checkWatchedFiles(host, []);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, [], /*recursive*/ true);
}
});
describe("tsc-watch when watchDirectories implementation", () => {
function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) {
const projectFolder = "/a/username/project";
const projectSrcFolder = `${projectFolder}/src`;
const configFile: File = {
path: `${projectFolder}/tsconfig.json`,
content: "{}"
};
const file: File = {
path: `${projectSrcFolder}/file1.ts`,
content: ""
};
const programFiles = [file, libFile];
const files = [file, configFile, libFile];
const environmentVariables = createMap<string>();
environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory);
const host = createWatchedSystem(files, { environmentVariables });
const watch = createWatchOfConfigFile(configFile.path, host);
const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`];
// Watching files config file, file, lib file
const expectedWatchedFiles = files.map(f => f.path);
const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray;
if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) {
expectedWatchedFiles.push(...projectFolders);
}
verifyProgram(checkOutputErrorsInitial);
// Rename the file:
file.path = file.path.replace("file1.ts", "file2.ts");
expectedWatchedFiles[0] = file.path;
host.reloadFS(files);
if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) {
// With dynamic polling the fs change would be detected only by running timeouts
host.runQueuedTimeoutCallbacks();
}
// Delayed update program
host.runQueuedTimeoutCallbacks();
verifyProgram(checkOutputErrorsIncremental);
function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray<Diagnostic>) => void) {
checkProgramActualFiles(watch(), programFiles.map(f => f.path));
checkOutputErrors(host, emptyArray);
const outputFile = changeExtension(file.path, ".js");
assert(host.fileExists(outputFile));
assert.equal(host.readFile(outputFile), file.content);
checkWatchedDirectories(host, emptyArray, /*recursive*/ true);
// Watching config file, file, lib file and directories
checkWatchedFilesDetailed(host, expectedWatchedFiles, 1);
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false);
}
}
it("uses watchFile when renaming file in subfolder", () => {
verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile);
});
it("uses non recursive watchDirectory when renaming file in subfolder", () => {
verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory);
});
it("uses non recursive dynamic polling when renaming file in subfolder", () => {
verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling);
});
it("when there are symlinks to folders in recursive folders", () => {
const cwd = "/home/user/projects/myproject";
const file1: File = {
path: `${cwd}/src/file.ts`,
content: `import * as a from "a"`
};
const tsconfig: File = {
path: `${cwd}/tsconfig.json`,
content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}`
};
const realA: File = {
path: `${cwd}/node_modules/reala/index.d.ts`,
content: `export {}`
};
const realB: File = {
path: `${cwd}/node_modules/realb/index.d.ts`,
content: `export {}`
};
const symLinkA: SymLink = {
path: `${cwd}/node_modules/a`,
symLink: `${cwd}/node_modules/reala`
};
const symLinkB: SymLink = {
path: `${cwd}/node_modules/b`,
symLink: `${cwd}/node_modules/realb`
};
const symLinkBInA: SymLink = {
path: `${cwd}/node_modules/reala/node_modules/b`,
symLink: `${cwd}/node_modules/b`
};
const symLinkAInB: SymLink = {
path: `${cwd}/node_modules/realb/node_modules/a`,
symLink: `${cwd}/node_modules/a`
};
const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB];
const environmentVariables = createMap<string>();
environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory);
const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd });
createWatchOfConfigFile("tsconfig.json", host);
checkWatchedDirectories(host, emptyArray, /*recursive*/ true);
checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`,
`${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false);
});
});
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,271 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: cancellationToken", () => {
// Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test
let oldPrepare: AnyFunction;
before(() => {
oldPrepare = (Error as any).prepareStackTrace;
delete (Error as any).prepareStackTrace;
});
after(() => {
(Error as any).prepareStackTrace = oldPrepare;
});
it("is attached to request", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let xyz = 1;"
};
const host = createServerHost([f1]);
let expectedRequestId: number;
const cancellationToken: server.ServerCancellationToken = {
isCancellationRequested: () => false,
setRequest: requestId => {
if (expectedRequestId === undefined) {
assert.isTrue(false, "unexpected call");
}
assert.equal(requestId, expectedRequestId);
},
resetRequest: noop
};
const session = createSession(host, { cancellationToken });
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.OccurrencesRequest>{
command: "occurrences",
arguments: { file: f1.path, line: 1, offset: 6 }
});
expectedRequestId = 2;
host.runQueuedImmediateCallbacks();
expectedRequestId = 2;
host.runQueuedImmediateCallbacks();
});
it("Geterr is cancellable", () => {
const f1 = {
path: "/a/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {}
})
};
const cancellationToken = new TestServerCancellationToken();
const host = createServerHost([f1, config]);
const session = createSession(host, {
canUseEvents: true,
eventHandler: noop,
cancellationToken
});
{
session.executeCommandSeq(<protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
// send geterr for missing file
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: ["/a/missing"] }
});
// no files - expect 'completed' event
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(session.getSeq(), 0);
}
{
const getErrId = session.getNextSeq();
// send geterr for a valid file
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run new request
session.executeCommandSeq(<protocol.ProjectInfoRequest>{
command: "projectInfo",
arguments: { file: f1.path }
});
session.clearMessages();
// cancel previously issued Geterr
cancellationToken.setRequestToCancel(getErrId);
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(getErrId, 0);
cancellationToken.resetToken();
}
{
const getErrId = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
cancellationToken.setRequestToCancel(getErrId);
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(getErrId, 0);
cancellationToken.resetToken();
}
{
const getErrId = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
// the semanticDiag message
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 1);
const e2 = <protocol.Event>getMessage(0);
assert.equal(e2.event, "semanticDiag");
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
assert.equal(host.getOutput().length, 2);
const e3 = <protocol.Event>getMessage(0);
assert.equal(e3.event, "suggestionDiag");
verifyRequestCompleted(getErrId, 1);
cancellationToken.resetToken();
}
{
const getErr1 = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
// make sure that getErr1 is completed
verifyRequestCompleted(getErr1, 0);
}
function verifyRequestCompleted(expectedSeq: number, n: number) {
const event = <protocol.RequestCompletedEvent>getMessage(n);
assert.equal(event.event, "requestCompleted");
assert.equal(event.body.request_seq, expectedSeq, "expectedSeq");
session.clearMessages();
}
function getMessage(n: number) {
return JSON.parse(server.extractMessage(host.getOutput()[n]));
}
});
it("Lower priority tasks are cancellable", () => {
const f1 = {
path: "/a/app.ts",
content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";`
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {}
})
};
const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3);
const host = createServerHost([f1, config]);
const session = createSession(host, {
canUseEvents: true,
eventHandler: noop,
cancellationToken,
throttleWaitMilliseconds: 0
});
{
session.executeCommandSeq(<protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
// send navbar request (normal priority)
session.executeCommandSeq(<protocol.NavBarRequest>{
command: "navbar",
arguments: { file: f1.path }
});
// ensure the nav bar request can be canceled
verifyExecuteCommandSeqIsCancellable(<protocol.NavBarRequest>{
command: "navbar",
arguments: { file: f1.path }
});
// send outlining spans request (normal priority)
session.executeCommandSeq(<protocol.OutliningSpansRequestFull>{
command: "outliningSpans",
arguments: { file: f1.path }
});
// ensure the outlining spans request can be canceled
verifyExecuteCommandSeqIsCancellable(<protocol.OutliningSpansRequestFull>{
command: "outliningSpans",
arguments: { file: f1.path }
});
}
function verifyExecuteCommandSeqIsCancellable<T extends server.protocol.Request>(request: Partial<T>) {
// Set the next request to be cancellable
// The cancellation token will cancel the request the third time
// isCancellationRequested() is called.
cancellationToken.setRequestToCancel(session.getNextSeq());
let operationCanceledExceptionThrown = false;
try {
session.executeCommandSeq(request);
}
catch (e) {
assert(e instanceof OperationCanceledException);
operationCanceledExceptionThrown = true;
}
assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request));
}
});
});
}
@@ -6,7 +6,7 @@ namespace ts.projectSystem {
return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
}
describe("CompileOnSave affected list", () => {
describe("unittests:: tsserver:: compileOnSave:: affected list", () => {
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) {
const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[];
const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName));
@@ -502,9 +502,57 @@ namespace ts.projectSystem {
]);
});
});
describe("tsserverProjectSystem emit with outFile or out setting", () => {
function test(opts: CompilerOptions, expectedUsesOutFile: boolean) {
const f1 = {
path: "/a/a.ts",
content: "let x = 1"
};
const f2 = {
path: "/a/b.ts",
content: "let y = 1"
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: opts,
compileOnSave: true
})
};
const host = createServerHost([f1, f2, config]);
const session = projectSystem.createSession(host);
session.executeCommand(<protocol.OpenRequest>{
seq: 1,
type: "request",
command: "open",
arguments: { file: f1.path }
});
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 });
const { response } = session.executeCommand(<protocol.CompileOnSaveAffectedFileListRequest>{
seq: 2,
type: "request",
command: "compileOnSaveAffectedFileList",
arguments: { file: f1.path }
});
assert.equal((<protocol.CompileOnSaveAffectedFileListSingleProject[]>response).length, 1, "expected output for 1 project");
assert.equal((<protocol.CompileOnSaveAffectedFileListSingleProject[]>response)[0].fileNames.length, 2, "expected output for 1 project");
assert.equal((<protocol.CompileOnSaveAffectedFileListSingleProject[]>response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile");
}
it("projectUsesOutFile should not be returned if not set", () => {
test({}, /*expectedUsesOutFile*/ false);
});
it("projectUsesOutFile should be true if outFile is set", () => {
test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true);
});
it("projectUsesOutFile should be true if out is set", () => {
test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true);
});
});
});
describe("EmitFile test", () => {
describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => {
it("should respect line endings", () => {
test("\n");
test("\r\n");
@@ -646,4 +694,100 @@ namespace ts.projectSystem {
}
});
});
describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => {
const projectRoot = "/user/username/projects/myproject";
const core: File = {
path: `${projectRoot}/core/core.ts`,
content: "let z = 10;"
};
const app1: File = {
path: `${projectRoot}/app1/app.ts`,
content: "let x = 10;"
};
const app2: File = {
path: `${projectRoot}/app2/app.ts`,
content: "let y = 10;"
};
const app1Config: File = {
path: `${projectRoot}/app1/tsconfig.json`,
content: JSON.stringify({
files: ["app.ts", "../core/core.ts"],
compilerOptions: { outFile: "build/output.js" },
compileOnSave: true
})
};
const app2Config: File = {
path: `${projectRoot}/app2/tsconfig.json`,
content: JSON.stringify({
files: ["app.ts", "../core/core.ts"],
compilerOptions: { outFile: "build/output.js" },
compileOnSave: true
})
};
const files = [libFile, core, app1, app2, app1Config, app2Config];
function insertString(session: TestSession, file: File) {
session.executeCommandSeq<protocol.ChangeRequest>({
command: protocol.CommandTypes.Change,
arguments: {
file: file.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: "let k = 1"
}
});
}
function getSession() {
const host = createServerHost(files);
const session = createSession(host);
openFilesForSession([app1, app2, core], session);
const service = session.getProjectService();
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 });
const project1 = service.configuredProjects.get(app1Config.path)!;
const project2 = service.configuredProjects.get(app2Config.path)!;
checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]);
checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]);
insertString(session, app1);
insertString(session, app2);
assert.equal(project1.dirty, true);
assert.equal(project2.dirty, true);
return session;
}
it("when projectFile is specified", () => {
const session = getSession();
const response = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
arguments: {
file: core.path,
projectFileName: app1Config.path
}
}).response;
assert.deepEqual(response, [
{ projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }
]);
assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false);
assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true);
});
it("when projectFile is not specified", () => {
const session = getSession();
const response = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
arguments: {
file: core.path
}
}).response;
assert.deepEqual(response, [
{ projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true },
{ projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true }
]);
assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false);
assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false);
});
});
}
@@ -0,0 +1,122 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: completions", () => {
it("works", () => {
const aTs: File = {
path: "/a.ts",
content: "export const foo = 0;",
};
const bTs: File = {
path: "/b.ts",
content: "foo",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const session = createSession(createServerHost([aTs, bTs, tsconfig]));
openFilesForSession([aTs, bTs], session);
const requestLocation: protocol.FileLocationRequestArgs = {
file: bTs.path,
line: 1,
offset: 3,
};
const response = executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
...requestLocation,
includeExternalModuleExports: true,
prefix: "foo",
});
const entry: protocol.CompletionEntry = {
hasAction: true,
insertText: undefined,
isRecommended: undefined,
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
name: "foo",
replacementSpan: undefined,
sortText: "0",
source: "/a",
};
assert.deepEqual<protocol.CompletionInfo | undefined>(response, {
isGlobalCompletion: true,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: [entry],
});
const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = {
...requestLocation,
entryNames: [{ name: "foo", source: "/a" }],
};
const detailsResponse = executeSessionRequest<protocol.CompletionDetailsRequest, protocol.CompletionDetailsResponse>(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs);
const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = {
displayParts: [
keywordPart(SyntaxKind.ConstKeyword),
spacePart(),
displayPart("foo", SymbolDisplayPartKind.localName),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
displayPart("0", SymbolDisplayPartKind.stringLiteral),
],
documentation: emptyArray,
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
name: "foo",
source: [{ text: "./a", kind: "text" }],
tags: undefined,
};
assert.deepEqual<ReadonlyArray<protocol.CompletionEntryDetails> | undefined>(detailsResponse, [
{
codeActions: [
{
description: `Import 'foo' from module "./a"`,
changes: [
{
fileName: "/b.ts",
textChanges: [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: 'import { foo } from "./a";\n\n',
},
],
},
],
commands: undefined,
},
],
...detailsCommon,
},
]);
interface CompletionDetailsFullRequest extends protocol.FileLocationRequest {
readonly command: protocol.CommandTypes.CompletionDetailsFull;
readonly arguments: protocol.CompletionDetailsRequestArgs;
}
interface CompletionDetailsFullResponse extends protocol.Response {
readonly body?: ReadonlyArray<CompletionEntryDetails>;
}
const detailsFullResponse = executeSessionRequest<CompletionDetailsFullRequest, CompletionDetailsFullResponse>(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs);
assert.deepEqual<ReadonlyArray<CompletionEntryDetails> | undefined>(detailsFullResponse, [
{
codeActions: [
{
description: `Import 'foo' from module "./a"`,
changes: [
{
fileName: "/b.ts",
textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')],
},
],
commands: undefined,
}
],
...detailsCommon,
}
]);
});
});
}
@@ -0,0 +1,174 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: searching for config file", () => {
it("should stop at projectRootPath if given", () => {
const f1 = {
path: "/a/file1.ts",
content: ""
};
const configFile = {
path: "/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, configFile]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a");
checkNumberOfConfiguredProjects(service, 0);
checkNumberOfInferredProjects(service, 1);
service.closeClientFile(f1.path);
service.openClientFile(f1.path);
checkNumberOfConfiguredProjects(service, 1);
checkNumberOfInferredProjects(service, 0);
});
it("should use projectRootPath when searching for inferred project again", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
const typeRootLocations = getTypeRootsFromLocation(configFileLocation);
checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project and not configured project
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true);
});
it("should use projectRootPath when searching for inferred project again 2", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true });
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project with project root path set
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.equal(service.inferredProjects[0].projectRootPath, projectDir);
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true);
});
describe("when the opened file is not from project root", () => {
const projectRoot = "/a/b/projects/project";
const file: File = {
path: `${projectRoot}/src/index.ts`,
content: "let y = 10"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: "{}"
};
const files = [file, libFile];
const filesWithConfig = files.concat(tsconfig);
const dirOfFile = getDirectoryPath(file.path);
function openClientFile(files: File[]) {
const host = createServerHost(files);
const projectService = createProjectService(host);
projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj");
return { host, projectService };
}
function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) {
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 });
const project = Debug.assertDefined(projectService.configuredProjects.get(tsconfig.path));
if (orphanInferredProject) {
const inferredProject = projectService.inferredProjects[0];
assert.isTrue(inferredProject.isOrphan());
}
checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]);
checkWatchedFiles(host, [libFile.path, tsconfig.path]);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true);
}
function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) {
projectService.checkNumberOfProjects({ inferredProjects: 1 });
const project = projectService.inferredProjects[0];
assert.isDefined(project);
const filesToWatch = [libFile.path];
forEachAncestorDirectory(dirOfFile, ancestor => {
filesToWatch.push(combinePaths(ancestor, "tsconfig.json"));
filesToWatch.push(combinePaths(ancestor, "jsconfig.json"));
});
checkProjectActualFiles(project, [file.path, libFile.path]);
checkWatchedFiles(host, filesToWatch);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true);
}
it("tsconfig for the file exists", () => {
const { host, projectService } = openClientFile(filesWithConfig);
verifyConfiguredProject(host, projectService);
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyInferredProject(host, projectService);
host.reloadFS(filesWithConfig);
host.runQueuedTimeoutCallbacks();
verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true);
});
it("tsconfig for the file does not exist", () => {
const { host, projectService } = openClientFile(files);
verifyInferredProject(host, projectService);
host.reloadFS(filesWithConfig);
host.runQueuedTimeoutCallbacks();
verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true);
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyInferredProject(host, projectService);
});
});
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,566 @@
namespace ts.projectSystem {
function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan {
return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) };
}
function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan {
return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) };
}
function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation {
return documentSpanFromSubstring(file, substring, options);
}
function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
return {
...protocolFileSpanFromSubstring(file, text, options),
isDefinition,
isWriteAccess: isDefinition,
lineText,
};
}
function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry {
return {
...documentSpanFromSubstring(file, text, options),
isDefinition,
isWriteAccess: isDefinition,
isInString: undefined,
};
}
function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray<File>): void {
openFilesForSession([file], session);
const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false));
const program = project.getCurrentProgram()!;
const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true);
closeFilesForSession([file], session);
Debug.assert(!output.emitSkipped);
assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false })));
}
describe("unittests:: tsserver:: with declaration file maps:: project references", () => {
const aTs: File = {
path: "/a/a.ts",
content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};",
};
const compilerOptions: CompilerOptions = {
outDir: "bin",
declaration: true,
declarationMap: true,
composite: true,
};
const configContent = JSON.stringify({ compilerOptions });
const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent };
const aDtsMapContent: RawSourceMap = {
version: 3,
file: "a.d.ts",
sourceRoot: "",
sources: ["../a.ts"],
names: [],
mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC"
};
const aDtsMap: File = {
path: "/a/bin/a.d.ts.map",
content: JSON.stringify(aDtsMapContent),
};
const aDts: File = {
path: "/a/bin/a.d.ts",
// Need to mangle the sourceMappingURL part or it breaks the build
content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`,
};
const bTs: File = {
path: "/b/b.ts",
content: "export function fnB() {}",
};
const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent };
const bDtsMapContent: RawSourceMap = {
version: 3,
file: "b.d.ts",
sourceRoot: "",
sources: ["../b.ts"],
names: [],
mappings: "AAAA,wBAAgB,GAAG,SAAK",
};
const bDtsMap: File = {
path: "/b/bin/b.d.ts.map",
content: JSON.stringify(bDtsMapContent),
};
const bDts: File = {
// Need to mangle the sourceMappingURL part or it breaks the build
path: "/b/bin/b.d.ts",
content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`,
};
const dummyFile: File = {
path: "/dummy/dummy.ts",
content: "let a = 10;"
};
const userTs: File = {
path: "/user/user.ts",
content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }',
};
const userTsForConfigProject: File = {
path: "/user/user.ts",
content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }',
};
const userTsconfig: File = {
path: "/user/tsconfig.json",
content: JSON.stringify({
file: ["user.ts"],
references: [{ path: "../a" }, { path: "../b" }]
})
};
function makeSampleProjects(addUserTsConfig?: boolean) {
const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]);
const session = createSession(host);
checkDeclarationFiles(aTs, session, [aDtsMap, aDts]);
checkDeclarationFiles(bTs, session, [bDtsMap, bDts]);
// Testing what happens if we delete the original sources.
host.deleteFile(bTs.path);
openFilesForSession([userTs], session);
const service = session.getProjectService();
checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 });
return session;
}
function verifyInferredProjectUnchanged(session: TestSession) {
checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]);
}
function verifyDummyProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]);
}
function verifyOnlyOrphanInferredProject(session: TestSession) {
openFilesForSession([dummyFile], session);
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 });
verifyDummyProject(session);
}
function verifySingleInferredProject(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 });
verifyInferredProjectUnchanged(session);
// Close user file should close all the projects after opening dummy file
closeFilesForSession([userTs], session);
verifyOnlyOrphanInferredProject(session);
}
function verifyATsConfigProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]);
}
function verifyATsConfigOriginalProject(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyInferredProjectUnchanged(session);
verifyATsConfigProject(session);
// Close user file should close all the projects
closeFilesForSession([userTs], session);
verifyOnlyOrphanInferredProject(session);
}
function verifyATsConfigWhenOpened(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyInferredProjectUnchanged(session);
verifyATsConfigProject(session);
closeFilesForSession([userTs], session);
openFilesForSession([dummyFile], session);
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyDummyProject(session);
verifyATsConfigProject(session); // ATsConfig should still be alive
}
function verifyUserTsConfigProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]);
}
it("goToDefinition", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionRequest, protocol.DefinitionResponse>(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]);
verifySingleInferredProject(session);
});
it("getDefinitionAndBoundSpan", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionAndBoundSpanResponse>(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, {
textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
definitions: [protocolFileSpanFromSubstring(aTs, "fnA")],
});
verifySingleInferredProject(session);
});
it("getDefinitionAndBoundSpan with file navigation", () => {
const session = makeSampleProjects(/*addUserTsConfig*/ true);
const response = executeSessionRequest<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionAndBoundSpanResponse>(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, {
textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
definitions: [protocolFileSpanFromSubstring(aTs, "fnA")],
});
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 });
verifyUserTsConfigProject(session);
// Navigate to the definition
closeFilesForSession([userTs], session);
openFilesForSession([aTs], session);
// UserTs configured project should be alive
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 });
verifyUserTsConfigProject(session);
verifyATsConfigProject(session);
closeFilesForSession([aTs], session);
verifyOnlyOrphanInferredProject(session);
});
it("goToType", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.TypeDefinitionRequest, protocol.TypeDefinitionResponse>(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]);
verifySingleInferredProject(session);
});
it("goToImplementation", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ImplementationRequest, protocol.ImplementationResponse>(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]);
verifySingleInferredProject(session);
});
it("goToDefinition -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionRequest, protocol.DefinitionResponse>(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()"));
// bTs does not exist, so stick with bDts
assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]);
verifySingleInferredProject(session);
});
it("navigateTo", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.NavtoRequest, protocol.NavtoResponse>(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" });
assert.deepEqual<ReadonlyArray<protocol.NavtoItem> | undefined>(response, [
{
...protocolFileSpanFromSubstring(bDts, "export declare function fnB(): void;"),
name: "fnB",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export,declare",
},
{
...protocolFileSpanFromSubstring(userTs, "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
name: "fnUser",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
},
{
...protocolFileSpanFromSubstring(aTs, "export function fnA() {}"),
name: "fnA",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
},
]);
verifyATsConfigOriginalProject(session);
});
const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}");
const referencesUserTs = (userTs: File): ReadonlyArray<protocol.ReferencesResponseItem> => [
makeReferenceItem(userTs, /*isDefinition*/ false, "fnA", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
];
it("findAllReferences", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [...referencesUserTs(userTs), referenceATs(aTs)],
symbolName: "fnA",
symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset,
symbolDisplayString: "function fnA(): void",
});
verifyATsConfigOriginalProject(session);
});
it("findAllReferences -- starting at definition", () => {
const session = makeSampleProjects();
openFilesForSession([aTs], session); // If it's not opened, the reference isn't found.
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [referenceATs(aTs), ...referencesUserTs(userTs)],
symbolName: "fnA",
symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset,
symbolDisplayString: "function fnA(): void",
});
verifyATsConfigWhenOpened(session);
});
interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; }
interface ReferencesFullResponse extends protocol.Response { readonly body: ReadonlyArray<ReferencedSymbol>; }
it("findAllReferencesFull", () => {
const session = makeSampleProjects();
const responseFull = executeSessionRequest<ReferencesFullRequest, ReferencesFullResponse>(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<ReadonlyArray<ReferencedSymbol>>(responseFull, [
{
definition: {
...documentSpanFromSubstring(aTs, "fnA"),
kind: ScriptElementKind.functionElement,
name: "function fnA(): void",
containerKind: ScriptElementKind.unknown,
containerName: "",
displayParts: [
keywordPart(SyntaxKind.FunctionKeyword),
spacePart(),
displayPart("fnA", SymbolDisplayPartKind.functionName),
punctuationPart(SyntaxKind.OpenParenToken),
punctuationPart(SyntaxKind.CloseParenToken),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
keywordPart(SyntaxKind.VoidKeyword),
],
},
references: [
makeReferenceEntry(userTs, /*isDefinition*/ false, "fnA"),
makeReferenceEntry(aTs, /*isDefinition*/ true, "fnA"),
],
},
]);
verifyATsConfigOriginalProject(session);
});
it("findAllReferencesFull definition is in mapped file", () => {
const aTs: File = { path: "/a/a.ts", content: `function f() {}` };
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }),
};
const bTs: File = { path: "/b/b.ts", content: `f();` };
const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) };
const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` };
const aDtsMap: File = {
path: "/bin/a.d.ts.map",
content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }),
};
const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap]));
checkDeclarationFiles(aTs, session, [aDtsMap, aDts]);
openFilesForSession([bTs], session);
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 });
const responseFull = executeSessionRequest<ReferencesFullRequest, ReferencesFullResponse>(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()"));
assert.deepEqual<ReadonlyArray<ReferencedSymbol>>(responseFull, [
{
definition: {
containerKind: ScriptElementKind.unknown,
containerName: "",
displayParts: [
keywordPart(SyntaxKind.FunctionKeyword),
spacePart(),
displayPart("f", SymbolDisplayPartKind.functionName),
punctuationPart(SyntaxKind.OpenParenToken),
punctuationPart(SyntaxKind.CloseParenToken),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
keywordPart(SyntaxKind.VoidKeyword),
],
fileName: aTs.path,
kind: ScriptElementKind.functionElement,
name: "function f(): void",
textSpan: { start: 9, length: 1 },
},
references: [
{
fileName: bTs.path,
isDefinition: false,
isInString: undefined,
isWriteAccess: false,
textSpan: { start: 0, length: 1 },
},
{
fileName: aTs.path,
isDefinition: true,
isInString: undefined,
isWriteAccess: true,
textSpan: { start: 9, length: 1 },
},
],
}
]);
});
it("findAllReferences -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [
makeReferenceItem(bDts, /*isDefinition*/ true, "fnB", "export declare function fnB(): void;"),
makeReferenceItem(userTs, /*isDefinition*/ false, "fnB", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
],
symbolName: "fnB",
symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset,
symbolDisplayString: "function fnB(): void",
});
verifySingleInferredProject(session);
});
const renameATs = (aTs: File): protocol.SpanGroup => ({
file: aTs.path,
locs: [protocolRenameSpanFromSubstring(aTs.content, "fnA")],
});
const renameUserTs = (userTs: File): protocol.SpanGroup => ({
file: userTs.path,
locs: [protocolRenameSpanFromSubstring(userTs.content, "fnA")],
});
it("renameLocations", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnA",
fileToRename: undefined,
fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path.
kind: ScriptElementKind.functionElement,
kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","),
triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
},
locs: [renameUserTs(userTs), renameATs(aTs)],
});
verifyATsConfigOriginalProject(session);
});
it("renameLocations -- starting at definition", () => {
const session = makeSampleProjects();
openFilesForSession([aTs], session); // If it's not opened, the reference isn't found.
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnA",
fileToRename: undefined,
fullDisplayName: '"/a/a".fnA',
kind: ScriptElementKind.functionElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"),
},
locs: [renameATs(aTs), renameUserTs(userTs)],
});
verifyATsConfigWhenOpened(session);
});
it("renameLocationsFull", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameFullRequest, protocol.RenameFullResponse>(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<ReadonlyArray<RenameLocation>>(response, [
renameLocation(userTs, "fnA"),
renameLocation(aTs, "fnA"),
]);
verifyATsConfigOriginalProject(session);
});
it("renameLocations -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnB",
fileToRename: undefined,
fullDisplayName: '"/b/bin/b".fnB',
kind: ScriptElementKind.functionElement,
kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","),
triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"),
},
locs: [
{
file: bDts.path,
locs: [protocolRenameSpanFromSubstring(bDts.content, "fnB")],
},
{
file: userTs.path,
locs: [protocolRenameSpanFromSubstring(userTs.content, "fnB")],
},
],
});
verifySingleInferredProject(session);
});
it("getEditsForFileRename", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, protocol.CommandTypes.GetEditsForFileRename, {
oldFilePath: aTs.path,
newFilePath: "/a/aNew.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: userTs.path,
textChanges: [
{ ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" },
],
},
]);
verifySingleInferredProject(session);
});
it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => {
const aTs: File = { path: "/a/src/a.ts", content: "" };
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {
composite: true,
declaration: true,
declarationMap: true,
outDir: "./build",
}
}),
};
const bTs: File = { path: "/b/src/b.ts", content: "" };
const bTsconfig: File = {
path: "/b/tsconfig.json",
content: JSON.stringify({
compilerOptions: {
composite: true,
outDir: "./build",
},
include: ["./src"],
references: [{ path: "../a" }],
}),
};
const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: aTs.path,
newFilePath: "/a/src/a1.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, []); // Should not change anything
});
});
}
@@ -0,0 +1,94 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: document registry in project service", () => {
const projectRootPath = "/user/username/projects/project";
const importModuleContent = `import {a} from "./module1"`;
const file: File = {
path: `${projectRootPath}/index.ts`,
content: importModuleContent
};
const moduleFile: File = {
path: `${projectRootPath}/module1.d.ts`,
content: "export const a: number;"
};
const configFile: File = {
path: `${projectRootPath}/tsconfig.json`,
content: JSON.stringify({ files: ["index.ts"] })
};
function getProject(service: TestProjectService) {
return service.configuredProjects.get(configFile.path)!;
}
function checkProject(service: TestProjectService, moduleIsOrphan: boolean) {
// Update the project
const project = getProject(service);
project.getLanguageService();
checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
assert.isDefined(moduleInfo);
assert.equal(moduleInfo.isOrphan(), moduleIsOrphan);
const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings());
assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]);
}
function createServiceAndHost() {
const host = createServerHost([file, moduleFile, libFile, configFile]);
const service = createProjectService(host);
service.openClientFile(file.path);
checkProject(service, /*moduleIsOrphan*/ false);
return { host, service };
}
function changeFileToNotImportModule(service: TestProjectService) {
const info = service.getScriptInfo(file.path)!;
service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]);
checkProject(service, /*moduleIsOrphan*/ true);
}
function changeFileToImportModule(service: TestProjectService) {
const info = service.getScriptInfo(file.path)!;
service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]);
checkProject(service, /*moduleIsOrphan*/ false);
}
it("Caches the source file if script info is orphan", () => {
const { service } = createServiceAndHost();
const project = getProject(service);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
const sourceFile = moduleInfo.cacheSourceFile!.sourceFile;
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
// edit file
changeFileToNotImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
// write content back
changeFileToImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
});
it("Caches the source file if script info is orphan, and orphan script info changes", () => {
const { host, service } = createServiceAndHost();
const project = getProject(service);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
const sourceFile = moduleInfo.cacheSourceFile!.sourceFile;
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
// edit file
changeFileToNotImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
const updatedModuleContent = moduleFile.content + "\nexport const b: number;";
host.writeFile(moduleFile.path, updatedModuleContent);
// write content back
changeFileToImportModule(service);
assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent);
});
});
}
@@ -0,0 +1,54 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: duplicate packages", () => {
// Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier.
it("works with import fixes", () => {
const packageContent = "export const foo: number;";
const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" });
const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent };
const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent };
const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent };
const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent };
const userContent = 'import("foo");\nfoo';
const aUser: File = { path: "/a/user.ts", content: userContent };
const bUser: File = { path: "/b/user.ts", content: userContent };
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]);
const session = createSession(host);
openFilesForSession([aUser, bUser], session);
for (const user of [aUser, bUser]) {
const response = executeSessionRequest<protocol.CodeFixRequest, protocol.CodeFixResponse>(session, protocol.CommandTypes.GetCodeFixes, {
file: user.path,
startLine: 2,
startOffset: 1,
endLine: 2,
endOffset: 4,
errorCodes: [Diagnostics.Cannot_find_name_0.code],
});
assert.deepEqual<ReadonlyArray<protocol.CodeFixAction> | undefined>(response, [
{
description: `Import 'foo' from module "foo"`,
fixName: "import",
fixId: "fixMissingImport",
fixAllDescription: "Add all missing imports",
changes: [{
fileName: user.path,
textChanges: [{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: 'import { foo } from "foo";\n\n',
}],
}],
commands: undefined,
},
]);
}
});
});
}
@@ -0,0 +1,76 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => {
const projectRoot = "/user/username/projects/project";
function getLargeFile(useLargeTsFile: boolean) {
return `src/large.${useLargeTsFile ? "ts" : "js"}`;
}
function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) {
const largeFile: File = {
path: `${projectRoot}/${getLargeFile(useLargeTsFile)}`,
content: "export var x = 10;",
fileSize: server.maxFileSize + 1
};
files.push(largeFile);
const host = createServerHost(files);
const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking<server.LargeFileReferencedEvent>(host, server.LargeFileReferencedEvent);
return { session, verifyLargeFile };
function verifyLargeFile(project: server.Project) {
checkProjectActualFiles(project, files.map(f => f.path));
// large file for non ts file should be empty and for ts file should have content
const service = session.getProjectService();
const info = service.getScriptInfo(largeFile.path)!;
assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : "");
assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{
eventName: server.LargeFileReferencedEvent,
data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize }
}]);
}
}
function verifyLargeFile(useLargeTsFile: boolean) {
it("when large file is included by tsconfig", () => {
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: "export var y = 10;"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } })
};
const files = [file, libFile, tsconfig];
const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile);
const service = session.getProjectService();
openFilesForSession([file], session);
checkNumberOfProjects(service, { configuredProjects: 1 });
verifyLargeFile(service.configuredProjects.get(tsconfig.path)!);
});
it("when large file is included by module resolution", () => {
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: `export var y = 10;import {x} from "./large"`
};
const files = [file, libFile];
const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile);
const service = session.getProjectService();
openFilesForSession([file], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
verifyLargeFile(service.inferredProjects[0]);
});
}
describe("large file is ts file", () => {
verifyLargeFile(/*useLargeTsFile*/ true);
});
describe("large file is js file", () => {
verifyLargeFile(/*useLargeTsFile*/ false);
});
});
}
@@ -0,0 +1,51 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => {
it("language service disabled events are triggered", () => {
const f1 = {
path: "/a/app.js",
content: "let x = 1;"
};
const f2 = {
path: "/a/largefile.js",
content: ""
};
const config = {
path: "/a/jsconfig.json",
content: "{}"
};
const configWithExclude = {
path: config.path,
content: JSON.stringify({ exclude: ["largefile.js"] })
};
const host = createServerHost([f1, f2, config]);
const originalGetFileSize = host.getFileSize;
host.getFileSize = (filePath: string) =>
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
const { session, events } = createSessionWithEventTracking<server.ProjectLanguageServiceStateEvent>(host, server.ProjectLanguageServiceStateEvent);
session.executeCommand(<protocol.OpenRequest>{
seq: 0,
type: "request",
command: "open",
arguments: { file: f1.path }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = configuredProjectAt(projectService, 0);
assert.isFalse(project.languageServiceEnabled, "Language service enabled");
assert.equal(events.length, 1, "should receive event");
assert.equal(events[0].data.project, project, "project name");
assert.equal(events[0].data.project.getProjectName(), config.path, "config path");
assert.isFalse(events[0].data.languageServiceEnabled, "Language service state");
host.reloadFS([f1, f2, configWithExclude]);
host.checkTimeoutQueueLengthAndRun(2);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.isTrue(project.languageServiceEnabled, "Language service enabled");
assert.equal(events.length, 2, "should receive event");
assert.equal(events[1].data.project, project, "project");
assert.equal(events[1].data.project.getProjectName(), config.path, "config path");
assert.isTrue(events[1].data.languageServiceEnabled, "Language service state");
});
});
}
@@ -0,0 +1,193 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => {
const projectRoot = "/user/username/projects";
const aTs: File = {
path: `${projectRoot}/a/a.ts`,
content: "export class A { }"
};
const configA: File = {
path: `${projectRoot}/a/tsconfig.json`,
content: "{}"
};
const bTsPath = `${projectRoot}/b/b.ts`;
const configBPath = `${projectRoot}/b/tsconfig.json`;
const files = [libFile, aTs, configA];
function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => {
session: TestSession;
getNumberOfEvents: () => number;
clearEvents: () => void;
verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void;
}) {
function createSessionToVerifyEvent(files: ReadonlyArray<File>) {
const host = createServerHost(files);
const originalReadFile = host.readFile;
const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host);
host.readFile = file => {
if (file === configA.path || file === configBPath) {
assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file");
}
return originalReadFile.call(host, file);
};
const service = session.getProjectService();
return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents };
function verifyEvent(project: server.Project, reason: string) {
verifyProjectLoadEvents([
{ eventName: server.ProjectLoadingStartEvent, data: { project, reason } },
{ eventName: server.ProjectLoadingFinishEvent, data: { project } }
]);
clearEvents();
}
function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) {
openFilesForSession([file], session);
checkNumberOfProjects(service, { configuredProjects });
const project = service.configuredProjects.get(configPath)!;
assert.isDefined(project);
verifyEvent(project, `Creating possible configured project for ${file.path} to open`);
}
}
it("when project is created by open file", () => {
const bTs: File = {
path: bTsPath,
content: "export class B {}"
};
const configB: File = {
path: configBPath,
content: "{}"
};
const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB));
verifyEventWithOpenTs(aTs, configA.path, 1);
verifyEventWithOpenTs(bTs, configB.path, 2);
});
it("when change is detected in the config file", () => {
const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files);
verifyEventWithOpenTs(aTs, configA.path, 1);
host.writeFile(configA.path, configA.content);
host.checkTimeoutQueueLengthAndRun(2);
const project = service.configuredProjects.get(configA.path)!;
verifyEvent(project, `Change in config file detected`);
});
it("when opening original location project", () => {
const aDTs: File = {
path: `${projectRoot}/a/a.d.ts`,
content: `export declare class A {
}
//# sourceMappingURL=a.d.ts.map
`
};
const aDTsMap: File = {
path: `${projectRoot}/a/a.d.ts.map`,
content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}`
};
const bTs: File = {
path: bTsPath,
content: `import {A} from "../a/a"; new A();`
};
const configB: File = {
path: configBPath,
content: JSON.stringify({
references: [{ path: "../a" }]
})
};
const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB));
verifyEventWithOpenTs(bTs, configB.path, 1);
session.executeCommandSeq<protocol.ReferencesRequest>({
command: protocol.CommandTypes.References,
arguments: {
file: bTs.path,
...protocolLocationFromSubstring(bTs.content, "A()")
}
});
checkNumberOfProjects(service, { configuredProjects: 2 });
const project = service.configuredProjects.get(configA.path)!;
assert.isDefined(project);
verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`);
});
describe("with external projects and config files ", () => {
const projectFileName = `${projectRoot}/a/project.csproj`;
function createSession(lazyConfiguredProjectsFromExternalProject: boolean) {
const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
service.openExternalProject(<protocol.ExternalProject>{
projectFileName,
rootFiles: toExternalFiles([aTs.path, configA.path]),
options: {}
});
checkNumberOfProjects(service, { configuredProjects: 1 });
return { session, service, verifyEvent, getNumberOfEvents };
function verifyEvent() {
const projectA = service.configuredProjects.get(configA.path)!;
assert.isDefined(projectA);
verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`);
}
}
it("when lazyConfiguredProjectsFromExternalProject is false", () => {
const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false);
verifyEvent();
});
it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => {
const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
assert.equal(getNumberOfEvents(), 0);
openFilesForSession([aTs], session);
verifyEvent();
});
it("when lazyConfiguredProjectsFromExternalProject is disabled", () => {
const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
assert.equal(getNumberOfEvents(), 0);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } });
verifyEvent();
});
});
}
describe("when using event handler", () => {
verifyProjectLoadingStartAndFinish(host => {
const { session, events } = createSessionWithEventTracking<server.ProjectLoadingStartEvent | server.ProjectLoadingFinishEvent>(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent);
return {
session,
getNumberOfEvents: () => events.length,
clearEvents: () => events.length = 0,
verifyProjectLoadEvents: expected => assert.deepEqual(events, expected)
};
});
});
describe("when using default event handler", () => {
verifyProjectLoadingStartAndFinish(host => {
const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<protocol.ProjectLoadingStartEvent | protocol.ProjectLoadingFinishEvent>(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]);
return {
session,
getNumberOfEvents: () => getEvents().length,
clearEvents,
verifyProjectLoadEvents
};
function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) {
const actual = getEvents().map(e => ({ eventName: e.event, data: e.body }));
const mappedExpected = expected.map(e => {
const { project, ...rest } = e.data;
return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } };
});
assert.deepEqual(actual, mappedExpected);
}
});
});
});
}
@@ -0,0 +1,591 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
function verifyFiles(caption: string, actual: ReadonlyArray<string>, expected: ReadonlyArray<string>) {
assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`);
const seen = createMap<true>();
forEach(actual, f => {
assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`);
seen.set(f, true);
assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`);
});
}
function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) {
return (file: File) => {
session.executeCommandSeq(<protocol.OpenRequest>{
command: server.CommandNames.Open,
arguments: {
file: file.path
}
});
verifyProjectsUpdatedInBackgroundEventHandler([]);
};
}
interface ProjectsUpdatedInBackgroundEventVerifier {
session: TestSession;
verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void;
verifyInitialOpen(file: File): void;
}
function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) {
it("when adding new file", () => {
const commonFile1: File = {
path: "/a/b/file1.ts",
content: "export var x = 10;"
};
const commonFile2: File = {
path: "/a/b/file2.ts",
content: "export var y = 10;"
};
const commonFile3: File = {
path: "/a/b/file3.ts",
content: "export var z = 10;"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{}`
};
const openFiles = [commonFile1.path];
const host = createServerHost([commonFile1, libFile, configFile]);
const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host);
verifyInitialOpen(commonFile1);
host.reloadFS([commonFile1, libFile, configFile, commonFile2]);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
});
describe("with --out or --outFile setting", () => {
function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) {
const config: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions
})
};
const f1: File = {
path: "/a/a.ts",
content: "export let x = 1"
};
const f2: File = {
path: "/a/b.ts",
content: "export let y = 1"
};
const openFiles = [f1.path];
const files = [f1, config, libFile];
const host = createServerHost(files);
const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host);
verifyInitialOpen(f1);
files.push(f2);
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
f2.content = "export let x = 11";
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
}
it("when both options are not set", () => {
verifyEventWithOutSettings();
});
it("when --out is set", () => {
const outJs = "/a/out.js";
verifyEventWithOutSettings({ out: outJs });
});
it("when --outFile is set", () => {
const outJs = "/a/out.js";
verifyEventWithOutSettings({ outFile: outJs });
});
});
describe("with modules and configured project", () => {
const file1Consumer1Path = "/a/b/file1Consumer1.ts";
const moduleFile1Path = "/a/b/moduleFile1.ts";
const configFilePath = "/a/b/tsconfig.json";
interface InitialStateParams {
/** custom config file options */
configObj?: any;
/** Additional files and folders to add */
getAdditionalFileOrFolder?(): File[];
/** initial list of files to reload in fs and first file in this list being the file to open */
firstReloadFileList?: string[];
}
function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) {
const moduleFile1: File = {
path: moduleFile1Path,
content: "export function Foo() { };",
};
const file1Consumer1: File = {
path: file1Consumer1Path,
content: `import {Foo} from "./moduleFile1"; export var y = 10;`,
};
const file1Consumer2: File = {
path: "/a/b/file1Consumer2.ts",
content: `import {Foo} from "./moduleFile1"; let z = 10;`,
};
const moduleFile2: File = {
path: "/a/b/moduleFile2.ts",
content: `export var Foo4 = 10;`,
};
const globalFile3: File = {
path: "/a/b/globalFile3.ts",
content: `interface GlobalFoo { age: number }`
};
const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : [];
const configFile = {
path: configFilePath,
content: JSON.stringify(configObj || { compilerOptions: {} })
};
const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile];
const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files;
const host = createServerHost([filesToReload[0], configFile]);
// Initial project creation
const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host);
const openFiles = [filesToReload[0].path];
verifyInitialOpen(filesToReload[0]);
// Since this is first event, it will have all the files
verifyProjectsUpdatedInBackgroundEvent(filesToReload);
return {
moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile,
files,
updateContentOfOpenFile,
verifyNoProjectsUpdatedInBackgroundEvent,
verifyProjectsUpdatedInBackgroundEvent
};
function getFiles(filelist: string[]) {
return map(filelist, getFile);
}
function getFile(fileName: string) {
return find(files, file => file.path === fileName)!;
}
function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) {
host.reloadFS(filesToReload || files);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([]);
}
function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) {
host.reloadFS(filesToReload || files);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
}
function updateContentOfOpenFile(file: File, newContent: string) {
session.executeCommandSeq<protocol.ChangeRequest>({
command: server.CommandNames.Change,
arguments: {
file: file.path,
insertString: newContent,
endLine: 1,
endOffset: file.content.length,
line: 1,
offset: 1
}
});
file.content = newContent;
}
}
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`
moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`;
verifyProjectsUpdatedInBackgroundEvent();
});
it("should be up-to-date with the reference map changes", () => {
const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState();
// Change file1Consumer1 content to `export let y = Foo();`
updateContentOfOpenFile(file1Consumer1, "export let y = Foo();");
verifyNoProjectsUpdatedInBackgroundEvent();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
// Add the import statements back to file1Consumer1
updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`);
verifyNoProjectsUpdatedInBackgroundEvent();
// Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`
moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
// Multiple file edits in one go:
// Change file1Consumer1 content to `export let y = Foo();`
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`);
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
});
it("should be up-to-date with deleted files", () => {
const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
moduleFile1.content = `export var T: number;export function Foo() { };`;
// Delete file1Consumer2
const filesToLoad = filter(files, file => file !== file1Consumer2);
verifyProjectsUpdatedInBackgroundEvent(filesToLoad);
});
it("should be up-to-date with newly created files", () => {
const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState();
const file1Consumer3: File = {
path: "/a/b/file1Consumer3.ts",
content: `import {Foo} from "./moduleFile1"; let y = Foo();`
};
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3));
});
it("should detect changes in non-root files", () => {
const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
configObj: { files: [file1Consumer1Path] },
});
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
// change file1 internal, and verify only file1 is affected
moduleFile1.content += "var T1: number;";
verifyProjectsUpdatedInBackgroundEvent();
});
it("should return all files if a global file changed shape", () => {
const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
globalFile3.content += "var T2: string;";
verifyProjectsUpdatedInBackgroundEvent();
});
it("should always return the file itself if '--isolatedModules' is specified", () => {
const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
configObj: { compilerOptions: { isolatedModules: true } }
});
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
});
it("should always return the file itself if '--out' or '--outFile' is specified", () => {
const outFilePath = "/a/b/out.js";
const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
configObj: { compilerOptions: { module: "system", outFile: outFilePath } }
});
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
});
it("should return cascaded affected file list", () => {
const file1Consumer1Consumer1: File = {
path: "/a/b/file1Consumer1Consumer1.ts",
content: `import {y} from "./file1Consumer1";`
};
const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
getAdditionalFileOrFolder: () => [file1Consumer1Consumer1]
});
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;");
verifyNoProjectsUpdatedInBackgroundEvent();
// Doesnt change the shape of file1Consumer1
moduleFile1.content = `export var T: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
// Change both files before the timeout
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;");
moduleFile1.content = `export var T2: number;export function Foo() { };`;
verifyProjectsUpdatedInBackgroundEvent();
});
it("should work fine for files with circular references", () => {
const file1: File = {
path: "/a/b/file1.ts",
content: `
/// <reference path="./file2.ts" />
export var t1 = 10;`
};
const file2: File = {
path: "/a/b/file2.ts",
content: `
/// <reference path="./file1.ts" />
export var t2 = 10;`
};
const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
getAdditionalFileOrFolder: () => [file1, file2],
firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath]
});
file2.content += "export var t3 = 10;";
verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]);
});
it("should detect removed code file", () => {
const referenceFile1: File = {
path: "/a/b/referenceFile1.ts",
content: `
/// <reference path="./moduleFile1.ts" />
export var x = Foo();`
};
const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath]
});
verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]);
});
it("should detect non-existing code file", () => {
const referenceFile1: File = {
path: "/a/b/referenceFile1.ts",
content: `
/// <reference path="./moduleFile2.ts" />
export var x = Foo();`
};
const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath]
});
updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();");
verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]);
// Create module File2 and see both files are saved
verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]);
});
});
describe("resolution when resolution cache size", () => {
function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) {
const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/";
const file1: File = {
path: rootFolder + "a/b/project/file1.ts",
content: 'import a from "file2"'
};
const file2: File = {
path: rootFolder + "a/b/node_modules/file2.d.ts",
content: "export class a { }"
};
const file3: File = {
path: rootFolder + "a/b/project/file3.ts",
content: "export class c { }"
};
const configFile: File = {
path: rootFolder + "a/b/project/tsconfig.json",
content: JSON.stringify({ compilerOptions: { typeRoots: [] } })
};
const projectFiles = [file1, file3, libFile, configFile];
const openFiles = [file1.path];
const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ?
// Folders of node_modules lookup not in changedRoot
["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) :
// Folder of tsconfig
["/a/b/project", "/a/b/project/node_modules"];
const host = createServerHost(projectFiles);
const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host);
const projectService = session.getProjectService();
verifyInitialOpen(file1);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = projectService.configuredProjects.get(configFile.path)!;
verifyProject();
if (limitHit) {
(project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1;
}
file3.content += "export class d {}";
host.reloadFS(projectFiles);
host.checkTimeoutQueueLengthAndRun(2);
// Since this is first event
verifyProject();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
projectFiles.push(file2);
host.reloadFS(projectFiles);
host.runQueuedTimeoutCallbacks();
if (useSlashRootAsSomeNotRootFolderInUserDirectory) {
watchedRecursiveDirectories.length = 3;
}
else {
// file2 addition wont be detected
projectFiles.pop();
assert.isTrue(host.fileExists(file2.path));
}
verifyProject();
verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{
eventName: server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}] : []);
function verifyProject() {
checkProjectActualFiles(project, map(projectFiles, file => file.path));
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true);
}
}
it("limit not hit and project is not at root level", () => {
verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true);
});
it("limit hit and project is not at root level", () => {
verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true);
});
it("limit not hit and project is at root level", () => {
verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false);
});
it("limit hit and project is at root level", () => {
verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false);
});
});
}
describe("when event handler is set in the session", () => {
verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler);
function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier {
const { session, events: projectChangedEvents } = createSessionWithEventTracking<server.ProjectsUpdatedInBackgroundEvent>(host, server.ProjectsUpdatedInBackgroundEvent);
return {
session,
verifyProjectsUpdatedInBackgroundEventHandler,
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler)
};
function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) {
return JSON.stringify(event && { eventName: event.eventName, data: event.data });
}
function eventsToString(events: ReadonlyArray<server.ProjectsUpdatedInBackgroundEvent>) {
return "[" + map(events, eventToString).join(",") + "]";
}
function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: ReadonlyArray<server.ProjectsUpdatedInBackgroundEvent>) {
assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`);
forEach(projectChangedEvents, (actualEvent, i) => {
const expectedEvent = expectedEvents[i];
assert.strictEqual(actualEvent.eventName, expectedEvent.eventName);
verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles);
});
// Verified the events, reset them
projectChangedEvents.length = 0;
}
}
});
describe("when event handler is not set but session is created with canUseEvents = true", () => {
describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => {
verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents);
});
describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => {
verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true));
});
function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier {
const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<protocol.ProjectsUpdatedInBackgroundEvent>(host, server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate });
return {
session,
verifyProjectsUpdatedInBackgroundEventHandler,
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler)
};
function verifyProjectsUpdatedInBackgroundEventHandler(expected: ReadonlyArray<server.ProjectsUpdatedInBackgroundEvent>) {
const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => {
return {
openFiles: e.data.openFiles
};
});
const events = getEvents();
assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`);
forEach(events, (actualEvent, i) => {
const expectedEvent = expectedEvents[i];
verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles);
});
// Verified the events, reset them
clearEvents();
if (events.length) {
host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate
}
}
}
});
});
}
@@ -0,0 +1,111 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: events:: SurveyReady", () => {
function createSessionWithEventHandler(host: TestServerHost) {
const { session, events: surveyEvents } = createSessionWithEventTracking<server.SurveyReady>(host, server.SurveyReady);
return { session, verifySurveyReadyEvent };
function verifySurveyReadyEvent(numberOfEvents: number) {
assert.equal(surveyEvents.length, numberOfEvents);
const expectedEvents = numberOfEvents === 0 ? [] : [{
eventName: server.SurveyReady,
data: { surveyId: "checkJs" }
}];
assert.deepEqual(surveyEvents, expectedEvents);
}
}
it("doesn't log an event when checkJs isn't set", () => {
const projectRoot = "/user/username/projects/project";
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: "export var y = 10;"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: {} }),
};
const host = createServerHost([file, tsconfig]);
const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host);
const service = session.getProjectService();
openFilesForSession([file], session);
checkNumberOfProjects(service, { configuredProjects: 1 });
const project = service.configuredProjects.get(tsconfig.path)!;
checkProjectActualFiles(project, [file.path, tsconfig.path]);
verifySurveyReadyEvent(0);
});
it("logs an event when checkJs is set", () => {
const projectRoot = "/user/username/projects/project";
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: "export var y = 10;"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { checkJs: true } }),
};
const host = createServerHost([file, tsconfig]);
const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host);
openFilesForSession([file], session);
verifySurveyReadyEvent(1);
});
it("logs an event when checkJs is set, only the first time", () => {
const projectRoot = "/user/username/projects/project";
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: "export var y = 10;"
};
const rando: File = {
path: `/rando/calrissian.ts`,
content: "export function f() { }"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { checkJs: true } }),
};
const host = createServerHost([file, tsconfig]);
const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host);
openFilesForSession([file], session);
verifySurveyReadyEvent(1);
closeFilesForSession([file], session);
openFilesForSession([rando], session);
openFilesForSession([file], session);
verifySurveyReadyEvent(1);
});
it("logs an event when checkJs is set after closing and reopening", () => {
const projectRoot = "/user/username/projects/project";
const file: File = {
path: `${projectRoot}/src/file.ts`,
content: "export var y = 10;"
};
const rando: File = {
path: `/rando/calrissian.ts`,
content: "export function f() { }"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: JSON.stringify({}),
};
const host = createServerHost([file, tsconfig]);
const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host);
openFilesForSession([file], session);
verifySurveyReadyEvent(0);
closeFilesForSession([file], session);
openFilesForSession([rando], session);
host.writeFile(tsconfig.path, JSON.stringify({ compilerOptions: { checkJs: true } }));
openFilesForSession([file], session);
verifySurveyReadyEvent(1);
});
});
}
@@ -0,0 +1,764 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: ExternalProjects", () => {
describe("can handle tsconfig file name with difference casing", () => {
function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({
include: []
})
};
const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false });
const service = createProjectService(host);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path));
service.openExternalProject(<protocol.ExternalProject>{
projectFileName: "/a/b/project.csproj",
rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]),
options: {}
});
service.checkNumberOfProjects({ configuredProjects: 1 });
const project = service.configuredProjects.get(config.path)!;
if (lazyConfiguredProjectsFromExternalProject) {
assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded
checkProjectActualFiles(project, emptyArray);
}
else {
assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
checkProjectActualFiles(project, [upperCaseConfigFilePath]);
}
service.openClientFile(f1.path);
service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated
checkProjectActualFiles(project, [upperCaseConfigFilePath]);
checkProjectActualFiles(service.inferredProjects[0], [f1.path]);
}
it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});
it("remove not-listed external projects", () => {
const f1 = {
path: "/a/app.ts",
content: "let x = 1"
};
const f2 = {
path: "/b/app.ts",
content: "let x = 1"
};
const f3 = {
path: "/c/app.ts",
content: "let x = 1"
};
const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} });
const p1 = makeProject(f1);
const p2 = makeProject(f2);
const p3 = makeProject(f3);
const host = createServerHost([f1, f2, f3]);
const session = createSession(host);
session.executeCommand(<protocol.OpenExternalProjectsRequest>{
seq: 1,
type: "request",
command: "openExternalProjects",
arguments: { projects: [p1, p2] }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { externalProjects: 2 });
assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName);
assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName);
session.executeCommand(<protocol.OpenExternalProjectsRequest>{
seq: 2,
type: "request",
command: "openExternalProjects",
arguments: { projects: [p1, p3] }
});
checkNumberOfProjects(projectService, { externalProjects: 2 });
assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName);
assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName);
session.executeCommand(<protocol.OpenExternalProjectsRequest>{
seq: 3,
type: "request",
command: "openExternalProjects",
arguments: { projects: [] }
});
checkNumberOfProjects(projectService, { externalProjects: 0 });
session.executeCommand(<protocol.OpenExternalProjectsRequest>{
seq: 3,
type: "request",
command: "openExternalProjects",
arguments: { projects: [p2] }
});
assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName);
});
it("should not close external project with no open files", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x =1;"
};
const file2 = {
path: "/a/b/f2.ts",
content: "let y =1;"
};
const externalProjectName = "externalproject";
const host = createServerHost([file1, file2]);
const projectService = createProjectService(host);
projectService.openExternalProject({
rootFiles: toExternalFiles([file1.path, file2.path]),
options: {},
projectFileName: externalProjectName
});
checkNumberOfExternalProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
// open client file - should not lead to creation of inferred project
projectService.openClientFile(file1.path, file1.content);
checkNumberOfExternalProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
// close client file - external project should still exists
projectService.closeClientFile(file1.path);
checkNumberOfExternalProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
projectService.closeExternalProject(externalProjectName);
checkNumberOfExternalProjects(projectService, 0);
checkNumberOfInferredProjects(projectService, 0);
});
it("external project for dynamic file", () => {
const externalProjectName = "^ScriptDocument1 file1.ts";
const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]);
const host = createServerHost([]);
const projectService = createProjectService(host);
projectService.openExternalProject({
rootFiles: externalFiles,
options: {},
projectFileName: externalProjectName
});
checkNumberOfExternalProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
externalFiles[0].content = "let x =1;";
projectService.applyChangesInOpenFiles(externalFiles, [], []);
});
it("external project that included config files", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x =1;"
};
const config1 = {
path: "/a/b/tsconfig.json",
content: JSON.stringify(
{
compilerOptions: {},
files: ["f1.ts"]
}
)
};
const file2 = {
path: "/a/c/f2.ts",
content: "let y =1;"
};
const config2 = {
path: "/a/c/tsconfig.json",
content: JSON.stringify(
{
compilerOptions: {},
files: ["f2.ts"]
}
)
};
const file3 = {
path: "/a/d/f3.ts",
content: "let z =1;"
};
const externalProjectName = "externalproject";
const host = createServerHost([file1, file2, file3, config1, config2]);
const projectService = createProjectService(host);
projectService.openExternalProject({
rootFiles: toExternalFiles([config1.path, config2.path, file3.path]),
options: {},
projectFileName: externalProjectName
});
checkNumberOfProjects(projectService, { configuredProjects: 2 });
const proj1 = projectService.configuredProjects.get(config1.path);
const proj2 = projectService.configuredProjects.get(config2.path);
assert.isDefined(proj1);
assert.isDefined(proj2);
// open client file - should not lead to creation of inferred project
projectService.openClientFile(file1.path, file1.content);
checkNumberOfProjects(projectService, { configuredProjects: 2 });
assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2);
projectService.openClientFile(file3.path, file3.content);
checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2);
projectService.closeExternalProject(externalProjectName);
// open file 'file1' from configured project keeps project alive
checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
assert.isUndefined(projectService.configuredProjects.get(config2.path));
projectService.closeClientFile(file3.path);
checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
assert.isUndefined(projectService.configuredProjects.get(config2.path));
assert.isTrue(projectService.inferredProjects[0].isOrphan());
projectService.closeClientFile(file1.path);
checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
assert.isUndefined(projectService.configuredProjects.get(config2.path));
assert.isTrue(projectService.inferredProjects[0].isOrphan());
projectService.openClientFile(file2.path, file2.content);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.isUndefined(projectService.configuredProjects.get(config1.path));
assert.isDefined(projectService.configuredProjects.get(config2.path));
});
it("external project with included config file opened after configured project", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x = 1"
};
const configFile = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: {} })
};
const externalProjectName = "externalproject";
const host = createServerHost([file1, configFile]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
projectService.openExternalProject({
rootFiles: toExternalFiles([configFile.path]),
options: {},
projectFileName: externalProjectName
});
checkNumberOfProjects(projectService, { configuredProjects: 1 });
projectService.closeClientFile(file1.path);
// configured project is alive since it is opened as part of external project
checkNumberOfProjects(projectService, { configuredProjects: 1 });
projectService.closeExternalProject(externalProjectName);
checkNumberOfProjects(projectService, { configuredProjects: 0 });
});
it("external project with included config file opened after configured project and then closed", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x = 1"
};
const file2 = {
path: "/a/f2.ts",
content: "let x = 1"
};
const configFile = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: {} })
};
const externalProjectName = "externalproject";
const host = createServerHost([file1, file2, libFile, configFile]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = projectService.configuredProjects.get(configFile.path);
projectService.openExternalProject({
rootFiles: toExternalFiles([configFile.path]),
options: {},
projectFileName: externalProjectName
});
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
projectService.closeExternalProject(externalProjectName);
// configured project is alive since file is still open
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
projectService.closeClientFile(file1.path);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
projectService.openClientFile(file2.path);
checkNumberOfProjects(projectService, { inferredProjects: 1 });
assert.isUndefined(projectService.configuredProjects.get(configFile.path));
});
it("can correctly update external project when set of root files has changed", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x = 1"
};
const file2 = {
path: "/a/b/f2.ts",
content: "let y = 1"
};
const host = createServerHost([file1, file2]);
const projectService = createProjectService(host);
projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) });
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectActualFiles(projectService.externalProjects[0], [file1.path]);
projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) });
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
});
it("can update external project when set of root files was not changed", () => {
const file1 = {
path: "/a/b/f1.ts",
content: `export * from "m"`
};
const file2 = {
path: "/a/b/f2.ts",
content: "export let y = 1"
};
const file3 = {
path: "/a/m.ts",
content: "export let y = 1"
};
const host = createServerHost([file1, file2, file3]);
const projectService = createProjectService(host);
projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) });
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]);
projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) });
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]);
});
it("language service disabled state is updated in external projects", () => {
const f1 = {
path: "/a/app.js",
content: "var x = 1"
};
const f2 = {
path: "/a/largefile.js",
content: ""
};
const host = createServerHost([f1, f2]);
const originalGetFileSize = host.getFileSize;
host.getFileSize = (filePath: string) =>
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
const service = createProjectService(host);
const projectFileName = "/a/proj.csproj";
service.openExternalProject({
projectFileName,
rootFiles: toExternalFiles([f1.path, f2.path]),
options: {}
});
service.checkNumberOfProjects({ externalProjects: 1 });
assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1");
service.openExternalProject({
projectFileName,
rootFiles: toExternalFiles([f1.path]),
options: {}
});
service.checkNumberOfProjects({ externalProjects: 1 });
assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled");
service.openExternalProject({
projectFileName,
rootFiles: toExternalFiles([f1.path, f2.path]),
options: {}
});
service.checkNumberOfProjects({ externalProjects: 1 });
assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2");
});
describe("deleting config file opened from the external project works", () => {
function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) {
const site = {
path: "/user/someuser/project/js/site.js",
content: ""
};
const configFile = {
path: "/user/someuser/project/tsconfig.json",
content: "{}"
};
const projectFileName = "/user/someuser/project/WebApplication6.csproj";
const host = createServerHost([libFile, site, configFile]);
const projectService = createProjectService(host);
projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
const externalProject: protocol.ExternalProject = {
projectFileName,
rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)],
options: { allowJs: false },
typeAcquisition: { include: [] }
};
projectService.openExternalProjects([externalProject]);
let knownProjects = projectService.synchronizeProjectList([]);
checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 });
const configProject = configuredProjectAt(projectService, 0);
checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ?
emptyArray : // Since no files opened from this project, its not loaded
[configFile.path]);
host.reloadFS([libFile, site]);
host.checkTimeoutQueueLengthAndRun(1);
knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039
checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 });
externalProject.rootFiles.length = 1;
projectService.openExternalProjects([externalProject]);
checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 });
checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]);
}
it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});
describe("correctly handling add/remove tsconfig - 1", () => {
function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1;"
};
const f2 = {
path: "/a/b/lib.ts",
content: ""
};
const tsconfig = {
path: "/a/b/tsconfig.json",
content: ""
};
const host = createServerHost([f1, f2]);
const projectService = createProjectService(host);
projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
// open external project
const projectName = "/a/b/proj1";
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, f2.path]),
options: {}
});
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
// rename lib.ts to tsconfig.json
host.reloadFS([f1, tsconfig]);
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, tsconfig.path]),
options: {}
});
projectService.checkNumberOfProjects({ configuredProjects: 1 });
if (lazyConfiguredProjectsFromExternalProject) {
checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
projectService.ensureInferredProjectsUpToDate_TestOnly();
}
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]);
// rename tsconfig.json back to lib.ts
host.reloadFS([f1, f2]);
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, f2.path]),
options: {}
});
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
}
it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});
describe("correctly handling add/remove tsconfig - 2", () => {
function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1;"
};
const cLib = {
path: "/a/b/c/lib.ts",
content: ""
};
const cTsconfig = {
path: "/a/b/c/tsconfig.json",
content: "{}"
};
const dLib = {
path: "/a/b/d/lib.ts",
content: ""
};
const dTsconfig = {
path: "/a/b/d/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]);
const projectService = createProjectService(host);
projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
// open external project
const projectName = "/a/b/proj1";
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path]),
options: {}
});
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
// add two config file as root files
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
options: {}
});
projectService.checkNumberOfProjects({ configuredProjects: 2 });
if (lazyConfiguredProjectsFromExternalProject) {
checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed
projectService.ensureInferredProjectsUpToDate_TestOnly();
}
checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]);
checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]);
// remove one config file
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, dTsconfig.path]),
options: {}
});
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]);
// remove second config file
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path]),
options: {}
});
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
// open two config files
// add two config file as root files
projectService.openExternalProject({
projectFileName: projectName,
rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
options: {}
});
projectService.checkNumberOfProjects({ configuredProjects: 2 });
if (lazyConfiguredProjectsFromExternalProject) {
checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed
projectService.ensureInferredProjectsUpToDate_TestOnly();
}
checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]);
checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]);
// close all projects - no projects should be opened
projectService.closeExternalProject(projectName);
projectService.checkNumberOfProjects({});
}
it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});
it("correctly handles changes in lib section of config file", () => {
const libES5 = {
path: "/compiler/lib.es5.d.ts",
content: "declare const eval: any"
};
const libES2015Promise = {
path: "/compiler/lib.es2015.promise.d.ts",
content: "declare class Promise<T> {}"
};
const app = {
path: "/src/app.ts",
content: "var x: Promise<string>;"
};
const config1 = {
path: "/src/tsconfig.json",
content: JSON.stringify(
{
compilerOptions: {
module: "commonjs",
target: "es5",
noImplicitAny: true,
sourceMap: false,
lib: [
"es5"
]
}
})
};
const config2 = {
path: config1.path,
content: JSON.stringify(
{
compilerOptions: {
module: "commonjs",
target: "es5",
noImplicitAny: true,
sourceMap: false,
lib: [
"es5",
"es2015.promise"
]
}
})
};
const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" });
const projectService = createProjectService(host);
projectService.openClientFile(app.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]);
host.reloadFS([libES5, libES2015Promise, app, config2]);
host.checkTimeoutQueueLengthAndRun(2);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]);
});
it("should handle non-existing directories in config file", () => {
const f = {
path: "/a/src/app.ts",
content: "let x = 1;"
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {},
include: [
"src/**/*",
"notexistingfolder/*"
]
})
};
const host = createServerHost([f, config]);
const projectService = createProjectService(host);
projectService.openClientFile(f.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const project = projectService.configuredProjects.get(config.path)!;
assert.isTrue(project.hasOpenRef()); // f
projectService.closeClientFile(f.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config.path), project);
assert.isFalse(project.hasOpenRef()); // No files
assert.isFalse(project.isClosed());
projectService.openClientFile(f.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
assert.strictEqual(projectService.configuredProjects.get(config.path), project);
assert.isTrue(project.hasOpenRef()); // f
assert.isFalse(project.isClosed());
});
it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({})
};
const projectFileName = "/a/b/project.csproj";
const host = createServerHost([f1, config]);
const service = createProjectService(host);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } });
service.openExternalProject(<protocol.ExternalProject>{
projectFileName,
rootFiles: toExternalFiles([f1.path, config.path]),
options: {}
});
service.checkNumberOfProjects({ configuredProjects: 1 });
const project = service.configuredProjects.get(config.path)!;
assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded
checkProjectActualFiles(project, emptyArray);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } });
assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
checkProjectActualFiles(project, [config.path, f1.path]);
service.closeExternalProject(projectFileName);
service.checkNumberOfProjects({});
service.openExternalProject(<protocol.ExternalProject>{
projectFileName,
rootFiles: toExternalFiles([f1.path, config.path]),
options: {}
});
service.checkNumberOfProjects({ configuredProjects: 1 });
const project2 = service.configuredProjects.get(config.path)!;
assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
checkProjectActualFiles(project2, [config.path, f1.path]);
});
});
}
@@ -0,0 +1,45 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => {
it("works when extends is specified with a case insensitive file system", () => {
const rootPath = "/Users/username/dev/project";
const file1: File = {
path: `${rootPath}/index.ts`,
content: 'import {x} from "file2";',
};
const file2: File = {
path: `${rootPath}/file2.js`,
content: "",
};
const file2Dts: File = {
path: `${rootPath}/types/file2/index.d.ts`,
content: "export declare const x: string;",
};
const tsconfigAll: File = {
path: `${rootPath}/tsconfig.all.json`,
content: JSON.stringify({
compilerOptions: {
baseUrl: ".",
paths: { file2: ["./file2.js"] },
typeRoots: ["./types"],
forceConsistentCasingInFileNames: true,
},
}),
};
const tsconfig: File = {
path: `${rootPath}/tsconfig.json`,
content: JSON.stringify({ extends: "./tsconfig.all.json" }),
};
const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false });
const session = createSession(host);
openFilesForSession([file1], session);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
assert.deepEqual(diagnostics, []);
});
});
}
@@ -0,0 +1,39 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: format settings", () => {
it("can be set globally", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x;"
};
const host = createServerHost([f1]);
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath);
// set global settings
const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks };
projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 });
// get format options for file - should be equal to new global settings
const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings");
// set per file format options
const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter };
projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path });
// get format options for file - should be equal to new per-file settings
const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings");
// set new global settings - they should not affect ones that were set per-file
const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements };
projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 });
// get format options for file - should be equal to new per-file settings
const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings");
});
});
}
@@ -0,0 +1,12 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: getApplicableRefactors", () => {
it("works when taking position", () => {
const aTs: File = { path: "/a.ts", content: "" };
const session = createSession(createServerHost([aTs]));
openFilesForSession([aTs], session);
const response = executeSessionRequest<protocol.GetApplicableRefactorsRequest, protocol.GetApplicableRefactorsResponse>(
session, protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 });
assert.deepEqual<ReadonlyArray<protocol.ApplicableRefactorInfo> | undefined>(response, []);
});
});
}
@@ -0,0 +1,105 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: getEditsForFileRename", () => {
it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => {
const userTs: File = {
path: "/user.ts",
content: 'import { x } from "./old";',
};
const newTs: File = {
path: "/new.ts",
content: "export const x = 0;",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const host = createServerHost([userTs, newTs, tsconfig]);
const projectService = createProjectService(host);
projectService.openClientFile(userTs.path);
const project = projectService.configuredProjects.get(tsconfig.path)!;
Debug.assert(!!project.resolveModuleNames);
const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions);
assert.deepEqual<ReadonlyArray<FileTextChanges>>(edits, [{
fileName: "/user.ts",
textChanges: [{
span: textSpanFromSubstring(userTs.content, "./old"),
newText: "./new",
}],
}]);
});
it("works with multiple projects", () => {
const aUserTs: File = {
path: "/a/user.ts",
content: 'import { x } from "./old";',
};
const aOldTs: File = {
path: "/a/old.ts",
content: "export const x = 0;",
};
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }),
};
const bUserTs: File = {
path: "/b/user.ts",
content: 'import { x } from "../a/old";',
};
const bTsconfig: File = {
path: "/b/tsconfig.json",
content: "{}",
};
const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]);
const session = createSession(host);
openFilesForSession([aUserTs, bUserTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: aOldTs.path,
newFilePath: "/a/new.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: aTsconfig.path,
textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }],
},
{
fileName: aUserTs.path,
textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }],
},
{
fileName: bUserTs.path,
textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }],
},
]);
});
it("works with file moved to inferred project", () => {
const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' };
const cTs: File = { path: "/c.ts", content: "export {};" };
const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) };
const host = createServerHost([aTs, cTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs, cTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: "/b.ts",
newFilePath: cTs.path,
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: "/tsconfig.json",
textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }],
},
{
fileName: "/a.ts",
textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }],
},
]);
});
});
}
@@ -0,0 +1,650 @@
namespace ts.projectSystem {
export import TI = server.typingsInstaller;
export import protocol = server.protocol;
export import CommandNames = server.CommandNames;
export import TestServerHost = TestFSWithWatch.TestServerHost;
export type File = TestFSWithWatch.File;
export type SymLink = TestFSWithWatch.SymLink;
export type Folder = TestFSWithWatch.Folder;
export import createServerHost = TestFSWithWatch.createServerHost;
export import checkArray = TestFSWithWatch.checkArray;
export import libFile = TestFSWithWatch.libFile;
export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles;
export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed;
export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
export import commonFile1 = tscWatch.commonFile1;
export import commonFile2 = tscWatch.commonFile2;
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
export function mapOutputToJson(s: string) {
return convertToObject(
parseJsonText("json.json", s.replace(outputEventRegex, "")),
[]
);
}
export const customTypesMap = {
path: <Path>"/typesMap.json",
content: `{
"typesMap": {
"jquery": {
"match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$",
"types": ["jquery"]
},
"quack": {
"match": "/duckquack-(\\\\d+)\\\\.min\\\\.js",
"types": ["duck-types"]
}
},
"simpleMap": {
"Bacon": "baconjs",
"bliss": "blissfuljs",
"commander": "commander",
"cordova": "cordova",
"react": "react",
"lodash": "lodash"
}
}`
};
export interface PostExecAction {
readonly success: boolean;
readonly callback: TI.RequestCompletedAction;
}
export const nullLogger: server.Logger = {
close: noop,
hasLevel: () => false,
loggingEnabled: () => false,
perftrc: noop,
info: noop,
msg: noop,
startGroup: noop,
endGroup: noop,
getLogFileName: () => undefined,
};
export function createHasErrorMessageLogger() {
let hasErrorMsg = false;
const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger;
const logger: server.Logger = {
close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc,
msg: (s, type) => {
Debug.fail(`Error: ${s}, type: ${type}`);
hasErrorMsg = true;
}
};
return { logger, hasErrorMsg: () => hasErrorMsg };
}
export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller {
protected projectService!: server.ProjectService;
constructor(
readonly globalTypingsCacheLocation: string,
throttleLimit: number,
installTypingHost: server.ServerHost,
readonly typesRegistry = createMap<MapLike<string>>(),
log?: TI.Log) {
super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log);
}
protected postExecActions: PostExecAction[] = [];
isKnownTypesPackageName = notImplemented;
installPackage = notImplemented;
inspectValue = notImplemented;
executePendingCommands() {
const actionsToRun = this.postExecActions;
this.postExecActions = [];
for (const action of actionsToRun) {
action.callback(action.success);
}
}
checkPendingCommands(expectedCount: number) {
assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`);
}
onProjectClosed = noop;
attach(projectService: server.ProjectService) {
this.projectService = projectService;
}
getInstallTypingHost() {
return this.installTypingHost;
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
this.addPostExecAction("success", cb);
}
sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) {
this.projectService.updateTypingsForProject(response);
}
enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>) {
const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation);
this.install(request);
}
addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) {
const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout);
const action: PostExecAction = {
success: !!out,
callback: cb
};
this.postExecActions.push(action);
}
}
function createNpmPackageJsonString(installedTypings: string[]): string {
const dependencies: MapLike<any> = {};
for (const typing of installedTypings) {
dependencies[typing] = "1.0.0";
}
return JSON.stringify({ dependencies });
}
export function createTypesRegistry(...list: string[]): Map<MapLike<string>> {
const versionMap = {
"latest": "1.3.0",
"ts2.0": "1.0.0",
"ts2.1": "1.0.0",
"ts2.2": "1.2.0",
"ts2.3": "1.3.0",
"ts2.4": "1.3.0",
"ts2.5": "1.3.0",
"ts2.6": "1.3.0",
"ts2.7": "1.3.0"
};
const map = createMap<MapLike<string>>();
for (const l of list) {
map.set(l, versionMap);
}
return map;
}
export function toExternalFile(fileName: string): protocol.ExternalFile {
return { fileName };
}
export function toExternalFiles(fileNames: string[]) {
return map(fileNames, toExternalFile);
}
export function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats {
return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats };
}
export interface ConfigFileDiagnostic {
fileName: string | undefined;
start: number | undefined;
length: number | undefined;
messageText: string;
category: DiagnosticCategory;
code: number;
reportsUnnecessary?: {};
source?: string;
relatedInformation?: DiagnosticRelatedInformation[];
}
export class TestServerEventManager {
private events: server.ProjectServiceEvent[] = [];
readonly session: TestSession;
readonly service: server.ProjectService;
readonly host: TestServerHost;
constructor(files: File[], suppressDiagnosticEvents?: boolean) {
this.host = createServerHost(files);
this.session = createSession(this.host, {
canUseEvents: true,
eventHandler: event => this.events.push(event),
suppressDiagnosticEvents,
});
this.service = this.session.getProjectService();
}
getEvents(): ReadonlyArray<server.ProjectServiceEvent> {
const events = this.events;
this.events = [];
return events;
}
getEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"]): T["data"] {
let eventData: T["data"] | undefined;
filterMutate(this.events, e => {
if (e.eventName === eventName) {
if (eventData !== undefined) {
assert(false, "more than one event found");
}
eventData = e.data;
return false;
}
return true;
});
return Debug.assertDefined(eventData);
}
hasZeroEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"]) {
this.events.forEach(event => assert.notEqual(event.eventName, eventName));
}
checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray<ConfigFileDiagnostic>) {
const eventData = this.getEvent<server.ConfigFileDiagEvent>(server.ConfigFileDiagEvent);
assert.equal(eventData.configFileName, configFileName);
assert.equal(eventData.triggerFile, triggerFile);
const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest }));
if (errors) {
assert.deepEqual(actual, errors);
}
}
assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>, configFile = "/tsconfig.json"): void {
assert.deepEqual<server.ProjectInfoTelemetryEventData>(this.getEvent<server.ProjectInfoTelemetryEvent>(server.ProjectInfoTelemetryEvent), {
projectId: sys.createSHA256Hash!(configFile),
fileStats: fileStats({ ts: 1 }),
compilerOptions: {},
extends: false,
files: false,
include: false,
exclude: false,
compileOnSave: false,
typeAcquisition: {
enable: false,
exclude: false,
include: false,
},
configFileName: "tsconfig.json",
projectType: "configured",
languageServiceEnabled: true,
version,
...partial,
});
}
assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void {
assert.deepEqual<server.OpenFileInfoTelemetryEventData>(this.getEvent<server.OpenFileInfoTelemetryEvent>(server.OpenFileInfoTelemetryEvent), { info });
}
assertNoOpenFilesTelemetryEvent(): void {
this.hasZeroEvent<server.OpenFileInfoTelemetryEvent>(server.OpenFileInfoTelemetryEvent);
}
}
export class TestSession extends server.Session {
private seq = 0;
public events: protocol.Event[] = [];
public host!: TestServerHost;
getProjectService() {
return this.projectService;
}
public getSeq() {
return this.seq;
}
public getNextSeq() {
return this.seq + 1;
}
public executeCommandSeq<T extends server.protocol.Request>(request: Partial<T>) {
this.seq++;
request.seq = this.seq;
request.type = "request";
return this.executeCommand(<T>request);
}
public event<T extends object>(body: T, eventName: string) {
this.events.push(server.toEvent(eventName, body));
super.event(body, eventName);
}
public clearMessages() {
clear(this.events);
this.host.clearOutput();
}
}
export function createSession(host: server.ServerHost, opts: Partial<server.SessionOptions> = {}) {
if (opts.typingsInstaller === undefined) {
opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host);
}
if (opts.eventHandler !== undefined) {
opts.canUseEvents = true;
}
const sessionOptions: server.SessionOptions = {
host,
cancellationToken: server.nullCancellationToken,
useSingleInferredProject: false,
useInferredProjectPerProjectRoot: false,
typingsInstaller: undefined!, // TODO: GH#18217
byteLength: Utils.byteLength,
hrtime: process.hrtime,
logger: opts.logger || createHasErrorMessageLogger().logger,
canUseEvents: false
};
return new TestSession({ ...sessionOptions, ...opts });
}
export function createSessionWithEventTracking<T extends server.ProjectServiceEvent>(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) {
const events: T[] = [];
const session = createSession(host, {
eventHandler: e => {
if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) {
events.push(e as T);
}
}
});
return { session, events };
}
export function createSessionWithDefaultEventHandler<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<server.SessionOptions> = {}) {
const session = createSession(host, { canUseEvents: true, ...opts });
return {
session,
getEvents,
clearEvents
};
function getEvents() {
return mapDefined(host.getOutput(), s => {
const e = mapOutputToJson(s);
return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
});
}
function clearEvents() {
session.clearMessages();
}
}
export interface CreateProjectServiceParameters {
cancellationToken?: HostCancellationToken;
logger?: server.Logger;
useSingleInferredProject?: boolean;
typingsInstaller?: server.ITypingsInstaller;
eventHandler?: server.ProjectServiceEventHandler;
}
export class TestProjectService extends server.ProjectService {
constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean,
typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial<server.ProjectServiceOptions> = {}) {
super({
host,
logger,
cancellationToken,
useSingleInferredProject,
useInferredProjectPerProjectRoot: false,
typingsInstaller,
typesMapLocation: customTypesMap.path,
eventHandler,
...opts
});
}
checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) {
checkNumberOfProjects(this, count);
}
}
export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial<server.ProjectServiceOptions>) {
const cancellationToken = parameters.cancellationToken || server.nullCancellationToken;
const logger = parameters.logger || createHasErrorMessageLogger().logger;
const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false;
return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217
}
export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`);
}
export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`);
}
export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`);
}
export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) {
checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0);
checkNumberOfExternalProjects(projectService, count.externalProjects || 0);
checkNumberOfInferredProjects(projectService, count.inferredProjects || 0);
}
export function configuredProjectAt(projectService: server.ProjectService, index: number) {
const values = projectService.configuredProjects.values();
while (index > 0) {
values.next();
index--;
}
return values.next().value;
}
export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray<string>) {
checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles);
}
export function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray<string>) {
checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles);
}
export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) {
dir = normalizePath(dir);
const result: string[] = [];
forEachAncestorDirectory(dir, ancestor => {
if (mapAncestor(ancestor)) {
result.push(combinePaths(ancestor, path2));
}
});
return result;
}
export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) {
return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4);
}
export const nodeModules = "node_modules";
export function getNodeModuleDirectories(dir: string) {
return getRootsToWatchWithAncestorDirectory(dir, nodeModules);
}
export const nodeModulesAtTypes = "node_modules/@types";
export function getTypeRootsFromLocation(currentDirectory: string) {
return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes);
}
export function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) {
checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path));
}
export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray<string>) {
checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles);
}
export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location {
const start = str.indexOf(substring);
Debug.assert(start !== -1);
return protocolToLocation(str)(start);
}
function protocolToLocation(text: string): (pos: number) => protocol.Location {
const lineStarts = computeLineStarts(text);
return pos => {
const x = computeLineAndCharacterOfPosition(lineStarts, pos);
return { line: x.line + 1, offset: x.character + 1 };
};
}
export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan {
const span = textSpanFromSubstring(str, substring, options);
const toLocation = protocolToLocation(str);
return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) };
}
export function protocolRenameSpanFromSubstring(
str: string,
substring: string,
options?: SpanFromSubstringOptions,
prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string },
): protocol.RenameTextSpan {
return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText };
}
export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan {
const start = nthIndexOf(str, substring, options ? options.index : 0);
Debug.assert(start !== -1);
return createTextSpan(start, substring.length);
}
export function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs {
return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) };
}
export interface SpanFromSubstringOptions {
readonly index: number;
}
function nthIndexOf(str: string, substr: string, n: number): number {
let index = -1;
for (; n >= 0; n--) {
index = str.indexOf(substr, index + 1);
if (index === -1) return -1;
}
return index;
}
/**
* Test server cancellation token used to mock host token cancellation requests.
* The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls
* should be made before canceling the token. The id of the request to cancel should be set with
* setRequestToCancel();
*/
export class TestServerCancellationToken implements server.ServerCancellationToken {
private currentId: number | undefined = -1;
private requestToCancel = -1;
private isCancellationRequestedCount = 0;
constructor(private cancelAfterRequest = 0) {
}
setRequest(requestId: number) {
this.currentId = requestId;
}
setRequestToCancel(requestId: number) {
this.resetToken();
this.requestToCancel = requestId;
}
resetRequest(requestId: number) {
assert.equal(requestId, this.currentId, "unexpected request id in cancellation");
this.currentId = undefined;
}
isCancellationRequested() {
this.isCancellationRequestedCount++;
// If the request id is the request to cancel and isCancellationRequestedCount
// has been met then cancel the request. Ex: cancel the request if it is a
// nav bar request & isCancellationRequested() has already been called three times.
return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest;
}
resetToken() {
this.currentId = -1;
this.isCancellationRequestedCount = 0;
this.requestToCancel = -1;
}
}
export function makeSessionRequest<T>(command: string, args: T): protocol.Request {
return {
seq: 0,
type: "request",
command,
arguments: args
};
}
export function executeSessionRequest<TRequest extends protocol.Request, TResponse extends protocol.Response>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] {
return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"];
}
export function executeSessionRequestNoResponse<TRequest extends protocol.Request>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void {
session.executeCommand(makeSessionRequest(command, args));
}
export function openFilesForSession(files: ReadonlyArray<File | { readonly file: File | string, readonly projectRootPath: string }>, session: server.Session): void {
for (const file of files) {
session.executeCommand(makeSessionRequest<protocol.OpenRequestArgs>(CommandNames.Open,
"projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path }));
}
}
export function closeFilesForSession(files: ReadonlyArray<File>, session: server.Session): void {
for (const file of files) {
session.executeCommand(makeSessionRequest<protocol.FileRequestArgs>(CommandNames.Close, { file: file.path }));
}
}
export interface ErrorInformation {
diagnosticMessage: DiagnosticMessage;
errorTextArguments?: string[];
}
function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) {
return formatStringFromArgs(diagnosticMessage.message, errorTextArguments);
}
export function verifyDiagnostics(actual: ReadonlyArray<server.protocol.Diagnostic>, expected: ReadonlyArray<ErrorInformation>) {
const expectedErrors = expected.map(getProtocolDiagnosticMessage);
assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors);
}
export function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) {
verifyDiagnostics(actual, []);
}
export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void {
checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent);
}
export function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray<string> = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic {
return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined };
}
export function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void {
checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent);
}
export function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) {
checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true);
}
export function checkNoDiagnosticEvents(session: TestSession) {
for (const event of session.events) {
assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event));
}
}
export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) {
const events = session.events;
assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`);
const outputs = session.host.getOutput();
assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine));
if (isMostRecent) {
assert.strictEqual(events.length, index + 1, JSON.stringify(events));
assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs));
}
}
}
@@ -0,0 +1,18 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: import helpers", () => {
it("should not crash in tsserver", () => {
const f1 = {
path: "/a/app.ts",
content: "export async function foo() { return 100; }"
};
const tslib = {
path: "/a/node_modules/tslib/index.d.ts",
content: ""
};
const host = createServerHost([f1, tslib]);
const service = createProjectService(host);
service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } });
service.checkNumberOfProjects({ externalProjects: 1 });
});
});
}
@@ -0,0 +1,349 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Inferred projects", () => {
it("create inferred project", () => {
const appFile: File = {
path: "/a/b/c/app.ts",
content: `
import {f} from "./module"
console.log(f)
`
};
const moduleFile: File = {
path: "/a/b/c/module.d.ts",
content: `export let x: number`
};
const host = createServerHost([appFile, moduleFile, libFile]);
const projectService = createProjectService(host);
const { configFileName } = projectService.openClientFile(appFile.path);
assert(!configFileName, `should not find config, got: '${configFileName}`);
checkNumberOfConfiguredProjects(projectService, 0);
checkNumberOfInferredProjects(projectService, 1);
const project = projectService.inferredProjects[0];
checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]);
const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"];
const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]);
checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path));
checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false);
checkWatchedDirectories(host, [combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true);
});
it("should use only one inferred project if 'useOneInferredProject' is set", () => {
const file1 = {
path: "/a/b/main.ts",
content: "let x =1;"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6"
},
"files": [ "main.ts" ]
}`
};
const file2 = {
path: "/a/c/main.ts",
content: "let x =1;"
};
const file3 = {
path: "/a/d/main.ts",
content: "let x =1;"
};
const host = createServerHost([file1, file2, file3, libFile]);
const projectService = createProjectService(host, { useSingleInferredProject: true });
projectService.openClientFile(file1.path);
projectService.openClientFile(file2.path);
projectService.openClientFile(file3.path);
checkNumberOfConfiguredProjects(projectService, 0);
checkNumberOfInferredProjects(projectService, 1);
checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]);
host.reloadFS([file1, configFile, file2, file3, libFile]);
host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles
checkNumberOfConfiguredProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 1);
checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]);
});
it("disable inferred project", () => {
const file1 = {
path: "/a/b/f1.ts",
content: "let x =1;"
};
const host = createServerHost([file1]);
const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true });
projectService.openClientFile(file1.path, file1.content);
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const proj = projectService.inferredProjects[0];
assert.isDefined(proj);
assert.isFalse(proj.languageServiceEnabled);
});
it("project settings for inferred projects", () => {
const file1 = {
path: "/a/b/app.ts",
content: `import {x} from "mod"`
};
const modFile = {
path: "/a/mod.ts",
content: "export let x: number"
};
const host = createServerHost([file1, modFile]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
projectService.openClientFile(modFile.path);
checkNumberOfProjects(projectService, { inferredProjects: 2 });
const inferredProjects = projectService.inferredProjects.slice();
checkProjectActualFiles(inferredProjects[0], [file1.path]);
checkProjectActualFiles(inferredProjects[1], [modFile.path]);
projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic });
host.checkTimeoutQueueLengthAndRun(3);
checkNumberOfProjects(projectService, { inferredProjects: 2 });
assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]);
assert.isTrue(inferredProjects[1].isOrphan());
});
it("should support files without extensions", () => {
const f = {
path: "/a/compile",
content: "let x = 1"
};
const host = createServerHost([f]);
const session = createSession(host);
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 1,
type: "request",
command: "compilerOptionsForInferredProjects",
arguments: {
options: {
allowJs: true
}
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 2,
type: "request",
command: "open",
arguments: {
file: f.path,
fileContent: f.content,
scriptKindName: "JS"
}
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
checkProjectActualFiles(projectService.inferredProjects[0], [f.path]);
});
it("inferred projects per project root", () => {
const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" };
const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" };
const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" };
const file4 = { path: "/c/file3.ts", content: "let z = 4;" };
const host = createServerHost([file1, file2, file3, file4]);
const session = createSession(host, {
useSingleInferredProject: true,
useInferredProjectPerProjectRoot: true
});
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 1,
type: "request",
command: CommandNames.CompilerOptionsForInferredProjects,
arguments: {
options: {
allowJs: true,
target: ScriptTarget.ESNext
}
}
});
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 2,
type: "request",
command: CommandNames.CompilerOptionsForInferredProjects,
arguments: {
options: {
allowJs: true,
target: ScriptTarget.ES2015
},
projectRootPath: "/b"
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 3,
type: "request",
command: CommandNames.Open,
arguments: {
file: file1.path,
fileContent: file1.content,
scriptKindName: "JS",
projectRootPath: file1.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 4,
type: "request",
command: CommandNames.Open,
arguments: {
file: file2.path,
fileContent: file2.content,
scriptKindName: "JS",
projectRootPath: file2.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 5,
type: "request",
command: CommandNames.Open,
arguments: {
file: file3.path,
fileContent: file3.content,
scriptKindName: "JS",
projectRootPath: file3.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 6,
type: "request",
command: CommandNames.Open,
arguments: {
file: file4.path,
fileContent: file4.content,
scriptKindName: "JS"
}
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 3 });
checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]);
checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]);
checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]);
assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext);
assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext);
assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015);
});
function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) {
checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path));
assert.equal(inferredProject.getCompilationSettings().target, target);
}
function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) {
const files: [File, File, File, File] = [
{ path: "/a/file1.ts", content: "let x = 1;" },
{ path: "/A/file2.ts", content: "let y = 2;" },
{ path: "/b/file2.ts", content: "let x = 3;" },
{ path: "/c/file3.ts", content: "let z = 4;" }
];
const host = createServerHost(files, { useCaseSensitiveFileNames });
const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true });
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ESNext
});
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ES2015
}, "/a");
openClientFiles(["/a", "/a", "/b", undefined]);
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2015],
[[files[2]], ScriptTarget.ESNext]
]);
closeClientFiles();
openClientFiles(["/a", "/A", "/b", undefined]);
if (useCaseSensitiveFileNames) {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0]], ScriptTarget.ES2015],
[[files[1]], ScriptTarget.ESNext],
[[files[2]], ScriptTarget.ESNext]
]);
}
else {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2015],
[[files[2]], ScriptTarget.ESNext]
]);
}
closeClientFiles();
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ES2017
}, "/A");
openClientFiles(["/a", "/a", "/b", undefined]);
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
closeClientFiles();
openClientFiles(["/a", "/A", "/b", undefined]);
if (useCaseSensitiveFileNames) {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0]], ScriptTarget.ES2015],
[[files[1]], ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
}
else {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
}
closeClientFiles();
function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) {
files.forEach((file, index) => {
projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]);
});
}
function closeClientFiles() {
files.forEach(file => projectService.closeClientFile(file.path));
}
function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) {
checkNumberOfProjects(projectService, { inferredProjects: expected.length });
projectService.inferredProjects.forEach((p, index) => {
const [actualFiles, target] = expected[index];
checkInferredProject(p, actualFiles, target);
});
}
}
it("inferred projects per project root with case sensitive system", () => {
verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true);
});
it("inferred projects per project root with case insensitive system", () => {
verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false);
});
});
}
@@ -0,0 +1,19 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Language service", () => {
it("should work correctly on case-sensitive file systems", () => {
const lib = {
path: "/a/Lib/lib.d.ts",
content: "let x: number"
};
const f = {
path: "/a/b/app.ts",
content: "let x = 1;"
};
const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true });
const projectService = createProjectService(host);
projectService.openClientFile(f.path);
projectService.checkNumberOfProjects({ inferredProjects: 1 });
projectService.inferredProjects[0].getLanguageService().getProgram();
});
});
}
@@ -0,0 +1,55 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => {
it("should be set to 2 if the project has js root files", () => {
const file1: File = {
path: "/a/b/file1.js",
content: `var t = require("test"); t.`
};
const moduleFile: File = {
path: "/a/b/node_modules/test/index.js",
content: `var v = 10; module.exports = v;`
};
const host = createServerHost([file1, moduleFile]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
let project = projectService.inferredProjects[0];
let options = project.getCompilationSettings();
assert.isTrue(options.maxNodeModuleJsDepth === 2);
// Assert the option sticks
projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 });
project = projectService.inferredProjects[0];
options = project.getCompilationSettings();
assert.isTrue(options.maxNodeModuleJsDepth === 2);
});
it("should return to normal state when all js root files are removed from project", () => {
const file1 = {
path: "/a/file1.ts",
content: "let x =1;"
};
const file2 = {
path: "/a/file2.js",
content: "let x =1;"
};
const host = createServerHost([file1, file2, libFile]);
const projectService = createProjectService(host, { useSingleInferredProject: true });
projectService.openClientFile(file1.path);
checkNumberOfInferredProjects(projectService, 1);
let project = projectService.inferredProjects[0];
assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth);
projectService.openClientFile(file2.path);
project = projectService.inferredProjects[0];
assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2);
projectService.closeClientFile(file2.path);
project = projectService.inferredProjects[0];
assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth);
});
});
}
@@ -0,0 +1,99 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: with metadata in response", () => {
const metadata = "Extra Info";
function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) {
const output = host.getOutput().map(mapOutputToJson);
assert.deepEqual(output, [expectedResponse]);
host.clearOutput();
}
function verifyCommandWithMetadata<T extends server.protocol.Request, U = undefined>(session: TestSession, host: TestServerHost, command: Partial<T>, expectedResponseBody: U) {
command.seq = session.getSeq();
command.type = "request";
session.onMessage(JSON.stringify(command));
verifyOutput(host, expectedResponseBody ?
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } :
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." }
);
}
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` };
const tsconfig: File = {
path: "/tsconfig.json",
content: JSON.stringify({
compilerOptions: { plugins: [{ name: "myplugin" }] }
})
};
function createHostWithPlugin(files: ReadonlyArray<File>) {
const host = createServerHost(files);
host.require = (_initialPath, moduleName) => {
assert.equal(moduleName, "myplugin");
return {
module: () => ({
create(info: server.PluginCreateInfo) {
const proxy = Harness.LanguageService.makeDefaultProxy(info);
proxy.getCompletionsAtPosition = (filename, position, options) => {
const result = info.languageService.getCompletionsAtPosition(filename, position, options);
if (result) {
result.metadata = metadata;
}
return result;
};
return proxy;
}
}),
error: undefined
};
};
return host;
}
describe("With completion requests", () => {
const completionRequestArgs: protocol.CompletionsRequestArgs = {
file: aTs.path,
line: 1,
offset: aTs.content.indexOf("this.") + 1 + "this.".length
};
const expectedCompletionEntries: ReadonlyArray<protocol.CompletionEntry> = [
{ name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" },
{ name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" }
];
it("can pass through metadata when the command returns array", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, ReadonlyArray<protocol.CompletionEntry>>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: completionRequestArgs
}, expectedCompletionEntries);
});
it("can pass through metadata when the command returns object", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, protocol.CompletionInfo>(session, host, {
command: protocol.CommandTypes.CompletionInfo,
arguments: completionRequestArgs
}, {
isGlobalCompletion: false,
isMemberCompletion: true,
isNewIdentifierLocation: false,
entries: expectedCompletionEntries
});
});
it("returns undefined correctly", () => {
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` };
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 }
}, /*expectedResponseBody*/ undefined);
});
});
});
}
@@ -0,0 +1,30 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: navigate-to for javascript project", () => {
function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) {
return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined;
}
it("should not include type symbols", () => {
const file1: File = {
path: "/a/b/file1.js",
content: "function foo() {}"
};
const configFile: File = {
path: "/a/b/jsconfig.json",
content: "{}"
};
const host = createServerHost([file1, configFile, libFile]);
const session = createSession(host);
openFilesForSession([file1], session);
// Try to find some interface type defined in lib.d.ts
const libTypeNavToRequest = makeSessionRequest<protocol.NavtoRequestArgs>(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path });
const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[];
assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`);
const localFunctionNavToRequst = makeSessionRequest<protocol.NavtoRequestArgs>(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path });
const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[];
assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`);
});
});
}
@@ -0,0 +1,47 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: occurence highlight on string", () => {
it("should be marked if only on string values", () => {
const file1: File = {
path: "/a/b/file1.ts",
content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];`
};
const host = createServerHost([file1]);
const session = createSession(host);
const projectService = session.getProjectService();
projectService.openClientFile(file1.path);
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 1, offset: 11 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
const firstOccurence = highlightResponse[0];
assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString");
}
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 3, offset: 13 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
assert.isTrue(highlightResponse.length === 2);
const firstOccurence = highlightResponse[0];
assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name");
}
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 4, offset: 14 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
assert.isTrue(highlightResponse.length === 2);
const firstOccurence = highlightResponse[0];
assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer");
}
});
});
}
@@ -0,0 +1,108 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Open-file", () => {
it("can be reloaded with empty content", () => {
const f = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const projectFileName = "externalProject";
const host = createServerHost([f]);
const projectService = createProjectService(host);
// create a project
projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} });
projectService.checkNumberOfProjects({ externalProjects: 1 });
const p = projectService.externalProjects[0];
// force to load the content of the file
p.updateGraph();
const scriptInfo = p.getScriptInfo(f.path)!;
checkSnapLength(scriptInfo.getSnapshot(), f.content.length);
// open project and replace its content with empty string
projectService.openClientFile(f.path, "");
checkSnapLength(scriptInfo.getSnapshot(), 0);
});
function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) {
assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size");
}
function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) {
const file1: File = {
path: "/a/b/src/app.ts",
content: "let x = 10;"
};
const file2: File = {
path: "/a/B/lib/module2.ts",
content: "let z = 10;"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: ""
};
const configFile2: File = {
path: "/a/tsconfig.json",
content: ""
};
const host = createServerHost([file1, file2, configFile, configFile2], {
useCaseSensitiveFileNames
});
const service = createProjectService(host);
// Open file1 -> configFile
verifyConfigFileName(file1, "/a", configFile);
verifyConfigFileName(file1, "/a/b", configFile);
verifyConfigFileName(file1, "/a/B", configFile);
// Open file2 use root "/a/b"
verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile);
verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile);
verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile);
function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) {
const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot);
assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path);
service.closeClientFile(file.path);
}
}
it("works when project root is used with case-sensitive system", () => {
verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true);
});
it("works when project root is used with case-insensitive system", () => {
verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false);
});
it("uses existing project even if project refresh is pending", () => {
const projectFolder = "/user/someuser/projects/myproject";
const aFile: File = {
path: `${projectFolder}/src/a.ts`,
content: "export const x = 0;"
};
const configFile: File = {
path: `${projectFolder}/tsconfig.json`,
content: "{}"
};
const files = [aFile, configFile, libFile];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder);
verifyProject();
const bFile: File = {
path: `${projectFolder}/src/b.ts`,
content: `export {}; declare module "./a" { export const y: number; }`
};
files.push(bFile);
host.reloadFS(files);
service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder);
verifyProject();
function verifyProject() {
assert.isDefined(service.configuredProjects.get(configFile.path));
const project = service.configuredProjects.get(configFile.path)!;
checkProjectActualFiles(project, files.map(f => f.path));
}
});
});
}
@@ -0,0 +1,849 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Project Errors", () => {
function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray<string>): void {
assert.isTrue(projectFiles !== undefined, "missing project files");
checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors);
}
function checkProjectErrorsWorker(errors: ReadonlyArray<Diagnostic>, expectedErrors: ReadonlyArray<string>): void {
assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
if (expectedErrors.length) {
for (let i = 0; i < errors.length; i++) {
const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n");
const expectedMessage = expectedErrors[i];
assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
}
}
}
function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) {
assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
if (expectedErrors.length) {
zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => {
assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
});
}
}
it("external project - diagnostics for missing files", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/applib.ts",
content: ""
};
const host = createServerHost([file1, libFile]);
const session = createSession(host);
const projectService = session.getProjectService();
const projectFileName = "/a/b/test.csproj";
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName }
};
{
projectService.openExternalProject({
projectFileName,
options: {},
rootFiles: toExternalFiles([file1.path, file2.path])
});
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
// only file1 exists - expect error
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
}
host.reloadFS([file2, libFile]);
{
// only file2 exists - expect error
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]);
}
host.reloadFS([file1, file2, libFile]);
{
// both files exist - expect no errors
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, []);
}
});
it("configured projects - diagnostics for missing files", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/applib.ts",
content: ""
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const host = createServerHost([file1, config, libFile]);
const session = createSession(host);
const projectService = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = configuredProjectAt(projectService, 0);
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName: project.getProjectName() }
};
let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
host.reloadFS([file1, file2, config, libFile]);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
checkDiagnosticsWithLinePos(diags, []);
});
it("configured projects - diagnostics for corrupted config 1", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/lib.ts",
content: ""
};
const correctConfig = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const corruptedConfig = {
path: correctConfig.path,
content: correctConfig.content.substr(1)
};
const host = createServerHost([file1, file2, corruptedConfig]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, [
"'{' expected."
]);
assert.isNotNull(projectErrors[0].file);
assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path);
}
// fix config and trigger watcher
host.reloadFS([file1, file2, correctConfig]);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, []);
}
});
it("configured projects - diagnostics for corrupted config 2", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const file2 = {
path: "/a/b/lib.ts",
content: ""
};
const correctConfig = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
};
const corruptedConfig = {
path: correctConfig.path,
content: correctConfig.content.substr(1)
};
const host = createServerHost([file1, file2, correctConfig]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, []);
}
// break config and trigger watcher
host.reloadFS([file1, file2, corruptedConfig]);
{
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!;
assert.isTrue(configuredProject !== undefined, "should find configured project");
checkProjectErrors(configuredProject, []);
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
checkProjectErrorsWorker(projectErrors, [
"'{' expected."
]);
assert.isNotNull(projectErrors[0].file);
assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path);
}
});
});
describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => {
function createErrorLogger() {
let hasError = false;
const errorLogger: server.Logger = {
close: noop,
hasLevel: () => true,
loggingEnabled: () => true,
perftrc: noop,
info: noop,
msg: (_s, type) => {
if (type === server.Msg.Err) {
hasError = true;
}
},
startGroup: noop,
endGroup: noop,
getLogFileName: () => undefined
};
return {
errorLogger,
hasError: () => hasError
};
}
it("document is not contained in project", () => {
const file1 = {
path: "/a/b/app.ts",
content: ""
};
const corruptedConfig = {
path: "/a/b/tsconfig.json",
content: "{"
};
const host = createServerHost([file1, corruptedConfig]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
const project = projectService.findProject(corruptedConfig.path)!;
checkProjectRootFiles(project, [file1.path]);
});
describe("when opening new file that doesnt exist on disk yet", () => {
function verifyNonExistentFile(useProjectRoot: boolean) {
const folderPath = "/user/someuser/projects/someFolder";
const fileInRoot: File = {
path: `/src/somefile.d.ts`,
content: "class c { }"
};
const fileInProjectRoot: File = {
path: `${folderPath}/src/somefile.d.ts`,
content: "class c { }"
};
const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]);
const { hasError, errorLogger } = createErrorLogger();
const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true });
const projectService = session.getProjectService();
const untitledFile = "untitled:Untitled-1";
const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts";
const refPathNotFound2 = "./src/somefile.d.ts";
const fileContent = `/// <reference path="${refPathNotFound1}" />
/// <reference path="${refPathNotFound2}" />`;
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: {
file: untitledFile,
fileContent,
scriptKindName: "TS",
projectRootPath: useProjectRoot ? folderPath : undefined
}
});
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path);
const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path);
const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path);
const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path);
if (useProjectRoot) {
assert.isDefined(infoForUntitledAtProjectRoot);
assert.isUndefined(infoForUnitiledAtRoot);
}
else {
assert.isDefined(infoForUnitiledAtRoot);
assert.isUndefined(infoForUntitledAtProjectRoot);
}
assert.isUndefined(infoForSomefileAtRoot);
assert.isUndefined(infoForSomefileAtProjectRoot);
// Since this is not js project so no typings are queued
host.checkTimeoutQueueLength(0);
const newTimeoutId = host.getNextTimeoutId();
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [untitledFile]
}
});
host.checkTimeoutQueueLength(1);
// Run the last one = get error request
host.runQueuedTimeoutCallbacks(newTimeoutId);
assert.isFalse(hasError());
host.checkTimeoutQueueLength(0);
checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks();
assert.isFalse(hasError());
const errorOffset = fileContent.indexOf(refPathNotFound1) + 1;
checkErrorMessage(session, "semanticDiag", {
file: untitledFile,
diagnostics: [
createDiagnostic({ line: 1, offset: errorOffset }, { line: 1, offset: errorOffset + refPathNotFound1.length }, Diagnostics.File_0_not_found, [refPathNotFound1], "error"),
createDiagnostic({ line: 2, offset: errorOffset }, { line: 2, offset: errorOffset + refPathNotFound2.length }, Diagnostics.File_0_not_found, [refPathNotFound2.substr(2)], "error")
]
});
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
assert.isFalse(hasError());
checkErrorMessage(session, "suggestionDiag", { file: untitledFile, diagnostics: [] });
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
}
it("has projectRoot", () => {
verifyNonExistentFile(/*useProjectRoot*/ true);
});
it("does not have projectRoot", () => {
verifyNonExistentFile(/*useProjectRoot*/ false);
});
});
it("folder rename updates project structure and reports no errors", () => {
const projectDir = "/a/b/projects/myproject";
const app: File = {
path: `${projectDir}/bar/app.ts`,
content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }"
};
const foo: File = {
path: `${projectDir}/foo/foo.ts`,
content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }"
};
const configFile: File = {
path: `${projectDir}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] })
};
const host = createServerHost([app, foo, configFile]);
const session = createSession(host, { canUseEvents: true, });
const projectService = session.getProjectService();
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: { file: app.path, }
});
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.isDefined(projectService.configuredProjects.get(configFile.path));
verifyErrorsInApp();
host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`);
host.runQueuedTimeoutCallbacks();
host.runQueuedTimeoutCallbacks();
verifyErrorsInApp();
function verifyErrorsInApp() {
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [app.path]
}
});
host.checkTimeoutQueueLengthAndRun(1);
checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks();
checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "suggestionDiag", { file: app.path, diagnostics: [] });
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
}
});
it("Getting errors before opening file", () => {
const file: File = {
path: "/a/b/project/file.ts",
content: "let x: number = false;"
};
const host = createServerHost([file, libFile]);
const { hasError, errorLogger } = createErrorLogger();
const session = createSession(host, { canUseEvents: true, logger: errorLogger });
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [file.path]
}
});
host.runQueuedImmediateCallbacks();
assert.isFalse(hasError());
checkCompleteEvent(session, 1, expectedSequenceId);
session.clearMessages();
});
it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => {
const projectRoot = "/user/username/projects/myproject";
const app: File = {
path: `${projectRoot}/src/client/app.js`,
content: ""
};
const serverUtilities: File = {
path: `${projectRoot}/src/server/utilities.js`,
content: `function getHostName() { return "hello"; } export { getHostName };`
};
const backendTest: File = {
path: `${projectRoot}/test/backend/index.js`,
content: `import { getHostName } from '../../src/server/utilities';export default getHostName;`
};
const files = [libFile, app, serverUtilities, backendTest];
const host = createServerHost(files);
const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true });
openFilesForSession([{ file: app, projectRootPath: projectRoot }], session);
const service = session.getProjectService();
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, app.path]);
openFilesForSession([{ file: backendTest, projectRootPath: projectRoot }], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, files.map(f => f.path));
checkErrors([backendTest.path, app.path]);
closeFilesForSession([backendTest], session);
openFilesForSession([{ file: serverUtilities.path, projectRootPath: projectRoot }], session);
checkErrors([serverUtilities.path, app.path]);
function checkErrors(openFiles: [string, string]) {
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: protocol.CommandTypes.Geterr,
arguments: {
delay: 0,
files: openFiles
}
});
for (const openFile of openFiles) {
session.clearMessages();
host.checkTimeoutQueueLength(3);
host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1);
checkErrorMessage(session, "syntaxDiag", { file: openFile, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks();
checkErrorMessage(session, "semanticDiag", { file: openFile, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "suggestionDiag", { file: openFile, diagnostics: [] });
}
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
}
});
});
describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => {
function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic {
const d = Diagnostics.Unknown_compiler_option_0;
const start = configFile.content.indexOf(prop) - 1; // start at "prop"
return {
fileName: configFile.path,
start,
length: prop.length + 2,
messageText: formatStringFromArgs(d.message, [prop]),
category: d.category,
code: d.code,
reportsUnnecessary: undefined
};
}
function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic {
const findString = `{"path":"./${relativeFileName}"}`;
const d = Diagnostics.File_0_not_found;
const start = configFile.content.indexOf(findString);
return {
fileName: configFile.path,
start,
length: findString.length,
messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]),
category: d.category,
code: d.code,
reportsUnnecessary: undefined
};
}
it("are generated when the config file has errors", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"foo": "bar",
"allowJS": true
}
}`
};
const serverEventManager = new TestServerEventManager([file, libFile, configFile]);
openFilesForSession([file], serverEventManager.session);
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [
getUnknownCompilerOptionDiagnostic(configFile, "foo"),
getUnknownCompilerOptionDiagnostic(configFile, "allowJS")
]);
});
it("are generated when the config file doesn't have errors", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {}
}`
};
const serverEventManager = new TestServerEventManager([file, libFile, configFile]);
openFilesForSession([file], serverEventManager.session);
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray);
});
it("are generated when the config file changes", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const configFile = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {}
}`
};
const files = [file, libFile, configFile];
const serverEventManager = new TestServerEventManager(files);
openFilesForSession([file], serverEventManager.session);
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray);
configFile.content = `{
"compilerOptions": {
"haha": 123
}
}`;
serverEventManager.host.reloadFS(files);
serverEventManager.host.runQueuedTimeoutCallbacks();
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [
getUnknownCompilerOptionDiagnostic(configFile, "haha")
]);
configFile.content = `{
"compilerOptions": {}
}`;
serverEventManager.host.reloadFS(files);
serverEventManager.host.runQueuedTimeoutCallbacks();
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray);
});
it("are not generated when the config file does not include file opened and config file has errors", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const file2: File = {
path: "/a/b/test.ts",
content: "let x = 10"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"foo": "bar",
"allowJS": true
},
"files": ["app.ts"]
}`
};
const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]);
openFilesForSession([file2], serverEventManager.session);
serverEventManager.hasZeroEvent("configFileDiag");
});
it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"foo": "bar",
"allowJS": true
}
}`
};
const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true);
openFilesForSession([file], serverEventManager.session);
serverEventManager.hasZeroEvent("configFileDiag");
});
it("are not generated when the config file does not include file opened and doesnt contain any errors", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const file2: File = {
path: "/a/b/test.ts",
content: "let x = 10"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"files": ["app.ts"]
}`
};
const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]);
openFilesForSession([file2], serverEventManager.session);
serverEventManager.hasZeroEvent("configFileDiag");
});
it("contains the project reference errors", () => {
const file: File = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const noSuchTsconfig = "no-such-tsconfig.json";
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"files": ["app.ts"],
"references": [{"path":"./${noSuchTsconfig}"}]
}`
};
const serverEventManager = new TestServerEventManager([file, libFile, configFile]);
openFilesForSession([file], serverEventManager.session);
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [
getFileNotFoundDiagnostic(configFile, noSuchTsconfig)
]);
});
});
describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => {
it("for inferred project", () => {
const f1 = {
path: "/a/b/f1.js",
content: "function test1() { }"
};
const host = createServerHost([f1, libFile]);
const session = createSession(host);
openFilesForSession([f1], session);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const projectName = projectService.inferredProjects[0].getProjectName();
const diags = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName: projectName }
}).response as ReadonlyArray<protocol.DiagnosticWithLinePosition>;
assert.isTrue(diags.length === 0);
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
type: "request",
command: server.CommandNames.CompilerOptionsForInferredProjects,
seq: 3,
arguments: { options: { module: ModuleKind.CommonJS } }
});
const diagsAfterUpdate = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 4,
arguments: { projectFileName: projectName }
}).response as ReadonlyArray<protocol.DiagnosticWithLinePosition>;
assert.isTrue(diagsAfterUpdate.length === 0);
});
it("for external project", () => {
const f1 = {
path: "/a/b/f1.js",
content: "function test1() { }"
};
const host = createServerHost([f1, libFile]);
const session = createSession(host);
const projectService = session.getProjectService();
const projectFileName = "/a/b/project.csproj";
const externalFiles = toExternalFiles([f1.path]);
projectService.openExternalProject(<protocol.ExternalProject>{
projectFileName,
rootFiles: externalFiles,
options: {}
});
checkNumberOfProjects(projectService, { externalProjects: 1 });
const diags = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 2,
arguments: { projectFileName }
}).response as ReadonlyArray<server.protocol.DiagnosticWithLinePosition>;
assert.isTrue(diags.length === 0);
session.executeCommand(<server.protocol.OpenExternalProjectRequest>{
type: "request",
command: server.CommandNames.OpenExternalProject,
seq: 3,
arguments: {
projectFileName,
rootFiles: externalFiles,
options: { module: ModuleKind.CommonJS }
}
});
const diagsAfterUpdate = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{
type: "request",
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
seq: 4,
arguments: { projectFileName }
}).response as ReadonlyArray<server.protocol.DiagnosticWithLinePosition>;
assert.isTrue(diagsAfterUpdate.length === 0);
});
});
describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => {
it("when options change", () => {
const file = {
path: "/a/b/app.ts",
content: "let x = 10"
};
const configFileContentBeforeComment = `{`;
const configFileContentComment = `
// comment`;
const configFileContentAfterComment = `
"compilerOptions": {
"allowJs": true,
"declaration": true
}
}`;
const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment;
const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment;
const configFile = {
path: "/a/b/tsconfig.json",
content: configFileContentWithComment
};
const host = createServerHost([file, libFile, configFile]);
const session = createSession(host);
openFilesForSession([file], session);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const projectName = configuredProjectAt(projectService, 0).getProjectName();
const diags = session.executeCommand(<server.protocol.SemanticDiagnosticsSyncRequest>{
type: "request",
command: server.CommandNames.SemanticDiagnosticsSync,
seq: 2,
arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true }
}).response as ReadonlyArray<server.protocol.DiagnosticWithLinePosition>;
assert.isTrue(diags.length === 2);
configFile.content = configFileContentWithoutCommentLine;
host.reloadFS([file, configFile]);
const diagsAfterEdit = session.executeCommand(<server.protocol.SemanticDiagnosticsSyncRequest>{
type: "request",
command: server.CommandNames.SemanticDiagnosticsSync,
seq: 2,
arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true }
}).response as ReadonlyArray<server.protocol.DiagnosticWithLinePosition>;
assert.isTrue(diagsAfterEdit.length === 2);
verifyDiagnostic(diags[0], diagsAfterEdit[0]);
verifyDiagnostic(diags[1], diagsAfterEdit[1]);
function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) {
assert.equal(beforeEditDiag.message, afterEditDiag.message);
assert.equal(beforeEditDiag.code, afterEditDiag.code);
assert.equal(beforeEditDiag.category, afterEditDiag.category);
assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1);
assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset);
assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1);
assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset);
}
});
});
describe("unittests:: tsserver:: Project Errors with config file change", () => {
it("Updates diagnostics when '--noUnusedLabels' changes", () => {
const aTs: File = { path: "/a.ts", content: "label: while (1) {}" };
const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`;
const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) };
const host = createServerHost([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false));
host.runQueuedTimeoutCallbacks();
const response = executeSessionRequest<protocol.SemanticDiagnosticsSyncRequest, protocol.SemanticDiagnosticsSyncResponse>(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined;
assert.deepEqual<protocol.Diagnostic[] | undefined>(response, [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 + "label".length },
text: "Unused label.",
category: "error",
code: Diagnostics.Unused_label.code,
relatedInformation: undefined,
reportsUnnecessary: true,
source: undefined,
},
]);
});
});
}
@@ -0,0 +1,658 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: with project references and tsbuild", () => {
function createHost(files: ReadonlyArray<File>, rootNames: ReadonlyArray<string>) {
const host = createServerHost(files);
// ts build should succeed
const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {});
solutionBuilder.buildAllProjects();
assert.equal(host.getOutput().length, 0);
return host;
}
describe("with container project", () => {
function getProjectFiles(project: string): [File, File] {
return [
TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"),
TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"),
];
}
const project = "container";
const containerLib = getProjectFiles("container/lib");
const containerExec = getProjectFiles("container/exec");
const containerCompositeExec = getProjectFiles("container/compositeExec");
const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json");
const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig];
it("does not error on container only project", () => {
const host = createHost(files, [containerConfig.path]);
// Open external project for the folder
const session = createSession(host);
const service = session.getProjectService();
service.openExternalProjects([{
projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project),
rootFiles: files.map(f => ({ fileName: f.path })),
options: {}
}]);
checkNumberOfProjects(service, { configuredProjects: 4 });
files.forEach(f => {
const args: protocol.FileRequestArgs = {
file: f.path,
projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined
};
const syntaxDiagnostics = session.executeCommandSeq<protocol.SyntacticDiagnosticsSyncRequest>({
command: protocol.CommandTypes.SyntacticDiagnosticsSync,
arguments: args
}).response;
assert.deepEqual(syntaxDiagnostics, []);
const semanticDiagnostics = session.executeCommandSeq<protocol.SemanticDiagnosticsSyncRequest>({
command: protocol.CommandTypes.SemanticDiagnosticsSync,
arguments: args
}).response;
assert.deepEqual(semanticDiagnostics, []);
});
const containerProject = service.configuredProjects.get(containerConfig.path)!;
checkProjectActualFiles(containerProject, [containerConfig.path]);
const optionsDiagnostics = session.executeCommandSeq<protocol.CompilerOptionsDiagnosticsRequest>({
command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull,
arguments: { projectFileName: containerProject.projectName }
}).response;
assert.deepEqual(optionsDiagnostics, []);
});
it("can successfully find references with --out options", () => {
const host = createHost(files, [containerConfig.path]);
const session = createSession(host);
openFilesForSession([containerCompositeExec[1]], session);
const service = session.getProjectService();
checkNumberOfProjects(service, { configuredProjects: 1 });
const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst");
const response = session.executeCommandSeq<protocol.RenameRequest>({
command: protocol.CommandTypes.Rename,
arguments: {
file: containerCompositeExec[1].path,
...locationOfMyConst
}
}).response as protocol.RenameResponseBody;
const myConstLen = "myConst".length;
const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst");
assert.deepEqual(response.locs, [
{ file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] },
{ file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] }
]);
});
});
describe("with main and depedency project", () => {
const projectLocation = "/user/username/projects/myproject";
const dependecyLocation = `${projectLocation}/dependency`;
const mainLocation = `${projectLocation}/main`;
const dependencyTs: File = {
path: `${dependecyLocation}/FnS.ts`,
content: `export function fn1() { }
export function fn2() { }
export function fn3() { }
export function fn4() { }
export function fn5() { }
`
};
const dependencyConfig: File = {
path: `${dependecyLocation}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } })
};
const mainTs: File = {
path: `${mainLocation}/main.ts`,
content: `import {
fn1,
fn2,
fn3,
fn4,
fn5
} from '../dependency/fns'
fn1();
fn2();
fn3();
fn4();
fn5();
`
};
const mainConfig: File = {
path: `${mainLocation}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: { composite: true, declarationMap: true },
references: [{ path: "../dependency" }]
})
};
const randomFile: File = {
path: `${projectLocation}/random/random.ts`,
content: "let a = 10;"
};
const randomConfig: File = {
path: `${projectLocation}/random/tsconfig.json`,
content: "{}"
};
const dtsLocation = `${dependecyLocation}/FnS.d.ts`;
const dtsPath = dtsLocation.toLowerCase() as Path;
const dtsMapLocation = `${dtsLocation}.map`;
const dtsMapPath = dtsMapLocation.toLowerCase() as Path;
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos));
checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()));
}
function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path));
}
function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) {
verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]);
}
// Returns request and expected Response, expected response when no map file
interface SessionAction<Req = protocol.Request, Response = {}> {
reqName: string;
request: Partial<Req>;
expectedResponse: Response;
expectedResponseNoMap?: Response;
expectedResponseNoDts?: Response;
}
function gotoDefintinionFromMainTs(fn: number): SessionAction<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionInfoAndBoundSpan> {
const textSpan = usageSpan(fn);
const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) };
const declareSpaceLength = "declare ".length;
return {
reqName: "goToDef",
request: {
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: { file: mainTs.path, ...textSpan.start }
},
expectedResponse: {
// To dependency
definitions: [definition],
textSpan
},
expectedResponseNoMap: {
// To the dts
definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }],
textSpan
},
expectedResponseNoDts: {
// To import declaration
definitions: [{ file: mainTs.path, ...importSpan(fn) }],
textSpan
}
};
}
function definitionSpan(fn: number): protocol.TextSpan {
return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } };
}
function importSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } };
}
function usageSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } };
}
function renameFromDependencyTs(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const triggerSpan = definitionSpan(fn);
return {
reqName: "rename",
request: {
command: protocol.CommandTypes.Rename,
arguments: { file: dependencyTs.path, ...triggerSpan.start }
},
expectedResponse: {
info: {
canRename: true,
fileToRename: undefined,
displayName: `fn${fn}`,
fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
triggerSpan
},
locs: [
{ file: dependencyTs.path, locs: [triggerSpan] }
]
}
};
}
function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const { reqName, request, expectedResponse } = renameFromDependencyTs(fn);
const { info, locs } = expectedResponse;
return {
reqName,
request,
expectedResponse: {
info,
locs: [
locs[0],
{
file: mainTs.path,
locs: [
importSpan(fn),
usageSpan(fn)
]
}
]
},
// Only dependency result
expectedResponseNoMap: expectedResponse,
expectedResponseNoDts: expectedResponse
};
}
// Returns request and expected Response
type SessionActionGetter<Req = protocol.Request, Response = {}> = (fn: number) => SessionAction<Req, Response>;
// Open File, expectedProjectActualFiles, actionGetter, openFileLastLine
interface DocumentPositionMapperVerifier {
openFile: File;
expectedProjectActualFiles: ReadonlyArray<string>;
actionGetter: SessionActionGetter;
openFileLastLine: number;
}
function verifyDocumentPositionMapperUpdates(
mainScenario: string,
verifier: ReadonlyArray<DocumentPositionMapperVerifier>,
closedInfos: ReadonlyArray<string>) {
const openFiles = verifier.map(v => v.openFile);
const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles);
const actionGetters = verifier.map(v => v.actionGetter);
const openFileLastLines = verifier.map(v => v.openFileLastLine);
const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`);
const openInfos = openFiles.map(f => f.path);
// When usage and dependency are used, dependency config is part of closedInfo so ignore
const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles;
function openTsFile(onHostCreate?: (host: TestServerHost) => void) {
const host = createHost(files, [mainConfig.path]);
if (onHostCreate) {
onHostCreate(host);
}
const session = createSession(host);
openFilesForSession([...openFiles, randomFile], session);
return { host, session };
}
function checkProject(session: TestSession, noDts?: true) {
const service = session.getProjectService();
checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length });
configFiles.forEach((configFile, index) => {
checkProjectActualFiles(
service.configuredProjects.get(configFile)!,
noDts ?
expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) :
expectedProjectActualFiles[index]
);
});
}
function verifyInfos(session: TestSession, host: TestServerHost) {
verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles);
}
function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)),
dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles
);
}
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)),
// When project actual file contains dts, it needs to be watched
dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ?
otherWatchedFiles.concat(dtsClosedInfo) :
otherWatchedFiles
);
}
function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) {
assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap);
if (notEqual) {
assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
else {
assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
}
function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) {
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn);
const { response } = session.executeCommandSeq(request);
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts };
}
function firstAction(session: TestSession) {
actionGetters.forEach(actionGetter => action(actionGetter, 1, session));
}
function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType<typeof action>, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) {
// action
let isFirst = true;
for (const actionGetter of actionGetters) {
for (let fn = 1; fn <= 5; fn++) {
const result = action(actionGetter, fn, session);
const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath);
if (dtsAbsent) {
assert.isUndefined(dtsInfo);
}
else {
assert.isDefined(dtsInfo);
}
verifyAction(result, dtsInfo, isFirst);
isFirst = false;
}
}
}
function verifyAllFnAction(
session: TestSession,
host: TestServerHost,
firstDocumentPositionMapperNotEquals?: true,
dependencyMap?: server.ScriptInfo,
documentPositionMapper?: server.ScriptInfo["documentPositionMapper"]
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`);
verifyInfos(session, host);
assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath);
if (isFirst) {
if (dependencyMap) {
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals);
documentPositionMapper = dependencyMap.documentPositionMapper;
}
else {
dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
documentPositionMapper = dependencyMap.documentPositionMapper;
}
}
else {
verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper);
}
});
return { dependencyMap: dependencyMap!, documentPositionMapper };
}
function verifyAllFnActionWithNoMap(
session: TestSession,
host: TestServerHost,
dependencyTsOK?: true
) {
let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"];
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoMapFile(session, host, dependencyTsOK);
assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath));
if (isFirst) {
assert.isNotString(dtsInfo!.sourceMapFilePath);
assert.isNotFalse(dtsInfo!.sourceMapFilePath);
assert.isDefined(dtsInfo!.sourceMapFilePath);
sourceMapFilePath = dtsInfo!.sourceMapFilePath;
}
else {
assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath);
}
});
return sourceMapFilePath;
}
function verifyAllFnActionWithNoDts(
session: TestSession,
host: TestServerHost,
dependencyTsAndMapOk?: true
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => {
assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk);
}, /*dtsAbsent*/ true);
}
function verifyScenarioWithChangesWorker(
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals: true | undefined,
timeoutBeforeAction: boolean
) {
const { host, session } = openTsFile();
// Create DocumentPositionMapper
firstAction(session);
const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
const documentPositionMapper = dependencyMap.documentPositionMapper;
// change
change(host, session);
if (timeoutBeforeAction) {
host.runQueuedTimeoutCallbacks();
checkProject(session);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
}
// action
verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper);
}
function verifyScenarioWithChanges(
scenarioName: string,
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals?: true
) {
describe(scenarioName, () => {
it("when timeout occurs before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true);
});
it("when timeout does not occur before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false);
});
});
}
function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) {
// Main scenario action
const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host);
checkProject(session);
verifyInfos(session, host);
// Collecting at this point retains dependency.d.ts and map
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfos(session, host);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) {
// Main scenario action
verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoMapFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
// Main scenario action
verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoDtsFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyScenarioWhenFileNotPresent(
scenarioName: string,
fileLocation: string,
verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void,
noDts?: true
) {
describe(scenarioName, () => {
it(mainScenario, () => {
const { host, session } = openTsFile(host => host.deleteFile(fileLocation));
checkProject(session, noDts);
verifyScenarioAndScriptInfoCollection(session, host);
});
it("when file is created", () => {
let fileContents: string | undefined;
const { host, session } = openTsFile(host => {
fileContents = host.readFile(fileLocation);
host.deleteFile(fileLocation);
});
firstAction(session);
host.writeFile(fileLocation, fileContents!);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
it("when file is deleted", () => {
const { host, session } = openTsFile();
firstAction(session);
// The dependency file is deleted when orphan files are collected
host.deleteFile(fileLocation);
verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true);
});
});
}
it(mainScenario, () => {
const { host, session } = openTsFile();
checkProject(session);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
// Edit
verifyScenarioWithChanges(
"when usage file changes, document position mapper doesnt change",
(_host, session) => openFiles.forEach(
(openFile, index) => session.executeCommandSeq<protocol.ChangeRequest>({
command: protocol.CommandTypes.Change,
arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" }
})
)
);
// Edit dts to add new fn
verifyScenarioWithChanges(
"when dependency .d.ts changes, document position mapper doesnt change",
host => host.writeFile(
dtsLocation,
host.readFile(dtsLocation)!.replace(
"//# sourceMappingURL=FnS.d.ts.map",
`export declare function fn6(): void;
//# sourceMappingURL=FnS.d.ts.map`
)
)
);
// Edit map file to represent added new line
verifyScenarioWithChanges(
"when dependency file's map changes",
host => host.writeFile(
dtsMapLocation,
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
),
/*afterActionDocumentPositionMapperNotEquals*/ true
);
verifyScenarioWhenFileNotPresent(
"when map file is not present",
dtsMapLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoMap
);
verifyScenarioWhenFileNotPresent(
"when .d.ts file is not present",
dtsLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoDts,
/*noDts*/ true
);
}
const usageVerifier: DocumentPositionMapperVerifier = {
openFile: mainTs,
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
actionGetter: gotoDefintinionFromMainTs,
openFileLastLine: 14
};
describe("from project that uses dependency", () => {
const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"can go to definition correctly",
[usageVerifier],
closedInfos
);
});
const definingVerifier: DocumentPositionMapperVerifier = {
openFile: dependencyTs,
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
actionGetter: renameFromDependencyTs,
openFileLastLine: 6
};
describe("from defining project", () => {
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"rename locations from dependency",
[definingVerifier],
closedInfos
);
});
describe("when opening depedency and usage project", () => {
const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path];
verifyDocumentPositionMapperUpdates(
"goto Definition in usage and rename locations from defining project",
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
closedInfos
);
});
});
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: refactors", () => {
it("use formatting options", () => {
const file = {
path: "/a.ts",
content: "function f() {\n 1;\n}",
};
const host = createServerHost([file]);
const session = createSession(host);
openFilesForSession([file], session);
const response0 = session.executeCommandSeq<server.protocol.ConfigureRequest>({
command: server.protocol.CommandTypes.Configure,
arguments: {
formatOptions: {
indentSize: 2,
},
},
}).response;
assert.deepEqual(response0, /*expected*/ undefined);
const response1 = session.executeCommandSeq<server.protocol.GetEditsForRefactorRequest>({
command: server.protocol.CommandTypes.GetEditsForRefactor,
arguments: {
refactor: "Extract Symbol",
action: "function_scope_1",
file: "/a.ts",
startLine: 2,
startOffset: 3,
endLine: 2,
endOffset: 4,
},
}).response;
assert.deepEqual(response1, {
edits: [
{
fileName: "/a.ts",
textChanges: [
{
start: { line: 2, offset: 3 },
end: { line: 2, offset: 5 },
newText: "newFunction();",
},
{
start: { line: 3, offset: 2 },
end: { line: 3, offset: 2 },
newText: "\n\nfunction newFunction() {\n 1;\n}\n",
},
]
}
],
renameFilename: "/a.ts",
renameLocation: { line: 2, offset: 3 },
});
});
it("handles text changes in tsconfig.json", () => {
const aTs = {
path: "/a.ts",
content: "export const a = 0;",
};
const tsconfig = {
path: "/tsconfig.json",
content: '{ "files": ["./a.ts"] }',
};
const session = createSession(createServerHost([aTs, tsconfig]));
openFilesForSession([aTs], session);
const response1 = session.executeCommandSeq<server.protocol.GetEditsForRefactorRequest>({
command: server.protocol.CommandTypes.GetEditsForRefactor,
arguments: {
refactor: "Move to a new file",
action: "Move to a new file",
file: "/a.ts",
startLine: 1,
startOffset: 1,
endLine: 1,
endOffset: 20,
},
}).response;
assert.deepEqual(response1, {
edits: [
{
fileName: "/a.ts",
textChanges: [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 20 },
newText: "",
},
],
},
{
fileName: "/tsconfig.json",
textChanges: [
{
start: { line: 1, offset: 21 },
end: { line: 1, offset: 21 },
newText: ", \"./a.1.ts\"",
},
],
},
{
fileName: "/a.1.ts",
textChanges: [
{
start: { line: 0, offset: 0 },
end: { line: 0, offset: 0 },
newText: "export const a = 0;\n",
},
],
}
],
renameFilename: undefined,
renameLocation: undefined,
});
});
});
}
+151
View File
@@ -0,0 +1,151 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: reload", () => {
it("should work with temp file", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const tmp = {
path: "/a/b/app.tmp",
content: "const y = 42"
};
const host = createServerHost([f1, tmp]);
const session = createSession(host);
// send open request
session.executeCommand(<server.protocol.OpenRequest>{
type: "request",
command: "open",
seq: 1,
arguments: { file: f1.path }
});
// reload from tmp file
session.executeCommand(<server.protocol.ReloadRequest>{
type: "request",
command: "reload",
seq: 2,
arguments: { file: f1.path, tmpfile: tmp.path }
});
// verify content
const projectServiice = session.getProjectService();
const snap1 = projectServiice.getScriptInfo(f1.path)!.getSnapshot();
assert.equal(getSnapshotText(snap1), tmp.content, "content should be equal to the content of temp file");
// reload from original file file
session.executeCommand(<server.protocol.ReloadRequest>{
type: "request",
command: "reload",
seq: 2,
arguments: { file: f1.path }
});
// verify content
const snap2 = projectServiice.getScriptInfo(f1.path)!.getSnapshot();
assert.equal(getSnapshotText(snap2), f1.content, "content should be equal to the content of original file");
});
it("should work when script info doesnt have any project open", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const tmp = {
path: "/a/b/app.tmp",
content: "const y = 42"
};
const host = createServerHost([f1, tmp, libFile]);
const session = createSession(host);
const openContent = "let z = 1";
// send open request
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: server.protocol.CommandTypes.Open,
arguments: { file: f1.path, fileContent: openContent }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const info = projectService.getScriptInfo(f1.path)!;
assert.isDefined(info);
checkScriptInfoContents(openContent, "contents set during open request");
// send close request
session.executeCommandSeq(<server.protocol.CloseRequest>{
command: server.protocol.CommandTypes.Close,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(f1.content, "contents of closed file");
checkInferredProjectIsOrphan();
// Can reload contents of the file when its not open and has no project
// reload from temp file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path, tmpfile: tmp.path }
});
checkScriptInfoAndProjects(tmp.content, "contents of temp file");
checkInferredProjectIsOrphan();
// reload from own file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(f1.content, "contents of closed file");
checkInferredProjectIsOrphan();
// Open file again without setting its content
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: server.protocol.CommandTypes.Open,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(f1.content, "contents of file when opened without specifying contents");
const snap = info.getSnapshot();
// send close request
session.executeCommandSeq(<server.protocol.CloseRequest>{
command: server.protocol.CommandTypes.Close,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(f1.content, "contents of closed file");
assert.strictEqual(info.getSnapshot(), snap);
checkInferredProjectIsOrphan();
// reload from temp file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path, tmpfile: tmp.path }
});
checkScriptInfoAndProjects(tmp.content, "contents of temp file");
assert.notStrictEqual(info.getSnapshot(), snap);
checkInferredProjectIsOrphan();
// reload from own file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(f1.content, "contents of closed file");
assert.notStrictEqual(info.getSnapshot(), snap);
checkInferredProjectIsOrphan();
function checkInferredProjectIsOrphan() {
assert.isTrue(projectService.inferredProjects[0].isOrphan());
assert.equal(info.containingProjects.length, 0);
}
function checkScriptInfoAndProjects(contentsOfInfo: string, captionForContents: string) {
checkNumberOfProjects(projectService, { inferredProjects: 1 });
assert.strictEqual(projectService.getScriptInfo(f1.path), info);
checkScriptInfoContents(contentsOfInfo, captionForContents);
}
function checkScriptInfoContents(contentsOfInfo: string, captionForContents: string) {
const snap = info.getSnapshot();
assert.equal(getSnapshotText(snap), contentsOfInfo, "content should be equal to " + captionForContents);
}
});
});
}
@@ -0,0 +1,53 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: rename", () => {
it("works with fileToRename", () => {
const aTs: File = { path: "/a.ts", content: "export const a = 0;" };
const bTs: File = { path: "/b.ts", content: 'import { a } from "./a";' };
const session = createSession(createServerHost([aTs, bTs]));
openFilesForSession([bTs], session);
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";'));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
fileToRename: aTs.path,
displayName: aTs.path,
fullDisplayName: aTs.path,
kind: ScriptElementKind.moduleElement,
kindModifiers: "",
triggerSpan: protocolTextSpanFromSubstring(bTs.content, "a", { index: 1 }),
},
locs: [{ file: bTs.path, locs: [protocolRenameSpanFromSubstring(bTs.content, "./a")] }],
});
});
it("works with prefixText and suffixText", () => {
const aTs: File = { path: "/a.ts", content: "const x = 0; const o = { x };" };
const session = createSession(createServerHost([aTs]));
openFilesForSession([aTs], session);
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
fileToRename: undefined,
displayName: "x",
fullDisplayName: "x",
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.none,
triggerSpan: protocolTextSpanFromSubstring(aTs.content, "x"),
},
locs: [
{
file: aTs.path,
locs: [
protocolRenameSpanFromSubstring(aTs.content, "x"),
protocolRenameSpanFromSubstring(aTs.content, "x", { index: 1 }, { prefixText: "x: " }),
],
},
],
});
});
});
}
@@ -0,0 +1,979 @@
namespace ts.projectSystem {
function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) {
const resolutionTrace: string[] = [];
host.trace = resolutionTrace.push.bind(resolutionTrace);
return resolutionTrace;
}
describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => {
it("can load typings that are proper modules", () => {
const file1 = {
path: "/a/b/app.js",
content: `var x = require("lib")`
};
const lib = {
path: "/a/cache/node_modules/@types/lib/index.d.ts",
content: "export let x = 1"
};
const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]);
const resolutionTrace = createHostModuleResolutionTrace(host);
const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) });
projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true });
projectService.openClientFile(file1.path);
projectService.checkNumberOfProjects({ inferredProjects: 1 });
const proj = projectService.inferredProjects[0];
assert.deepEqual(resolutionTrace, [
"======== Resolving module 'lib' from '/a/b/app.js'. ========",
"Module resolution kind is not specified, using 'NodeJs'.",
"Loading module 'lib' from 'node_modules' folder, target file type 'TypeScript'.",
"Directory '/a/b/node_modules' does not exist, skipping all lookups in it.",
"Directory '/a/node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"Loading module 'lib' from 'node_modules' folder, target file type 'JavaScript'.",
"Directory '/a/b/node_modules' does not exist, skipping all lookups in it.",
"Directory '/a/node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"======== Module name 'lib' was not resolved. ========",
`Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`,
"File '/a/cache/node_modules/lib.d.ts' does not exist.",
"File '/a/cache/node_modules/@types/lib/package.json' does not exist.",
"File '/a/cache/node_modules/@types/lib.d.ts' does not exist.",
"File '/a/cache/node_modules/@types/lib/index.d.ts' exist - use it as a name resolution result.",
]);
checkProjectActualFiles(proj, [file1.path, lib.path]);
});
});
describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem watching @types", () => {
it("works correctly when typings are added or removed", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1;"
};
const t1 = {
path: "/a/b/node_modules/@types/lib1/index.d.ts",
content: "export let a: number"
};
const t2 = {
path: "/a/b/node_modules/@types/lib2/index.d.ts",
content: "export let b: number"
};
const tsconfig = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({
compilerOptions: {},
exclude: ["node_modules"]
})
};
const host = createServerHost([f1, t1, tsconfig]);
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t1.path, tsconfig.path]);
// delete t1
host.reloadFS([f1, tsconfig]);
// run throttled operation
host.runQueuedTimeoutCallbacks();
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]);
// create t2
host.reloadFS([f1, tsconfig, t2]);
// run throttled operation
host.runQueuedTimeoutCallbacks();
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t2.path, tsconfig.path]);
});
});
describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => {
it("should remove the `module not found` error", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
content: "export function bar() { };"
};
const file1 = {
path: "/a/b/file1.ts",
content: "import * as T from './moduleFile'; T.bar();"
};
const host = createServerHost([file1]);
const session = createSession(host);
openFilesForSession([file1], session);
const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>(
server.CommandNames.SemanticDiagnosticsSync,
{ file: file1.path }
);
let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyDiagnostics(diags, [
{ diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] }
]);
host.reloadFS([file1, moduleFile]);
host.runQueuedTimeoutCallbacks();
// Make a change to trigger the program rebuild
const changeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(
server.CommandNames.Change,
{ file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" }
);
session.executeCommand(changeRequest);
// Recheck
diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyNoDiagnostics(diags);
});
it("npm install @types works", () => {
const folderPath = "/a/b/projects/temp";
const file1: File = {
path: `${folderPath}/a.ts`,
content: 'import f = require("pad"); f;'
};
const files = [file1, libFile];
const host = createServerHost(files);
const session = createSession(host, { canUseEvents: true });
const service = session.getProjectService();
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: {
file: file1.path,
fileContent: file1.content,
scriptKindName: "TS",
projectRootPath: folderPath
}
});
checkNumberOfProjects(service, { inferredProjects: 1 });
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [file1.path]
}
});
host.checkTimeoutQueueLengthAndRun(1);
checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks();
const startOffset = file1.content.indexOf('"') + 1;
checkErrorMessage(session, "semanticDiag", {
file: file1.path,
diagnostics: [
createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + '"pad"'.length }, Diagnostics.Cannot_find_module_0, ["pad"])
],
});
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "suggestionDiag", { file: file1.path, diagnostics: [] });
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
const padIndex: File = {
path: `${folderPath}/node_modules/@types/pad/index.d.ts`,
content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;"
};
files.push(padIndex);
host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true });
host.runQueuedTimeoutCallbacks();
checkProjectUpdatedInBackgroundEvent(session, [file1.path]);
session.clearMessages();
host.runQueuedTimeoutCallbacks();
checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks();
checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] });
});
it("suggestion diagnostics", () => {
const file: File = {
path: "/a.js",
content: "function f(p) {}",
};
const host = createServerHost([file]);
const session = createSession(host, { canUseEvents: true });
const service = session.getProjectService();
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: { file: file.path, fileContent: file.content },
});
checkNumberOfProjects(service, { inferredProjects: 1 });
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
host.checkTimeoutQueueLengthAndRun(2);
checkProjectUpdatedInBackgroundEvent(session, [file.path]);
session.clearMessages();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [file.path],
}
});
host.checkTimeoutQueueLengthAndRun(1);
checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true);
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "suggestionDiag", {
file: file.path,
diagnostics: [
createDiagnostic({ line: 1, offset: 12 }, { line: 1, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["p"], "suggestion", /*reportsUnnecessary*/ true),
],
});
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
});
it("disable suggestion diagnostics", () => {
const file: File = {
path: "/a.js",
content: 'require("b")',
};
const host = createServerHost([file]);
const session = createSession(host, { canUseEvents: true });
const service = session.getProjectService();
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: { file: file.path, fileContent: file.content },
});
session.executeCommandSeq<protocol.ConfigureRequest>({
command: server.CommandNames.Configure,
arguments: {
preferences: { disableSuggestions: true }
},
});
checkNumberOfProjects(service, { inferredProjects: 1 });
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
host.checkTimeoutQueueLengthAndRun(2);
checkProjectUpdatedInBackgroundEvent(session, [file.path]);
session.clearMessages();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [file.path],
}
});
host.checkTimeoutQueueLengthAndRun(1);
checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true);
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] });
// No suggestion event, we're done.
checkCompleteEvent(session, 2, expectedSequenceId);
session.clearMessages();
});
it("suppressed diagnostic events", () => {
const file: File = {
path: "/a.ts",
content: "1 = 2;",
};
const host = createServerHost([file]);
const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true });
const service = session.getProjectService();
session.executeCommandSeq<protocol.OpenRequest>({
command: server.CommandNames.Open,
arguments: { file: file.path, fileContent: file.content },
});
checkNumberOfProjects(service, { inferredProjects: 1 });
host.checkTimeoutQueueLength(0);
checkNoDiagnosticEvents(session);
session.clearMessages();
let expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [file.path],
}
});
host.checkTimeoutQueueLength(0);
checkNoDiagnosticEvents(session);
checkCompleteEvent(session, 1, expectedSequenceId);
session.clearMessages();
expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrForProjectRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
file: file.path,
}
});
host.checkTimeoutQueueLength(0);
checkNoDiagnosticEvents(session);
checkCompleteEvent(session, 1, expectedSequenceId);
session.clearMessages();
});
});
describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => {
it("should restore the states for inferred projects", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
content: "export function bar() { };"
};
const file1 = {
path: "/a/b/file1.ts",
content: "import * as T from './moduleFile'; T.bar();"
};
const host = createServerHost([moduleFile, file1]);
const session = createSession(host);
openFilesForSession([file1], session);
const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>(
server.CommandNames.SemanticDiagnosticsSync,
{ file: file1.path }
);
let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyNoDiagnostics(diags);
const moduleFileOldPath = moduleFile.path;
const moduleFileNewPath = "/a/b/moduleFile1.ts";
moduleFile.path = moduleFileNewPath;
host.reloadFS([moduleFile, file1]);
host.runQueuedTimeoutCallbacks();
diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyDiagnostics(diags, [
{ diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] }
]);
assert.equal(diags.length, 1);
moduleFile.path = moduleFileOldPath;
host.reloadFS([moduleFile, file1]);
host.runQueuedTimeoutCallbacks();
// Make a change to trigger the program rebuild
const changeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(
server.CommandNames.Change,
{ file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" }
);
session.executeCommand(changeRequest);
host.runQueuedTimeoutCallbacks();
diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyNoDiagnostics(diags);
});
it("should restore the states for configured projects", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
content: "export function bar() { };"
};
const file1 = {
path: "/a/b/file1.ts",
content: "import * as T from './moduleFile'; T.bar();"
};
const configFile = {
path: "/a/b/tsconfig.json",
content: `{}`
};
const host = createServerHost([moduleFile, file1, configFile]);
const session = createSession(host);
openFilesForSession([file1], session);
const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>(
server.CommandNames.SemanticDiagnosticsSync,
{ file: file1.path }
);
let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyNoDiagnostics(diags);
const moduleFileOldPath = moduleFile.path;
const moduleFileNewPath = "/a/b/moduleFile1.ts";
moduleFile.path = moduleFileNewPath;
host.reloadFS([moduleFile, file1, configFile]);
host.runQueuedTimeoutCallbacks();
diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyDiagnostics(diags, [
{ diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] }
]);
moduleFile.path = moduleFileOldPath;
host.reloadFS([moduleFile, file1, configFile]);
host.runQueuedTimeoutCallbacks();
diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
verifyNoDiagnostics(diags);
});
it("should property handle missing config files", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: "{}"
};
const projectName = "project1";
const host = createServerHost([f1]);
const projectService = createProjectService(host);
projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName });
// should have one external project since config file is missing
projectService.checkNumberOfProjects({ externalProjects: 1 });
host.reloadFS([f1, config]);
projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName });
projectService.checkNumberOfProjects({ configuredProjects: 1 });
});
it("types should load from config file path if config exists", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } })
};
const node = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: "declare var process: any"
};
const cwd = {
path: "/a/c"
};
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]);
});
});
describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem module resolution caching", () => {
const projectLocation = "/user/username/projects/myproject";
const configFile: File = {
path: `${projectLocation}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { traceResolution: true } })
};
function getModules(module1Path: string, module2Path: string) {
const module1: File = {
path: module1Path,
content: `export function module1() {}`
};
const module2: File = {
path: module2Path,
content: `export function module2() {}`
};
return { module1, module2 };
}
function verifyTrace(resolutionTrace: string[], expected: string[]) {
assert.deepEqual(resolutionTrace, expected);
resolutionTrace.length = 0;
}
function getExpectedFileDoesNotExistResolutionTrace(host: TestServerHost, expectedTrace: string[], foundModule: boolean, module: File, directory: string, file: string, ignoreIfParentMissing?: boolean) {
if (!foundModule) {
const path = combinePaths(directory, file);
if (!ignoreIfParentMissing || host.directoryExists(getDirectoryPath(path))) {
if (module.path === path) {
foundModule = true;
}
else {
expectedTrace.push(`File '${path}' does not exist.`);
}
}
}
return foundModule;
}
function getExpectedMissedLocationResolutionTrace(host: TestServerHost, expectedTrace: string[], dirPath: string, module: File, moduleName: string, useNodeModules: boolean, cacheLocation?: string) {
let foundModule = false;
forEachAncestorDirectory(dirPath, dirPath => {
if (dirPath === cacheLocation) {
return foundModule;
}
const directory = useNodeModules ? combinePaths(dirPath, nodeModules) : dirPath;
if (useNodeModules && !foundModule && !host.directoryExists(directory)) {
expectedTrace.push(`Directory '${directory}' does not exist, skipping all lookups in it.`);
return undefined;
}
foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/package.json`, /*ignoreIfParentMissing*/ true);
foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.ts`);
foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.tsx`);
foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.d.ts`);
foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/index.ts`, /*ignoreIfParentMissing*/ true);
if (useNodeModules && !foundModule) {
expectedTrace.push(`Directory '${directory}/@types' does not exist, skipping all lookups in it.`);
}
return foundModule ? true : undefined;
});
}
function getExpectedResolutionTraceHeader(expectedTrace: string[], file: File, moduleName: string) {
expectedTrace.push(
`======== Resolving module '${moduleName}' from '${file.path}'. ========`,
`Module resolution kind is not specified, using 'NodeJs'.`
);
}
function getExpectedResolutionTraceFooter(expectedTrace: string[], module: File, moduleName: string, addRealPathTrace: boolean, ignoreModuleFileFound?: boolean) {
if (!ignoreModuleFileFound) {
expectedTrace.push(`File '${module.path}' exist - use it as a name resolution result.`);
}
if (addRealPathTrace) {
expectedTrace.push(`Resolving real path for '${module.path}', result '${module.path}'.`);
}
expectedTrace.push(`======== Module name '${moduleName}' was successfully resolved to '${module.path}'. ========`);
}
function getExpectedRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) {
getExpectedResolutionTraceHeader(expectedTrace, file, moduleName);
expectedTrace.push(`Loading module as file / folder, candidate module location '${removeFileExtension(module.path)}', target file type 'TypeScript'.`);
getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(normalizePath(combinePaths(getDirectoryPath(file.path), moduleName))), module, moduleName.substring(moduleName.lastIndexOf("/") + 1), /*useNodeModules*/ false);
getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false);
return expectedTrace;
}
function getExpectedNonRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) {
getExpectedResolutionTraceHeader(expectedTrace, file, moduleName);
expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`);
getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true);
getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ true);
return expectedTrace;
}
function getExpectedNonRelativeModuleResolutionFromCacheTrace(host: TestServerHost, file: File, module: File, moduleName: string, cacheLocation: string, expectedTrace: string[] = []) {
getExpectedResolutionTraceHeader(expectedTrace, file, moduleName);
expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`);
getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true, cacheLocation);
expectedTrace.push(`Resolution for module '${moduleName}' was found in cache from location '${cacheLocation}'.`);
getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false, /*ignoreModuleFileFound*/ true);
return expectedTrace;
}
function getExpectedReusingResolutionFromOldProgram(file: File, moduleName: string) {
return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`;
}
function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray<string>) {
const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]);
checkWatchedFiles(host, mapDefined(files, f => {
if (f === openFile) {
return undefined;
}
const indexOfNodeModules = f.path.indexOf("/node_modules/");
if (indexOfNodeModules === -1) {
return f.path;
}
expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true);
return undefined;
}));
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true);
}
describe("from files in same folder", () => {
function getFiles(fileContent: string) {
const file1: File = {
path: `${projectLocation}/src/file1.ts`,
content: fileContent
};
const file2: File = {
path: `${projectLocation}/src/file2.ts`,
content: fileContent
};
return { file1, file2 };
}
it("relative module name", () => {
const module1Name = "./module1";
const module2Name = "../module2";
const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`;
const { file1, file2 } = getFiles(fileContent);
const { module1, module2 } = getModules(`${projectLocation}/src/module1.ts`, `${projectLocation}/module2.ts`);
const files = [module1, module2, file1, file2, configFile, libFile];
const host = createServerHost(files);
const resolutionTrace = createHostModuleResolutionTrace(host);
const service = createProjectService(host);
service.openClientFile(file1.path);
const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name);
getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace);
verifyTrace(resolutionTrace, expectedTrace);
verifyWatchesWithConfigFile(host, files, file1);
file1.content += fileContent;
file2.content += fileContent;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyTrace(resolutionTrace, [
getExpectedReusingResolutionFromOldProgram(file1, module1Name),
getExpectedReusingResolutionFromOldProgram(file1, module2Name)
]);
verifyWatchesWithConfigFile(host, files, file1);
});
it("non relative module name", () => {
const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`];
const module1Name = "module1";
const module2Name = "module2";
const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`;
const { file1, file2 } = getFiles(fileContent);
const { module1, module2 } = getModules(`${projectLocation}/src/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`);
const files = [module1, module2, file1, file2, configFile, libFile];
const host = createServerHost(files);
const resolutionTrace = createHostModuleResolutionTrace(host);
const service = createProjectService(host);
service.openClientFile(file1.path);
const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name);
getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace);
verifyTrace(resolutionTrace, expectedTrace);
verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories);
file1.content += fileContent;
file2.content += fileContent;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyTrace(resolutionTrace, [
getExpectedReusingResolutionFromOldProgram(file1, module1Name),
getExpectedReusingResolutionFromOldProgram(file1, module2Name)
]);
verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories);
});
});
describe("from files in different folders", () => {
function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) {
const file1: File = {
path: `${projectLocation}/product/src/file1.ts`,
content: fileContent1
};
const file2: File = {
path: `${projectLocation}/product/src/feature/file2.ts`,
content: fileContent2
};
const file3: File = {
path: `${projectLocation}/product/test/src/file3.ts`,
content: fileContent3
};
const file4: File = {
path: `${projectLocation}/product/test/file4.ts`,
content: fileContent4
};
return { file1, file2, file3, file4 };
}
it("relative module name", () => {
const module1Name = "./module1";
const module2Name = "../module2";
const module3Name = "../module1";
const module4Name = "../../module2";
const module5Name = "../../src/module1";
const module6Name = "../src/module1";
const fileContent1 = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`;
const fileContent2 = `import { module1 } from "${module3Name}";import { module2 } from "${module4Name}";`;
const fileContent3 = `import { module1 } from "${module5Name}";import { module2 } from "${module4Name}";`;
const fileContent4 = `import { module1 } from "${module6Name}";import { module2 } from "${module2Name}";`;
const { file1, file2, file3, file4 } = getFiles(fileContent1, fileContent2, fileContent3, fileContent4);
const { module1, module2 } = getModules(`${projectLocation}/product/src/module1.ts`, `${projectLocation}/product/module2.ts`);
const files = [module1, module2, file1, file2, file3, file4, configFile, libFile];
const host = createServerHost(files);
const resolutionTrace = createHostModuleResolutionTrace(host);
const service = createProjectService(host);
service.openClientFile(file1.path);
const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name);
getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file2, module1, module3Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file2, module2, module4Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file4, module1, module6Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file3, module1, module5Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file3, module2, module4Name, expectedTrace);
verifyTrace(resolutionTrace, expectedTrace);
verifyWatchesWithConfigFile(host, files, file1);
file1.content += fileContent1;
file2.content += fileContent2;
file3.content += fileContent3;
file4.content += fileContent4;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyTrace(resolutionTrace, [
getExpectedReusingResolutionFromOldProgram(file1, module1Name),
getExpectedReusingResolutionFromOldProgram(file1, module2Name)
]);
verifyWatchesWithConfigFile(host, files, file1);
});
it("non relative module name", () => {
const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`];
const module1Name = "module1";
const module2Name = "module2";
const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`;
const { file1, file2, file3, file4 } = getFiles(fileContent);
const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`);
const files = [module1, module2, file1, file2, file3, file4, configFile, libFile];
const host = createServerHost(files);
const resolutionTrace = createHostModuleResolutionTrace(host);
const service = createProjectService(host);
service.openClientFile(file1.path);
const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name);
getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace);
verifyTrace(resolutionTrace, expectedTrace);
verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories);
file1.content += fileContent;
file2.content += fileContent;
file3.content += fileContent;
file4.content += fileContent;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyTrace(resolutionTrace, [
getExpectedReusingResolutionFromOldProgram(file1, module1Name),
getExpectedReusingResolutionFromOldProgram(file1, module2Name)
]);
verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories);
});
it("non relative module name from inferred project", () => {
const module1Name = "module1";
const module2Name = "module2";
const file2Name = "./feature/file2";
const file3Name = "../test/src/file3";
const file4Name = "../test/file4";
const importModuleContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`;
const { file1, file2, file3, file4 } = getFiles(`import "${file2Name}"; import "${file4Name}"; import "${file3Name}"; ${importModuleContent}`, importModuleContent, importModuleContent, importModuleContent);
const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`);
const files = [module1, module2, file1, file2, file3, file4, libFile];
const host = createServerHost(files);
const resolutionTrace = createHostModuleResolutionTrace(host);
const service = createProjectService(host);
service.setCompilerOptionsForInferredProjects({ traceResolution: true });
service.openClientFile(file1.path);
const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, file2, file2Name);
getExpectedRelativeModuleResolutionTrace(host, file1, file4, file4Name, expectedTrace);
getExpectedRelativeModuleResolutionTrace(host, file1, file3, file3Name, expectedTrace);
getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name, expectedTrace);
getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace);
getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace);
verifyTrace(resolutionTrace, expectedTrace);
const currentDirectory = getDirectoryPath(file1.path);
const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path);
forEachAncestorDirectory(currentDirectory, d => {
watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json"));
});
const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([
`${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${projectLocation}/product/${nodeModules}`,
`${projectLocation}/${nodeModules}`, `${projectLocation}/product/test/${nodeModules}`,
`${projectLocation}/product/test/src/${nodeModules}`
]);
checkWatches();
file1.content += importModuleContent;
file2.content += importModuleContent;
file3.content += importModuleContent;
file4.content += importModuleContent;
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyTrace(resolutionTrace, [
getExpectedReusingResolutionFromOldProgram(file1, file2Name),
getExpectedReusingResolutionFromOldProgram(file1, file4Name),
getExpectedReusingResolutionFromOldProgram(file1, file3Name),
getExpectedReusingResolutionFromOldProgram(file1, module1Name),
getExpectedReusingResolutionFromOldProgram(file1, module2Name)
]);
checkWatches();
function checkWatches() {
checkWatchedFiles(host, watchedFiles);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true);
}
});
});
describe("when watching directories for failed lookup locations in amd resolution", () => {
const projectRoot = "/user/username/projects/project";
const nodeFile: File = {
path: `${projectRoot}/src/typings/node.d.ts`,
content: `
declare module "fs" {
export interface something {
}
}`
};
const electronFile: File = {
path: `${projectRoot}/src/typings/electron.d.ts`,
content: `
declare module 'original-fs' {
import * as fs from 'fs';
export = fs;
}`
};
const srcFile: File = {
path: `${projectRoot}/src/somefolder/srcfile.ts`,
content: `
import { x } from "somefolder/module1";
import { x } from "somefolder/module2";
const y = x;`
};
const moduleFile: File = {
path: `${projectRoot}/src/somefolder/module1.ts`,
content: `
export const x = 10;`
};
const configFile: File = {
path: `${projectRoot}/src/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
module: "amd",
moduleResolution: "classic",
target: "es5",
outDir: "../out",
baseUrl: "./",
typeRoots: ["typings"]
}
})
};
function verifyModuleResolution(useNodeFile: boolean) {
const files = [...(useNodeFile ? [nodeFile] : []), electronFile, srcFile, moduleFile, configFile, libFile];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(srcFile.path, srcFile.content, ScriptKind.TS, projectRoot);
checkProjectActualFiles(service.configuredProjects.get(configFile.path)!, files.map(f => f.path));
checkWatchedFilesDetailed(host, mapDefined(files, f => f === srcFile ? undefined : f.path), 1);
if (useNodeFile) {
checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup
}
else {
checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs
}
const expectedWatchedDirectories = createMap<number>();
expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card
expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2
expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2
expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2
expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs
expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true);
}
it("when resolves to ambient module", () => {
verifyModuleResolution(/*useNodeFile*/ true);
});
it("when resolution fails", () => {
verifyModuleResolution(/*useNodeFile*/ false);
});
});
describe("ignores files/folder changes in node_modules that start with '.'", () => {
const projectPath = "/user/username/projects/project";
const npmCacheFile: File = {
path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`,
content: JSON.stringify({ something: 10 })
};
const file1: File = {
path: `${projectPath}/test.ts`,
content: `import { x } from "somemodule";`
};
const file2: File = {
path: `${projectPath}/node_modules/somemodule/index.d.ts`,
content: `export const x = 10;`
};
it("when watching node_modules in inferred project for failed lookup/closed script infos", () => {
const files = [libFile, file1, file2];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(file1.path);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, files.map(f => f.path));
(project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1;
host.checkTimeoutQueueLength(0);
host.ensureFileOrFolder(npmCacheFile);
host.checkTimeoutQueueLength(0);
});
it("when watching node_modules as part of wild card directories in config project", () => {
const config: File = {
path: `${projectPath}/tsconfig.json`,
content: "{}"
};
const files = [libFile, file1, file2, config];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(file1.path);
checkNumberOfProjects(service, { configuredProjects: 1 });
const project = Debug.assertDefined(service.configuredProjects.get(config.path));
checkProjectActualFiles(project, files.map(f => f.path));
host.checkTimeoutQueueLength(0);
host.ensureFileOrFolder(npmCacheFile);
host.checkTimeoutQueueLength(0);
});
});
});
}
@@ -35,7 +35,7 @@ namespace ts.server {
}
}
describe("the Session class", () => {
describe("unittests:: tsserver:: Session:: General functionality", () => {
let session: TestSession;
let lastSent: protocol.Message;
@@ -418,7 +418,7 @@ namespace ts.server {
});
});
describe("exceptions", () => {
describe("unittests:: tsserver:: Session:: exceptions", () => {
// Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test
let oldPrepare: AnyFunction;
@@ -489,7 +489,7 @@ namespace ts.server {
});
});
describe("how Session is extendable via subclassing", () => {
describe("unittests:: tsserver:: Session:: how Session is extendable via subclassing", () => {
class TestSession extends Session {
lastSent: protocol.Message | undefined;
customHandler = "testhandler";
@@ -558,7 +558,7 @@ namespace ts.server {
});
});
describe("an example of using the Session API to create an in-process server", () => {
describe("unittests:: tsserver:: Session:: an example of using the Session API to create an in-process server", () => {
class InProcSession extends Session {
private queue: protocol.Request[] = [];
constructor(private client: InProcClient) {
@@ -710,7 +710,7 @@ namespace ts.server {
});
});
describe("helpers", () => {
describe("unittests:: tsserver:: Session:: helpers", () => {
it(getLocationInNewDocument.name, () => {
const text = `// blank line\nconst x = 0;`;
const renameLocationInOldText = text.indexOf("0");
@@ -0,0 +1,229 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: with skipLibCheck", () => {
it("should be turned on for js-only inferred projects", () => {
const file1 = {
path: "/a/b/file1.js",
content: `
/// <reference path="file2.d.ts" />
var x = 1;`
};
const file2 = {
path: "/a/b/file2.d.ts",
content: `
interface T {
name: string;
};
interface T {
name: number;
};`
};
const host = createServerHost([file1, file2]);
const session = createSession(host);
openFilesForSession([file1, file2], session);
const file2GetErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: file2.path }
);
let errorResult = <protocol.Diagnostic[]>session.executeCommand(file2GetErrRequest).response;
assert.isTrue(errorResult.length === 0);
const closeFileRequest = makeSessionRequest<protocol.FileRequestArgs>(CommandNames.Close, { file: file1.path });
session.executeCommand(closeFileRequest);
errorResult = <protocol.Diagnostic[]>session.executeCommand(file2GetErrRequest).response;
assert.isTrue(errorResult.length !== 0);
openFilesForSession([file1], session);
errorResult = <protocol.Diagnostic[]>session.executeCommand(file2GetErrRequest).response;
assert.isTrue(errorResult.length === 0);
});
it("should be turned on for js-only external projects", () => {
const jsFile = {
path: "/a/b/file1.js",
content: "let x =1;"
};
const dTsFile = {
path: "/a/b/file2.d.ts",
content: `
interface T {
name: string;
};
interface T {
name: number;
};`
};
const host = createServerHost([jsFile, dTsFile]);
const session = createSession(host);
const openExternalProjectRequest = makeSessionRequest<protocol.OpenExternalProjectArgs>(
CommandNames.OpenExternalProject,
{
projectFileName: "project1",
rootFiles: toExternalFiles([jsFile.path, dTsFile.path]),
options: {}
}
);
session.executeCommand(openExternalProjectRequest);
const dTsFileGetErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: dTsFile.path }
);
const errorResult = <protocol.Diagnostic[]>session.executeCommand(dTsFileGetErrRequest).response;
assert.isTrue(errorResult.length === 0);
});
it("should be turned on for js-only external projects with skipLibCheck=false", () => {
const jsFile = {
path: "/a/b/file1.js",
content: "let x =1;"
};
const dTsFile = {
path: "/a/b/file2.d.ts",
content: `
interface T {
name: string;
};
interface T {
name: number;
};`
};
const host = createServerHost([jsFile, dTsFile]);
const session = createSession(host);
const openExternalProjectRequest = makeSessionRequest<protocol.OpenExternalProjectArgs>(
CommandNames.OpenExternalProject,
{
projectFileName: "project1",
rootFiles: toExternalFiles([jsFile.path, dTsFile.path]),
options: { skipLibCheck: false }
}
);
session.executeCommand(openExternalProjectRequest);
const dTsFileGetErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: dTsFile.path }
);
const errorResult = <protocol.Diagnostic[]>session.executeCommand(dTsFileGetErrRequest).response;
assert.isTrue(errorResult.length === 0);
});
it("should not report bind errors for declaration files with skipLibCheck=true", () => {
const jsconfigFile = {
path: "/a/jsconfig.json",
content: "{}"
};
const jsFile = {
path: "/a/jsFile.js",
content: "let x = 1;"
};
const dTsFile1 = {
path: "/a/dTsFile1.d.ts",
content: `
declare var x: number;`
};
const dTsFile2 = {
path: "/a/dTsFile2.d.ts",
content: `
declare var x: string;`
};
const host = createServerHost([jsconfigFile, jsFile, dTsFile1, dTsFile2]);
const session = createSession(host);
openFilesForSession([jsFile], session);
const dTsFile1GetErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: dTsFile1.path }
);
const error1Result = <protocol.Diagnostic[]>session.executeCommand(dTsFile1GetErrRequest).response;
assert.isTrue(error1Result.length === 0);
const dTsFile2GetErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: dTsFile2.path }
);
const error2Result = <protocol.Diagnostic[]>session.executeCommand(dTsFile2GetErrRequest).response;
assert.isTrue(error2Result.length === 0);
});
it("should report semantic errors for loose JS files with '// @ts-check' and skipLibCheck=true", () => {
const jsFile = {
path: "/a/jsFile.js",
content: `
// @ts-check
let x = 1;
x === "string";`
};
const host = createServerHost([jsFile]);
const session = createSession(host);
openFilesForSession([jsFile], session);
const getErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: jsFile.path }
);
const errorResult = <protocol.Diagnostic[]>session.executeCommand(getErrRequest).response;
assert.isTrue(errorResult.length === 1);
assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code);
});
it("should report semantic errors for configured js project with '// @ts-check' and skipLibCheck=true", () => {
const jsconfigFile = {
path: "/a/jsconfig.json",
content: "{}"
};
const jsFile = {
path: "/a/jsFile.js",
content: `
// @ts-check
let x = 1;
x === "string";`
};
const host = createServerHost([jsconfigFile, jsFile]);
const session = createSession(host);
openFilesForSession([jsFile], session);
const getErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: jsFile.path }
);
const errorResult = <protocol.Diagnostic[]>session.executeCommand(getErrRequest).response;
assert.isTrue(errorResult.length === 1);
assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code);
});
it("should report semantic errors for configured js project with checkJs=true and skipLibCheck=true", () => {
const jsconfigFile = {
path: "/a/jsconfig.json",
content: JSON.stringify({
compilerOptions: {
checkJs: true,
skipLibCheck: true
},
})
};
const jsFile = {
path: "/a/jsFile.js",
content: `let x = 1;
x === "string";`
};
const host = createServerHost([jsconfigFile, jsFile]);
const session = createSession(host);
openFilesForSession([jsFile], session);
const getErrRequest = makeSessionRequest<protocol.SemanticDiagnosticsSyncRequestArgs>(
CommandNames.SemanticDiagnosticsSync,
{ file: jsFile.path }
);
const errorResult = <protocol.Diagnostic[]>session.executeCommand(getErrRequest).response;
assert.isTrue(errorResult.length === 1);
assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code);
});
});
}
@@ -0,0 +1,282 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: symLinks", () => {
it("rename in common file renames all project", () => {
const projects = "/users/username/projects";
const folderA = `${projects}/a`;
const aFile: File = {
path: `${folderA}/a.ts`,
content: `import {C} from "./c/fc"; console.log(C)`
};
const aTsconfig: File = {
path: `${folderA}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { module: "commonjs" } })
};
const aC: SymLink = {
path: `${folderA}/c`,
symLink: "../c"
};
const aFc = `${folderA}/c/fc.ts`;
const folderB = `${projects}/b`;
const bFile: File = {
path: `${folderB}/b.ts`,
content: `import {C} from "./c/fc"; console.log(C)`
};
const bTsconfig: File = {
path: `${folderB}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { module: "commonjs" } })
};
const bC: SymLink = {
path: `${folderB}/c`,
symLink: "../c"
};
const bFc = `${folderB}/c/fc.ts`;
const folderC = `${projects}/c`;
const cFile: File = {
path: `${folderC}/fc.ts`,
content: `export const C = 8`
};
const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC];
const host = createServerHost(files);
const session = createSession(host);
const projectService = session.getProjectService();
openFilesForSession(
[
{ file: aFile, projectRootPath: folderA },
{ file: bFile, projectRootPath: folderB },
{ file: aFc, projectRootPath: folderA },
{ file: bFc, projectRootPath: folderB },
],
session);
checkNumberOfProjects(projectService, { configuredProjects: 2 });
assert.isDefined(projectService.configuredProjects.get(aTsconfig.path));
assert.isDefined(projectService.configuredProjects.get(bTsconfig.path));
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, { file: aFc, ...protocolLocationFromSubstring(cFile.content, "C") });
assert.equal(aFile.content, bFile.content);
const abLocs: protocol.RenameTextSpan[] = [
protocolRenameSpanFromSubstring(aFile.content, "C"),
protocolRenameSpanFromSubstring(aFile.content, "C", { index: 1 }),
];
const span = protocolRenameSpanFromSubstring(cFile.content, "C");
const cLocs: protocol.RenameTextSpan[] = [span];
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "C",
fileToRename: undefined,
fullDisplayName: '"/users/username/projects/a/c/fc".C',
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
triggerSpan: protocolTextSpanFromSubstring(cFile.content, "C"),
},
locs: [
{ file: aFc, locs: cLocs },
{ file: aFile.path, locs: abLocs },
{ file: bFc, locs: cLocs },
{ file: bFile.path, locs: abLocs },
],
});
});
describe("module resolution when symlinked folder contents change and resolve modules", () => {
const projectRootPath = "/users/username/projects/myproject";
const packages = `${projectRootPath}/javascript/packages`;
const recognizersDateTime = `${packages}/recognizers-date-time`;
const recognizersText = `${packages}/recognizers-text`;
const recognizersTextDist = `${recognizersText}/dist`;
const moduleName = "@microsoft/recognizers-text";
const moduleNameInFile = `"${moduleName}"`;
const recognizersDateTimeSrcFile: File = {
path: `${recognizersDateTime}/src/datetime/baseDate.ts`,
content: `import {C} from ${moduleNameInFile};
new C();`
};
const recognizerDateTimeTsconfigPath = `${recognizersDateTime}/tsconfig.json`;
const recognizerDateTimeTsconfigWithoutPathMapping: File = {
path: recognizerDateTimeTsconfigPath,
content: JSON.stringify({
include: ["src"]
})
};
const recognizerDateTimeTsconfigWithPathMapping: File = {
path: recognizerDateTimeTsconfigPath,
content: JSON.stringify({
compilerOptions: {
rootDir: "src",
baseUrl: "./",
paths: {
"@microsoft/*": ["../*"]
}
},
include: ["src"]
})
};
const nodeModulesRecorgnizersText: SymLink = {
path: `${recognizersDateTime}/node_modules/@microsoft/recognizers-text`,
symLink: recognizersText
};
const recognizerTextSrcFile: File = {
path: `${recognizersText}/src/recognizers-text.ts`,
content: `export class C { method () { return 10; } }`
};
const recongnizerTextDistTypingFile: File = {
path: `${recognizersTextDist}/types/recognizers-text.d.ts`,
content: `export class C { method(): number; }`
};
const recongnizerTextPackageJson: File = {
path: `${recognizersText}/package.json`,
content: JSON.stringify({
typings: "dist/types/recognizers-text.d.ts"
})
};
const filesInProjectWithUnresolvedModule = [recognizerDateTimeTsconfigPath, libFile.path, recognizersDateTimeSrcFile.path];
const filesInProjectWithResolvedModule = [...filesInProjectWithUnresolvedModule, recongnizerTextDistTypingFile.path];
function verifyErrors(session: TestSession, semanticErrors: protocol.Diagnostic[]) {
session.clearMessages();
const expectedSequenceId = session.getNextSeq();
session.executeCommandSeq<protocol.GeterrRequest>({
command: server.CommandNames.Geterr,
arguments: {
delay: 0,
files: [recognizersDateTimeSrcFile.path],
}
});
const host = session.host;
host.checkTimeoutQueueLengthAndRun(1);
checkErrorMessage(session, "syntaxDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: [] });
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "semanticDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: semanticErrors });
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
checkErrorMessage(session, "suggestionDiag", {
file: recognizersDateTimeSrcFile.path,
diagnostics: [],
});
checkCompleteEvent(session, 2, expectedSequenceId);
}
function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: ReadonlyMap<number>, nonRecursiveDirectories: string[]) {
checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1);
checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false);
checkWatchedDirectoriesDetailed(host, recursiveDirectories, /*recursive*/ true);
}
function createSessionAndOpenFile(host: TestServerHost) {
const session = createSession(host, { canUseEvents: true });
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: {
file: recognizersDateTimeSrcFile.path,
projectRootPath
}
});
return session;
}
function verifyModuleResolution(withPathMapping: boolean) {
describe(withPathMapping ? "when tsconfig file contains path mapping" : "when tsconfig does not contain path mapping", () => {
const filesWithSources = [libFile, recognizersDateTimeSrcFile, withPathMapping ? recognizerDateTimeTsconfigWithPathMapping : recognizerDateTimeTsconfigWithoutPathMapping, recognizerTextSrcFile, recongnizerTextPackageJson];
const filesWithNodeModulesSetup = [...filesWithSources, nodeModulesRecorgnizersText];
const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile];
const watchedDirectoriesWithResolvedModule = arrayToMap(getTypeRootsFromLocation(recognizersDateTime), k => k, () => 1);
watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/src`, withPathMapping ? 1 : 2); // wild card + failed lookups
if (!withPathMapping) {
watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/node_modules`, 1); // failed lookups
}
const watchedDirectoriesWithUnresolvedModule = cloneMap(watchedDirectoriesWithResolvedModule);
watchedDirectoriesWithUnresolvedModule.set(`${recognizersDateTime}/src`, 2); // wild card + failed lookups
[`${recognizersDateTime}/node_modules`, ...(withPathMapping ? [recognizersText] : emptyArray), ...getNodeModuleDirectories(packages)].forEach(d => {
watchedDirectoriesWithUnresolvedModule.set(d, 1);
});
const nonRecursiveWatchedDirectories = withPathMapping ? [packages] : emptyArray;
function verifyProjectWithResolvedModule(session: TestSession) {
const projectService = session.getProjectService();
const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!;
checkProjectActualFiles(project, filesInProjectWithResolvedModule);
verifyWatchedFilesAndDirectories(session.host, filesInProjectWithResolvedModule, watchedDirectoriesWithResolvedModule, nonRecursiveWatchedDirectories);
verifyErrors(session, []);
}
function verifyProjectWithUnresolvedModule(session: TestSession) {
const projectService = session.getProjectService();
const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!;
checkProjectActualFiles(project, filesInProjectWithUnresolvedModule);
verifyWatchedFilesAndDirectories(session.host, filesInProjectWithUnresolvedModule, watchedDirectoriesWithUnresolvedModule, nonRecursiveWatchedDirectories);
const startOffset = recognizersDateTimeSrcFile.content.indexOf('"') + 1;
verifyErrors(session, [
createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + moduleNameInFile.length }, Diagnostics.Cannot_find_module_0, [moduleName])
]);
}
it("when project compiles from sources", () => {
const host = createServerHost(filesWithSources);
const session = createSessionAndOpenFile(host);
verifyProjectWithUnresolvedModule(session);
host.reloadFS(filesAfterCompilation);
host.runQueuedTimeoutCallbacks();
verifyProjectWithResolvedModule(session);
});
it("when project has node_modules setup but doesnt have modules in typings folder and then recompiles", () => {
const host = createServerHost(filesWithNodeModulesSetup);
const session = createSessionAndOpenFile(host);
verifyProjectWithUnresolvedModule(session);
host.reloadFS(filesAfterCompilation);
host.runQueuedTimeoutCallbacks();
if (withPathMapping) {
verifyProjectWithResolvedModule(session);
}
else {
// Cannot handle the resolution update
verifyProjectWithUnresolvedModule(session);
}
});
it("when project recompiles after deleting generated folders", () => {
const host = createServerHost(filesAfterCompilation);
const session = createSessionAndOpenFile(host);
verifyProjectWithResolvedModule(session);
host.deleteFolder(recognizersTextDist, /*recursive*/ true);
host.runQueuedTimeoutCallbacks();
verifyProjectWithUnresolvedModule(session);
host.ensureFileOrFolder(recongnizerTextDistTypingFile);
host.runQueuedTimeoutCallbacks();
if (withPathMapping) {
verifyProjectWithResolvedModule(session);
}
else {
// Cannot handle the resolution update
verifyProjectWithUnresolvedModule(session);
}
});
});
}
verifyModuleResolution(/*withPathMapping*/ false);
verifyModuleResolution(/*withPathMapping*/ true);
});
});
}
@@ -0,0 +1,98 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: syntax operations", () => {
function navBarFull(session: TestSession, file: File) {
return JSON.stringify(session.executeCommandSeq<protocol.FileRequest>({
command: protocol.CommandTypes.NavBarFull,
arguments: { file: file.path }
}).response);
}
function openFile(session: TestSession, file: File) {
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: { file: file.path, fileContent: file.content }
});
}
it("works when file is removed and added with different content", () => {
const projectRoot = "/user/username/projects/myproject";
const app: File = {
path: `${projectRoot}/app.ts`,
content: "console.log('Hello world');"
};
const unitTest1: File = {
path: `${projectRoot}/unitTest1.ts`,
content: `import assert = require('assert');
describe("Test Suite 1", () => {
it("Test A", () => {
assert.ok(true, "This shouldn't fail");
});
it("Test B", () => {
assert.ok(1 === 1, "This shouldn't fail");
assert.ok(false, "This should fail");
});
});`
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: "{}"
};
const files = [app, libFile, tsconfig];
const host = createServerHost(files);
const session = createSession(host);
const service = session.getProjectService();
openFile(session, app);
checkNumberOfProjects(service, { configuredProjects: 1 });
const project = service.configuredProjects.get(tsconfig.path)!;
const expectedFilesWithoutUnitTest1 = files.map(f => f.path);
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
host.writeFile(unitTest1.path, unitTest1.content);
host.runQueuedTimeoutCallbacks();
const expectedFilesWithUnitTest1 = expectedFilesWithoutUnitTest1.concat(unitTest1.path);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
openFile(session, unitTest1);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
const navBarResultUnitTest1 = navBarFull(session, unitTest1);
host.deleteFile(unitTest1.path);
host.checkTimeoutQueueLengthAndRun(2);
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
session.executeCommandSeq<protocol.CloseRequest>({
command: protocol.CommandTypes.Close,
arguments: { file: unitTest1.path }
});
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
const unitTest1WithChangedContent: File = {
path: unitTest1.path,
content: `import assert = require('assert');
export function Test1() {
assert.ok(true, "This shouldn't fail");
};
export function Test2() {
assert.ok(1 === 1, "This shouldn't fail");
assert.ok(false, "This should fail");
};`
};
host.writeFile(unitTest1.path, unitTest1WithChangedContent.content);
host.runQueuedTimeoutCallbacks();
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
openFile(session, unitTest1WithChangedContent);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
const sourceFile = project.getLanguageService().getNonBoundSourceFile(unitTest1WithChangedContent.path);
assert.strictEqual(sourceFile.text, unitTest1WithChangedContent.content);
const navBarResultUnitTest1WithChangedContent = navBarFull(session, unitTest1WithChangedContent);
assert.notStrictEqual(navBarResultUnitTest1WithChangedContent, navBarResultUnitTest1, "With changes in contents of unitTest file, we should see changed naviagation bar item result");
});
});
}
@@ -1,5 +1,5 @@
namespace ts.projectSystem {
describe("project telemetry", () => {
describe("unittests:: tsserver:: project telemetry", () => {
it("does nothing for inferred project", () => {
const file = makeFile("/a.js");
const et = new TestServerEventManager([file]);
@@ -1,5 +1,5 @@
namespace ts.textStorage {
describe("Text storage", () => {
describe("unittests:: tsserver:: Text storage", () => {
const f = {
path: "/a/app.ts",
content: `
@@ -0,0 +1,52 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: autoDiscovery", () => {
it("does not depend on extension", () => {
const file1 = {
path: "/a/b/app.html",
content: ""
};
const file2 = {
path: "/a/b/app.d.ts",
content: ""
};
const host = createServerHost([file1, file2]);
const projectService = createProjectService(host);
projectService.openExternalProject({
projectFileName: "/a/b/proj.csproj",
rootFiles: [toExternalFile(file2.path), { fileName: file1.path, hasMixedContent: true, scriptKind: ScriptKind.JS }],
options: {}
});
projectService.checkNumberOfProjects({ externalProjects: 1 });
const typeAcquisition = projectService.externalProjects[0].getTypeAcquisition();
assert.isTrue(typeAcquisition.enable, "Typine acquisition should be enabled");
});
});
describe("unittests:: tsserver:: prefer typings to js", () => {
it("during second resolution pass", () => {
const typingsCacheLocation = "/a/typings";
const f1 = {
path: "/a/b/app.js",
content: "var x = require('bar')"
};
const barjs = {
path: "/a/b/node_modules/bar/index.js",
content: "export let x = 1"
};
const barTypings = {
path: `${typingsCacheLocation}/node_modules/@types/bar/index.d.ts`,
content: "export let y: number"
};
const config = {
path: "/a/b/jsconfig.json",
content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: ["node_modules"] })
};
const host = createServerHost([f1, barjs, barTypings, config]);
const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller(typingsCacheLocation, /*throttleLimit*/ 5, host) });
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, barTypings.path, config.path]);
});
});
}

Some files were not shown because too many files have changed in this diff Show More