1import * as ts from "./_namespaces/ts"; 2import * as fakes from "./_namespaces/fakes"; 3import * as vpath from "./_namespaces/vpath"; 4import * as documents from "./_namespaces/documents"; 5import * as vfs from "./_namespaces/vfs"; 6import * as collections from "./_namespaces/collections"; 7import * as Harness from "./_namespaces/Harness"; 8 9/** 10 * Test harness compiler functionality. 11 */ 12 13export interface Project { 14 file: string; 15 config?: ts.ParsedCommandLine; 16 errors?: ts.Diagnostic[]; 17} 18 19export function readProject(host: fakes.ParseConfigHost, project: string | undefined, existingOptions?: ts.CompilerOptions): Project | undefined { 20 if (project) { 21 project = vpath.isTsConfigFile(project) ? project : vpath.combine(project, "tsconfig.json"); 22 } 23 else { 24 [project] = host.vfs.scanSync(".", "ancestors-or-self", { 25 accept: (path, stats) => stats.isFile() && host.vfs.stringComparer(vpath.basename(path), "tsconfig.json") === 0 26 }); 27 } 28 29 if (project) { 30 // TODO(rbuckton): Do we need to resolve this? Resolving breaks projects tests. 31 // project = vpath.resolve(host.vfs.currentDirectory, project); 32 33 // read the config file 34 const readResult = ts.readConfigFile(project, path => host.readFile(path)); 35 if (readResult.error) { 36 return { file: project, errors: [readResult.error] }; 37 } 38 39 // parse the config file 40 const config = ts.parseJsonConfigFileContent(readResult.config, host, vpath.dirname(project), existingOptions); 41 return { file: project, errors: config.errors, config }; 42 } 43} 44 45/** 46 * Correlates compilation inputs and outputs 47 */ 48export interface CompilationOutput { 49 readonly inputs: readonly documents.TextDocument[]; 50 readonly js: documents.TextDocument | undefined; 51 readonly dts: documents.TextDocument | undefined; 52 readonly map: documents.TextDocument | undefined; 53} 54 55export class CompilationResult { 56 public readonly host: fakes.CompilerHost; 57 public readonly program: ts.Program | undefined; 58 public readonly result: ts.EmitResult | undefined; 59 public readonly options: ts.CompilerOptions; 60 public readonly diagnostics: readonly ts.Diagnostic[]; 61 public readonly js: ReadonlyMap<string, documents.TextDocument>; 62 public readonly dts: ReadonlyMap<string, documents.TextDocument>; 63 public readonly maps: ReadonlyMap<string, documents.TextDocument>; 64 public symlinks?: vfs.FileSet; // Location to store original symlinks so they may be used in both original and declaration file compilations 65 66 private _inputs: documents.TextDocument[] = []; 67 private _inputsAndOutputs: collections.SortedMap<string, CompilationOutput>; 68 69 constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: readonly ts.Diagnostic[]) { 70 this.host = host; 71 this.program = program; 72 this.result = result; 73 this.diagnostics = diagnostics; 74 this.options = program ? program.getCompilerOptions() : options; 75 76 // collect outputs 77 const js = this.js = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" }); 78 const dts = this.dts = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" }); 79 const maps = this.maps = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" }); 80 for (const document of this.host.outputs) { 81 if (vpath.isJavaScript(document.file) || ts.fileExtensionIs(document.file, ts.Extension.Json)) { 82 js.set(document.file, document); 83 } 84 else if (vpath.isDeclaration(document.file)) { 85 dts.set(document.file, document); 86 } 87 else if (vpath.isSourceMap(document.file)) { 88 maps.set(document.file, document); 89 } 90 } 91 92 // correlate inputs and outputs 93 this._inputsAndOutputs = new collections.SortedMap<string, CompilationOutput>({ comparer: this.vfs.stringComparer, sort: "insertion" }); 94 if (program) { 95 if (this.options.out || this.options.outFile) { 96 const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out); 97 const inputs: documents.TextDocument[] = []; 98 for (const sourceFile of program.getSourceFiles()) { 99 if (sourceFile) { 100 const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text); 101 this._inputs.push(input); 102 if (!vpath.isDeclaration(sourceFile.fileName)) { 103 inputs.push(input); 104 } 105 } 106 } 107 108 const outputs: CompilationOutput = { 109 inputs, 110 js: js.get(outFile), 111 dts: dts.get(vpath.changeExtension(outFile, ".d.ts")), 112 map: maps.get(outFile + ".map") 113 }; 114 115 if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs); 116 if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs); 117 if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs); 118 119 for (const input of inputs) { 120 this._inputsAndOutputs.set(input.file, outputs); 121 } 122 } 123 else { 124 for (const sourceFile of program.getSourceFiles()) { 125 if (sourceFile) { 126 const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text); 127 this._inputs.push(input); 128 if (!vpath.isDeclaration(sourceFile.fileName)) { 129 const extname = ts.getOutputExtension(sourceFile.fileName, this.options); 130 const outputs: CompilationOutput = { 131 inputs: [input], 132 js: js.get(this.getOutputPath(sourceFile.fileName, extname)), 133 dts: dts.get(this.getOutputPath(sourceFile.fileName, ts.getDeclarationEmitExtensionForPath(sourceFile.fileName))), 134 map: maps.get(this.getOutputPath(sourceFile.fileName, extname + ".map")) 135 }; 136 137 this._inputsAndOutputs.set(sourceFile.fileName, outputs); 138 if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs); 139 if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs); 140 if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs); 141 } 142 } 143 } 144 } 145 } 146 } 147 148 public get vfs(): vfs.FileSystem { 149 return this.host.vfs; 150 } 151 152 public get inputs(): readonly documents.TextDocument[] { 153 return this._inputs; 154 } 155 156 public get outputs(): readonly documents.TextDocument[] { 157 return this.host.outputs; 158 } 159 160 public get traces(): readonly string[] { 161 return this.host.traces; 162 } 163 164 public get emitSkipped(): boolean { 165 return this.result && this.result.emitSkipped || false; 166 } 167 168 public get singleFile(): boolean { 169 return !!this.options.outFile || !!this.options.out; 170 } 171 172 public get commonSourceDirectory(): string { 173 const common = this.program && this.program.getCommonSourceDirectory() || ""; 174 return common && vpath.combine(this.vfs.cwd(), common); 175 } 176 177 public getInputsAndOutputs(path: string): CompilationOutput | undefined { 178 return this._inputsAndOutputs.get(vpath.resolve(this.vfs.cwd(), path)); 179 } 180 181 public getInputs(path: string): readonly documents.TextDocument[] | undefined { 182 const outputs = this.getInputsAndOutputs(path); 183 return outputs && outputs.inputs; 184 } 185 186 public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined { 187 const outputs = this.getInputsAndOutputs(path); 188 return outputs && outputs[kind]; 189 } 190 191 public getSourceMapRecord(): string | undefined { 192 const maps = this.result!.sourceMaps; 193 if (maps && maps.length > 0) { 194 return Harness.SourceMapRecorder.getSourceMapRecord(maps, this.program!, Array.from(this.js.values()).filter(d => !ts.fileExtensionIs(d.file, ts.Extension.Json)), Array.from(this.dts.values())); 195 } 196 } 197 198 public getSourceMap(path: string): documents.SourceMap | undefined { 199 if (this.options.noEmit || vpath.isDeclaration(path)) return undefined; 200 if (this.options.inlineSourceMap) { 201 const document = this.getOutput(path, "js"); 202 return document && documents.SourceMap.fromSource(document.text); 203 } 204 if (this.options.sourceMap) { 205 const document = this.getOutput(path, "map"); 206 return document && new documents.SourceMap(document.file, document.text); 207 } 208 } 209 210 public getOutputPath(path: string, ext: string): string { 211 if (this.options.outFile || this.options.out) { 212 path = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out); 213 } 214 else { 215 path = vpath.resolve(this.vfs.cwd(), path); 216 const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir; 217 if (outDir) { 218 const common = this.commonSourceDirectory; 219 if (common) { 220 path = vpath.relative(common, path, this.vfs.ignoreCase); 221 path = vpath.combine(vpath.resolve(this.vfs.cwd(), this.options.outDir), path); 222 } 223 } 224 } 225 return vpath.changeExtension(path, ext); 226 } 227 228 public getNumberOfJsFiles(includeJson: boolean) { 229 if (includeJson) { 230 return this.js.size; 231 } 232 else { 233 let count = this.js.size; 234 this.js.forEach(document => { 235 if (ts.fileExtensionIs(document.file, ts.Extension.Json)) { 236 count--; 237 } 238 }); 239 return count; 240 } 241 } 242} 243 244export function compileFiles(host: fakes.CompilerHost, rootFiles: string[] | undefined, compilerOptions: ts.CompilerOptions): CompilationResult { 245 if (compilerOptions.project || !rootFiles || rootFiles.length === 0) { 246 const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions); 247 if (project) { 248 if (project.errors && project.errors.length > 0) { 249 return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors); 250 } 251 if (project.config) { 252 rootFiles = project.config.fileNames; 253 compilerOptions = project.config.options; 254 } 255 } 256 delete compilerOptions.project; 257 } 258 259 // establish defaults (aligns with old harness) 260 if (compilerOptions.target === undefined && compilerOptions.module !== ts.ModuleKind.Node16 && compilerOptions.module !== ts.ModuleKind.NodeNext) compilerOptions.target = ts.ScriptTarget.ES3; 261 if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed; 262 if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true; 263 if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true; 264 265 // pre-emit/post-emit error comparison requires declaration emit twice, which can be slow. If it's unlikely to flag any error consistency issues 266 // and if the test is running `skipLibCheck` - an indicator that we want the tets to run quickly - skip the before/after error comparison, too 267 const skipErrorComparison = ts.length(rootFiles) >= 100 || (!!compilerOptions.skipLibCheck && !!compilerOptions.declaration); 268 269 const preProgram = !skipErrorComparison ? ts.createProgram(rootFiles || [], { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host) : undefined; 270 const preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram); 271 272 const program = ts.createProgram(rootFiles || [], compilerOptions, host); 273 const emitResult = program.emit(); 274 const postErrors = ts.getPreEmitDiagnostics(program); 275 const longerErrors = ts.length(preErrors) > postErrors.length ? preErrors : postErrors; 276 const shorterErrors = longerErrors === preErrors ? postErrors : preErrors; 277 const errors = preErrors && (preErrors.length !== postErrors.length) ? [...shorterErrors!, 278 ts.addRelatedInfo( 279 ts.createCompilerDiagnostic({ 280 category: ts.DiagnosticCategory.Error, 281 code: -1, 282 key: "-1", 283 message: `Pre-emit (${preErrors.length}) and post-emit (${postErrors.length}) diagnostic counts do not match! This can indicate that a semantic _error_ was added by the emit resolver - such an error may not be reflected on the command line or in the editor, but may be captured in a baseline here!` 284 }), 285 ts.createCompilerDiagnostic({ 286 category: ts.DiagnosticCategory.Error, 287 code: -1, 288 key: "-1", 289 message: `The excess diagnostics are:` 290 }), 291 ...ts.filter(longerErrors!, p => !ts.some(shorterErrors, p2 => ts.compareDiagnostics(p, p2) === ts.Comparison.EqualTo)) 292 ) 293 ] : postErrors; 294 return new CompilationResult(host, compilerOptions, program, emitResult, errors); 295} 296