UNPKG

grunt-ts

Version:

Compile and manage your TypeScript project

420 lines (344 loc) 16.8 kB
/// <reference path="../../defs/tsd.d.ts"/> /// <reference path="./interfaces.d.ts"/> 'use strict'; 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; }