1namespace ts.tscWatch { 2 describe("unittests:: tsc-watch:: emit file --incremental", () => { 3 const project = "/users/username/projects/project"; 4 5 const configFile: File = { 6 path: `${project}/tsconfig.json`, 7 content: JSON.stringify({ compilerOptions: { incremental: true } }) 8 }; 9 10 interface VerifyIncrementalWatchEmitInput { 11 subScenario: string; 12 files: () => readonly File[]; 13 optionsToExtend?: readonly string[]; 14 modifyFs?: (host: WatchedSystem) => void; 15 } 16 function verifyIncrementalWatchEmit(input: VerifyIncrementalWatchEmitInput) { 17 describe(input.subScenario, () => { 18 it("with tsc --w", () => { 19 verifyIncrementalWatchEmitWorker(input, /*incremental*/ false); 20 }); 21 it("with tsc", () => { 22 verifyIncrementalWatchEmitWorker(input, /*incremental*/ true); 23 }); 24 }); 25 } 26 27 function verifyIncrementalWatchEmitWorker( 28 { subScenario, files, optionsToExtend, modifyFs }: VerifyIncrementalWatchEmitInput, 29 incremental: boolean 30 ) { 31 const { sys, baseline, oldSnap } = createBaseline(createWatchedSystem(files(), { currentDirectory: project })); 32 if (incremental) sys.exit = exitCode => sys.exitCode = exitCode; 33 const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || emptyArray)]; 34 baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`); 35 const { cb, getPrograms } = commandLineCallbacks(sys); 36 build(oldSnap); 37 38 if (modifyFs) { 39 const oldSnap = applyChange(sys, baseline, modifyFs); 40 build(oldSnap); 41 } 42 43 Harness.Baseline.runBaseline(`${isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n")); 44 45 function build(oldSnap: SystemSnap) { 46 const closer = executeCommandLine( 47 sys, 48 cb, 49 argsToPass, 50 ); 51 watchBaseline({ 52 baseline, 53 getPrograms, 54 sys, 55 oldSnap 56 }); 57 if (closer) closer.close(); 58 } 59 } 60 61 describe("non module compilation", () => { 62 const file1: File = { 63 path: `${project}/file1.ts`, 64 content: "const x = 10;" 65 }; 66 const file2: File = { 67 path: `${project}/file2.ts`, 68 content: "const y = 20;" 69 }; 70 describe("own file emit without errors", () => { 71 function verify(subScenario: string, optionsToExtend?: readonly string[]) { 72 const modifiedFile2Content = file2.content.replace("y", "z").replace("20", "10"); 73 verifyIncrementalWatchEmit({ 74 files: () => [libFile, file1, file2, configFile], 75 optionsToExtend, 76 subScenario: `own file emit without errors/${subScenario}`, 77 modifyFs: host => host.writeFile(file2.path, modifiedFile2Content), 78 }); 79 } 80 verify("without commandline options"); 81 verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]); 82 }); 83 84 verifyIncrementalWatchEmit({ 85 files: () => [libFile, file1, configFile, { 86 path: file2.path, 87 content: `const y: string = 20;` 88 }], 89 subScenario: "own file emit with errors", 90 modifyFs: host => host.writeFile(file1.path, file1.content.replace("x", "z")), 91 }); 92 93 verifyIncrementalWatchEmit({ 94 files: () => [libFile, file1, file2, { 95 path: configFile.path, 96 content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } }) 97 }], 98 subScenario: "with --out", 99 }); 100 }); 101 102 describe("module compilation", () => { 103 const file1: File = { 104 path: `${project}/file1.ts`, 105 content: "export const x = 10;" 106 }; 107 const file2: File = { 108 path: `${project}/file2.ts`, 109 content: "export const y = 20;" 110 }; 111 const config: File = { 112 path: configFile.path, 113 content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) 114 }; 115 116 verifyIncrementalWatchEmit({ 117 files: () => [libFile, file1, file2, config], 118 subScenario: "module compilation/own file emit without errors", 119 modifyFs: host => host.writeFile(file2.path, file2.content.replace("y", "z").replace("20", "10")), 120 }); 121 122 describe("own file emit with errors", () => { 123 const fileModified: File = { 124 path: file2.path, 125 content: `export const y: string = 20;` 126 }; 127 128 verifyIncrementalWatchEmit({ 129 files: () => [libFile, file1, fileModified, config], 130 subScenario: "module compilation/own file emit with errors", 131 modifyFs: host => host.writeFile(file1.path, file1.content.replace("x = 10", "z = 10")), 132 }); 133 134 it("verify that state is read correctly", () => { 135 const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project }); 136 const reportDiagnostic = createDiagnosticReporter(system); 137 const parsedConfig = parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!; 138 performIncrementalCompilation({ 139 rootNames: parsedConfig.fileNames, 140 options: parsedConfig.options, 141 projectReferences: parsedConfig.projectReferences, 142 configFileParsingDiagnostics: getConfigFileParsingDiagnostics(parsedConfig), 143 reportDiagnostic, 144 system 145 }); 146 147 const command = parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, noop)!; 148 const builderProgram = createIncrementalProgram({ 149 rootNames: command.fileNames, 150 options: command.options, 151 projectReferences: command.projectReferences, 152 configFileParsingDiagnostics: getConfigFileParsingDiagnostics(command), 153 host: createIncrementalCompilerHost(command.options, system) 154 }); 155 156 const state = builderProgram.getState(); 157 assert.equal(state.changedFilesSet!.size, 0, "changes"); 158 159 assert.equal(state.fileInfos.size, 3, "FileInfo size"); 160 assert.deepEqual(state.fileInfos.get(libFile.path as Path), { 161 version: system.createHash(libFile.content), 162 signature: system.createHash(libFile.content), 163 affectsGlobalScope: true, 164 }); 165 assert.deepEqual(state.fileInfos.get(file1.path as Path), { 166 version: system.createHash(file1.content), 167 signature: system.createHash(`${file1.content.replace("export ", "export declare ")}\n`), 168 affectsGlobalScope: false, 169 }); 170 assert.deepEqual(state.fileInfos.get(file2.path as Path), { 171 version: system.createHash(fileModified.content), 172 signature: system.createHash("export declare const y: string;\n"), 173 affectsGlobalScope: false, 174 }); 175 176 assert.deepEqual(state.compilerOptions, { 177 incremental: true, 178 module: ModuleKind.AMD, 179 configFilePath: config.path 180 }); 181 182 assert.equal(state.referencedMap!.size, 0); 183 assert.equal(state.exportedModulesMap!.size, 0); 184 185 assert.equal(state.semanticDiagnosticsPerFile!.size, 3); 186 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(libFile.path as Path), emptyArray); 187 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path as Path), emptyArray); 188 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path as Path), [{ 189 file: state.program!.getSourceFileByPath(file2.path as Path)!, 190 start: 13, 191 length: 1, 192 code: Diagnostics.Type_0_is_not_assignable_to_type_1.code, 193 category: Diagnostics.Type_0_is_not_assignable_to_type_1.category, 194 messageText: "Type 'number' is not assignable to type 'string'.", 195 relatedInformation: undefined, 196 reportsUnnecessary: undefined, 197 reportsDeprecated: undefined, 198 source: undefined, 199 skippedOn: undefined, 200 }]); 201 }); 202 }); 203 204 verifyIncrementalWatchEmit({ 205 files: () => [libFile, file1, file2, { 206 path: configFile.path, 207 content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } }) 208 }], 209 subScenario: "module compilation/with --out", 210 }); 211 }); 212 213 verifyIncrementalWatchEmit({ 214 files: () => { 215 const config: File = { 216 path: configFile.path, 217 content: JSON.stringify({ 218 compilerOptions: { 219 incremental: true, 220 target: "es5", 221 module: "commonjs", 222 declaration: true, 223 emitDeclarationOnly: true 224 } 225 }) 226 }; 227 const aTs: File = { 228 path: `${project}/a.ts`, 229 content: `import { B } from "./b"; 230export interface A { 231 b: B; 232} 233` 234 }; 235 const bTs: File = { 236 path: `${project}/b.ts`, 237 content: `import { C } from "./c"; 238export interface B { 239 b: C; 240} 241` 242 }; 243 const cTs: File = { 244 path: `${project}/c.ts`, 245 content: `import { A } from "./a"; 246export interface C { 247 a: A; 248} 249` 250 }; 251 const indexTs: File = { 252 path: `${project}/index.ts`, 253 content: `export { A } from "./a"; 254export { B } from "./b"; 255export { C } from "./c"; 256` 257 }; 258 return [libFile, aTs, bTs, cTs, indexTs, config]; 259 }, 260 subScenario: "incremental with circular references", 261 modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b"; 262export interface A { 263 b: B; 264 foo: any; 265} 266`) 267 }); 268 269 verifyIncrementalWatchEmit({ 270 subScenario: "when file with ambient global declaration file is deleted", 271 files: () => [ 272 { path: libFile.path, content: libContent }, 273 { path: `${project}/globals.d.ts`, content: `declare namespace Config { const value: string;} ` }, 274 { path: `${project}/index.ts`, content: `console.log(Config.value);` }, 275 { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, } }) } 276 ], 277 modifyFs: host => host.deleteFile(`${project}/globals.d.ts`) 278 }); 279 280 describe("with option jsxImportSource", () => { 281 const jsxImportSourceOptions = { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" }; 282 const jsxLibraryContent = `export namespace JSX { 283 interface Element {} 284 interface IntrinsicElements { 285 div: { 286 propA?: boolean; 287 }; 288 } 289} 290export function jsx(...args: any[]): void; 291export function jsxs(...args: any[]): void; 292export const Fragment: unique symbol; 293`; 294 295 verifyIncrementalWatchEmit({ 296 subScenario: "jsxImportSource option changed", 297 files: () => [ 298 { path: libFile.path, content: libContent }, 299 { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, 300 { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, 301 { path: `${project}/node_modules/preact/jsx-runtime/index.d.ts`, content: jsxLibraryContent.replace("propA", "propB") }, 302 { path: `${project}/node_modules/preact/package.json`, content: JSON.stringify({ name: "preact", version: "0.0.1" }) }, 303 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 304 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 305 ], 306 modifyFs: host => host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { ...jsxImportSourceOptions, jsxImportSource: "preact" } })), 307 optionsToExtend: ["--explainFiles"] 308 }); 309 310 verifyIncrementalWatchEmit({ 311 subScenario: "jsxImportSource backing types added", 312 files: () => [ 313 { path: libFile.path, content: libContent }, 314 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 315 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 316 ], 317 modifyFs: host => { 318 host.createDirectory(`${project}/node_modules`); 319 host.createDirectory(`${project}/node_modules/react`); 320 host.createDirectory(`${project}/node_modules/react/jsx-runtime`); 321 host.writeFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`, jsxLibraryContent); 322 host.writeFile(`${project}/node_modules/react/package.json`, JSON.stringify({ name: "react", version: "0.0.1" })); 323 } 324 }); 325 326 verifyIncrementalWatchEmit({ 327 subScenario: "jsxImportSource backing types removed", 328 files: () => [ 329 { path: libFile.path, content: libContent }, 330 { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, 331 { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, 332 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 333 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 334 ], 335 modifyFs: host => { 336 host.deleteFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`); 337 host.deleteFile(`${project}/node_modules/react/package.json`); 338 } 339 }); 340 341 verifyIncrementalWatchEmit({ 342 subScenario: "importHelpers backing types removed", 343 files: () => [ 344 { path: libFile.path, content: libContent }, 345 { path: `${project}/node_modules/tslib/index.d.ts`, content: "export function __assign(...args: any[]): any;" }, 346 { path: `${project}/node_modules/tslib/package.json`, content: JSON.stringify({ name: "tslib", version: "0.0.1" }) }, 347 { path: `${project}/index.tsx`, content: `export const x = {...{}};` }, 348 { path: configFile.path, content: JSON.stringify({ compilerOptions: { importHelpers: true } }) } 349 ], 350 modifyFs: host => { 351 host.deleteFile(`${project}/node_modules/tslib/index.d.ts`); 352 host.deleteFile(`${project}/node_modules/tslib/package.json`); 353 } 354 }); 355 }); 356 }); 357} 358