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, cb, getPrograms } = 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 let oldPrograms: readonly CommandLineProgram[] = emptyArray; 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 oldPrograms = watchBaseline({ 52 baseline, 53 getPrograms, 54 oldPrograms, 55 sys, 56 oldSnap 57 }); 58 if (closer) closer.close(); 59 } 60 } 61 62 describe("non module compilation", () => { 63 const file1: File = { 64 path: `${project}/file1.ts`, 65 content: "const x = 10;" 66 }; 67 const file2: File = { 68 path: `${project}/file2.ts`, 69 content: "const y = 20;" 70 }; 71 describe("own file emit without errors", () => { 72 function verify(subScenario: string, optionsToExtend?: readonly string[]) { 73 const modifiedFile2Content = file2.content.replace("y", "z").replace("20", "10"); 74 verifyIncrementalWatchEmit({ 75 files: () => [libFile, file1, file2, configFile], 76 optionsToExtend, 77 subScenario: `own file emit without errors/${subScenario}`, 78 modifyFs: host => host.writeFile(file2.path, modifiedFile2Content), 79 }); 80 } 81 verify("without commandline options"); 82 verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]); 83 }); 84 85 verifyIncrementalWatchEmit({ 86 files: () => [libFile, file1, configFile, { 87 path: file2.path, 88 content: `const y: string = 20;` 89 }], 90 subScenario: "own file emit with errors", 91 modifyFs: host => host.writeFile(file1.path, file1.content.replace("x", "z")), 92 }); 93 94 verifyIncrementalWatchEmit({ 95 files: () => [libFile, file1, file2, { 96 path: configFile.path, 97 content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } }) 98 }], 99 subScenario: "with --out", 100 }); 101 }); 102 103 describe("module compilation", () => { 104 const file1: File = { 105 path: `${project}/file1.ts`, 106 content: "export const x = 10;" 107 }; 108 const file2: File = { 109 path: `${project}/file2.ts`, 110 content: "export const y = 20;" 111 }; 112 const config: File = { 113 path: configFile.path, 114 content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) 115 }; 116 117 verifyIncrementalWatchEmit({ 118 files: () => [libFile, file1, file2, config], 119 subScenario: "module compilation/own file emit without errors", 120 modifyFs: host => host.writeFile(file2.path, file2.content.replace("y", "z").replace("20", "10")), 121 }); 122 123 describe("own file emit with errors", () => { 124 const fileModified: File = { 125 path: file2.path, 126 content: `export const y: string = 20;` 127 }; 128 129 verifyIncrementalWatchEmit({ 130 files: () => [libFile, file1, fileModified, config], 131 subScenario: "module compilation/own file emit with errors", 132 modifyFs: host => host.writeFile(file1.path, file1.content.replace("x = 10", "z = 10")), 133 }); 134 135 it("verify that state is read correctly", () => { 136 const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project }); 137 const reportDiagnostic = createDiagnosticReporter(system); 138 const parsedConfig = parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!; 139 performIncrementalCompilation({ 140 rootNames: parsedConfig.fileNames, 141 options: parsedConfig.options, 142 projectReferences: parsedConfig.projectReferences, 143 configFileParsingDiagnostics: getConfigFileParsingDiagnostics(parsedConfig), 144 reportDiagnostic, 145 system 146 }); 147 148 const command = parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, noop)!; 149 const builderProgram = createIncrementalProgram({ 150 rootNames: command.fileNames, 151 options: command.options, 152 projectReferences: command.projectReferences, 153 configFileParsingDiagnostics: getConfigFileParsingDiagnostics(command), 154 host: createIncrementalCompilerHost(command.options, system) 155 }); 156 157 const state = builderProgram.getState(); 158 assert.equal(state.changedFilesSet!.size, 0, "changes"); 159 160 assert.equal(state.fileInfos.size, 3, "FileInfo size"); 161 assert.deepEqual(state.fileInfos.get(libFile.path as Path), { 162 version: system.createHash(libFile.content), 163 signature: system.createHash(libFile.content), 164 affectsGlobalScope: true, 165 impliedFormat: undefined, 166 }); 167 assert.deepEqual(state.fileInfos.get(file1.path as Path), { 168 version: system.createHash(file1.content), 169 signature: system.createHash(file1.content), 170 affectsGlobalScope: undefined, 171 impliedFormat: undefined, 172 }); 173 assert.deepEqual(state.fileInfos.get(file2.path as Path), { 174 version: system.createHash(fileModified.content), 175 signature: system.createHash(fileModified.content), 176 affectsGlobalScope: undefined, 177 impliedFormat: undefined, 178 }); 179 180 assert.deepEqual(state.compilerOptions, { 181 incremental: true, 182 module: ModuleKind.AMD, 183 configFilePath: config.path 184 }); 185 186 assert.equal(arrayFrom(state.referencedMap!.keys()).length, 0); 187 assert.equal(arrayFrom(state.exportedModulesMap!.keys()).length, 0); 188 189 assert.equal(state.semanticDiagnosticsPerFile!.size, 3); 190 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(libFile.path as Path), emptyArray); 191 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path as Path), emptyArray); 192 assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path as Path), [{ 193 file: state.program!.getSourceFileByPath(file2.path as Path)!, 194 start: 13, 195 length: 1, 196 code: Diagnostics.Type_0_is_not_assignable_to_type_1.code, 197 category: Diagnostics.Type_0_is_not_assignable_to_type_1.category, 198 messageText: "Type 'number' is not assignable to type 'string'.", 199 relatedInformation: undefined, 200 reportsUnnecessary: undefined, 201 reportsDeprecated: undefined, 202 source: undefined, 203 skippedOn: undefined, 204 }]); 205 }); 206 }); 207 208 verifyIncrementalWatchEmit({ 209 files: () => [libFile, file1, file2, { 210 path: configFile.path, 211 content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } }) 212 }], 213 subScenario: "module compilation/with --out", 214 }); 215 }); 216 217 verifyIncrementalWatchEmit({ 218 files: () => { 219 const config: File = { 220 path: configFile.path, 221 content: JSON.stringify({ 222 compilerOptions: { 223 incremental: true, 224 target: "es5", 225 module: "commonjs", 226 declaration: true, 227 emitDeclarationOnly: true 228 } 229 }) 230 }; 231 const aTs: File = { 232 path: `${project}/a.ts`, 233 content: `import { B } from "./b"; 234export interface A { 235 b: B; 236} 237` 238 }; 239 const bTs: File = { 240 path: `${project}/b.ts`, 241 content: `import { C } from "./c"; 242export interface B { 243 b: C; 244} 245` 246 }; 247 const cTs: File = { 248 path: `${project}/c.ts`, 249 content: `import { A } from "./a"; 250export interface C { 251 a: A; 252} 253` 254 }; 255 const indexTs: File = { 256 path: `${project}/index.ts`, 257 content: `export { A } from "./a"; 258export { B } from "./b"; 259export { C } from "./c"; 260` 261 }; 262 return [libFile, aTs, bTs, cTs, indexTs, config]; 263 }, 264 subScenario: "incremental with circular references", 265 modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b"; 266export interface A { 267 b: B; 268 foo: any; 269} 270`) 271 }); 272 273 verifyIncrementalWatchEmit({ 274 subScenario: "when file with ambient global declaration file is deleted", 275 files: () => [ 276 { path: libFile.path, content: libContent }, 277 { path: `${project}/globals.d.ts`, content: `declare namespace Config { const value: string;} ` }, 278 { path: `${project}/index.ts`, content: `console.log(Config.value);` }, 279 { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, } }) } 280 ], 281 modifyFs: host => host.deleteFile(`${project}/globals.d.ts`) 282 }); 283 284 describe("with option jsxImportSource", () => { 285 const jsxImportSourceOptions = { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" }; 286 const jsxLibraryContent = `export namespace JSX { 287 interface Element {} 288 interface IntrinsicElements { 289 div: { 290 propA?: boolean; 291 }; 292 } 293} 294export function jsx(...args: any[]): void; 295export function jsxs(...args: any[]): void; 296export const Fragment: unique symbol; 297`; 298 299 verifyIncrementalWatchEmit({ 300 subScenario: "jsxImportSource option changed", 301 files: () => [ 302 { path: libFile.path, content: libContent }, 303 { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, 304 { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, 305 { path: `${project}/node_modules/preact/jsx-runtime/index.d.ts`, content: jsxLibraryContent.replace("propA", "propB") }, 306 { path: `${project}/node_modules/preact/package.json`, content: JSON.stringify({ name: "preact", version: "0.0.1" }) }, 307 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 308 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 309 ], 310 modifyFs: host => host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { ...jsxImportSourceOptions, jsxImportSource: "preact" } })), 311 optionsToExtend: ["--explainFiles"] 312 }); 313 314 verifyIncrementalWatchEmit({ 315 subScenario: "jsxImportSource backing types added", 316 files: () => [ 317 { path: libFile.path, content: libContent }, 318 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 319 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 320 ], 321 modifyFs: host => { 322 host.createDirectory(`${project}/node_modules`); 323 host.createDirectory(`${project}/node_modules/react`); 324 host.createDirectory(`${project}/node_modules/react/jsx-runtime`); 325 host.writeFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`, jsxLibraryContent); 326 host.writeFile(`${project}/node_modules/react/package.json`, JSON.stringify({ name: "react", version: "0.0.1" })); 327 } 328 }); 329 330 verifyIncrementalWatchEmit({ 331 subScenario: "jsxImportSource backing types removed", 332 files: () => [ 333 { path: libFile.path, content: libContent }, 334 { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, 335 { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, 336 { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` }, 337 { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } 338 ], 339 modifyFs: host => { 340 host.deleteFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`); 341 host.deleteFile(`${project}/node_modules/react/package.json`); 342 } 343 }); 344 345 verifyIncrementalWatchEmit({ 346 subScenario: "importHelpers backing types removed", 347 files: () => [ 348 { path: libFile.path, content: libContent }, 349 { path: `${project}/node_modules/tslib/index.d.ts`, content: "export function __assign(...args: any[]): any;" }, 350 { path: `${project}/node_modules/tslib/package.json`, content: JSON.stringify({ name: "tslib", version: "0.0.1" }) }, 351 { path: `${project}/index.tsx`, content: `export const x = {...{}};` }, 352 { path: configFile.path, content: JSON.stringify({ compilerOptions: { importHelpers: true } }) } 353 ], 354 modifyFs: host => { 355 host.deleteFile(`${project}/node_modules/tslib/index.d.ts`); 356 host.deleteFile(`${project}/node_modules/tslib/package.json`); 357 } 358 }); 359 }); 360 361 describe("editing module augmentation", () => { 362 verifyIncrementalWatchEmit({ 363 subScenario: "editing module augmentation", 364 files: () => [ 365 { path: libFile.path, content: libContent }, 366 { path: `${project}/node_modules/classnames/index.d.ts`, content: `export interface Result {} export default function classNames(): Result;` }, 367 { path: `${project}/src/types/classnames.d.ts`, content: `export {}; declare module "classnames" { interface Result { foo } }` }, 368 { path: `${project}/src/index.ts`, content: `import classNames from "classnames"; classNames().foo;` }, 369 { path: configFile.path, content: JSON.stringify({ compilerOptions: { module: "commonjs", incremental: true } }) }, 370 ], 371 modifyFs: host => { 372 // delete 'foo' 373 host.writeFile(`${project}/src/types/classnames.d.ts`, `export {}; declare module "classnames" { interface Result {} }`); 374 }, 375 }); 376 }); 377 378 verifyTscWatch({ 379 scenario: "incremental", 380 subScenario: "tsbuildinfo has error", 381 sys: () => createWatchedSystem({ 382 "/src/project/main.ts": "export const x = 10;", 383 "/src/project/tsconfig.json": "{}", 384 "/src/project/tsconfig.tsbuildinfo": "Some random string", 385 [libFile.path]: libFile.content, 386 }), 387 commandLineArgs: ["--p", "src/project", "-i", "-w"], 388 changes: emptyArray 389 }); 390 }); 391} 392