grunt-ts
Version:
Compile and manage your TypeScript project
392 lines (324 loc) • 14.2 kB
text/typescript
;
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);
}
});
}