generator-confit
Version:
Yeoman generator for creating the development process, tools and a sample project for current-generation web applications
283 lines (242 loc) • 10.1 kB
JavaScript
'use strict';
const path = require('path');
const _ = require('lodash');
module.exports = {
copyGeneratorTemplates,
copyToolTemplates,
copyIfNotExist,
filterPath,
updateJSFile,
updateTextFile,
updateYAMLFile,
validatePath,
};
const JS_BLOCK_DELIMS = {
startTag: '// START_CONFIT_GENERATED_CONTENT',
endTag: '// END_CONFIT_GENERATED_CONTENT',
regEx: /\/\/ START_CONFIT_GENERATED_CONTENT\n(.*?\n)+?\/\/ END_CONFIT_GENERATED_CONTENT.*\n/g,
};
const YML_BLOCK_DELIMS = {
startTag: '# START_CONFIT_GENERATED_CONTENT',
endTag: '# END_CONFIT_GENERATED_CONTENT',
regEx: /# START_CONFIT_GENERATED_CONTENT\n(.*?\n)+?# END_CONFIT_GENERATED_CONTENT.*\n/g,
};
const FILE_TYPE_MAP = {
'.yml': YML_BLOCK_DELIMS,
'.yaml': YML_BLOCK_DELIMS,
'.js': JS_BLOCK_DELIMS,
};
/**
* Update the contents of existing JavaScript files that have JS_START_TAG & JS_END_TAGs
* inside them, changing the content between these tags only
*
* @param {string} templateFile JS file to update
* @param {string} destinationFile Destination file
* @param {string} templateData Template data
* @this generator
*/
function updateJSFile(templateFile, destinationFile, templateData) {
updateTextFile.call(this, templateFile, destinationFile, templateData, JS_BLOCK_DELIMS);
}
/**
* Update the contents of existing YAML files that have YML_START_TAG & YML_END_TAGs
* inside them, changing the content between these tags only
*
* @param {string} templateFile YAML file to update
* @param {string} destinationFile Destination file
* @param {string} templateData Template data
* @this generator
*/
function updateYAMLFile(templateFile, destinationFile, templateData) {
updateTextFile.call(this, templateFile, destinationFile, templateData, YML_BLOCK_DELIMS);
}
/**
* Update the contents of any text files that has the required start and end tag blocks
* changing the content between these tags only
*
* @param {string} templateFile JS file to update
* @param {string} destinationFile Destination file
* @param {string} templateData Template data
* @param {object} delimObj An object containing the startTag string, endTag string, and a regular expression for matching the content between the start and end tag
* @this generator
*/
function updateTextFile(templateFile, destinationFile, templateData, delimObj) {
let tData = templateData || {};
let destExists = this.fs.exists(destinationFile);
// If we don't get a delimObj, look at the source file name to figure out what kind of delims to use
if (!delimObj) {
delimObj = FILE_TYPE_MAP[path.extname(templateFile)] ||
FILE_TYPE_MAP[path.extname(templateFile.replace(path.extname(templateFile), ''))] || // For files called 'yaml.tpl' -> 'yaml'
JS_BLOCK_DELIMS;
}
if (destExists) {
let templateFileContents = this.fs.read(templateFile);
let existingJSFile = this.fs.read(destinationFile);
let startIndex = 0;
let endIndex = 0;
let fragments = [];
// Determine whether the template contains the start AND end tags. If it does, we only want to place that content into the existing file, not the entire contents.
while (true) { // eslint-disable-line no-constant-condition
startIndex = templateFileContents.indexOf(delimObj.startTag, endIndex);
endIndex = templateFileContents.indexOf(delimObj.endTag, startIndex);
if (startIndex >= 0 && endIndex > startIndex) {
fragments.push(templateFileContents.substring(startIndex, endIndex + delimObj.endTag.length) + '\n');
} else {
break;
}
}
let numMatches = (existingJSFile.match(delimObj.regEx) || []).length;
if (numMatches === 0 && fragments.length === 0) {
// console.log('num matches', numMatches);
destExists = false; // Overwrite the file
} else {
// In the case where the existing file DOES NOT have the special tags, it won't be changed.
existingJSFile = existingJSFile.replace(delimObj.regEx, function() {
let result = '';
// If there are multiple fragments left but no more places to insert them in the existing file,
// return ALL of the remaining fragments
if (numMatches === 1 && fragments.length > 1) {
result = fragments.join('\n');
} else {
result = fragments.shift() || '';
}
numMatches--;
return result;
});
// Use EJS directly because we are changing the existing file (and Yeoman is getting confused).
this.fs.write(destinationFile, this.renderEJS(existingJSFile, tData, templateFile));
}
}
if (!destExists) {
let existingFile = this.fs.read(templateFile);
this.fs.write(destinationFile, this.renderEJS(existingFile, tData, templateFile));
}
}
/**
* Only copies the srcFile to the destinationFile if the destination file does not exist
* @param {string} srcFile File to copy
* @param {string} destinationFile Where to copy the file
* @param {string} templateData Template data
* @this generator
*/
function copyIfNotExist(srcFile, destinationFile, templateData) {
if (!this.fs.exists(destinationFile)) {
let existingFile = this.fs.read(srcFile);
this.fs.write(destinationFile, this.renderEJS(existingFile, templateData, srcFile));
}
}
/**
*
* @param {string[]} arrayOfTemplateFiles An array of {src, dest, overwrite<boolean>} objects
* @param {Object} templateData Template data
* @param {string} srcPathFn Destination path
* @this generator
*/
function copyTemplateFiles(arrayOfTemplateFiles, templateData, srcPathFn) {
let tData = this.getStandardTemplateData(templateData);
(arrayOfTemplateFiles || []).forEach((fileDef) => {
// If there is a condition which when rendered using EJS evaluates to false, skip this file
if (this.evalExpression(fileDef.condition) === false) {
return;
}
// Do not try to use templates when noParse === true
if (fileDef.noParse === true) {
this.fs.copy(
this[srcPathFn](fileDef.src),
this.destinationPath(this.renderEJS(fileDef.dest, tData))
);
} else if (fileDef.src.indexOf('*') > -1) { // If we are using wildcards, use copyTpl()
this.fs.copyTpl(
this[srcPathFn](fileDef.src),
this.destinationPath(this.renderEJS(fileDef.dest, tData)),
tData
);
} else {
let fileOperationFn = fileDef.overwrite ? updateTextFile : copyIfNotExist;
fileOperationFn.call(
this,
this[srcPathFn](fileDef.src),
this.destinationPath(this.renderEJS(fileDef.dest, tData)),
tData
);
}
});
}
/**
* This method is designed to be used on the "templateFiles" array in "...resource.yml" files.
* Each item in "templateFiles" contains a "src" property (which is relative to the buildTool
* generator which is calling this method), a "dest" property which is relative to the destination
* path, and a flag indicating whether the file should overwrite existing files or not.
*
* @param {string[]} arrayOfTemplateFiles An array of {src, dest, overwrite<boolean>} objects
* @param {Object} templateData Template data
* @this generator
*/
function copyToolTemplates(arrayOfTemplateFiles, templateData) {
copyTemplateFiles.call(this, arrayOfTemplateFiles, templateData, 'getToolTemplatePath');
}
/**
* This method is designed to be used on the "templateFiles" array in the main "resource.yml" file.
* Each item in "templateFiles" contains a "src" property (which is relative to the generator which
* is calling this method), a "dest" property which is relative to the destination
* path, and a flag indicating whether the file should overwrite existing files or not.
*
* @param {string[]} arrayOfTemplateFiles An array of {src, dest, overwrite<boolean>} objects
* @param {Object} templateData Template data
* @this generator
*/
function copyGeneratorTemplates(arrayOfTemplateFiles, templateData) {
copyTemplateFiles.call(this, arrayOfTemplateFiles, templateData, 'templatePath');
}
/**
* Modify the path to make sure it is valid
*
* @param {string} pathStr The path to evaluate
* @param {string} promptKey The name of the path (to display in any error messages)
* @param {Object} defaultPaths An object containing the default paths to use when the path is invalid (except for paths.input.modulesSubDir, which may be blank)
* @return {string} The filtered path, or an environment error
* @this generator
*/
function filterPath(pathStr, promptKey, defaultPaths) {
let newPath = pathStr;
if (typeof pathStr !== 'string') {
newPath = '';
}
if (path.isAbsolute(pathStr)) {
return this.env.error('"' + promptKey + '" cannot be an absolute path: ' + pathStr);
}
if (pathStr.indexOf('..' + path.sep) !== -1) {
this.env.error('"' + promptKey + '" cannot contain "..' + path.sep + '"');
}
newPath = _.trim(newPath);
if (!newPath) {
// Only the input.modulesSubDir may be blank, otherwise use the default value
return promptKey === 'paths.input.modulesSubDir' ? newPath : _.get(defaultPaths, promptKey);
}
// remove the ./ at the start
if (newPath.startsWith('.' + path.sep)) {
newPath = newPath.substr(2);
}
// Replace any path separators with the Posix ones
return path.posix.normalize(newPath + path.posix.sep);
}
/**
* A path is valid if it's trimmed value is non-blank and does not start with . and is not absolute
* @param {String} pathStr Path to validate
* @return {Boolean|String} Returns true if the path is valid, or a string indicating the error
*/
function validatePath(pathStr) {
if (!(typeof pathStr === 'string' && _.trim(pathStr))) {
return 'Path must not be blank';
}
if (pathStr.startsWith('.' + path.sep)) {
return 'Path must not start with ".' + path.sep + '"';
}
if (pathStr.indexOf('..' + path.sep) !== -1) {
return 'Path must not contain "..' + path.sep + '"';
}
if (path.isAbsolute(pathStr)) {
return 'Absolute paths are not permitted';
}
return true;
}