UNPKG

@interopio/desktop-cli

Version:

CLI tool for setting up, building and packaging io.Connect Desktop projects

306 lines 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TemplateService = void 0; const node_path_1 = require("node:path"); const node_fs_1 = require("node:fs"); const logger_1 = require("../utils/logger"); const error_handler_1 = require("../utils/error.handler"); const fs_extra_1 = __importDefault(require("fs-extra")); /** * TemplateService handles the generation of io.Connect Desktop projects using a modular template system. * * This service supports two types of templates: * - Base templates: Core project structure and essential files (e.g., ioconnect-desktop) * - Feature templates: Optional functionality modules that extend the base template * * The service processes templates by: * 1. Copying base template files to the target directory * 2. Applying variable substitutions ({{variable}} patterns) * 3. Conditionally applying feature templates based on user selection: * - Validates that requested features exist as separate template directories * - Only processes features that are explicitly selected by the user * - Each feature template contains its own template.json with type: "feature" * - Features are independent and can be combined in any combination * 4. Merging feature-specific files into the project structure: * - Feature templates overlay their files onto the base project structure * - Files from features are copied to their corresponding paths in the target * - Directory structures are preserved (e.g., modifications/iocd/assets/feature/) * - No conflicts occur since features target different subdirectories * - Variable substitution is applied to feature files just like base template files * * Key capabilities: * - Template discovery and validation from filesystem * - Variable interpolation for dynamic content generation * - Modular feature system for extensible project setup * - Comprehensive error handling and logging * - Support for nested directory structures and file copying */ class TemplateService { logger = logger_1.Logger.getInstance(); templatesPath; constructor() { this.templatesPath = (0, node_path_1.join)(__dirname, '../templates'); } /** * Get the path to embedded templates */ getTemplatesPath() { return this.templatesPath; } /** * List available templates */ getAvailableTemplates() { try { if (!(0, node_fs_1.existsSync)(this.templatesPath)) { this.logger.warn('Templates directory does not exist:', this.templatesPath); return []; } return (0, node_fs_1.readdirSync)(this.templatesPath, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); } catch (error) { this.logger.error('Failed to list templates:', error); throw new error_handler_1.CLIError('Failed to list available templates', { code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR, cause: error, suggestions: [ 'Check if the CLI installation is complete', 'Try reinstalling the CLI tool' ] }); } } /** * Check if a template exists */ templateExists(templateName) { const templatePath = (0, node_path_1.join)(this.templatesPath, templateName); return (0, node_fs_1.existsSync)(templatePath) && (0, node_fs_1.statSync)(templatePath).isDirectory(); } /** * Generate a project from a template */ async generateProject(templateName, options) { this.logger.info(`Generating project from template: ${templateName}`); this.logger.debug(`Generating project with options:\n`, options); // Validate template exists if (!this.templateExists(templateName)) { throw new error_handler_1.CLIError(`Template '${templateName}' not found`, { code: error_handler_1.ErrorCode.NOT_FOUND, suggestions: [ 'Check available templates with: iocd-cli templates list', 'Verify the template name is spelled correctly' ] }); } // Ensure only base templates can be used for project generation const templateInfo = this.getTemplateInfo(templateName); if (templateInfo.type !== 'base') { throw new error_handler_1.CLIError(`Template '${templateName}' is not a base template`, { code: error_handler_1.ErrorCode.VALIDATION_ERROR, suggestions: [ 'Use a base template like "ioconnect-desktop"', 'Feature templates are applied automatically through the features option' ] }); } // Check if target directory already exists if ((0, node_fs_1.existsSync)(options.targetDirectory)) { throw new error_handler_1.CLIError(`Directory '${options.targetDirectory}' already exists`, { code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR, suggestions: [ 'Choose a different project name', 'Remove the existing directory', 'Use a different target location' ] }); } const templatePath = (0, node_path_1.join)(this.templatesPath, templateName); try { // Create target directory (0, node_fs_1.mkdirSync)(options.targetDirectory, { recursive: true }); this.logger.debug(`Created target directory: ${options.targetDirectory}`); // Prepare template variables const variables = this.prepareTemplateVariables(options); // Copy base template files with variable replacement await this.copyTemplateFiles(templatePath, options.targetDirectory, variables); // Process feature-specific files if features are selected if (options.features && options.features.length > 0) { await this.processFeatureFiles(options.targetDirectory, variables, options.features); } this.logger.info(`Project generated successfully at: ${options.targetDirectory}`); } catch (error) { this.logger.error('Failed to generate project:', error); throw new error_handler_1.CLIError('Failed to generate project from template', { code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR, cause: error, suggestions: [ 'Check write permissions for the target directory', 'Ensure sufficient disk space is available', 'Try running with --verbose for more details' ] }); } } /** * Prepare template variables for replacement */ prepareTemplateVariables(options) { return [ { key: '{{PRODUCT_NAME}}', value: options.productName, description: 'The product name for the application' }, { key: '{{FOLDER_NAME}}', value: options.folderName, description: 'The folder name for the project' }, { key: '{{PRODUCT_SLUG}}', value: this.generatePackageName(options.productSlug), description: 'NPM-compatible package name' }, { key: '{{FEATURES}}', value: JSON.stringify(options.features || []), description: 'Selected features array' }, { key: '{{YEAR}}', value: new Date().getFullYear().toString(), description: 'Current year' } ]; } /** * Generate NPM-compatible package name from product name */ generatePackageName(productName) { return productName .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } /** * Recursively copy template files with variable replacement */ async copyTemplateFiles(sourcePath, targetPath, variables) { const items = (0, node_fs_1.readdirSync)(sourcePath, { withFileTypes: true }); for (const item of items) { const sourceItemPath = (0, node_path_1.join)(sourcePath, item.name); const targetItemPath = (0, node_path_1.join)(targetPath, item.name); // Skip template.json files - they are metadata for the template system if (item.isFile() && item.name === 'template.json') { continue; } if (item.isDirectory()) { // Create directory and recurse (0, node_fs_1.mkdirSync)(targetItemPath, { recursive: true }); await this.copyTemplateFiles(sourceItemPath, targetItemPath, variables); } else if (item.isFile()) { // the logic here is to check the extension and only replace known text-based extensions // the rest should be copied as-is (e.g. images, binaries), otherwise they may get corrupted // the check is a bit complicated because of the .json.merge and .json.merge-<<suffix>> files const ext = (0, node_path_1.extname)(sourceItemPath); const allowedExtensions = ['.env', '.json', '.merge']; const isAllowedExtension = allowedExtensions.some(e => ext.startsWith(e) || ext === e); if (ext && isAllowedExtension) { await this.copyFileWithReplacement(sourceItemPath, targetItemPath, variables); } else { await this.copyFileNoReplacement(sourceItemPath, targetItemPath); } } } } /** * Copy a single file with variable replacement */ async copyFileWithReplacement(sourcePath, targetPath, variables) { const sourceContent = (0, node_fs_1.readFileSync)(sourcePath, 'utf-8'); // Replace template variables let targetContent = sourceContent; for (const variable of variables) { const regex = new RegExp(this.escapeRegExp(variable.key), 'g'); targetContent = targetContent.replace(regex, variable.value); } (0, node_fs_1.writeFileSync)(targetPath, targetContent, 'utf-8'); this.logger.debug(`Copied and processed: ${(0, node_path_1.relative)(process.cwd(), targetPath)}`); } copyFileNoReplacement(source, destination) { fs_extra_1.default.copyFile(source, destination); } /** * Process feature-specific templates */ async processFeatureFiles(targetPath, variables, features) { for (const feature of features) { const featureTemplatePath = (0, node_path_1.join)(this.templatesPath, feature); if (!(0, node_fs_1.existsSync)(featureTemplatePath)) { this.logger.warn(`Feature template '${feature}' not found in templates directory`); continue; } // Verify it's a feature template const templateInfo = this.getTemplateInfo(feature); if (templateInfo.type !== 'feature') { this.logger.warn(`Template '${feature}' is not a feature template, skipping`); continue; } this.logger.debug(`Processing feature template: ${feature}`); await this.copyTemplateFiles(featureTemplatePath, targetPath, variables); } } /** * Escape special regex characters */ escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Get template information */ getTemplateInfo(templateName) { const templatePath = (0, node_path_1.join)(this.templatesPath, templateName); const templateInfoPath = (0, node_path_1.join)(templatePath, 'template.json'); if ((0, node_fs_1.existsSync)(templateInfoPath)) { try { return JSON.parse((0, node_fs_1.readFileSync)(templateInfoPath, 'utf-8')); } catch (error) { this.logger.warn(`Failed to parse template.json for ${templateName}:`, error); } } return { name: templateName, description: `${templateName} template`, version: '1.0.0', type: 'base' // Default fallback }; } /** * Get available feature templates with their metadata */ getAvailableFeatureTemplates() { const allTemplates = this.getAvailableTemplates(); const featureTemplates = []; for (const templateName of allTemplates) { const templateInfo = this.getTemplateInfo(templateName); if (templateInfo.type === 'feature') { featureTemplates.push(templateInfo); } } return featureTemplates; } } exports.TemplateService = TemplateService; //# sourceMappingURL=template.service.js.map