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
JavaScript
;
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();