///
///
'use strict';
import * as path from 'path';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as utils from './utils';
import * as cache from './cacheUtils';
import * as semver from 'semver';
import {Promise} from 'es6-promise';
export var grunt: IGrunt = require('grunt');
///////////////////////////
// Helper
///////////////////////////
var executeNode: ICompilePromise;
var executeNodeDefault : ICompilePromise = function(args, optionalInfo) {
return new Promise((resolve, reject) => {
grunt.util.spawn({
cmd: process.execPath,
args: args
}, (error, result, code) => {
var ret: ICompileResult = {
code: code,
// New TypeScript compiler uses stdout for user code errors. Old one used stderr.
output: result.stdout || result.stderr
};
resolve(ret);
});
});
};
/////////////////////////////////////////////////////////////////
// Fast Compilation
/////////////////////////////////////////////////////////////////
// Map to store if the cache was cleared after the gruntfile was parsed
var cacheClearedOnce: { [targetName: string]: boolean } = {};
function getChangedFiles(files, targetName: string, cacheDir: string, verbose: boolean) {
files = cache.getNewFilesForTarget(files, targetName, cacheDir);
if (verbose) {
_.forEach(files, (file) => {
grunt.log.writeln(('### Fast Compile >>' + file).cyan);
}
);
}
return files;
}
function resetChangedFiles(files, targetName: string, cacheDir: string) {
cache.compileSuccessfull(files, targetName, cacheDir);
}
function clearCache(targetName: string, cacheDir: string) {
cache.clearCache(targetName, cacheDir);
cacheClearedOnce[targetName] = true;
}
/////////////////////////////////////////////////////////////////////
// tsc handling
////////////////////////////////////////////////////////////////////
function resolveTypeScriptBinPath(): string {
var ownRoot = path.resolve(path.dirname(module.filename), '../..');
var userRoot = path.resolve(ownRoot, '..', '..');
var binSub = path.join('node_modules', 'typescript', 'bin');
if (fs.existsSync(path.join(userRoot, binSub))) {
// Using project override
return path.join(userRoot, binSub);
}
return path.join(ownRoot, binSub);
}
function getTsc(binPath: string): string {
return path.join(binPath, 'tsc');
}
export function compileResultMeansFastCacheShouldBeRefreshed(options: Partial, result: ICompileResult) {
return (options.fast !== 'never' &&
(result.code === 0 || (result.code === 2 && !options.failOnTypeErrors)));
}
export function compileAllFiles(options: Partial, compilationInfo: IGruntTSCompilationInfo): Promise {
let targetFiles: string[] = compilationInfo.src;
// Make a local copy so we can modify files without having external side effects
let files = _.map(targetFiles, file => file);
var newFiles: string[] = files;
if (options.fast === 'watch') { // if we only do fast compile if target is watched
// if this is the first time its running after this file was loaded
if (cacheClearedOnce[grunt.task.current.target] === undefined) {
// Then clear the cache for this target
clearCache(options.targetName, options.tsCacheDir);
}
}
if (options.fast !== 'never') {
if (compilationInfo.out) {
grunt.log.writeln('Fast compile will not work when --out is specified. Ignoring fast compilation'.cyan);
}
else {
newFiles = getChangedFiles(files, options.targetName, options.tsCacheDir, options.verbose);
if (newFiles.length !== 0 || options.testExecute || utils.shouldPassThrough(options)) {
if (options.forceCompileRegex) {
const regex = new RegExp(options.forceCompileRegex);
// Finds all force compile files
const additionalFiles = files.filter((file) => {
return regex.test(file);
});
// Adds them to newFiles and unique the array
newFiles = newFiles.concat(additionalFiles).filter((value, index, self) => {
return self.indexOf(value) === index;
});
}
files = newFiles;
// If outDir is specified but no baseDir is specified we need to determine one
if (compilationInfo.outDir && !options.baseDir) {
options.baseDir = utils.findCommonPath(files, '/');
}
}
else {
grunt.log.writeln('No file changes were detected. Skipping Compile'.green);
return new Promise((resolve) => {
var ret: ICompileResult = {
code: 0,
fileCount: 0,
output: 'No files compiled as no change detected'
};
resolve(ret);
});
}
}
}
const tsconfig = options.tsconfig;
let tsc: string,
tscVersion: string = '';
if (options.compiler) {
// Custom compiler (task.compiler)
grunt.log.writeln('Using the custom compiler : ' + options.compiler);
tsc = options.compiler;
tscVersion = '';
} else {
// the bundled OR npm module based compiler
const tscPath = resolveTypeScriptBinPath();
tsc = getTsc(tscPath);
tscVersion = getTscVersion(tscPath);
grunt.log.writeln('Using tsc v' + tscVersion);
}
// If baseDir is specified create a temp tsc file to make sure that `--outDir` works fine
// see https://github.com/grunt-ts/grunt-ts/issues/77
if (compilationInfo.outDir && options.baseDir && files.length > 0 && !options.rootDir) {
const baseDirFile: string = '.baseDir.ts',
baseDirFilePath = path.join(options.baseDir, baseDirFile),
settingsSource = !!tsconfig ? 'tsconfig.json' : 'Gruntfile ts `options`',
settingsSection = !!tsconfig ? 'in the `compilerOptions` section' : 'under the task or ' +
'target `options` object';
if (!fs.existsSync(baseDirFilePath)) {
const baseDir_Message = `// grunt-ts creates this file to help TypeScript find ` +
`the compilation root of your project. If you wish to get to stop creating ` +
`it, specify a \`rootDir\` setting in the ${settingsSource}. See ` +
`https://github.com/TypeStrong/grunt-ts#rootdir for details. Note that ` +
`\`rootDir\` goes under \`options\`, and is case-sensitive. This message ` +
`was revised in grunt-ts v6. Note that \`rootDir\` requires TypeScript 1.5 ` +
` or higher.`;
grunt.file.write(baseDirFilePath, baseDir_Message);
}
if (tscVersion && semver.satisfies(tscVersion, '>=1.5.0')) {
grunt.log.warn((`Warning: created ${baseDirFilePath} file because \`outDir\` was ` +
`specified in the ${settingsSource}, but not \`rootDir\`. Add \`rootDir\` ` +
` ${settingsSection} to fix this warning.`).magenta);
}
files.push(baseDirFilePath);
}
// If reference and out are both specified.
// Then only compile the updated reference file as that contains the correct order
if (options.reference && compilationInfo.out) {
var referenceFile = path.resolve(options.reference);
files = [referenceFile];
}
// Quote the files to compile. Needed for command line parsing by tsc
files = _.map(files, item => utils.possiblyQuotedRelativePath(item));
let args: string[] = files.slice(0);
grunt.log.verbose.writeln(`TypeScript path: ${tsc}`);
if (tsconfig && tsconfig.passThrough) {
args.push('--project', tsconfig.tsconfig);
} else {
if (options.sourceMap) {
args.push('--sourcemap');
}
if (options.emitDecoratorMetadata) {
args.push('--emitDecoratorMetadata');
}
if (options.declaration) {
args.push('--declaration');
}
if (options.removeComments) {
args.push('--removeComments');
}
if (options.noImplicitAny) {
args.push('--noImplicitAny');
}
if (options.noResolve) {
args.push('--noResolve');
}
if (options.noStrictGenericChecks) {
args.push('--noStrictGenericChecks');
}
if (options.noEmitOnError) {
args.push('--noEmitOnError');
}
if (options.preserveConstEnums) {
args.push('--preserveConstEnums');
}
if (options.preserveSymlinks) {
args.push('--preserveSymlinks');
}
if (options.suppressImplicitAnyIndexErrors) {
args.push('--suppressImplicitAnyIndexErrors');
}
if (options.noEmit) {
args.push('--noEmit');
}
if (options.inlineSources) {
args.push('--inlineSources');
}
if (options.inlineSourceMap) {
args.push('--inlineSourceMap');
}
if (options.newLine && !utils.newLineIsRedundantForTsc(options.newLine)) {
args.push('--newLine', options.newLine);
}
if (options.isolatedModules) {
args.push('--isolatedModules');
}
if (options.noEmitHelpers) {
args.push('--noEmitHelpers');
}
if (options.experimentalDecorators) {
args.push('--experimentalDecorators');
}
if (options.experimentalAsyncFunctions) {
args.push('--experimentalAsyncFunctions');
}
if (options.jsx) {
args.push('--jsx', options.jsx.toLocaleLowerCase());
}
if (options.moduleResolution) {
args.push('--moduleResolution', options.moduleResolution.toLocaleLowerCase());
}
if (options.rootDir) {
args.push('--rootDir', options.rootDir);
}
if (options.noLib) {
args.push('--noLib');
}
if (options.emitBOM) {
args.push('--emitBOM');
}
if (options.locale) {
args.push('--locale', options.locale);
}
if (options.suppressExcessPropertyErrors) {
args.push('--suppressExcessPropertyErrors');
}
if (options.stripInternal) {
args.push('--stripInternal');
}
if (options.allowSyntheticDefaultImports) {
args.push('--allowSyntheticDefaultImports');
}
if (options.reactNamespace) {
args.push('--reactNamespace', options.reactNamespace);
}
if (options.skipLibCheck) {
args.push('--skipLibCheck');
}
if (options.skipDefaultLibCheck) {
args.push('--skipDefaultLibCheck');
}
if (options.pretty) {
args.push('--pretty');
}
if (options.allowUnusedLabels) {
args.push('--allowUnusedLabels');
}
if (options.noImplicitReturns) {
args.push('--noImplicitReturns');
}
if (options.noFallthroughCasesInSwitch) {
args.push('--noFallthroughCasesInSwitch');
}
if (options.allowUnreachableCode) {
args.push('--allowUnreachableCode');
}
if (options.forceConsistentCasingInFileNames) {
args.push('--forceConsistentCasingInFileNames');
}
if (options.allowJs) {
args.push('--allowJs');
}
if (options.checkJs) {
args.push('--checkJs');
}
if (options.noImplicitUseStrict) {
args.push('--noImplicitUseStrict');
}
if (options.alwaysStrict) {
args.push('--alwaysStrict');
}
if (options.diagnostics) {
args.push('--diagnostics');
}
if (options.importHelpers) {
args.push('--importHelpers');
}
if (options.listFiles) {
args.push('--listFiles');
}
if (options.listEmittedFiles) {
args.push('--listEmittedFiles');
}
if (options.noImplicitThis) {
args.push('--noImplicitThis');
}
if (options.noUnusedLocals) {
args.push('--noUnusedLocals');
}
if (options.noUnusedParameters) {
args.push('--noUnusedParameters');
}
if (options.strictFunctionTypes) {
args.push('--strictFunctionTypes');
}
if (options.esModuleInterop) {
args.push('--esModuleInterop');
}
if (options.strictPropertyInitialization) {
args.push('--strictPropertyInitialization');
}
if (options.strictNullChecks) {
args.push('--strictNullChecks');
}
if (options.traceResolution) {
args.push('--traceResolution');
}
if (options.baseUrl) {
args.push('--baseUrl', utils.enclosePathInQuotesIfRequired(options.baseUrl));
}
if (options.charset) {
args.push('--charset', options.charset);
}
if (options.declarationDir) {
args.push('--declarationDir', utils.possiblyQuotedRelativePath(options.declarationDir));
}
if (options.jsxFactory) {
args.push('--jsxFactory', options.jsxFactory);
}
if (options.lib) {
let possibleOptions = [
'es5',
'es6',
'es2015',
'es7',
'es2016',
'es2017',
'esnext',
'dom',
'dom.iterable',
'webworker',
'scripthost',
'es2015.core',
'es2015.collection',
'es2015.generator',
'es2015.iterable',
'es2015.promise',
'es2015.proxy',
'es2015.reflect',
'es2015.symbol',
'es2015.symbol.wellknown',
'es2016.array.include',
'es2017.object',
'es2017.sharedmemory',
'esnext.asynciterable'
];
options.lib.forEach(option => {
if (possibleOptions.indexOf((option + '').toLocaleLowerCase()) === -1) {
grunt.log.warn(`WARNING: Option "lib" does not support ${option} `.magenta);
}
});
args.push('--lib', options.lib.join(','));
}
if (options.maxNodeModuleJsDepth > 0 || options.maxNodeModuleJsDepth === 0) {
args.push('--maxNodeModuleJsDepth', options.maxNodeModuleJsDepth + '');
}
if (options.types) {
args.push('--types', `"${_.map(options.types, t => utils.stripQuotesIfQuoted(t.trim())).join(',')}"`);
}
if (options.typeRoots) {
args.push('--typeRoots', `"${_.map(options.typeRoots, t => utils.stripQuotesIfQuoted(t.trim())).join(',')}"`);
}
if (options.downlevelIteration) {
args.push('--downlevelIteration');
}
if (options.disableSizeLimit) {
args.push('--disableSizeLimit');
}
if (options.strict) {
args.push('--strict');
}
args.push('--target', options.target.toUpperCase());
if (options.module) {
const moduleOptionString: string = ('' + options.module).toLowerCase();
if ('none|amd|commonjs|system|umd|es6|es2015|esnext'.indexOf(moduleOptionString) > -1) {
args.push('--module', moduleOptionString);
} else {
console.warn(('WARNING: Option "module" only supports "none" | "amd" | "commonjs" |' +
' "system" | "umd" | "es6" | "es2015" | "esnext" ').magenta);
}
}
if (compilationInfo.outDir) {
if (compilationInfo.out) {
console.warn('WARNING: Option "out" and "outDir" should not be used together'.magenta);
}
args.push('--outDir', compilationInfo.outDir);
}
if (compilationInfo.out) {
// We only pass --out instead of --outFile for backward-compatability reasons.
// It is the same for purposes of the command-line (the subtle difference is handled in the tsconfig code
// and the value of --outFile is copied to --out).
args.push('--out', compilationInfo.out);
}
if (compilationInfo.dest && (!compilationInfo.out) && (!compilationInfo.outDir)) {
if (utils.isJavaScriptFile(compilationInfo.dest)) {
args.push('--out', compilationInfo.dest);
} else {
if (compilationInfo.dest === 'src') {
console.warn(('WARNING: Destination for target "' + options.targetName + '" is "src", which is the default. If you have' +
' forgotten to specify a "dest" parameter, please add it. If this is correct, you may wish' +
' to change the "dest" parameter to "src/" or just ignore this warning.').magenta);
}
if (Array.isArray(compilationInfo.dest)) {
if ((compilationInfo.dest).length === 0) {
// ignore it and do nothing.
} else if ((compilationInfo.dest).length > 0) {
console.warn((('WARNING: "dest" for target "' + options.targetName + '" is an array. This is not supported by the' +
' TypeScript compiler or grunt-ts.' +
(((compilationInfo.dest).length > 1) ? ' Only the first "dest" will be used. The' +
' remaining items will be truncated.' : ''))).magenta);
args.push('--outDir', (compilationInfo.dest)[0]);
}
} else {
args.push('--outDir', compilationInfo.dest);
}
}
}
if (args.indexOf('--out') > -1 && args.indexOf('--module') > -1) {
if (tscVersion === '' && options.compiler) {
// don't warn if they are using a custom compiler.
}
else if (semver.satisfies(tscVersion, '>=1.8.0')) {
if ((options.module === 'system' || options.module === 'amd')) {
// this is fine.
} else {
console.warn(('WARNING: TypeScript 1.8+ requires "module" to be set to' +
'system or amd for concatenation of external modules to work.').magenta);
}
} else {
console.warn(('WARNING: TypeScript < 1.8 does not allow external modules to be concatenated with' +
' --out. Any exported code may be truncated. See TypeScript issue #1544 for' +
' more details.').magenta);
}
}
if (options.sourceRoot) {
args.push('--sourceRoot', options.sourceRoot);
}
if (options.mapRoot) {
args.push('--mapRoot', options.mapRoot);
}
}
if (options.additionalFlags) {
args.push(options.additionalFlags);
}
/** Reads the tsc version from the package.json of the relevant TypeScript install */
function getTscVersion(tscPath: string) {
const pkg = JSON.parse(fs.readFileSync(path.resolve(tscPath, '..', 'package.json')).toString());
return '' + pkg.version;
}
// To debug the tsc command
if (options.verbose) {
console.log(args.join(' ').yellow);
}
else {
grunt.log.verbose.writeln(args.join(' ').yellow);
}
// Create a temp last command file and use that to guide tsc.
// Reason: passing all the files on the command line causes TSC to go in an infinite loop.
let tempfilename = utils.getTempFile('tscommand');
if (!tempfilename) {
throw (new Error('cannot create temp file'));
}
fs.writeFileSync(tempfilename, args.join(' '));
let command: string[];
// Switch implementation if a test version of executeNode exists.
if ('testExecute' in options) {
if (_.isFunction(options.testExecute)) {
command = [tsc, args.join(' ')];
executeNode = options.testExecute;
} else {
const invalidTestExecuteError = 'Invalid testExecute node present on target "' +
options.targetName + '". Value of testExecute must be a function.';
throw (new Error(invalidTestExecuteError));
}
} else {
// this is the normal path.
command = [tsc, '@' + tempfilename];
executeNode = executeNodeDefault;
}
// Execute command
return executeNode(command, options).then((result: ICompileResult) => {
if (compileResultMeansFastCacheShouldBeRefreshed(options, result)) {
resetChangedFiles(newFiles, options.targetName, options.tsCacheDir);
}
result.fileCount = files.length;
fs.unlinkSync(tempfilename);
grunt.log.writeln(result.output);
return (Promise).cast(result);
}, (err) => {
fs.unlinkSync(tempfilename);
throw err;
});
}