UNPKG

skaya

Version:

CLI SDK for full-stack automation: scaffold frontend, backend & blockchain. Future-ready for Web3, integrations, server components & logging.

315 lines (314 loc) 15.6 kB
"use strict"; 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 }); // src/services/TemplateService.ts const child_process_1 = require("child_process"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const inquirer_1 = __importDefault(require("inquirer")); const enums_1 = require("../../bin/types/enums"); const util_1 = require("util"); const ProjectScanner_1 = require("../../bin/utils/ProjectScanner"); class TemplateService { constructor() { this.loadTemplatesConfig(); } loadTemplatesConfig() { const configPath = path_1.default.join(__dirname, "../../bin/templates/githubTemplates.json"); if (!fs_extra_1.default.existsSync(configPath)) { throw new Error("Template configuration file not found at "); } this.templatesConfig = require(configPath); } promptTemplateSelection(projectType) { return __awaiter(this, void 0, void 0, function* () { // For frontend projects, first ask if they want to use a framework or template if (projectType === enums_1.ProjectType.FRONTEND) { const { creationMethod } = yield inquirer_1.default.prompt([ { type: "list", name: "creationMethod", message: "How would you like to create your frontend project?", choices: [ { name: "Use a framework (React, Next.js, Vite)", value: "framework", }, { name: "Use a template", value: "template" }, ], }, ]); if (creationMethod === "framework") { const { framework } = yield inquirer_1.default.prompt([ { type: "list", name: "framework", message: "Select frontend framework:", choices: [ { name: "React (via create-react-app)", value: "react" }, { name: "Next.js", value: "next" }, { name: "Vite", value: "vite" }, ], }, ]); return { templateType: framework }; } } // For backend or template-based frontend projects const categories = this.templatesConfig[`${projectType}Categories`]; if (!categories) { throw new Error(`No templates available for project type: ${projectType}`); } const { category } = yield inquirer_1.default.prompt([ { type: "list", name: "category", message: `Select ${projectType} template category:`, choices: Object.keys(categories).map((cat) => ({ name: this.formatCategoryName(cat), value: cat, })), }, ]); const templateChoices = categories[category].map((t) => ({ name: this.formatTemplateName(t), value: t, })); const { templateType } = yield inquirer_1.default.prompt([ { type: "list", name: "templateType", message: `Select a ${projectType} template:`, choices: templateChoices, }, ]); if (templateType === "custom-repo") { const { customRepo } = yield inquirer_1.default.prompt([ { type: "input", name: "customRepo", message: "Enter GitHub repository URL:", validate: (input) => !!input.trim() || "URL cannot be empty", }, ]); return { templateType, customRepo }; } return { templateType }; }); } cloneTemplate(templateType, customRepo, targetPath, projectType) { return __awaiter(this, void 0, void 0, function* () { try { // Handle framework initialization if (projectType === enums_1.ProjectType.FRONTEND && ["react", "next", "vite"].includes(templateType)) { yield this.initializeFramework(templateType, targetPath); return; } // Handle template cloning const repoUrl = templateType === "custom-repo" ? customRepo : this.templatesConfig[projectType][templateType]; if (!repoUrl) { throw new Error(`Repository URL not found for template: ${templateType}`); } console.log(`🚀 Cloning ${this.formatTemplateName(templateType)} template...`); (0, child_process_1.execSync)(`git clone ${repoUrl} ${targetPath}`, { stdio: "inherit" }); // Post-clone setup yield this.postCloneSetup(targetPath, templateType); } catch (error) { throw new Error(`❌ Failed to initialize project: ${error instanceof Error ? error.message : error}`); } }); } initializeFramework(framework, targetPath) { return __awaiter(this, void 0, void 0, function* () { console.log(`🚀 Initializing ${framework} project...`); switch (framework) { case "react": (0, child_process_1.execSync)(`npx create-react-app ${targetPath} --template typescript`, { stdio: "inherit", }); break; case "next": // Construct the npx create-next-app command with recommended settings const nextAppCommand = `npx create-next-app ${targetPath} ` + `--ts ` + // Enable TypeScript `--eslint ` + // Enable ESLint `--src-dir ` + // Enable src/ directory `--app ` + // Enable App Router `--import-alias "@/*" ` + // Set import alias to @/* `--no-turbo`; // Disable Turbopack for better Web3 compatibility console.log(`Executing: ${nextAppCommand}`); (0, child_process_1.execSync)(nextAppCommand, { stdio: "inherit" }); break; case "vite": const { viteTemplate } = yield inquirer_1.default.prompt([ { type: "list", name: "viteTemplate", message: "Select Vite template:", choices: ["react-ts", "react", "vanilla-ts", "vanilla"], }, ]); // Ensure target directory exists yield fs_extra_1.default.ensureDir(targetPath); // Get just the directory name (last part of path) const dirName = path_1.default.basename(targetPath); // Run command in parent directory const parentDir = path_1.default.dirname(targetPath); process.chdir(parentDir); (0, child_process_1.execSync)(`npm create vite@latest ${dirName} -- --template ${viteTemplate}`, { stdio: "inherit" }); break; default: throw new Error(`Unsupported framework: ${framework}`); } console.log(`✅ Successfully initialized ${framework} project`); }); } postCloneSetup(targetPath, templateType) { return __awaiter(this, void 0, void 0, function* () { // Remove .git to detach history yield fs_extra_1.default.remove(path_1.default.join(targetPath, ".git")); // Initialize new git repo process.chdir(targetPath); (0, child_process_1.execSync)("git init", { stdio: "inherit" }); // Add all files and make initial commit // execSync('git add .', { stdio: 'inherit' }); // execSync('git commit -m "skaya init"', { stdio: 'inherit' }); process.chdir(".."); console.log(`✅ Successfully initialized project with ${this.formatTemplateName(templateType)} template`); }); } formatCategoryName(category) { return category .split("-") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); } formatTemplateName(template) { return template .replace("skaya-", "") .split("-") .map((word) => word.toUpperCase()) .join(" "); } /** * Saves template files to disk * @param {Object} params - Saving parameters * @param {TemplateFileInfo[]} params.templateFiles - Template files to save * @param {string} params.fileName - The base name for the component * @param {string} params.targetFolder - The target folder path * @param {ComponentType} params.componentType - The type of component * @returns {Promise<string[]>} Array of created file paths */ saveTemplateFiles(params) { return __awaiter(this, void 0, void 0, function* () { const { templateFiles, fileName, targetFolder, componentType } = params; const createdFiles = []; for (const templateFile of templateFiles) { let content = templateFile.content; if (!content) { throw new Error(`Template file ${templateFile.originalFileName} is empty.`); } // Handle the special Storybook 'component: Component' case content = content.replace(/component: Component/g, `component: ${fileName}`); // Do general replacements content = content .replace(/{{component}}/g, fileName.toLowerCase()) .replace(/{{Component}}/g, fileName) .replace(/{{COMPONENT}}/g, fileName.toUpperCase()) .replace(new RegExp(`(?<!React\\.)(\\b|_)${componentType}(?![:])(\\b|_)`, "gi"), (match) => fileName); // Determine target file name based on component type let targetFileName = fileName; const targetPath = path_1.default.join(process.cwd(), targetFolder, targetFileName, templateFile.targetFileName); yield fs_extra_1.default.outputFile(targetPath, content); createdFiles.push(targetPath); } return createdFiles; }); } /** * Gets template files for a specific component type with their contents * @param {ComponentType} componentType - The type of component * @param {string} fileName - The name of the file to use for replacements * @param {string} templateDir - The directory where templates are located * @returns {Promise<TemplateFileInfo[]>} Array of template file information with contents */ getTemplateFilesForType(fileName, componentType, projectType) { return __awaiter(this, void 0, void 0, function* () { const templateDir = yield (0, ProjectScanner_1.getDefaultTemplateDirectory)(projectType, componentType); const baseFiles = this.getBaseTemplateFiles(componentType); const result = []; const formattedFileName = fileName.charAt(0).toUpperCase() + fileName.slice(1).toLowerCase(); for (const file of baseFiles) { const targetFileName = file.replace(new RegExp(componentType, "gi"), formattedFileName); const filePath = path_1.default.join(templateDir, file); try { const content = yield (0, util_1.promisify)(fs_extra_1.default.readFile)(filePath, "utf-8"); result.push({ originalFileName: file, targetFileName, content, }); } catch (error) { console.error(`Error reading template file ${filePath}:`, error); result.push({ originalFileName: file, targetFileName, content: "", // Fallback empty content if file can't be read }); } } return result; }); } getBaseTemplateFiles(componentType) { switch (componentType) { case enums_1.FrontendComponentType.COMPONENT: return [ `${enums_1.FrontendComponentType.COMPONENT}.tsx`, `${enums_1.FrontendComponentType.COMPONENT}.stories.tsx`, `${enums_1.FrontendComponentType.COMPONENT}.test.tsx`, `${enums_1.FrontendComponentType.COMPONENT}.css`, ]; case enums_1.FrontendComponentType.PAGE: return [ `${enums_1.FrontendComponentType.PAGE}.tsx`, `${enums_1.FrontendComponentType.PAGE}.test.tsx`, `${enums_1.FrontendComponentType.PAGE}.css`, ]; case enums_1.FrontendComponentType.API: return [`${enums_1.FrontendComponentType.API}Slice.tsx`, "backendRequest.ts"]; case enums_1.ApiType.REDUX: return ["redux/store.tsx", "redux/storeProvider.tsx"]; case enums_1.BackendComponentType.ROUTE: return [ `${enums_1.BackendComponentType.ROUTE}.ts`, `${enums_1.BackendComponentType.ROUTE}.test.ts`, ]; case enums_1.BackendComponentType.CONTROLLER: return [ `${enums_1.BackendComponentType.CONTROLLER}.ts`, `${enums_1.BackendComponentType.CONTROLLER}.test.ts`, ]; default: const exhaustiveCheck = componentType; throw new Error(`Unhandled component type for getTemplateFilesForType: ${exhaustiveCheck}`); } } } exports.default = new TemplateService();