UNPKG

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
"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;