templates-mo
Version:
Templates is a scaffolding framework that makes code generation simple, dynamic, and reusable. Generate files, parts of your app, or whole project structures—without the repetitive copy-pasting
437 lines (436 loc) • 20.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Build = void 0;
const ansi_colors_1 = __importDefault(require("ansi-colors"));
const path = __importStar(require("path"));
const fs_1 = require("fs");
const logger_1 = __importDefault(require("../utilities/logger"));
const fileSystem_1 = require("../utilities/fileSystem");
const errors_1 = require("../errors");
const utils = __importStar(require("./utils"));
const DEFAULT_OPTS = {
buildInDest: false,
buildNewFolder: true,
wipe: false,
force: false,
};
class Build {
constructor(
/**
* Full absolute build path
*
* @example "/Users/lornelas/Templates/my-instance"
* @example "/Users/lornelas/Templates/some/extra/path/my-instance"
* @example "/Users/lornelas/Templates"
*/
buildPath, template, options = {}) {
this.buildPath = buildPath;
this.template = template;
/**
* Files and directories that were created during render
*/
this.built = { files: [], directories: [] };
// should only happen if build in folder is false
// if (buildNewFolder) {
const { name, dir } = path.parse(buildPath);
// TODO: when `buildInDest` is true, `name` should be null
this.name = name;
this.directory = dir;
// }
this.options = Object.assign(Object.assign({}, DEFAULT_OPTS), options);
}
/**
* Final directory to create instance contents in.
*
* If `buildInDest` or `buildNewFolder` then we use the supplied buildPath. Note!
* when `buildInDest` is true, the build path wont have a instance name.
*
* TODO: when `buildInDest` is true, `name` should be null
*/
getDirectory() {
return this.options.buildInDest || this.options.buildNewFolder
? this.buildPath
: this.directory;
}
/**
* Checks to see if the final directory exists or not
*/
directoryExists() {
return __awaiter(this, void 0, void 0, function* () {
return (0, fileSystem_1.isDirAsync)(this.getDirectory());
});
}
createDirectory() {
return __awaiter(this, void 0, void 0, function* () {
return fs_1.promises.mkdir(this.getDirectory(), { recursive: true });
});
}
/**
* Destroy the final directory
*/
wipe() {
return __awaiter(this, void 0, void 0, function* () {
// we can only remove a directory thats going to be built.
if (this.options.buildInDest || !this.options.buildNewFolder) {
throw new Error('Cannot wipe directory that is being build in dest or without a new folder');
}
yield fs_1.promises.rm(this.getDirectory(), { force: true, recursive: true });
});
}
/**
* Wipes the directory if it should. Will return a boolean on whether or not
* directory was wiped.
*/
maybeWipe(hackyCallbackWhenFilesNeedToBeWiped) {
return __awaiter(this, void 0, void 0, function* () {
const loggerGroup = this.getLogger();
if (yield this.directoryExists()) {
/**
* If `wipe=true` then we need to delete the directory that we will be overriding.
* But if `newFolder=false` then we need to skip the wipe command because we are not creating a new directory.
*/
if (this.options.wipe && !this.options.buildInDest) {
if (!this.options.buildNewFolder) {
loggerGroup.info('Skipping wipe because we are not building a new folder');
hackyCallbackWhenFilesNeedToBeWiped === null || hackyCallbackWhenFilesNeedToBeWiped === void 0 ? void 0 : hackyCallbackWhenFilesNeedToBeWiped();
return false;
}
loggerGroup.info('Wiping destination %s', this.getDirectory());
yield this.wipe();
return true;
}
}
else {
loggerGroup.info('Build path does not exist...');
}
return false;
});
}
getLoggerName() {
return `render_${this.buildPath}`;
}
getLogger(clear = false) {
return logger_1.default.tps.group(this.getLoggerName(), {
clear,
});
}
checkForConflicts(dest, data) {
return __awaiter(this, void 0, void 0, function* () {
const { compiledFiles, defs } = this.template;
for (let i = 0; i < compiledFiles.length; i++) {
const file = compiledFiles[i];
const finalDest = file.dest(dest, data, defs);
// eslint-disable-next-line no-await-in-loop
if (yield (0, fileSystem_1.isFileAsync)(finalDest)) {
throw new errors_1.FileExistError(finalDest);
}
}
});
}
/**
* Render the build path
*/
render() {
return __awaiter(this, arguments, void 0, function* (answers = {}, data = {}) {
const realBuildPath = this.getDirectory();
const loggerGroup = this.getLogger();
const doesBuildPathExist = yield this.directoryExists();
/**
* @example
* if
* cwd: '/User/home/app'
* build path: 'test' // short build path
* new folder: true
* then
* realBuildPath: '/User/home/app/test'
* - A new directory named `test` needs to be created
*
* @example
* if
* cwd: '/User/home/app'
* build path: 'test/test2' // long build path
* new folder: true
* then
* realBuildPath: '/User/home/app/test/test2'
* - A new directory named `test` needs to be created if doesn't exist already, `test2` should be created regardless
*
* @example
* if
* cwd: '/User/home/app'
* build path: '' // build in dest
* new folder: true??
* then
* realBuildPath: '/User/home/app'
* - this directory should not be created or overridden since it should exist.
*
* @example
* if
* cwd: '/User/home/app'
* build path: 'test' // short build path
* new folder: false
* then
* realBuildPath: '/User/home/app'
* - this directory should not be created or overridden since it should exist.
*
* @example
* if
* cwd: '/User/home/app'
* build path: 'test/test2' // short build path
* new folder: false
* then
* realBuildPath: '/User/home/app'
* - A directory named `test` needs to be created if not already exists
*
*/
const renderData = Object.assign(Object.assign({}, data), { packages: this.template.packagesUsed, template: this.template.name, answers, a: answers, utils, u: utils, name: this.name, dir: this.directory });
const marker = ansi_colors_1.default.magenta('*'.repeat(this.buildPath.length + 12));
loggerGroup.info(`\n${marker}\nBuild Path: ${this.buildPath}\n${marker}`);
loggerGroup.info('Render config: %n', {
name: renderData.name,
buildPath: this.buildPath,
'Final Destination': realBuildPath,
doesBuildPathExist,
buildInDest: this.options.buildInDest,
buildNewFolder: this.options.buildNewFolder,
});
const wasWiped = yield this.maybeWipe(() => {
// super hacky yes i know. The reason this needs to happen is because
// when were using wipe but were not building a new folder we need to make sure all
// files that already exist get overridden
this.template.compiledFiles.forEach((file) => {
// eslint-disable-next-line no-param-reassign
file.options.force = true;
});
});
loggerGroup.info('Build was wiped', wasWiped);
/**
* when wipe=true but buildNewFolder=false we need to act like `force` and not
* check for files.
*/
const shouldWipeButNoNewFolder = this.options.wipe && !this.options.buildNewFolder;
/**
* Check for file conflicts when:
* - folder was not wiped
* - force option is not true
* - when wipe but no new folder
*/
if (!wasWiped && !this.options.force && !shouldWipeButNoNewFolder) {
loggerGroup.info('Checking to see if there are duplicate files');
yield this.checkForConflicts(realBuildPath, renderData);
}
// Create a new folder unless told not to
// if we are building the template in dest folder don't create new folder
if (!this.options.buildInDest &&
(this.options.buildNewFolder || !(yield this.directoryExists()))) {
loggerGroup.info('Creating real build path %s', realBuildPath);
yield this.createDirectory().catch((err) => {
loggerGroup.warn('Building build path folder had a issue %n', err);
});
}
else {
loggerGroup.info('Not creating real build path %s', realBuildPath);
}
yield this.renderDirectories();
yield this.renderFiles(renderData);
loggerGroup.success(`Build Path: %s ${ansi_colors_1.default.green.italic('(created)')}`, this.buildPath);
});
}
/**
* Creates all directories our instance needs. This will use all
* directories in any package that was loaded.
*/
renderDirectories() {
return __awaiter(this, void 0, void 0, function* () {
const dirTracker = {};
const directory = this.getDirectory();
const loggerGroup = this.getLogger();
loggerGroup.info('Rendering directories in %s', directory);
const dirsInProgress = this.template
.usedPackages()
.map((pkg) => __awaiter(this, void 0, void 0, function* () {
const dirs = pkg.find({ type: 'dir' });
const dirsGettingCreated = dirs.map((dirNode) => __awaiter(this, void 0, void 0, function* () {
/* skip if directory has already been made */
if (dirNode.path in dirTracker)
return;
const dirPathRelativeFromPkg = dirNode.getRelativePathFrom(pkg, false);
const dirPathInNewLocation = path.join(directory, dirPathRelativeFromPkg);
dirTracker[dirNode.path] = true;
if (yield (0, fileSystem_1.isDirAsync)(dirPathInNewLocation)) {
return;
}
try {
yield fs_1.promises.mkdir(dirPathInNewLocation, {
recursive: true,
});
this.built.directories.push(dirPathInNewLocation);
loggerGroup.info(` - %s ${ansi_colors_1.default.green.italic('(created)')}`, dirPathRelativeFromPkg);
}
catch (err) {
/* do nothing if dir already exist */
loggerGroup.warn(` - %s ${ansi_colors_1.default.red.italic('failed')} %n`, dirPathRelativeFromPkg, err);
return Promise.reject(err);
}
}));
yield Promise.all(dirsGettingCreated);
}));
yield Promise.all(dirsInProgress);
loggerGroup.info('Extra directories that need to be created %n', this.template.extraDirectories);
loggerGroup.info('Creating extra directories:');
/**
* Create all extra directories
*/
yield Promise.all(this.template.extraDirectories.map((dir) => __awaiter(this, void 0, void 0, function* () {
const newDir = path.join(directory, dir);
try {
yield fs_1.promises.mkdir(newDir, { recursive: true });
this.built.directories.push(newDir);
loggerGroup.info(` - %s ${ansi_colors_1.default.green.italic('(created)')}`, newDir);
}
catch (e) {
loggerGroup.info(` - %s ${ansi_colors_1.default.red.italic('(Failed)')}`, newDir);
throw e;
}
})));
loggerGroup.info('All directories have been created');
});
}
/**
* Creates all files that our template uses in `buildPath` folder
* @param {Object} [data={}] - data passed in for dot
*/
renderFiles(data) {
return __awaiter(this, void 0, void 0, function* () {
const loggerGroup = this.getLogger();
const location = this.getDirectory();
loggerGroup.info('Rendering files');
const results = yield Promise.allSettled(this.template.compiledFiles.map((file) => __awaiter(this, void 0, void 0, function* () {
const type = file.isDynamic ? 'Dynamic File' : 'File';
const dest = file.dest(this.buildPath, data, this.template.defs);
let failed = false;
try {
yield file.render(location, data, this.template.defs);
this.built.files.push(dest);
}
catch (error) {
failed = true;
throw error;
}
finally {
const status = failed
? ansi_colors_1.default.red('Failed')
: ansi_colors_1.default.green('Created');
loggerGroup.info(` - %s ${ansi_colors_1.default.cyan.italic(`(${type})`)} (${status})`, file.dest(this.buildPath, data, this.template.defs));
}
})));
const errors = results
.filter((result) => result.status === 'rejected')
.map((result) => result.reason);
if (errors.length) {
loggerGroup.error('Build path failed %s', this.buildPath);
throw new errors_1.BuildError(this.buildPath, errors);
}
});
}
/**
* Delete everything that was created in this build. This will run if any file or directory
* error when being created. We dont want to leave broken templates created
* so this function will delete everything that this template built
*/
clean(buildNewFolder) {
return __awaiter(this, void 0, void 0, function* () {
let buildPath = this.getDirectory();
logger_1.default.tps.info('Processing build cleanup %s %o', buildPath, {
buildNewFolder,
});
const buildPathNeedsSlash = buildPath[buildPath.length - 1] === path.sep;
if (!buildPathNeedsSlash) {
buildPath += path.sep;
}
if (buildNewFolder) {
yield fs_1.promises.rm(buildPath, { force: true, recursive: true });
}
// eslint-disable-next-line prefer-const
let { directories, files } = this.built;
const filesIsEmpty = !files.length;
const dirsIsEmpty = !directories.length;
if (filesIsEmpty && dirsIsEmpty) {
logger_1.default.tps.success('Nothing to clean... Moving on to next');
return;
}
if (!dirsIsEmpty) {
const dirsThatMatch = directories.filter((dir) => dir.includes(buildPath));
if (dirsThatMatch.length) {
logger_1.default.tps.info('Cleaning directories %n', dirsThatMatch);
}
for (let i = 0; i < dirsThatMatch.length; i++) {
const dir = dirsThatMatch[i];
try {
// eslint-disable-next-line no-await-in-loop
yield fs_1.promises.rm(dir, { force: true, recursive: true });
logger_1.default.tps.success(` - %s ${ansi_colors_1.default.green.italic('(deleted)')}`, dir);
}
catch (err) {
logger_1.default.tps.error('Clean up failed when deleting directories %n', err);
}
// if directory is removed then we can remove all child files
if (!filesIsEmpty) {
files = files.filter((file) => !file.includes(dir));
}
}
}
if (!filesIsEmpty) {
const filesThatMatch = files.filter((file) => file.includes(buildPath));
if (filesThatMatch.length) {
logger_1.default.tps.info('Cleaning files %n', filesThatMatch);
}
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
try {
yield fs_1.promises.rm(file, { force: true });
logger_1.default.tps.success(` - %s ${ansi_colors_1.default.green.italic('(deleted)')}`, file);
}
catch (err) {
logger_1.default.tps.error('Clean up failed when deleting files %n', err);
}
})));
}
logger_1.default.tps.success('Clean up finished');
});
}
}
exports.Build = Build;