UNPKG

@interopio/iocd-cli

Version:

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

354 lines 16.1 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")); const config_service_1 = require("./config/config.service"); const path_1 = require("../utils/path"); /** * 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 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); // add components configuration to package.json await this.updateComponentsConfig(options.targetDirectory, options.initialComponents); // Process template application files if selected if (options.templateApplications && options.templateApplications.length > 0) { await this.processApplicationsTemplates(options.targetDirectory, variables, options.templateApplications); } 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' ] }); } } /** Add a new template application to an already created seed project */ async addTemplateApplication(appName) { this.logger.info(`Adding template application: ${appName}`); const config = config_service_1.ConfigManager.config; // Implementation goes here const variables = this.prepareTemplateVariables({ productSlug: config.productSlug, productName: config.productName, company: config.company, copyright: config.copyright, version: config.version, folderName: config.productSlug, targetDirectory: path_1.PathUtils.getRootDir(), templateApplications: [appName], initialComponents: [] }); await this.processApplicationsTemplates('./', variables, [appName]); this.logger.info(`Template application ${appName} added successfully`); } /** Adds initialComponents to the iocd.cli.config.json -> components.list object in the target directory */ updateComponentsConfig(targetDirectory, initialComponents) { const configPath = (0, node_path_1.join)(targetDirectory, 'iocd.cli.config.json'); this.logger.debug(`Updating components configuration in ${configPath} to include:`, initialComponents); if (!(0, node_fs_1.existsSync)(configPath)) { this.logger.warn(`iocd.cli.config.json not found in target directory, skipping components configuration`); return; } const configContent = (0, node_fs_1.readFileSync)(configPath, 'utf-8'); const config = JSON.parse(configContent); // Ensure the components structure exists if (!config.components) { config.components = {}; } if (!config.components.list) { config.components.list = {}; } // Add initial components, avoiding duplicates for (const component of initialComponents) { config.components.list[component] = "latest"; } // write back the updated config (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2), 'utf-8'); this.logger.debug(`Updated components configuration in iocd.cli.config.json`); } /** * 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.templateApplications || []), 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 processApplicationsTemplates(targetPath, variables, applications) { for (const application of applications) { const featureTemplatePath = (0, node_path_1.join)(this.templatesPath, application); if (!(0, node_fs_1.existsSync)(featureTemplatePath)) { this.logger.warn(`Application template '${application}' not found in templates directory`); continue; } // Verify it's a application template const templateInfo = this.getTemplateInfo(application); if (templateInfo.type !== 'application') { this.logger.warn(`Template '${application}' is not a application template, skipping`); continue; } this.logger.debug(`Processing application template: ${application}`); 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 application templates with their metadata */ getAvailableApplicationTemplates() { const allTemplates = this.getAvailableTemplates(); const applicationTemplates = []; for (const templateName of allTemplates) { const templateInfo = this.getTemplateInfo(templateName); if (templateInfo.type === 'application') { applicationTemplates.push(templateInfo); } } return applicationTemplates; } } exports.TemplateService = TemplateService; //# sourceMappingURL=template.service.js.map