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

581 lines (580 loc) • 23.8 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.Templates = exports.DEFAULT_OPTIONS = void 0; /* eslint-disable max-classes-per-file */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment const path = __importStar(require("path")); const fs_1 = __importDefault(require("fs")); const is = __importStar(require("is")); const fileSystemTree_1 = require("../fileSystemTree"); const TPS = __importStar(require("../utilities/constants")); const fileSystem_1 = require("../utilities/fileSystem"); const prompter_1 = __importDefault(require("../prompter")); const helpers_1 = require("../utilities/helpers"); const errors_1 = require("../errors"); const logger_1 = __importDefault(require("../utilities/logger")); const colors = __importStar(require("ansi-colors")); const dot_1 = __importDefault(require("./dot")); const template_engine_1 = __importDefault(require("./template-engine")); const cosmiconfig_1 = require("cosmiconfig"); const build_1 = require("./build"); const template_1 = require("./template"); const File_1 = __importDefault(require("./File")); exports.DEFAULT_OPTIONS = { noLocalConfig: false, noGlobalConfig: false, defaultPackage: true, default: false, hidden: false, force: false, newFolder: true, wipe: false, tpsPath: null, extendedDest: '', experimentalTemplateEngine: true, }; if (TPS.IS_TESTING) { logger_1.default.tps.opts.disableLog = true; } fileSystemTree_1.FileSystemNode.ignoreFiles = ['**/.gitkeep', '**/.tpskeep']; const settingsConfig = (0, cosmiconfig_1.cosmiconfigSync)(TPS.TEMPLATE_SETTINGS_FILE, { cache: !TPS.IS_TESTING, searchPlaces: [ `${TPS.TEMPLATE_SETTINGS_FILE}.json`, `${TPS.TEMPLATE_SETTINGS_FILE}.js`, ], }); const tpsConfigName = 'tps'; const defaultTpsrcSearches = (0, cosmiconfig_1.getDefaultSearchPlacesSync)(tpsConfigName); const nestedTpsrcSearches = defaultTpsrcSearches.map((location) => { return `.tps/${location}`; }); /** * TODO: Remove these from the list * - .tps/.config/tpsrc.cjs * - .tps/.config/tpsrc.ts * - .tps/.config/tpsrc.js * - .tps/.config/tpsrc.yml * - .tps/.config/tpsrc.yaml * - .tps/.config/tpsrc.json * - .tps/.config/tpsrc * - .tps/package.json */ const tpsrcSearchPlaces = [...defaultTpsrcSearches, ...nestedTpsrcSearches]; const tpsrcConfig = (0, cosmiconfig_1.cosmiconfigSync)(tpsConfigName, { cache: !TPS.IS_TESTING, searchStrategy: 'global', loaders: cosmiconfig_1.defaultLoadersSync, searchPlaces: tpsrcSearchPlaces, }); /** * @class * @classdesc Create a new instance of a template */ class Templates { /** * Get all locations a template can be * * Templates can be in be: * - any `.tps/` directory from the callers cwd and any directory above it * - Any `node_module` directory from the callers cwd and any directory above it */ static getTemplateLocations(cwd = TPS.CWD) { const tpsDirectoryLocations = (0, helpers_1.getAllDirectoriesAndUp)(cwd).map((dir) => { return path.join(dir, TPS.TPS_FOLDER); }); // TODO: Sort this by directory return [ ...tpsDirectoryLocations, path.join(TPS.MAIN_DIR, TPS.TPS_FOLDER), ...(0, helpers_1.getNpmPaths)(cwd), ]; } /** * Get the path to a template or null if template doesnt exist */ static findTemplate(templateName, cwd = TPS.CWD) { const homeDirectory = Templates.getTemplateLocations(cwd).find((tpsDir) => { return (0, fileSystem_1.isDir)(path.join(tpsDir, templateName)); }); if (!homeDirectory) return null; return path.join(homeDirectory, templateName); } /** * Gets path to the global .tps/ directory */ static getGloablTpsPath() { return path.join(TPS.USER_HOME, TPS.TPS_FOLDER); } static getLocalTpsPath() { const tpsLocal = (0, fileSystem_1.findUp)(TPS.TPS_FOLDER, TPS.CWD); const hasLocalTpsFolder = tpsLocal && tpsLocal !== TPS.GLOBAL_PATH; if (!hasLocalTpsFolder) return null; return tpsLocal; } static directoryIsTpsInitialized(dir) { return (0, fileSystem_1.isDir)(path.join(dir, TPS.TPS_FOLDER)); } static hasGloablTps() { return Templates.directoryIsTpsInitialized(TPS.USER_HOME); } static hasLocalTps() { return !!Templates.getLocalTpsPath(); } constructor(templateName, opts = {}) { var _a, _b; if (!templateName || !is.string(templateName)) { throw new errors_1.RequiresTemplateError(); } this.template = templateName; const templateLocation = Templates.findTemplate(templateName) || Templates.findTemplate(`tps-${templateName}`); if (!templateLocation) { logger_1.default.tps.error('Template not found! \n%O', { searchedPaths: Templates.getTemplateLocations(), }); throw new errors_1.TemplateNotFoundError(templateName); } this.src = templateLocation; logger_1.default.tps.info('Template %n', { name: this.template, location: this.src, }); this.packages = {}; this.packagesUsed = []; this.compiledFiles = []; this._defs = {}; this.successfulBuilds = new SuccessfulBuild(); this.buildErrors = []; this.templateSettings = {}; this.templateSettingsPath = path.join(this.src, TPS.TEMPLATE_SETTINGS_FILE); logger_1.default.tps.info('Settings file location: %s', this.templateSettingsPath); try { logger_1.default.tps.info('Loading template settings file...'); // eslint-disable-next-line this.templateSettings = ((_a = settingsConfig.search(this.src)) === null || _a === void 0 ? void 0 : _a.config) || {}; } catch (e) { logger_1.default.tps.info(`Template has no Settings file`, e); this.templateSettings = {}; } logger_1.default.tps.info('Template settings: %n', this.templateSettings); this.opts = Object.assign(Object.assign(Object.assign({}, exports.DEFAULT_OPTIONS), (((_b = this.templateSettings) === null || _b === void 0 ? void 0 : _b.opts) || {})), opts); this.engine = this.opts.experimentalTemplateEngine ? template_engine_1.default : dot_1.default; logger_1.default.tps.info('Template Options: %n', this.opts); if (this.templateSettings.prompts) { logger_1.default.tps.info('Loading prompts... %o', { defaultValues: this.opts.default, showHiddenPrompts: this.opts.hidden, }); this._prompts = new prompter_1.default(this.templateSettings.prompts, { default: this.opts.default, showHiddenPrompts: this.opts.hidden, }); } else { logger_1.default.tps.info('No prompts to load!', this.templateSettings); } this._loadTpsrc(templateName); // load default package if applicable const defaultFolder = path.join(this.src, 'default'); const shouldLoadDefault = this.opts.defaultPackage && (0, fileSystem_1.isDir)(defaultFolder); logger_1.default.tps.info('Loading default package %n', { shouldLoadDefault, defaultLocation: defaultFolder, }); if (shouldLoadDefault) { this.loadPackage('default'); } } hasGloablTps() { return Templates.hasGloablTps(); } hasLocalTps() { if (!this.opts.tpsPath) { return Templates.hasLocalTps(); } return (0, fileSystem_1.isDir)(this.opts.tpsPath); } /** * Include packages to use in the render process */ loadPackages(newPackages) { let packages = newPackages; if (!Array.isArray(packages)) { if (is.string(packages) && packages) { packages = [packages]; } else { throw new TypeError('Argument must be a string or an array of stings'); } } packages.forEach((p) => this.loadPackage(p)); } /** * @param {String} newPackage - package from the template you would like to use */ loadPackage(newPackageName) { if (!this.src) { throw new errors_1.RequiresTemplateError(); } if (!is.string(newPackageName)) { throw new TypeError('Argument must be a string'); } if ((0, helpers_1.hasProp)(this.packages, newPackageName)) { throw new errors_1.PackageAlreadyCompiledError(newPackageName); } this.packages[newPackageName] = new fileSystemTree_1.DirNode(newPackageName, this.src); logger_1.default.tps.info('Loading package %s', newPackageName); this._compileFilesFromPackage(newPackageName); logger_1.default.tps.success('Added package %s', newPackageName); this.packagesUsed.push(newPackageName); } /** * Get directory tree representation of package */ pkg(packageName) { return this.packages[packageName]; } /** * Set answers for prompts */ hasPrompts() { return !!(this._prompts && this._prompts.hasPrompts()); } /** * Get answers */ getAnswers() { return this._prompts.answers; } /** * Set answers for prompts * @param answers - object of prompts answers. Key should be the name of the prompt and value should be the answer to it */ setAnswers(answers) { if (!this.hasPrompts()) { throw new errors_1.NoPromptsError(); } this._prompts.setAnswers(answers); } /** * @param dest - destination to render your new template to * @param buildPaths - Instances you would like to create * @param data - data to pass to doT. This will be used when rendering dot files/syntax * @returns {Promise} */ render(dest_1, buildPaths_1) { return __awaiter(this, arguments, void 0, function* (dest, buildPaths, data = {}) { let buildInDest = false; let pathsToCreate; let finalDest = dest; if (!buildPaths) { buildInDest = true; pathsToCreate = ['./']; } else if (typeof buildPaths === 'string') { pathsToCreate = [buildPaths]; } else { pathsToCreate = buildPaths; } // @ts-expect-error need to fix library if (is.array.empty(buildPaths)) { throw new Error('Param `buildPaths` need to be a string or array of strings'); } // if were building in the destination. then we aren't creating any new folders const buildNewFolder = buildInDest ? false : this.opts.newFolder; logger_1.default.tps.info('Build paths: %n', pathsToCreate); // Append dest config if (this.opts.extendedDest) { finalDest = path.join(dest, this.opts.extendedDest); } // Create absolute paths pathsToCreate = pathsToCreate.map((buildPath) => path.join(finalDest, buildPath)); logger_1.default.tps.info('Rendering templates to locations %n', pathsToCreate); if (!(yield (0, fileSystem_1.isDirAsync)(finalDest))) { logger_1.default.tps.error('final destination was not a directory %n', { finalDest, }); throw new errors_1.DirectoryNotFoundError(finalDest); } yield this._answerRestOfPrompts(); logger_1.default.tps.info('Rendering template at %s', finalDest); const template = new template_1.Template(this.template, this.src, this.templateSettings, this.packages, this.packagesUsed, this.compiledFiles, this._defs); yield this._emitEvent('onRender', { dest: finalDest, buildPaths: pathsToCreate, hasBuildPaths: !buildInDest, createFile: (name, content) => { template.createFile(name, content, { force: this.opts.force }); }, createDirectory: (dir) => { template.createDirectory(dir); }, }); const builders = pathsToCreate.map((buildPath) => { const build = new build_1.Build(buildPath, template, { buildInDest, buildNewFolder, wipe: this.opts.wipe, force: this.opts.force, }); return this._renderBuildPath(build, data); }); yield Promise.all(builders); // TODO: When a event fails should we clean up the build path? yield this._emitEvent('onRendered', { dest: finalDest, buildPaths: pathsToCreate, }); // @ts-expect-error need to fix library if (is.array.empty(this.buildErrors)) { logger_1.default.tps.success('Finished rendering templates'); // @ts-expect-error Not sure whats wrong here return Array.isArray(buildPaths) ? pathsToCreate : pathsToCreate[0]; } logger_1.default.tps.info('Build Errors: %o', this.buildErrors.length); logger_1.default.tps.info('Build Paths need to be cleaned %n', this.buildErrors.map(({ build }) => build.getDirectory())); yield Promise.all(this.buildErrors.map((_a) => __awaiter(this, [_a], void 0, function* ({ build, didBuildPathExist }) { yield build.clean(buildNewFolder && !didBuildPathExist); }))); const errors = this.buildErrors.map(({ error }) => error); return Promise.reject(errors.length === 1 ? errors[0] : errors); }); } _renderBuildPath(build, data) { return __awaiter(this, void 0, void 0, function* () { const loggerGroup = build.getLogger(); const doesBuildPathExist = yield build.directoryExists(); try { yield this._emitEvent('onBuildPathRender', { buildPath: build.buildPath, }); const answers = this.hasPrompts() ? this._prompts.answers : {}; yield build.render(answers, data); } catch (err) { loggerGroup.error('Build Path: %s %n', build.buildPath, err); this._scheduleCleanUpForBuild(build, err, doesBuildPathExist); } finally { logger_1.default.tps.printGroup(build.getLoggerName()); yield this._emitEvent('onBuildPathRendered', { buildPath: build.buildPath, }); } }); } _scheduleCleanUpForBuild(build, err, didBuildPathExist) { build .getLogger() .info('Build Path schedule for cleaning %s %o', build.buildPath, { didBuildPathExist, }); this.buildErrors.push({ build, error: err, didBuildPathExist, }); } /** * Compile all files that need to be made for render process * @private * @param {String} packageName - name of package */ _compileFilesFromPackage(packageName) { const pkg = this.pkg(packageName); const { force } = this.opts; const defFiles = pkg.find({ type: 'file', ext: '.def' }); // @ts-expect-error need to fix library if (!is.array.empty(defFiles)) { logger_1.default.tps.info('Compiling def files %o', { force }); defFiles.forEach((fileNode) => { logger_1.default.tps.info(` - %s ${colors.green.italic('compiled')}`, fileNode.name); const name = fileNode.name.substring(0, fileNode.name.indexOf('.')); this._defs[name] = fs_1.default.readFileSync(fileNode.path).toString(); // When def files have more than one def. In order to use them we need to call the main file def first. // this fixes problems when any def can be available at render time this.engine.template(`{{#def.${name}}}`, null, this._defs); }); } logger_1.default.tps.info('Compiling files %n', { force, useExperimentalTemplateEngine: this.opts.experimentalTemplateEngine, }); pkg .find({ type: 'file', ext: { not: '.def' } }) .forEach((fileNode) => { const file = File_1.default.fromFileNode(fileNode, { force, useExperimentalTemplateEngine: this.opts.experimentalTemplateEngine, }); logger_1.default.tps.info(` - %s ${colors.green.italic('compiled')}`, fileNode.path); this.compiledFiles.push(file); }); } _answerRestOfPrompts() { return __awaiter(this, void 0, void 0, function* () { if (!this._prompts) return; const answers = yield this._prompts.getAnswers(); logger_1.default.tps.info('Answers from prompts %n', answers); (0, helpers_1.eachObj)(answers, (answer, answerName) => { if (this._prompts.getPrompt(answerName).isPkg()) { switch (true) { // @ts-expect-error need to fix library case is.undef(answer): case answer === null: break; // @ts-expect-error need to fix library case is.bool(answer): if (answer) { this.loadPackage(answerName); } break; case is.string(answer) && !!answer.length: this.loadPackage(answer); break; // @ts-expect-error need to fix library case is.array(answer) && !is.array.empty(answer): this.loadPackages(answer); break; default: throw new Error(`Data type '${typeof answer}' is not supported as answer to a tps prompt`); } } }); }); } /** * Configurations */ _loadTpsrc(templateName) { const tpsrcfiles = (0, fileSystem_1.cosmiconfigAllExampleSync)(TPS.CWD, tpsrcConfig, tpsrcSearchPlaces); if (is.empty(tpsrcfiles)) { logger_1.default.tps.info('No tps files to find: %n', { cwd: TPS.CWD, tpsrcSearchPlaces, }); } tpsrcfiles.reverse().forEach((tpsrc) => { if (!tpsrc || (tpsrc === null || tpsrc === void 0 ? void 0 : tpsrc.isEmpty)) return; logger_1.default.tps.info('Loading tpsrc from: %s %n', tpsrc.filepath, tpsrc); this._loadTpsSpecificConfig(templateName, tpsrc.config); }); } _loadTpsSpecificConfig(templateName, config) { var _a, _b, _c; const templateConfig = (_c = (_b = (_a = config[templateName]) !== null && _a !== void 0 ? _a : config[`tps-${templateName}`]) !== null && _b !== void 0 ? _b : config[(0, helpers_1.stripPrefix)(templateName, 'tps-')]) !== null && _c !== void 0 ? _c : null; if (templateConfig && is.object(templateConfig)) { logger_1.default.tps.info('Loading configuration: %n', templateConfig); const { answers = {}, opts = {} } = templateConfig; this.opts = Object.assign(Object.assign({}, this.opts), opts); if (is.object(answers) && !is.empty(answers)) { // TODO: Is this the best way to handle this? this.setAnswers(answers); } } } _emitEvent(event, ...args) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; logger_1.default.tps.info(`Running event ${event}`); const events = (_b = (_a = this.templateSettings) === null || _a === void 0 ? void 0 : _a.events) !== null && _b !== void 0 ? _b : null; if (events && event in events && typeof events[event] === 'function') { logger_1.default.tps.info(`Running ${event} function...`); // @ts-expect-error idk lol yield ((_c = events[event]) === null || _c === void 0 ? void 0 : _c.call(events, this, ...args)); } }); } } exports.Templates = Templates; /** * All tpsrc config file names. * * @example * * [ * '.tps/tps.config.cjs', * '.tps/tps.config.ts', * '.tps/tps.config.js', * '.tps/.tpsrc.cjs', * '.tps/.tpsrc.ts', * '.tps/.tpsrc.js', * '.tps/.tpsrc.yml', * '.tps/.tpsrc.yaml', * '.tps/.tpsrc.json', * '.tps/.tpsrc', * 'tps.config.cjs', * 'tps.config.ts', * 'tps.config.js', * '.config/tpsrc.cjs', * '.config/tpsrc.ts', * '.config/tpsrc.js', * '.config/tpsrc.yml', * '.config/tpsrc.yaml', * '.config/tpsrc.json', * '.config/tpsrc', * '.tpsrc.cjs', * '.tpsrc.ts', * '.tpsrc.js', * '.tpsrc.yml', * '.tpsrc.yaml', * '.tpsrc.json', * '.tpsrc', * 'package.json' * ] */ Templates.tpsrcConfigNames = tpsrcSearchPlaces; class SuccessfulBuild { constructor() { /** * Paths of files that were successfully built */ this.files = []; /** * Paths of directories that were successfully built */ this.dirs = []; } }