1namespace ts { 2 const _chai: typeof import("chai") = require("chai"); 3 const expect: typeof _chai.expect = _chai.expect; 4 describe("unittests:: services:: languageService", () => { 5 const files: {[index: string]: string} = { 6 "foo.ts": `import Vue from "./vue"; 7import Component from "./vue-class-component"; 8import { vueTemplateHtml } from "./variables"; 9 10@Component({ 11 template: vueTemplateHtml, 12}) 13class Carousel<T> extends Vue { 14}`, 15 "variables.ts": `export const vueTemplateHtml = \`<div></div>\`;`, 16 "vue.d.ts": `export namespace Vue { export type Config = { template: string }; }`, 17 "vue-class-component.d.ts": `import Vue from "./vue"; 18export function Component(x: Config): any;` 19 }; 20 21 function createLanguageService() { 22 return ts.createLanguageService({ 23 getCompilationSettings() { 24 return {}; 25 }, 26 getScriptFileNames() { 27 return ["foo.ts", "variables.ts", "vue.d.ts", "vue-class-component.d.ts"]; 28 }, 29 getScriptVersion(_fileName) { 30 return ""; 31 }, 32 getScriptSnapshot(fileName) { 33 if (fileName === ".ts") { 34 return ScriptSnapshot.fromString(""); 35 } 36 return ScriptSnapshot.fromString(files[fileName] || ""); 37 }, 38 getCurrentDirectory: () => ".", 39 getDefaultLibFileName(options) { 40 return getDefaultLibFilePath(options); 41 }, 42 fileExists: name => !!files[name], 43 readFile: name => files[name] 44 }); 45 } 46 // Regression test for GH #18245 - bug in single line comment writer caused a debug assertion when attempting 47 // to write an alias to a module's default export was referrenced across files and had no default export 48 it("should be able to create a language service which can respond to deinition requests without throwing", () => { 49 const languageService = createLanguageService(); 50 const definitions = languageService.getDefinitionAtPosition("foo.ts", 160); // 160 is the latter `vueTemplateHtml` position 51 expect(definitions).to.exist; // eslint-disable-line @typescript-eslint/no-unused-expressions 52 }); 53 54 it("getEmitOutput on language service has way to force dts emit", () => { 55 const languageService = createLanguageService(); 56 assert.deepEqual( 57 languageService.getEmitOutput( 58 "foo.ts", 59 /*emitOnlyDtsFiles*/ true 60 ), 61 { 62 emitSkipped: true, 63 diagnostics: emptyArray, 64 outputFiles: emptyArray, 65 } 66 ); 67 68 assert.deepEqual( 69 languageService.getEmitOutput( 70 "foo.ts", 71 /*emitOnlyDtsFiles*/ true, 72 /*forceDtsEmit*/ true 73 ), 74 { 75 emitSkipped: false, 76 diagnostics: emptyArray, 77 outputFiles: [{ 78 name: "foo.d.ts", 79 text: "export {};\r\n", 80 writeByteOrderMark: false 81 }], 82 } 83 ); 84 }); 85 86 describe("detects program upto date correctly", () => { 87 function verifyProgramUptoDate(useProjectVersion: boolean) { 88 let projectVersion = "1"; 89 const files = new Map<string, { version: string, text: string; }>(); 90 files.set("/project/root.ts", { version: "1", text: `import { foo } from "./other"` }); 91 files.set("/project/other.ts", { version: "1", text: `export function foo() { }` }); 92 files.set("/lib/lib.d.ts", { version: "1", text: projectSystem.libFile.content }); 93 const host: LanguageServiceHost = { 94 useCaseSensitiveFileNames: returnTrue, 95 getCompilationSettings: getDefaultCompilerOptions, 96 fileExists: path => files.has(path), 97 readFile: path => files.get(path)?.text, 98 getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, 99 getScriptFileNames: () => ["/project/root.ts"], 100 getScriptVersion: path => files.get(path)?.version || "", 101 getScriptSnapshot: path => { 102 const text = files.get(path)?.text; 103 return text ? ScriptSnapshot.fromString(text) : undefined; 104 }, 105 getCurrentDirectory: () => "/project", 106 getDefaultLibFileName: () => "/lib/lib.d.ts" 107 }; 108 const ls = ts.createLanguageService(host); 109 const program1 = ls.getProgram()!; 110 const program2 = ls.getProgram()!; 111 assert.strictEqual(program1, program2); 112 verifyProgramFiles(program1); 113 114 // Change other 115 projectVersion = "2"; 116 files.set("/project/other.ts", { version: "2", text: `export function foo() { } export function bar() { }` }); 117 const program3 = ls.getProgram()!; 118 assert.notStrictEqual(program2, program3); 119 verifyProgramFiles(program3); 120 121 // change root 122 projectVersion = "3"; 123 files.set("/project/root.ts", { version: "2", text: `import { foo, bar } from "./other"` }); 124 const program4 = ls.getProgram()!; 125 assert.notStrictEqual(program3, program4); 126 verifyProgramFiles(program4); 127 128 function verifyProgramFiles(program: Program) { 129 assert.deepEqual( 130 program.getSourceFiles().map(f => f.fileName), 131 ["/lib/lib.d.ts", "/project/other.ts", "/project/root.ts"] 132 ); 133 } 134 } 135 it("when host implements getProjectVersion", () => { 136 verifyProgramUptoDate(/*useProjectVersion*/ true); 137 }); 138 it("when host does not implement getProjectVersion", () => { 139 verifyProgramUptoDate(/*useProjectVersion*/ false); 140 }); 141 }); 142 143 describe("detects program upto date when new file is added to the referenced project", () => { 144 function setup(useSourceOfProjectReferenceRedirect: (() => boolean) | undefined) { 145 const config1: TestFSWithWatch.File = { 146 path: `${tscWatch.projectRoot}/projects/project1/tsconfig.json`, 147 content: JSON.stringify({ 148 compilerOptions: { 149 module: "none", 150 composite: true 151 }, 152 exclude: ["temp"] 153 }) 154 }; 155 const class1: TestFSWithWatch.File = { 156 path: `${tscWatch.projectRoot}/projects/project1/class1.ts`, 157 content: `class class1 {}` 158 }; 159 const class1Dts: TestFSWithWatch.File = { 160 path: `${tscWatch.projectRoot}/projects/project1/class1.d.ts`, 161 content: `declare class class1 {}` 162 }; 163 const config2: TestFSWithWatch.File = { 164 path: `${tscWatch.projectRoot}/projects/project2/tsconfig.json`, 165 content: JSON.stringify({ 166 compilerOptions: { 167 module: "none", 168 composite: true 169 }, 170 references: [ 171 { path: "../project1" } 172 ] 173 }) 174 }; 175 const class2: TestFSWithWatch.File = { 176 path: `${tscWatch.projectRoot}/projects/project2/class2.ts`, 177 content: `class class2 {}` 178 }; 179 const system = projectSystem.createServerHost([config1, class1, class1Dts, config2, class2, projectSystem.libFile]); 180 const result = getParsedCommandLineOfConfigFile(`${tscWatch.projectRoot}/projects/project2/tsconfig.json`, /*optionsToExtend*/ undefined, { 181 useCaseSensitiveFileNames: true, 182 fileExists: path => system.fileExists(path), 183 readFile: path => system.readFile(path), 184 getCurrentDirectory: () => system.getCurrentDirectory(), 185 readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), 186 onUnRecoverableConfigFileDiagnostic: noop, 187 })!; 188 const host: LanguageServiceHost = { 189 useCaseSensitiveFileNames: returnTrue, 190 useSourceOfProjectReferenceRedirect, 191 getCompilationSettings: () => result.options, 192 fileExists: path => system.fileExists(path), 193 readFile: path => system.readFile(path), 194 getScriptFileNames: () => result.fileNames, 195 getScriptVersion: path => { 196 const text = system.readFile(path); 197 return text !== undefined ? system.createHash(path) : ""; 198 }, 199 getScriptSnapshot: path => { 200 const text = system.readFile(path); 201 return text ? ScriptSnapshot.fromString(text) : undefined; 202 }, 203 readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), 204 getCurrentDirectory: () => system.getCurrentDirectory(), 205 getDefaultLibFileName: () => projectSystem.libFile.path, 206 getProjectReferences: () => result.projectReferences, 207 }; 208 const ls = ts.createLanguageService(host); 209 return { system, ls, class1, class1Dts, class2 }; 210 } 211 it("detects program upto date when new file is added to the referenced project", () => { 212 const { ls, system, class1, class2 } = setup(returnTrue); 213 assert.deepEqual( 214 ls.getProgram()!.getSourceFiles().map(f => f.fileName), 215 [projectSystem.libFile.path, class1.path, class2.path] 216 ); 217 // Add new file to referenced project 218 const class3 = `${tscWatch.projectRoot}/projects/project1/class3.ts`; 219 system.writeFile(class3, `class class3 {}`); 220 const program = ls.getProgram()!; 221 assert.deepEqual( 222 program.getSourceFiles().map(f => f.fileName), 223 [projectSystem.libFile.path, class1.path, class3, class2.path] 224 ); 225 // Add excluded file to referenced project 226 system.ensureFileOrFolder({ path: `${tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); 227 assert.strictEqual(ls.getProgram(), program); 228 // Add output from new class to referenced project 229 system.writeFile(`${tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`); 230 assert.strictEqual(ls.getProgram(), program); 231 }); 232 233 it("detects program upto date when new file is added to the referenced project without useSourceOfProjectReferenceRedirect", () => { 234 const { ls, system, class1Dts, class2 } = setup(/*useSourceOfProjectReferenceRedirect*/ undefined); 235 const program1 = ls.getProgram()!; 236 assert.deepEqual( 237 program1.getSourceFiles().map(f => f.fileName), 238 [projectSystem.libFile.path, class1Dts.path, class2.path] 239 ); 240 // Add new file to referenced project 241 const class3 = `${tscWatch.projectRoot}/projects/project1/class3.ts`; 242 system.writeFile(class3, `class class3 {}`); 243 assert.notStrictEqual(ls.getProgram(), program1); 244 assert.deepEqual( 245 ls.getProgram()!.getSourceFiles().map(f => f.fileName), 246 [projectSystem.libFile.path, class1Dts.path, class2.path] 247 ); 248 // Add class3 output 249 const class3Dts = `${tscWatch.projectRoot}/projects/project1/class3.d.ts`; 250 system.writeFile(class3Dts, `declare class class3 {}`); 251 const program2 = ls.getProgram()!; 252 assert.deepEqual( 253 program2.getSourceFiles().map(f => f.fileName), 254 [projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path] 255 ); 256 // Add excluded file to referenced project 257 system.ensureFileOrFolder({ path: `${tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); 258 assert.strictEqual(ls.getProgram(), program2); 259 // Delete output from new class to referenced project 260 system.deleteFile(class3Dts); 261 assert.deepEqual( 262 ls.getProgram()!.getSourceFiles().map(f => f.fileName), 263 [projectSystem.libFile.path, class1Dts.path, class2.path] 264 ); 265 // Write output again 266 system.writeFile(class3Dts, `declare class class3 {}`); 267 assert.deepEqual( 268 ls.getProgram()!.getSourceFiles().map(f => f.fileName), 269 [projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path] 270 ); 271 }); 272 }); 273 }); 274} 275