grunt-ts
Version:
Compile and manage your TypeScript project
391 lines • 21.9 kB
JavaScript
/// <reference path="../defs/tsd.d.ts"/>
/// <reference path="./modules/interfaces.d.ts"/>
;
/*
* grunt-ts
* Licensed under the MIT license.
*/
var _ = require('lodash');
var path = require('path');
var fs = require('fs');
var es6_promise_1 = require('es6-promise');
var utils = require('./modules/utils');
var compileModule = require('./modules/compile');
var referenceModule = require('./modules/reference');
var amdLoaderModule = require('./modules/amdLoader');
var html2tsModule = require('./modules/html2ts');
var templateCacheModule = require('./modules/templateCache');
var transformers = require('./modules/transformers');
var optionsResolver = require('../tasks/modules/optionsResolver');
var asyncSeries = utils.asyncSeries, timeIt = utils.timeIt;
function pluginFn(grunt) {
/////////////////////////////////////////////////////////////////////
// The grunt task
////////////////////////////////////////////////////////////////////
grunt.registerMultiTask('ts', 'Compile TypeScript files', function () {
// tracks which index in the task "files" property is next for processing
var filesCompilationIndex = 0;
var done, options;
{
var currentTask = this;
var files = currentTask.files;
// make async
done = currentTask.async();
// get unprocessed templates from configuration
var rawTaskConfig = (grunt.config.getRaw(currentTask.name) || {});
var rawTargetConfig = (grunt.config.getRaw(currentTask.name + '.' + currentTask.target) || {});
optionsResolver.resolveAsync(rawTaskConfig, rawTargetConfig, currentTask.target, files, grunt.template.process, grunt.file.expand).then(function (result) {
options = result;
options.warnings.forEach(function (warning) {
grunt.log.writeln(warning.magenta);
});
options.errors.forEach(function (error) {
grunt.log.writeln(error.red);
});
if (options.errors.length > 0) {
done(false);
return;
}
proceed();
}).catch(function (error) {
grunt.log.writeln((error + '').red);
done(false);
});
}
function proceed() {
var srcFromVS_RelativePathsFromGruntFile = [];
// Run compiler
asyncSeries(options.CompilationTasks, function (currentFiles) {
// Create a reference file?
var reference = processIndividualTemplate(options.reference);
var referenceFile;
var referencePath;
if (!!reference) {
referenceFile = path.resolve(reference);
referencePath = path.dirname(referenceFile);
}
function isReferenceFile(filename) {
return path.resolve(filename) === referenceFile;
}
// Create an output file?
var outFile = currentFiles.out;
var outFile_d_ts;
if (!!outFile) {
outFile = path.resolve(outFile);
outFile_d_ts = outFile.replace('.js', '.d.ts');
}
function isOutFile(filename) {
return path.resolve(filename) === outFile_d_ts;
}
// see https://github.com/grunt-ts/grunt-ts/issues/77
function isBaseDirFile(filename, targetFiles) {
var baseDirFile = '.baseDir.ts';
var bd = '';
if (!options.baseDir) {
bd = utils.findCommonPath(targetFiles, '/');
options.baseDir = bd;
}
return path.resolve(filename) === path.resolve(path.join(bd, baseDirFile));
}
// Create an amd loader?
var amdloader = options.amdloader;
var amdloaderFile, amdloaderPath;
if (!!amdloader) {
amdloaderFile = path.resolve(amdloader);
amdloaderPath = path.dirname(amdloaderFile);
}
// Compiles all the files
// Uses the blind tsc compile task
// logs errors
function runCompilation(options, compilationInfo) {
grunt.log.writeln('Compiling...'.yellow);
// Time the compiler process
var starttime = new Date().getTime();
var endtime;
// Compile the files
return compileModule.compileAllFiles(options, compilationInfo)
.then(function (result) {
// End the timer
endtime = new Date().getTime();
grunt.log.writeln('');
// Analyze the results of our tsc execution,
// then tell the user our analysis results
// and mark the build as fail or success
if (!result) {
grunt.log.error('Error: No result from tsc.'.red);
return false;
}
if (result.code === 8) {
grunt.log.error('Error: Node was unable to run tsc. Possibly it could not be found?'.red);
return false;
}
// In TypeScript 1.3 and above, the result code corresponds to the ExitCode enum in
// TypeScript/src/compiler/sys.ts
var isError = (result.code !== 0);
// If the compilation errors contain only type errors, JS files are still
// generated. If tsc finds type errors, it will return an error code, even
// if JS files are generated. We should check this for this,
// only type errors, and call this a successful compilation.
// Assumptions:
// Level 1 errors = syntax errors - prevent JS emit.
// Level 2 errors = semantic errors - *not* prevents JS emit.
// Level 5 errors = compiler flag misuse - prevents JS emit.
var level1ErrorCount = 0, level5ErrorCount = 0, nonEmitPreventingWarningCount = 0;
var hasTS7017Error = false;
var hasPreventEmitErrors = _.foldl(result.output.split('\n'), function (memo, errorMsg) {
var isPreventEmitError = false;
if (errorMsg.search(/error TS7017:/g) >= 0) {
hasTS7017Error = true;
}
if (errorMsg.search(/error TS1\d+:/g) >= 0) {
level1ErrorCount += 1;
isPreventEmitError = true;
}
else if (errorMsg.search(/error TS5\d+:/) >= 0) {
level5ErrorCount += 1;
isPreventEmitError = true;
}
else if (errorMsg.search(/error TS\d+:/) >= 0) {
nonEmitPreventingWarningCount += 1;
}
return memo || isPreventEmitError;
}, false) || false;
// Because we can't think of a better way to determine it,
// assume that emitted JS in spite of error codes implies type-only errors.
var isOnlyTypeErrors = !hasPreventEmitErrors;
if (hasTS7017Error) {
grunt.log.writeln(('Note: You may wish to enable the suppressImplicitAnyIndexErrors' +
' grunt-ts option to allow dynamic property access by index. This will' +
' suppress TypeScript error TS7017.').magenta);
}
// Log error summary
if (level1ErrorCount + level5ErrorCount + nonEmitPreventingWarningCount > 0) {
if ((level1ErrorCount + level5ErrorCount > 0) || options.failOnTypeErrors) {
grunt.log.write(('>> ').red);
}
else {
grunt.log.write(('>> ').green);
}
if (level5ErrorCount > 0) {
grunt.log.write(level5ErrorCount.toString() + ' compiler flag error' +
(level5ErrorCount === 1 ? '' : 's') + ' ');
}
if (level1ErrorCount > 0) {
grunt.log.write(level1ErrorCount.toString() + ' syntax error' +
(level1ErrorCount === 1 ? '' : 's') + ' ');
}
if (nonEmitPreventingWarningCount > 0) {
grunt.log.write(nonEmitPreventingWarningCount.toString() +
' non-emit-preventing type warning' +
(nonEmitPreventingWarningCount === 1 ? '' : 's') + ' ');
}
grunt.log.writeln('');
if (isOnlyTypeErrors && !options.failOnTypeErrors) {
grunt.log.write(('>> ').green);
grunt.log.writeln('Type errors only.');
}
}
// !!! To do: To really be confident that the build was actually successful,
// we have to check timestamps of the generated files in the destination.
var isSuccessfulBuild = (!isError ||
(isError && isOnlyTypeErrors && !options.failOnTypeErrors));
if (isSuccessfulBuild) {
// Report successful build.
var time = (endtime - starttime) / 1000;
grunt.log.writeln('');
var message = 'TypeScript compilation complete: ' + time.toFixed(2) + 's';
if (utils.shouldPassThrough(options)) {
message += ' for TypeScript pass-through.';
}
else {
message += ' for ' + result.fileCount + ' TypeScript files.';
}
grunt.log.writeln(message.green);
}
else {
// Report unsuccessful build.
grunt.log.error(('Error: tsc return code: ' + result.code).yellow);
}
return isSuccessfulBuild;
}).catch(function (err) {
grunt.log.writeln(('Error: ' + err).red);
return false;
});
}
// Find out which files to compile, codegen etc.
// Then calls the appropriate functions + compile function on those files
function filterFilesAndCompile() {
var filesToCompile = [];
if (currentFiles.src || options.vs) {
_.map(currentFiles.src, function (file) {
if (filesToCompile.indexOf(file) === -1) {
filesToCompile.push(file);
}
});
_.map(srcFromVS_RelativePathsFromGruntFile, function (file) {
if (filesToCompile.indexOf(file) === -1) {
filesToCompile.push(file);
}
});
}
else {
// todo: fix this.
// if (_.isArray(options.files)) {
// filesToCompile = grunt.file.expand(files[filesCompilationIndex].src);
// } else if (options.files[target.dest]) {
// filesToCompile = grunt.file.expand(files[target.dest]);
// } else {
// filesToCompile = grunt.file.expand([(<{ src: string }><any>options.files).src]);
// }
filesCompilationIndex += 1;
}
// ignore directories, and clear the files of output.d.ts and baseDirFile
filesToCompile = filesToCompile.filter(function (file) {
var stats = fs.lstatSync(file);
return !stats.isDirectory() && !isOutFile(file) && !isBaseDirFile(file, filesToCompile);
});
///// Html files:
// Note:
// compile html files must be before reference file creation
var generatedFiles = [];
if (options.html) {
var html2tsOptions = {
moduleFunction: _.template(options.htmlModuleTemplate),
varFunction: _.template(options.htmlVarTemplate),
htmlOutputTemplate: options.htmlOutputTemplate,
htmlOutDir: options.htmlOutDir,
flatten: options.htmlOutDirFlatten,
eol: (options.newLine || utils.eol)
};
var htmlFiles = grunt.file.expand(options.html);
generatedFiles = _.map(htmlFiles, function (filename) { return html2tsModule.compileHTML(filename, html2tsOptions); });
}
///// Template cache
// Note: The template cache files do not go into generated files.
// Note: You are free to generate a `ts OR js` file for template cache, both should just work
if (options.templateCache) {
if (!options.templateCache.src || !options.templateCache.dest || !options.templateCache.baseUrl) {
grunt.log.writeln('templateCache : src, dest, baseUrl must be specified if templateCache option is used'.red);
}
else {
var templateCacheSrc = grunt.file.expand(options.templateCache.src); // manual reinterpolation
var templateCacheDest = path.resolve(options.templateCache.dest);
var templateCacheBasePath = path.resolve(options.templateCache.baseUrl);
templateCacheModule.generateTemplateCache(templateCacheSrc, templateCacheDest, templateCacheBasePath, (options.newLine || utils.eol));
}
}
///// Reference File
// Generate the reference file
// Create a reference file if specified
if (!!referencePath) {
var result = timeIt(function () {
return referenceModule.updateReferenceFile(filesToCompile.filter(function (f) { return !isReferenceFile(f); }), generatedFiles, referenceFile, referencePath, (options.newLine || utils.eol));
});
if (result.it === true) {
grunt.log.writeln(('Updated reference file (' + result.time + 'ms).').green);
}
}
///// AMD loader
// Create the amdLoader if specified
if (!!amdloaderPath) {
var referenceOrder = amdLoaderModule.getReferencesInOrder(referenceFile, referencePath, generatedFiles);
amdLoaderModule.updateAmdLoader(referenceFile, referenceOrder, amdloaderFile, amdloaderPath, currentFiles.outDir);
}
// Transform files as needed. Currently all of this logic in is one module
transformers.transformFiles(filesToCompile /*TODO: only unchanged files*/, filesToCompile, options);
currentFiles.src = filesToCompile;
// Return promise to compliation
if (utils.shouldCompile(options)) {
if (filesToCompile.length > 0 || options.testExecute || utils.shouldPassThrough(options)) {
return runCompilation(options, currentFiles).then(function (success) {
return success;
});
}
else {
// Nothing to do
grunt.log.writeln('No files to compile'.red);
return es6_promise_1.Promise.resolve(true);
}
}
else {
return es6_promise_1.Promise.resolve(true);
}
}
// Time (in ms) when last compile took place
var lastCompile = 0;
// Watch a folder?
if (!!options.watch) {
// get path(s)
var watchpath = grunt.file.expand([options.watch]);
// create a file watcher for path
var chokidar = require('chokidar');
var watcher = chokidar.watch(watchpath, { ignoreInitial: true, persistent: true });
// Log what we are doing
grunt.log.writeln(('Watching all TypeScript / Html files under : ' + watchpath).cyan);
// A file has been added/changed/deleted has occurred
watcher
.on('add', function (path) {
handleFileEvent(path, '+++ added ', true);
// Reset the time for last compile call
lastCompile = new Date().getTime();
})
.on('change', function (path) {
handleFileEvent(path, '### changed ', true);
// Reset the time for last compile call
lastCompile = new Date().getTime();
})
.on('unlink', function (path) {
handleFileEvent(path, '--- removed ');
// Reset the time for last compile call
lastCompile = new Date().getTime();
})
.on('error', function (error) {
console.error('Error happened in chokidar: ', error);
});
}
// Reset the time for last compile call
lastCompile = new Date().getTime();
// Run initial compile
return filterFilesAndCompile();
// local event to handle file event
function handleFileEvent(filepath, displaystr, addedOrChanged) {
if (addedOrChanged === void 0) { addedOrChanged = false; }
// Only ts and html :
if (!utils.endsWith(filepath.toLowerCase(), '.ts') && !utils.endsWith(filepath.toLowerCase(), '.html')) {
return;
}
// Do not run if just ran, behaviour same as grunt-watch
// These are the files our run modified
if ((new Date().getTime() - lastCompile) <= 100) {
// Uncomment for debugging which files were ignored
// grunt.log.writeln((' ///' + ' >>' + filepath).grey);
return;
}
// Log and run the debounced version.
grunt.log.writeln((displaystr + ' >>' + filepath).yellow);
filterFilesAndCompile();
}
}).then(function (res) {
// Ignore res? (either logs or throws)
if (!options.watch) {
if (res.some(function (success) {
return !success;
})) {
done(false);
}
else {
done();
}
}
}, done);
}
});
function processIndividualTemplate(template) {
if (template) {
return grunt.template.process(template, {});
}
return template;
}
}
module.exports = pluginFn;
//# sourceMappingURL=ts.js.map