UNPKG

grunt-ts

Version:

Compile and manage your TypeScript project

392 lines (324 loc) 14.2 kB
'use strict'; import {Promise} from 'es6-promise'; import * as fs from 'fs'; import * as path from 'path'; import * as stripBom from 'strip-bom'; import * as _ from 'lodash'; import * as utils from './utils'; let templateProcessor: (templateString: string, options: any) => string = null; let globExpander: (globs: string[]) => string[] = null; let gruntfileGlobs : string[] = null; let absolutePathToTSConfig: string; export function resolveAsync(applyTo: IGruntTSOptions, taskOptions: ITargetOptions, targetOptions: ITargetOptions, theTemplateProcessor: (templateString: string, options: any) => string, theGlobExpander: (globs: string[]) => string[] = null) { templateProcessor = theTemplateProcessor; globExpander = theGlobExpander; gruntfileGlobs = getGlobs(taskOptions, targetOptions); return new Promise<IGruntTSOptions>((resolve, reject) => { try { const taskTSConfig = getTSConfigSettings(taskOptions); const targetTSConfig = getTSConfigSettings(targetOptions); let tsconfig: ITSConfigSupport = null; if (taskTSConfig) { tsconfig = taskTSConfig; } if (targetTSConfig) { if (!tsconfig) { tsconfig = targetTSConfig; } if ('tsconfig' in targetTSConfig) { tsconfig.tsconfig = templateProcessor(targetTSConfig.tsconfig, {}); } if ('ignoreSettings' in targetTSConfig) { tsconfig.ignoreSettings = targetTSConfig.ignoreSettings; } if ('overwriteFilesGlob' in targetTSConfig) { tsconfig.overwriteFilesGlob = targetTSConfig.overwriteFilesGlob; } if ('updateFiles' in targetTSConfig) { tsconfig.updateFiles = targetTSConfig.updateFiles; } if ('passThrough' in targetTSConfig) { tsconfig.passThrough = targetTSConfig.passThrough; } } applyTo.tsconfig = tsconfig; } catch (ex) { return reject(ex); } if (!applyTo.tsconfig) { return resolve(applyTo); } if ((<ITSConfigSupport>applyTo.tsconfig).passThrough) { if (applyTo.CompilationTasks.length === 0) { applyTo.CompilationTasks.push({src: []}); } if (!(<ITSConfigSupport>applyTo.tsconfig).tsconfig) { (<ITSConfigSupport>applyTo.tsconfig).tsconfig = '.'; } } else { let projectFile = (<ITSConfigSupport>applyTo.tsconfig).tsconfig; try { var projectFileTextContent = fs.readFileSync(projectFile, 'utf8'); } catch (ex) { if (ex && ex.code === 'ENOENT') { return reject('Could not find file "' + projectFile + '".'); } else if (ex && ex.errno) { return reject('Error ' + ex.errno + ' reading "' + projectFile + '".'); } else { return reject('Error reading "' + projectFile + '": ' + JSON.stringify(ex)); } return reject(ex); } try { var projectSpec: ITSConfigFile; const content = stripBom(projectFileTextContent); if (content.trim() === '') { projectSpec = {}; } else { projectSpec = JSON.parse(content); } } catch (ex) { return reject('Error parsing "' + projectFile + '". It may not be valid JSON in UTF-8.'); } applyTo = warnOnBadConfiguration(applyTo, projectSpec); applyTo = applyCompilerOptions(applyTo, projectSpec); applyTo = resolve_output_locations(applyTo, projectSpec); } resolve(applyTo); }); } function warnOnBadConfiguration(options: IGruntTSOptions, projectSpec: ITSConfigFile) { if (projectSpec.compilerOptions) { if (projectSpec.compilerOptions.out && projectSpec.compilerOptions.outFile) { options.warnings.push('Warning: `out` and `outFile` should not be used together in tsconfig.json.'); } if (projectSpec.compilerOptions.out) { options.warnings.push('Warning: Using `out` in tsconfig.json can be unreliable because it will output relative' + ' to the tsc working directory. It is better to use `outFile` which is always relative to tsconfig.json, ' + ' but this requires TypeScript 1.6 or higher.'); } } return options; } function getGlobs(taskOptions: ITargetOptions, targetOptions: ITargetOptions) { let globs = null; if (taskOptions && (<any>taskOptions).src) { globs = (<any>taskOptions).src; } if (targetOptions && (<any>targetOptions).src) { globs = (<any>targetOptions).src; } return globs; } function resolve_output_locations(options: IGruntTSOptions, projectSpec: ITSConfigFile) { if (options.CompilationTasks && options.CompilationTasks.length > 0 && projectSpec && projectSpec.compilerOptions) { options.CompilationTasks.forEach((compilationTask) => { if (projectSpec.compilerOptions.out) { compilationTask.out = path.normalize( projectSpec.compilerOptions.out ).replace(/\\/g, '/'); } if (projectSpec.compilerOptions.outFile) { compilationTask.out = path.normalize(path.join( relativePathFromGruntfileToTSConfig(), projectSpec.compilerOptions.outFile)).replace(/\\/g, '/'); } if (projectSpec.compilerOptions.outDir) { compilationTask.outDir = path.normalize(path.join( relativePathFromGruntfileToTSConfig(), projectSpec.compilerOptions.outDir)).replace(/\\/g, '/'); } }); } return options; } function getTSConfigSettings(raw: ITargetOptions): ITSConfigSupport { try { if (!raw || !raw.tsconfig) { return null; } if (typeof raw.tsconfig === 'boolean') { return { tsconfig: path.join(path.resolve('.'), 'tsconfig.json') }; } else if (typeof raw.tsconfig === 'string') { let tsconfigName = templateProcessor(<string>raw.tsconfig, {}); let fileInfo = fs.lstatSync(tsconfigName); if (fileInfo.isDirectory()) { tsconfigName = path.join(tsconfigName, 'tsconfig.json'); } return { tsconfig: tsconfigName }; } return raw.tsconfig; } catch (ex) { if (ex.code === 'ENOENT') { throw ex; } let exception : NodeJS.ErrnoException = { name: 'Invalid tsconfig setting', message: 'Exception due to invalid tsconfig setting. Details: ' + ex, code: ex.code, errno: ex.errno }; throw exception; } } function applyCompilerOptions(applyTo: IGruntTSOptions, projectSpec: ITSConfigFile) { const result: IGruntTSOptions = applyTo || <any>{}; const co = projectSpec.compilerOptions; const tsconfig: ITSConfigSupport = applyTo.tsconfig; if (!tsconfig.ignoreSettings && co) { const sameNameInTSConfigAndGruntTS = ['declaration', 'emitDecoratorMetadata', 'experimentalAsyncFunctions', 'experimentalDecorators', 'isolatedModules', 'inlineSourceMap', 'inlineSources', 'jsx', 'mapRoot', 'module', 'moduleResolution', 'newLine', 'noEmit', 'noEmitHelpers', 'noEmitOnError', 'noImplicitAny', 'noLib', 'noResolve', 'out', 'outDir', 'preserveConstEnums', 'removeComments', 'rootDir', 'sourceMap', 'sourceRoot', 'suppressImplicitAnyIndexErrors', 'target']; sameNameInTSConfigAndGruntTS.forEach(propertyName => { if ((propertyName in co) && !(propertyName in result)) { result[propertyName] = co[propertyName]; } }); // now copy the ones that don't have the same names. // `outFile` was added in TypeScript 1.6 and is the same as out for command-line // purposes except that `outFile` is relative to the tsconfig.json. if (('outFile' in co) && !('out' in result)) { result['out'] = co['outFile']; } } if (!('updateFiles' in tsconfig)) { tsconfig.updateFiles = true; } if (applyTo.CompilationTasks.length === 0) { applyTo.CompilationTasks.push({src: []}); } const src = applyTo.CompilationTasks[0].src; absolutePathToTSConfig = path.resolve(tsconfig.tsconfig, '..'); if (tsconfig.overwriteFilesGlob) { if (!gruntfileGlobs) { throw new Error('The tsconfig option overwriteFilesGlob is set to true, but no glob was passed-in.'); } const relPath = relativePathFromGruntfileToTSConfig(); const gruntGlobsRelativeToTSConfig: string[] = []; for (let i = 0; i < gruntfileGlobs.length; i += 1) { gruntfileGlobs[i] = gruntfileGlobs[i].replace(/\\/g, '/'); gruntGlobsRelativeToTSConfig.push(path.relative(relPath, gruntfileGlobs[i]).replace(/\\/g, '/')); } if (_.difference(projectSpec.filesGlob, gruntGlobsRelativeToTSConfig).length > 0 || _.difference(gruntGlobsRelativeToTSConfig, projectSpec.filesGlob).length > 0) { projectSpec.filesGlob = gruntGlobsRelativeToTSConfig; if (projectSpec.files) { projectSpec.files = []; } saveTSConfigSync(tsconfig.tsconfig, projectSpec); } } if (tsconfig.updateFiles && projectSpec.filesGlob) { if (projectSpec.files === undefined) { projectSpec.files = []; } updateTSConfigAndFilesFromGlob(projectSpec.files, projectSpec.filesGlob, tsconfig.tsconfig ); } if (projectSpec.files) { addUniqueRelativeFilesToSrc(projectSpec.files, src, absolutePathToTSConfig); } else { if (!(<any>globExpander).isStub) { const virtualGlob: string[] = []; if (projectSpec.exclude && _.isArray(projectSpec.exclude)) { virtualGlob.push(path.resolve(absolutePathToTSConfig, './*.ts')); virtualGlob.push(path.resolve(absolutePathToTSConfig, './*.tsx')); const tsconfigExcludedDirectories: string[] = []; projectSpec.exclude.forEach(exc => { tsconfigExcludedDirectories.push(path.normalize(path.join(absolutePathToTSConfig, exc))); }); fs.readdirSync(absolutePathToTSConfig).forEach(item => { const filePath = path.normalize(path.join(absolutePathToTSConfig, item)); if (fs.statSync(filePath).isDirectory()) { if (tsconfigExcludedDirectories.indexOf(filePath) === -1) { virtualGlob.push(path.resolve(absolutePathToTSConfig, item, './**/*.ts')); virtualGlob.push(path.resolve(absolutePathToTSConfig, item, './**/*.tsx')); } } }); } else { virtualGlob.push(path.resolve(absolutePathToTSConfig, './**/*.ts')); virtualGlob.push(path.resolve(absolutePathToTSConfig, './**/*.tsx')); } const files = globExpander(virtualGlob); // make files relative to the tsconfig.json file for (let i = 0; i < files.length; i += 1) { files[i] = path.relative(absolutePathToTSConfig, files[i]).replace(/\\/g, '/'); } projectSpec.files = files; if (projectSpec.filesGlob) { saveTSConfigSync(tsconfig.tsconfig, projectSpec); } addUniqueRelativeFilesToSrc(files, src, absolutePathToTSConfig); } } return result; } function relativePathFromGruntfileToTSConfig() { if (!absolutePathToTSConfig) { throw 'attempt to get relative path to tsconfig.json before setting absolute path'; } return path.relative('.', absolutePathToTSConfig).replace(/\\/g, '/'); } function updateTSConfigAndFilesFromGlob(filesRelativeToTSConfig: string[], globRelativeToTSConfig: string[], tsconfigFileName: string) { if ((<any>globExpander).isStub) { return; } const absolutePathToTSConfig = path.resolve(tsconfigFileName, '..'); let filesGlobRelativeToGruntfile: string[] = []; for (let i = 0; i < globRelativeToTSConfig.length; i += 1) { filesGlobRelativeToGruntfile.push(path.relative(path.resolve('.'), path.join(absolutePathToTSConfig, globRelativeToTSConfig[i]))); } const filesRelativeToGruntfile = globExpander(filesGlobRelativeToGruntfile); { let filesRelativeToTSConfig_temp = []; const relativePathFromGruntfileToTSConfig = path.relative('.', absolutePathToTSConfig).replace(/\\/g, '/'); for (let i = 0; i < filesRelativeToGruntfile.length; i += 1) { filesRelativeToGruntfile[i] = filesRelativeToGruntfile[i].replace(/\\/g, '/'); filesRelativeToTSConfig_temp.push(path.relative(relativePathFromGruntfileToTSConfig, filesRelativeToGruntfile[i]).replace(/\\/g, '/')); } filesRelativeToTSConfig.length = 0; filesRelativeToTSConfig.push(...filesRelativeToTSConfig_temp); } const tsconfigJSONContent = utils.readAndParseJSONFromFileSync(tsconfigFileName); const tempTSConfigFiles = tsconfigJSONContent.files || []; if (_.difference(tempTSConfigFiles, filesRelativeToTSConfig).length > 0 || _.difference(filesRelativeToTSConfig, tempTSConfigFiles).length > 0) { try { tsconfigJSONContent.files = filesRelativeToTSConfig; saveTSConfigSync(tsconfigFileName, tsconfigJSONContent); } catch (ex) { const error = new Error('Error updating tsconfig.json: ' + ex); throw error; } } } function saveTSConfigSync(fileName: string, content: any) { fs.writeFileSync(fileName, JSON.stringify(content, null, ' ')); } function addUniqueRelativeFilesToSrc(tsconfigFilesArray: string[], compilationTaskSrc: string[], absolutePathToTSConfig: string) { const gruntfileFolder = path.resolve('.'); _.map(_.uniq(tsconfigFilesArray), (file) => { const absolutePathToFile = path.normalize(path.join(absolutePathToTSConfig, file)); const relativePathToFileFromGruntfile = path.relative(gruntfileFolder, absolutePathToFile).replace(new RegExp('\\' + path.sep, 'g'), '/'); if (compilationTaskSrc.indexOf(absolutePathToFile) === -1 && compilationTaskSrc.indexOf(relativePathToFileFromGruntfile) === -1) { compilationTaskSrc.push(relativePathToFileFromGruntfile); } }); }