mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Merge pull request #28902 from Microsoft/unitTestsSplit
Unittests refactoring
This commit is contained in:
@@ -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,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,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,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,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("compilerCore", () => {
|
||||
describe("unittests:: compilerCore", () => {
|
||||
describe("equalOwnProperties", () => {
|
||||
it("correctly equates objects", () => {
|
||||
assert.isTrue(equalOwnProperties({}, {}));
|
||||
|
||||
+2
-2
@@ -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);
|
||||
+1
-1
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("convertCompilerOptionsFromJson", () => {
|
||||
describe("unittests:: config:: convertCompilerOptionsFromJson", () => {
|
||||
const formatDiagnosticHost: FormatDiagnosticsHost = {
|
||||
getCurrentDirectory: () => "/apath/",
|
||||
getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true),
|
||||
+1
-1
@@ -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);
|
||||
+2
-2
@@ -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"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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 = {
|
||||
+6
-6
@@ -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
-1
@@ -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
-1
@@ -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,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("convertToBase64", () => {
|
||||
describe("unittests:: convertToBase64", () => {
|
||||
function runTest(input: string): void {
|
||||
const actual = convertToBase64(input);
|
||||
const expected = sys.base64encode!(input);
|
||||
|
||||
@@ -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,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,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("JSDocParsing", () => {
|
||||
describe("unittests:: JSDocParsing", () => {
|
||||
describe("TypeExpressions", () => {
|
||||
function parsesCorrectly(name: string, content: string) {
|
||||
it(name, () => {
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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, () => {
|
||||
|
||||
+30
-2
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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[],
|
||||
|
||||
@@ -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
-1
@@ -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();
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("extractConstants", () => {
|
||||
describe("unittests:: services:: extract:: extractConstants", () => {
|
||||
testExtractConstant("extractConstant_TopLevel",
|
||||
`let x = [#|1|];`);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("extractFunctions", () => {
|
||||
describe("unittests:: services:: extract:: extractFunctions", () => {
|
||||
testExtractFunction("extractFunction1",
|
||||
`namespace A {
|
||||
let x = 1;
|
||||
+2
-2
@@ -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]);
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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 {
|
||||
`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
-1
@@ -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
-2
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("Transpile", () => {
|
||||
describe("unittests:: services:: Transpile", () => {
|
||||
|
||||
interface TranspileTestSettings {
|
||||
options?: TranspileOptions;
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace ts {
|
||||
describe("TransformAPI", () => {
|
||||
describe("unittests:: TransformAPI", () => {
|
||||
function replaceUndefinedWithVoid0(context: TransformationContext) {
|
||||
const previousOnSubstituteNode = context.onSubstituteNode;
|
||||
context.enableSubstitution(SyntaxKind.Identifier);
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+146
-2
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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
-1
@@ -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
-1
@@ -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
Reference in New Issue
Block a user