boats
Version:
Beautiful Open / Async Template System - Write less yaml with BOATS and Nunjucks.
326 lines (325 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs_1 = require("fs");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const _ = tslib_1.__importStar(require("lodash"));
const nunjucks_1 = tslib_1.__importDefault(require("nunjucks"));
const upath_1 = tslib_1.__importDefault(require("upath"));
const calculateIndentFromLineBreak_1 = tslib_1.__importDefault(require("./calculateIndentFromLineBreak"));
const cloneObject_1 = tslib_1.__importDefault(require("./cloneObject"));
const defaults_1 = tslib_1.__importDefault(require("./defaults"));
const stripFromEndOfString_1 = tslib_1.__importDefault(require("./stripFromEndOfString"));
const apiTypeFromString_1 = tslib_1.__importDefault(require("./apiTypeFromString"));
const Injector_1 = tslib_1.__importDefault(require("./Injector"));
const autoChannelIndexer_1 = tslib_1.__importDefault(require("./nunjucksHelpers/autoChannelIndexer"));
const autoComponentIndexer_1 = tslib_1.__importDefault(require("./nunjucksHelpers/autoComponentIndexer"));
const autoPathIndexer_1 = tslib_1.__importDefault(require("./nunjucksHelpers/autoPathIndexer"));
const autoSummary_1 = tslib_1.__importDefault(require("./nunjucksHelpers/autoSummary"));
const schemaRef_1 = tslib_1.__importDefault(require("./nunjucksHelpers/schemaRef"));
const autoTag_1 = tslib_1.__importDefault(require("./nunjucksHelpers/autoTag"));
const fileName_1 = tslib_1.__importDefault(require("./nunjucksHelpers/fileName"));
const inject_1 = tslib_1.__importDefault(require("./nunjucksHelpers/inject"));
const merge_1 = tslib_1.__importDefault(require("./nunjucksHelpers/merge"));
const mixin_1 = tslib_1.__importDefault(require("./nunjucksHelpers/mixin"));
const packageJson_1 = tslib_1.__importDefault(require("./nunjucksHelpers/packageJson"));
const routePermission_1 = tslib_1.__importDefault(require("./nunjucksHelpers/routePermission"));
const uniqueOpId_1 = tslib_1.__importDefault(require("./nunjucksHelpers/uniqueOpId"));
const optionalProps_1 = tslib_1.__importDefault(require("./nunjucksHelpers/optionalProps"));
const pickProps_1 = tslib_1.__importDefault(require("./nunjucksHelpers/pickProps"));
const pathInjector_1 = require("./pathInjector");
const isAsyncApi_1 = tslib_1.__importDefault(require("./isAsyncApi"));
const constants_1 = require("./constants");
const dirListFilesSync_1 = require("./utils/dirListFilesSync");
const fileArraySortIndexToTop_1 = tslib_1.__importDefault(require("./utils/fileArraySortIndexToTop"));
class Template {
/**
* Parses all files in a folder against the nunjuck tpl engine and outputs in a mirrored path the in provided outputDirectory
* @param inputFile The input directory to start parsing from
* @param output The directory to output/mirror to
* @param originalIndent The original indent (currently hard coded to 2)
* @param stripValue The strip value for the uniqueOpIp
* @param variables The variables for the tpl engine
* @param helpFunctionPaths Array of fully qualified local file paths to nunjucks helper functions
* @param boatsrc
* @param oneFileOutput When passed will output the tpl compiled files into a tmp folder, TMP_COMPILED_DIR_NAME
*/
// eslint-disable-next-line max-lines-per-function
directoryParse(inputFile, output, originalIndent = defaults_1.default.DEFAULT_ORIGINAL_INDENTATION, stripValue = defaults_1.default.DEFAULT_STRIP_VALUE, variables, helpFunctionPaths, boatsrc, oneFileOutput) {
if (!inputFile || !output) {
throw new Error('You must pass an input file and output directory when parsing multiple files.');
}
this.originalIndentation = originalIndent;
this.mixinVarNamePrefix = defaults_1.default.DEFAULT_MIXIN_VAR_PREFIX;
this.helpFunctionPaths = helpFunctionPaths || [];
this.variables = variables || [];
this.boatsrc = boatsrc;
this.inputFile = inputFile = this.cleanInputString(inputFile);
this.isAsyncApiFile = (0, isAsyncApi_1.default)(this.inputFile);
// ensure we parse the input file 1st as this typically contains the inject function
// this will also allow us to determine the api type and correctly set the stripValue
let renderedIndex;
try {
console.log('Render index file 1st', inputFile);
renderedIndex = this.renderFile(fs_extra_1.default.readFileSync(inputFile, 'utf8'), inputFile);
}
catch (e) {
console.error(`Error parsing nunjucks file ${inputFile}: `.red.bold);
console.error('Common errors in the index file are JSON syntax errors with the `inject` tpl function'.red);
throw e;
}
this.stripValue = this.setDefaultStripValue(stripValue, renderedIndex);
let returnFileinput;
const files = (0, fileArraySortIndexToTop_1.default)((0, dirListFilesSync_1.dirListFilesSync)(upath_1.default.dirname(inputFile)));
for (let i = 0; i < files.length; i++) {
const file = upath_1.default.toUnix(files[i]);
try {
const outputFile = this.calculateOutputFile({
inputFile,
currentFile: file,
output,
oneFileOutput
});
const rendered = this.renderFile(fs_extra_1.default.readFileSync(file, 'utf8'), file);
if (upath_1.default.normalize(inputFile) === upath_1.default.normalize(file)) {
returnFileinput = outputFile;
}
fs_extra_1.default.outputFileSync(outputFile, rendered);
}
catch (e) {
console.error(`Error parsing nunjucks file ${file}: `.red.bold);
throw e;
}
}
return this.stripNjkExtension(returnFileinput);
}
setDefaultStripValue(stripValue, inputString) {
if (stripValue) {
return stripValue;
}
switch ((0, apiTypeFromString_1.default)(inputString)) {
case 'swagger':
return 'src/paths/';
case 'openapi':
return 'src/paths/';
case 'asyncapi':
return 'src/channels/';
}
throw new Error('Non supported api type provided. BOATS only supports swagger/openapi/asyncapi');
}
/**
* Cleans the input string to ensure a match with the walker package when mirroring
* @param relativeFilePath
*/
cleanInputString(relativeFilePath) {
relativeFilePath = upath_1.default.toUnix(relativeFilePath);
if (relativeFilePath.substring(0, 2) === './') {
return relativeFilePath.substring(2, relativeFilePath.length);
}
if (relativeFilePath.substring(0, 1) === '/') {
return relativeFilePath.substring(1, relativeFilePath.length);
}
return relativeFilePath;
}
/**
* Calculates the output file based on the input file, used for mirroring the input src dir.
* Any .njk ext will automatically be removed.
*/
calculateOutputFile(input) {
const inputDir = upath_1.default.dirname(input.inputFile);
const filePathWithoutBuildOrSrcPath = input.currentFile.replace(inputDir, '');
return this.stripNjkExtension(upath_1.default.join(process.cwd(),
// add the tmp folder name or not - the tmp folder has to be in the same relative positive as
// the final output to ensure included from directory traversing still function
(input.oneFileOutput) ? inputDir + constants_1.TMP_COMPILED_DIR_NAME : upath_1.default.dirname(input.output), filePathWithoutBuildOrSrcPath));
}
/**
* Strips out the njk ext from a given string
* @param input
* @return string
*/
stripNjkExtension(input) {
return (0, stripFromEndOfString_1.default)(input, '.njk');
}
/**
* After render use only, takes a rendered njk file and replaces the .yml.njk with .njk
* @param multiLineBlock
*/
stripNjkExtensionFrom$Refs(multiLineBlock) {
const pattern = '.yml.njk';
const regex = new RegExp(pattern, 'g');
return multiLineBlock.replace(regex, '.yml');
}
/**
* Loads and renders a tpl file
* @param inputString The string to parse
* @param fileLocation The file location the string for the current
*/
renderFile(inputString, fileLocation) {
try {
this.currentFilePointer = upath_1.default.toUnix(fileLocation);
this.mixinObject = this.setMixinPositions(inputString, this.originalIndentation);
this.mixinNumber = 0;
this.indentObject = this.setIndentPositions(inputString, 0);
this.indentNumber = 0;
this.nunjucksSetup();
const renderedYaml = Injector_1.default.injectAndRender(fileLocation, this.inputFile, this.boatsrc, this.isAsyncApiFile);
return this.stripNjkExtensionFrom$Refs(renderedYaml);
}
catch (e) {
console.log(`Template.renderFile() attemtped to render: ${fileLocation}`);
throw new Error({
fileAttemptedToRender: fileLocation,
...e
});
}
}
/**
*
* @param str The string to look for mixins
* @param originalIndentation The original indentation setting, defaults to 2
* @returns {Array}
*/
setMixinPositions(str, originalIndentation = 2) {
const regexp = RegExp(/(mixin\(["'`]([^"`']*)["'`].*\))/, 'g');
let matches;
const matched = [];
while ((matches = regexp.exec(str)) !== null) {
const mixinObj = {
index: regexp.lastIndex,
match: matches[0],
mixinPath: matches[2],
mixinLinePadding: ''
};
const indent = (0, calculateIndentFromLineBreak_1.default)(str, mixinObj.index) + originalIndentation;
for (let i = 0; i < indent; ++i) {
mixinObj.mixinLinePadding += ' ';
}
matched.push(mixinObj);
}
return matched;
}
/**
*
* @param str The string to look for helpers that need indentations
* @param originalIndentation The original indentation setting, defaults to 2
* @returns {Array}
*/
setIndentPositions(str, originalIndentation = 0) {
const regexp = RegExp(/((optionalProps|pickProps)\(.*\))/, 'g');
let matches;
const matched = [];
const preparedString = str
.split('\n')
.map((s) => (/^\s*\-/.test(s) ? s.replace('-', ' ') : s))
.join('\n');
while ((matches = regexp.exec(preparedString)) !== null) {
const indentObject = {
index: regexp.lastIndex,
match: matches[0],
linePadding: ''
};
const indent = (0, calculateIndentFromLineBreak_1.default)(preparedString, indentObject.index) + originalIndentation;
for (let i = 0; i < indent; ++i) {
indentObject.linePadding += ' ';
}
matched.push(indentObject);
}
return matched;
}
/**
* Sets up the tpl engine for the current file being rendered
*/
nunjucksSetup() {
const env = this.setupDefaultNunjucksEnv();
env.addGlobal('currentFilePointer', this.currentFilePointer);
env.addGlobal('mixinObject', this.mixinObject);
env.addGlobal('mixinNumber', this.mixinNumber);
env.addGlobal('indentObject', this.indentObject);
env.addGlobal('indentNumber', this.indentNumber);
env.addGlobal('pathInjector', new pathInjector_1.PathInjector(this.boatsrc.paths, upath_1.default.relative('.', upath_1.default.dirname(this.inputFile))));
}
/**
* Default nunjucks env configuration
*
* @return {nunjucks.Environment}
*/
setupDefaultNunjucksEnv() {
const env = nunjucks_1.default.configure(this.boatsrc.nunjucksOptions);
const processEnvVars = (0, cloneObject_1.default)(process.env);
for (const key in processEnvVars) {
env.addGlobal(key, processEnvVars[key]);
}
if (Array.isArray(this.variables)) {
this.variables.forEach((varObj) => {
const keys = Object.keys(varObj);
env.addGlobal(keys[0], varObj[keys[0]]);
});
}
env.addGlobal('_', _);
env.addGlobal('autoChannelIndexer', autoChannelIndexer_1.default);
env.addGlobal('autoComponentIndexer', autoComponentIndexer_1.default);
env.addGlobal('autoPathIndexer', autoPathIndexer_1.default);
env.addGlobal('autoSummary', autoSummary_1.default);
env.addGlobal('autoTag', autoTag_1.default);
env.addGlobal('boatsConfig', this.boatsrc);
env.addGlobal('fileName', fileName_1.default);
env.addGlobal('inject', inject_1.default);
env.addGlobal('merge', merge_1.default);
env.addGlobal('mixin', mixin_1.default);
env.addGlobal('mixinVarNamePrefix', this.mixinVarNamePrefix);
env.addGlobal('optionalProps', optionalProps_1.default);
env.addGlobal('packageJson', packageJson_1.default);
env.addGlobal('pickProps', pickProps_1.default);
env.addGlobal('routePermission', routePermission_1.default);
env.addGlobal('schemaRef', schemaRef_1.default);
env.addGlobal('uniqueOpId', uniqueOpId_1.default);
env.addGlobal('uniqueOpIdStripValue', this.stripValue);
this.loadHelpers(env);
return env;
}
/**
* Loads js and ts helpers from files / folders, overriding existing if they
* exist.
*
* @param {nunjucks.Environment} env The environment
*/
loadHelpers(env) {
let tsNodeLoaded = false;
const helpers = this.helpFunctionPaths.slice();
while (helpers === null || helpers === void 0 ? void 0 : helpers.length) {
const filePath = helpers.shift();
if ((0, fs_1.statSync)(filePath).isDirectory()) {
const files = (0, fs_1.readdirSync)(filePath).map((dir) => upath_1.default.join(filePath, dir));
helpers.push(...files);
continue;
}
if (filePath.endsWith('.ts') && !tsNodeLoaded) {
tsNodeLoaded = true;
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('ts-node').register();
}
if (!filePath.endsWith('.ts') && !filePath.endsWith('.js')) {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
let helper = require(filePath);
if (typeof helper !== 'function' && typeof helper.default === 'function') {
helper = helper.default;
}
const helperType = helper(nunjucks_1.default);
if (typeof helperType === 'function') {
helper = helperType;
}
env.addGlobal(this.getHelperFunctionNameFromPath(filePath), helper);
}
}
/**
* Returns an alpha numeric underscore helper function name
* @param filePath
*/
getHelperFunctionNameFromPath(filePath) {
return upath_1.default.basename(filePath, upath_1.default.extname(filePath)).replace(/[^0-9a-z_]/gi, '');
}
}
exports.default = new Template();