grunt-ts
Version:
Compile and manage your TypeScript project
420 lines (344 loc) • 16.8 kB
text/typescript
/// <reference path="../../defs/tsd.d.ts"/>
/// <reference path="./interfaces.d.ts"/>
;
import {GruntTSDefaults} from './defaults';
import * as utils from './utils';
import * as _ from 'lodash';
import {Promise} from 'es6-promise';
import {resolveVSOptionsAsync} from './visualStudioOptionsResolver';
import {resolveAsync as resolveTSConfigAsync} from './tsconfig';
const propertiesFromTarget = ['amdloader', 'html', 'htmlOutDir', 'htmlOutDirFlatten', 'reference', 'testExecute', 'tsconfig',
'templateCache', 'vs', 'watch'],
propertiesFromTargetOptions = ['additionalFlags', 'comments', 'compile', 'compiler', 'declaration',
'emitDecoratorMetadata', 'experimentalDecorators', 'failOnTypeErrors', 'fast', 'htmlModuleTemplate', 'htmlOutDir',
'htmlOutputTemplate', 'htmlOutDirFlatten', 'htmlVarTemplate', 'inlineSourceMap', 'inlineSources', 'isolatedModules',
'mapRoot', 'module', 'newLine', 'noEmit', 'noEmitHelpers', 'noEmitOnError', 'noImplicitAny', 'noResolve',
'preserveConstEnums', 'removeComments', 'sourceRoot', 'sourceMap', 'suppressImplicitAnyIndexErrors', 'target',
'verbose', 'jsx', 'moduleResolution', 'experimentalAsyncFunctions', 'rootDir'],
delayTemplateExpansion = ['htmlModuleTemplate', 'htmlVarTemplate', 'htmlOutputTemplate'];
let templateProcessor: (templateString: string, options: any) => string = null;
let globExpander: (globs: string[]) => string[] = null;
function noopTemplateProcessor(templateString: string, options: any) {
return templateString;
}
function emptyGlobExpander(globs: string[]): string[] {
return [];
}
(<any>emptyGlobExpander).isStub = true;
export function resolveAsync(rawTaskOptions: ITargetOptions,
rawTargetOptions: ITargetOptions,
targetName = '',
files: IGruntTSCompilationInfo[] = [],
theTemplateProcessor: (templateString: string, options: any) => string = null,
theGlobExpander: (globs: string[]) => string[] = null): Promise<IGruntTSOptions> {
return new Promise<IGruntTSOptions>((resolve, reject) => {
if (theTemplateProcessor && typeof theTemplateProcessor === 'function') {
templateProcessor = theTemplateProcessor;
} else {
templateProcessor = noopTemplateProcessor;
}
if (theGlobExpander && typeof theGlobExpander === 'function') {
globExpander = theGlobExpander;
} else {
globExpander = emptyGlobExpander;
}
fixMissingOptions(rawTaskOptions);
fixMissingOptions(rawTargetOptions);
let result = emptyOptionsResolveResult();
{
const {errors, warnings} = resolveAndWarnOnConfigurationIssues(rawTaskOptions, rawTargetOptions, targetName);
result.errors.push(...errors);
result.warnings.push(...warnings);
}
result = applyGruntOptions(result, rawTaskOptions);
result = applyGruntOptions(result, rawTargetOptions);
result = copyCompilationTasks(result, files);
resolveVSOptionsAsync(result, rawTaskOptions, rawTargetOptions, templateProcessor).then((result) => {
resolveTSConfigAsync(result, rawTaskOptions, rawTargetOptions, templateProcessor, globExpander).then((result) => {
result = addressAssociatedOptionsAndResolveConflicts(result);
result = enclosePathsInQuotesIfRequired(result);
result = logAdditionalConfigurationWarnings(result);
result = applyGruntTSDefaults(result);
if (result.targetName === undefined ||
(!result.targetName && targetName)) {
result.targetName = targetName;
}
return resolve(result);
}).catch((error) => {
return reject(error);
});
}).catch((error) => {
return reject(error);
});
});
}
function fixMissingOptions(config: ITargetOptions) {
if (config && !config.options) {
config.options = <any>{};
}
}
function emptyOptionsResolveResult() {
return <IGruntTSOptions><any>{
warnings: [],
errors: []
};
}
function logAdditionalConfigurationWarnings(options: IGruntTSOptions) {
return options;
}
function resolveAndWarnOnConfigurationIssues(task: ITargetOptions,
target: ITargetOptions, targetName: string) {
let errors : string[] = [], warnings: string[] = [];
const lowercaseTargetProps = _.map(propertiesFromTarget, (prop) => prop.toLocaleLowerCase());
const lowercaseTargetOptionsProps = _.map(propertiesFromTargetOptions, (prop) => prop.toLocaleLowerCase());
checkFixableCaseIssues(task, 'ts task');
checkFixableCaseIssues(target, `target "${targetName}"`);
checkLocations(task, 'ts task');
checkLocations(target, `target "${targetName}"`);
fixFilesUsedWithFast(task, 'ts task');
fixFilesUsedWithFast(target, `target "${targetName}"`);
warnings.push(...getAdditionalWarnings(task, target, targetName));
return {errors, warnings};
function getAdditionalWarnings(task: any, target: any, targetName: string) {
const additionalWarnings = [];
if (((task && task.src && targetName !== 'src') || (target && target.src)) &&
((task && task.files) || (target && target.files))) {
additionalWarnings.push(`Warning: In task "${targetName}", either "files" or "src" should be used - not both.`);
}
if (((task && task.vs) || (target && target.vs)) &&
((task && task.files) || (target && target.files))) {
additionalWarnings.push(`Warning: In task "${targetName}", either "files" or "vs" should be used - not both.`);
}
if (usingDestArray(task) || usingDestArray(target)) {
additionalWarnings.push(`Warning: target "${targetName}" has an array specified for the files.dest property.` +
` This is not supported. Taking first element and ignoring the rest.`);
}
return additionalWarnings;
function usingDestArray(task) {
let result = false;
if (task && task.files && _.isArray(task.files)) {
task.files.forEach(item => {
if (_.isArray(item.dest)) {
result = true;
};
});
}
return result;
}
}
function fixFilesUsedWithFast(task: any, configName: string) {
if (task && task.files && task.options && task.options.fast) {
warnings.push(`Warning: ${configName} is attempting to use fast compilation with "files". ` +
`This is not currently supported. Setting "fast" to "never".`);
task.options.fast = 'never';
}
}
function checkLocations(task: ITargetOptions, configName: string) {
// todo: clean this up. The top and bottom sections are largely the same.
if (task) {
for (let propertyName in task) {
if (propertiesFromTarget.indexOf(propertyName) === -1 && propertyName !== 'options') {
if (propertiesFromTargetOptions.indexOf(propertyName) > -1) {
let warningText = `Property "${propertyName}" in ${configName} is possibly in the wrong place and will be ignored. ` +
`It is expected on the options object.`;
warnings.push(warningText);
} else if (lowercaseTargetProps.indexOf(propertyName.toLocaleLowerCase()) === -1
&& lowercaseTargetOptionsProps.indexOf(propertyName.toLocaleLowerCase()) > -1) {
let index = lowercaseTargetOptionsProps.indexOf(propertyName.toLocaleLowerCase());
let correctPropertyName = propertiesFromTargetOptions[index];
let warningText = `Property "${propertyName}" in ${configName} is possibly in the wrong place and will be ignored. ` +
`It is expected on the options object. It is also the wrong case and should be ${correctPropertyName}.`;
warnings.push(warningText);
}
}
}
if (task.options) {
for (let propertyName in task.options) {
if (propertiesFromTargetOptions.indexOf(propertyName) === -1) {
if (propertiesFromTarget.indexOf(propertyName) > -1) {
let warningText = `Property "${propertyName}" in ${configName} is possibly in the wrong place and will be ignored. ` +
`It is expected on the task or target, not under options.`;
warnings.push(warningText);
} else if (lowercaseTargetOptionsProps.indexOf(propertyName.toLocaleLowerCase()) === -1
&& lowercaseTargetProps.indexOf(propertyName.toLocaleLowerCase()) > -1) {
let index = lowercaseTargetProps.indexOf(propertyName.toLocaleLowerCase());
let correctPropertyName = propertiesFromTarget[index];
let warningText = `Property "${propertyName}" in ${configName} is possibly in the wrong place and will be ignored. ` +
`It is expected on the task or target, not under options. It is also the wrong case and should be ${correctPropertyName}.`;
warnings.push(warningText);
}
}
}
}
}
}
function checkFixableCaseIssues(task: ITargetOptions, configName: string) {
if (task) {
for (let propertyName in task) {
if ((propertiesFromTarget.indexOf(propertyName) === -1)
&& (lowercaseTargetProps.indexOf(propertyName.toLocaleLowerCase()) > -1)
&& (propertiesFromTargetOptions.indexOf(propertyName) === -1)) {
let index = lowercaseTargetProps.indexOf(propertyName.toLocaleLowerCase());
let correctPropertyName = propertiesFromTarget[index];
let warningText = `Property "${propertyName}" in ${configName} is incorrectly cased; it should ` +
`be "${correctPropertyName}". Fixing it for you and proceeding.`;
warnings.push(warningText);
task[correctPropertyName] = task[propertyName];
delete task[propertyName];
}
}
for (let propertyName in task.options) {
if ((propertiesFromTargetOptions.indexOf(propertyName) === -1)
&& (lowercaseTargetOptionsProps.indexOf(propertyName.toLocaleLowerCase()) > -1)
&& (propertiesFromTarget.indexOf(propertyName) === -1)) {
let index = lowercaseTargetOptionsProps.indexOf(propertyName.toLocaleLowerCase());
let correctPropertyName = propertiesFromTargetOptions[index];
let warningText = `Property "${propertyName}" in ${configName} options is incorrectly cased; it should ` +
`be "${correctPropertyName}". Fixing it for you and proceeding.`;
warnings.push(warningText);
task.options[correctPropertyName] = task.options[propertyName];
delete task.options[propertyName];
}
}
}
}
}
function applyGruntOptions(applyTo: IGruntTSOptions, gruntOptions: ITargetOptions): IGruntTSOptions {
if (gruntOptions) {
for (const propertyName of propertiesFromTarget) {
if (propertyName in gruntOptions && propertyName !== 'vs') {
if (typeof gruntOptions[propertyName] === 'string' && utils.hasValue(gruntOptions[propertyName]) &&
delayTemplateExpansion.indexOf(propertyName) === -1) {
applyTo[propertyName] = templateProcessor(gruntOptions[propertyName], {});
} else {
applyTo[propertyName] = gruntOptions[propertyName];
}
}
}
if (gruntOptions.options) {
for (const propertyName of propertiesFromTargetOptions) {
if (propertyName in gruntOptions.options) {
if (typeof gruntOptions.options[propertyName] === 'string' && utils.hasValue(gruntOptions.options[propertyName]) &&
delayTemplateExpansion.indexOf(propertyName) === -1) {
applyTo[propertyName] = templateProcessor(gruntOptions.options[propertyName], {});
} else {
applyTo[propertyName] = gruntOptions.options[propertyName];
}
}
}
}
}
return applyTo;
}
function copyCompilationTasks(options: IGruntTSOptions, files: IGruntTSCompilationInfo[]) {
if (!utils.hasValue(options.CompilationTasks)) {
options.CompilationTasks = [];
}
if (!utils.hasValue(files)) {
return options;
}
for (let i = 0; i < files.length; i += 1) {
let compilationSet = {
src: _.map(files[i].src, (fileName) => utils.enclosePathInQuotesIfRequired(fileName)),
out: utils.enclosePathInQuotesIfRequired(files[i].out),
outDir: utils.enclosePathInQuotesIfRequired(files[i].outDir)
};
if ('dest' in files[i] && files[i].dest) {
let dest: string;
if (_.isArray(files[i].dest)) {
// using an array for dest is not supported. Only take first element.
dest = files[i].dest[0];
} else {
dest = files[i].dest;
}
if (utils.isJavaScriptFile(dest)) {
compilationSet.out = dest;
} else {
compilationSet.outDir = dest;
}
}
options.CompilationTasks.push(compilationSet);
}
return options;
}
function enclosePathsInQuotesIfRequired(options: IGruntTSOptions) {
if (options.rootDir) {
options.rootDir = utils.enclosePathInQuotesIfRequired(options.rootDir);
}
if (options.mapRoot) {
options.mapRoot = utils.enclosePathInQuotesIfRequired(options.mapRoot);
}
if (options.sourceRoot) {
options.sourceRoot = utils.enclosePathInQuotesIfRequired(options.sourceRoot);
}
return options;
}
function addressAssociatedOptionsAndResolveConflicts(options: IGruntTSOptions) {
if (options.emitDecoratorMetadata) {
options.experimentalDecorators = true;
}
if (options.inlineSourceMap && options.sourceMap) {
options.warnings.push('TypeScript cannot use inlineSourceMap and sourceMap together. Ignoring sourceMap.');
options.sourceMap = false;
}
if (options.inlineSources && !options.sourceMap) {
options.inlineSources = true;
options.inlineSourceMap = true;
options.sourceMap = false;
}
if ('comments' in options && 'removeComments' in options) {
options.warnings.push(`WARNING: Option "comments" and "removeComments" should not be used together. ` +
`The --removeComments value of ${!!options.removeComments} supercedes the --comments value of ${!!options.comments}`);
}
if ('comments' in options && !('removeComments' in options)) {
options.comments = !!options.comments;
options.removeComments = !options.comments;
} else if (!('comments' in options) && ('removeComments' in options)) {
options.removeComments = !!options.removeComments;
options.comments = !options.removeComments;
}
if ('html' in options && options.CompilationTasks.length === 0) {
options.errors.push(`ERROR: option \`html\` provided without specifying corresponding TypeScript source files to ` +
`compile. The transform will not occur unless grunt-ts also expects to compile these files.`);
}
options.CompilationTasks.forEach(compileTask => {
if (compileTask.out && compileTask.outDir) {
console.log(JSON.stringify(compileTask));
options.warnings.push(
'The parameter `out` is incompatible with `outDir`; pass one or the other - not both. Ignoring `out` and using `outDir`.'
);
compileTask.out = '';
}
});
return options;
}
function applyGruntTSDefaults(options: IGruntTSOptions) {
if (!('sourceMap' in options) && !('inlineSourceMap' in options)) {
options.sourceMap = GruntTSDefaults.sourceMap;
}
if (!('target' in options)) {
options.target = GruntTSDefaults.target;
}
if (!('fast' in options)) {
options.fast = GruntTSDefaults.fast;
}
if (!('compile' in options)) {
options.compile = GruntTSDefaults.compile;
}
if (!('htmlOutDir' in options)) {
options.htmlOutDir = null;
}
if (!('htmlOutDirFlatten' in options)) {
options.htmlOutDirFlatten = GruntTSDefaults.htmlOutDirFlatten;
}
if (!('htmlModuleTemplate' in options)) {
options.htmlModuleTemplate = GruntTSDefaults.htmlModuleTemplate;
}
if (!('htmlVarTemplate' in options)) {
options.htmlVarTemplate = GruntTSDefaults.htmlVarTemplate;
}
if (!('removeComments' in options) && !('comments' in options)) {
options.removeComments = GruntTSDefaults.removeComments;
}
return options;
}