UNPKG

karma-typescript-agile-preprocessor

Version:

Leverage the power of gulp-typescript for a simple yet powerful Karma preprocessor.

246 lines (245 loc) 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ts = require("typescript"); const path = require("path"); const input_1 = require("./input"); const host_1 = require("./host"); const reporter_1 = require("./reporter"); const utils = require("./utils"); /** * Compiles a whole project, with full type checking */ class ProjectCompiler { prepare(project, finalTransformers) { this.finalTransformers = finalTransformers; this.project = project; this.hasSourceMap = false; } inputFile(file) { if (file.gulp.sourceMap) this.hasSourceMap = true; } inputDone() { if (!this.project.input.firstSourceFile) { this.project.output.finish(reporter_1.emptyCompilationResult(this.project.options.noEmit)); return; } const rootFilenames = this.project.input.getFileNames(true); if (!this.project.singleOutput) { if (this.project.options.rootDir === undefined) { this.project.options.rootDir = utils.getCommonBasePathOfArray(rootFilenames.filter(fileName => fileName.substr(-5) !== ".d.ts") .map(fileName => this.project.input.getFile(fileName).gulp.base)); } } this.project.options.sourceMap = this.hasSourceMap; const currentDirectory = utils.getCommonBasePathOfArray(rootFilenames.map(fileName => this.project.input.getFile(fileName).gulp.cwd)); this.host = new host_1.Host(this.project.typescript, currentDirectory, this.project.input, this.project.options); // Calling `createProgram` with an object is only supported in 3.0. Only call this overload // if we have project references (also only supported in 3.0) this.program = this.project.projectReferences ? this.project.typescript.createProgram({ rootNames: rootFilenames, options: this.project.options, projectReferences: this.project.projectReferences, host: this.host, oldProgram: this.program }) : this.project.typescript.createProgram(rootFilenames, this.project.options, this.host, this.program); const result = reporter_1.emptyCompilationResult(this.project.options.noEmit); const optionErrors = this.program.getOptionsDiagnostics(); const syntaxErrors = this.program.getSyntacticDiagnostics(); const globalErrors = this.program.getGlobalDiagnostics(); const semanticErrors = this.program.getSemanticDiagnostics(); result.optionsErrors = optionErrors.length; result.syntaxErrors = syntaxErrors.length; result.globalErrors = globalErrors.length; result.semanticErrors = semanticErrors.length; let declarationErrors = []; if (this.project.options.declaration) { declarationErrors = this.program.getDeclarationDiagnostics(); result.declarationErrors = declarationErrors.length; } const preEmitDiagnostics = [...optionErrors, ...syntaxErrors, ...globalErrors, ...semanticErrors, ...declarationErrors]; if (this.project.singleOutput) { const output = { file: undefined }; this.emit(result, preEmitDiagnostics, (fileName, content) => { this.attachContentToFile(output, fileName, content); }); this.emitFile(output, currentDirectory); } else { const output = {}; const input = this.host.input.getFileNames(true); for (let i = 0; i < input.length; i++) { const fileName = utils.normalizePath(input[i]); const file = this.project.input.getFile(fileName); output[fileName] = { file }; } this.emit(result, preEmitDiagnostics, (fileName, content, writeByteOrderMark, onError, sourceFiles) => { if (sourceFiles.length !== 1) { throw new Error("Failure: sourceFiles in WriteFileCallback should have length 1, got " + sourceFiles.length); } const fileNameOriginal = utils.normalizePath(sourceFiles[0].fileName); const file = output[fileNameOriginal]; if (!file) return; this.attachContentToFile(file, fileName, content); }); for (let i = 0; i < input.length; i++) { const fileName = utils.normalizePath(input[i]); this.emitFile(output[fileName], currentDirectory); } } this.project.output.finish(result); } attachContentToFile(file, fileName, content) { const [, extension] = utils.splitExtension(fileName, ['d.ts', 'd.ts.map']); switch (extension) { case 'js': case 'jsx': file.jsFileName = fileName; file.jsContent = content; break; case 'd.ts.map': file.dtsMapFileName = fileName; file.dtsMapContent = content; break; case 'd.ts': file.dtsFileName = fileName; file.dtsContent = content; break; case 'map': file.jsMapContent = content; break; } } emit(result, preEmitDiagnostics, callback) { const emitOutput = this.program.emit(undefined, callback, undefined, false, this.finalTransformers ? this.finalTransformers(this.program) : undefined); result.emitSkipped = emitOutput.emitSkipped; // `emitOutput.diagnostics` might contain diagnostics that were already part of `preEmitDiagnostics`. // See https://github.com/Microsoft/TypeScript/issues/20876 // We use sortAndDeduplicateDiagnostics to remove duplicate diagnostics. // We then count the number of diagnostics in `diagnostics` that we not in `preEmitDiagnostics` // to count the number of emit diagnostics. const diagnostics = ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...emitOutput.diagnostics]); result.emitErrors += diagnostics.length - preEmitDiagnostics.length; for (const error of diagnostics) { this.project.output.diagnostic(error); } } emitFile({ file, jsFileName, dtsFileName, dtsMapFileName, jsContent, dtsContent, dtsMapContent, jsMapContent }, currentDirectory) { if (!jsFileName) return; let base; let baseDeclarations; if (file) { base = file.gulp.base; if (this.project.options.outDir) { const baseRelative = path.relative(this.project.options.rootDir, base); base = path.join(this.project.options.outDir, baseRelative); } baseDeclarations = base; if (this.project.options.declarationDir) { const baseRelative = path.relative(this.project.options.rootDir, file.gulp.base); baseDeclarations = path.join(this.project.options.declarationDir, baseRelative); } } else if (this.project.options.outFile) { base = this.project.directory; baseDeclarations = base; } else { base = this.project.directory; baseDeclarations = base; jsFileName = path.resolve(base, jsFileName); if (dtsFileName !== undefined) { dtsFileName = path.resolve(base, dtsFileName); } } if (jsContent !== undefined) { if (jsMapContent !== undefined) { jsContent = this.removeSourceMapComment(jsContent); } this.project.output.writeJs(base, jsFileName, jsContent, jsMapContent, file ? file.gulp.cwd : currentDirectory, file); } if (dtsContent !== undefined) { if (dtsMapContent !== undefined) { dtsContent = this.removeSourceMapComment(dtsContent); } this.project.output.writeDts(baseDeclarations, dtsFileName, dtsContent, dtsMapContent, file ? file.gulp.cwd : currentDirectory, file); } } removeSourceMapComment(content) { // By default the TypeScript automaticly inserts a source map comment. // This should be removed because gulp-sourcemaps takes care of that. // The comment is always on the last line, so it's easy to remove it // (But the last line also ends with a \n, so we need to look for the \n before the other) const index = content.lastIndexOf('\n', content.length - 2); return content.substring(0, index) + '\n'; } } exports.ProjectCompiler = ProjectCompiler; class FileCompiler { constructor() { this.output = {}; this.previousOutput = {}; this.compilationResult = undefined; } prepare(project, finalTransformers) { this.finalTransformers = finalTransformers; this.project = project; this.project.input.noParse = true; this.compilationResult = reporter_1.emptyCompilationResult(this.project.options.noEmit); } write(file, fileName, diagnostics, content, sourceMap) { this.output[file.fileNameNormalized] = { fileName, diagnostics, content, sourceMap }; for (const error of diagnostics) { this.project.output.diagnostic(error); } this.compilationResult.transpileErrors += diagnostics.length; this.project.output.writeJs(file.gulp.base, fileName, content, sourceMap, file.gulp.cwd, file); } inputFile(file) { if (file.fileNameNormalized.substr(file.fileNameNormalized.length - 5) === '.d.ts') { return; // Don't compile definition files } if (this.project.input.getFileChange(file.fileNameOriginal).state === input_1.FileChangeState.Equal) { // Not changed, re-use old file. const old = this.previousOutput[file.fileNameNormalized]; this.write(file, old.fileName, old.diagnostics, old.content, old.sourceMap); return; } const output = this.project.typescript.transpileModule(file.content, { compilerOptions: this.project.options, fileName: file.fileNameOriginal, reportDiagnostics: true, transformers: this.finalTransformers ? this.finalTransformers() : undefined, }); const outputString = output.outputText; let index = outputString.lastIndexOf('\n'); let mapString = outputString.substring(index + 1); if (mapString.substring(0, 1) === '\r') mapString = mapString.substring(1); const start = '//# sourceMappingURL=data:application/json;base64,'; if (mapString.substring(0, start.length) !== start) { console.log('Couldn\'t read the sourceMap generated by TypeScript. This is likely an issue with gulp-typescript.'); return; } mapString = mapString.substring(start.length); let map = JSON.parse(Buffer.from(mapString, 'base64').toString()); // TODO: Set paths correctly // map.sourceRoot = path.resolve(file.gulp.cwd, file.gulp.base); // map.sources[0] = path.relative(map.sourceRoot, file.gulp.path); const [fileNameExtensionless] = utils.splitExtension(file.fileNameOriginal); const [, extension] = utils.splitExtension(map.file); // js or jsx this.write(file, fileNameExtensionless + '.' + extension, output.diagnostics, outputString.substring(0, index), JSON.stringify(map)); } inputDone() { this.project.output.finish(this.compilationResult); this.previousOutput = this.output; this.output = {}; } } exports.FileCompiler = FileCompiler;