@gavbarosee/react-kickstart
Version:
A modern CLI tool for creating React applications with various frameworks
284 lines (239 loc) • 8.15 kB
JavaScript
import fs from "fs-extra";
import path from "path";
import { createErrorHandler, ERROR_TYPES } from "../errors/index.js";
import { setupApiManagement } from "../features/api-clients/index.js";
import { setupLinting } from "../features/linting/linting.js";
import { createDirectoryStructure } from "../features/project-files/index.js";
import { setupStateManagement } from "../features/state-management/index.js";
import { TestingSetup } from "../features/testing/index.js";
import { setupTypeScript } from "../features/typescript/typescript.js";
import { UI_UTILS } from "../utils/index.js";
/**
* Abstract base class for all project generators
* Implements the Template Method pattern to eliminate duplication
*/
export class BaseGenerator {
constructor(frameworkName) {
this.frameworkName = frameworkName;
this.errorHandler = createErrorHandler();
}
/**
* Main generation method - Template Method pattern
* This defines the skeleton of the generation algorithm
*/
async generate(projectPath, projectName, userChoices) {
this.errorHandler.setContext({
projectPath,
projectName,
framework: this.frameworkName,
userChoices,
});
return this.errorHandler.withErrorHandling(
async () => {
// Step 2: Create base structure
await this.createBaseStructure(projectPath);
// Step 3: Create package.json
await this.createPackageConfiguration(projectPath, projectName, userChoices);
// Step 4: Create framework-specific config files
await this.createFrameworkConfiguration(projectPath, userChoices);
// Step 5: Create project files (source, routing, etc.)
await this.createProjectFiles(projectPath, projectName, userChoices);
// Step 6: Setup optional features
await this.setupOptionalFeatures(projectPath, userChoices);
// Step 7: Create framework-specific files
await this.createFrameworkSpecificFiles(projectPath, userChoices);
return true;
},
{
type: ERROR_TYPES.FILESYSTEM,
shouldCleanup: true,
},
);
}
/**
* Step 2: Create base directory structure
*/
async createBaseStructure(projectPath) {
createDirectoryStructure(projectPath, this.frameworkName);
}
/**
* Step 3: Create package.json - Must be implemented by subclasses
*/
async createPackageConfiguration(projectPath, projectName, userChoices) {
throw new Error("createPackageConfiguration must be implemented by subclass");
}
/**
* Step 4: Create framework-specific configuration files - Must be implemented by subclasses
*/
async createFrameworkConfiguration(projectPath, userChoices) {
throw new Error("createFrameworkConfiguration must be implemented by subclass");
}
/**
* Step 5: Create project files (source, routing, etc.) - Must be implemented by subclasses
*/
async createProjectFiles(projectPath, projectName, userChoices) {
throw new Error("createProjectFiles must be implemented by subclass");
}
/**
* Step 6: Setup optional features (common across all frameworks)
*/
async setupOptionalFeatures(projectPath, userChoices) {
// Linting setup
if (userChoices.linting) {
setupLinting(projectPath, userChoices, this.frameworkName);
}
// TypeScript setup
if (userChoices.typescript) {
setupTypeScript(projectPath, userChoices, this.frameworkName);
}
// State management setup
await this.setupStateManagement(projectPath, userChoices);
// API setup
await this.setupApi(projectPath, userChoices);
// Testing setup
await this.setupTesting(projectPath, userChoices);
// Deployment setup
await this.setupDeployment(projectPath, userChoices);
}
/**
* Setup state management (common logic)
*/
async setupStateManagement(projectPath, userChoices) {
setupStateManagement(projectPath, userChoices, this.frameworkName);
}
/**
* Setup API configuration (common logic)
*/
async setupApi(projectPath, userChoices) {
setupApiManagement(projectPath, userChoices, this.frameworkName);
}
/**
* Setup testing configuration (common logic)
*/
async setupTesting(projectPath, userChoices) {
if (userChoices.testing && userChoices.testing !== "none") {
const testingSetup = new TestingSetup(projectPath, userChoices);
await testingSetup.generateExampleTests();
}
}
/**
* Setup deployment configuration (common logic)
*/
async setupDeployment(projectPath, userChoices) {
if (userChoices.deployment && userChoices.deployment !== "none") {
await this.createDeploymentConfiguration(projectPath, userChoices);
}
}
/**
* Create deployment configuration files
*/
async createDeploymentConfiguration(projectPath, userChoices) {
const { deployment, framework } = userChoices;
try {
if (deployment === "vercel") {
await this.createVercelConfiguration(projectPath, framework);
} else if (deployment === "netlify") {
await this.createNetlifyConfiguration(projectPath, framework, userChoices);
}
} catch (error) {
UI_UTILS.error(`Failed to create deployment configuration: ${error.message}`);
throw error;
}
}
/**
* Create Vercel configuration
*/
async createVercelConfiguration(projectPath, framework) {
const vercelConfig = {
version: 2,
builds: [
{
src: "package.json",
use: framework === "nextjs" ? "@vercel/next" : "@vercel/static-build",
},
],
};
// Add framework-specific settings
if (framework === "vite") {
vercelConfig.buildCommand = "npm run build";
vercelConfig.outputDirectory = "dist";
}
// Add default region for better performance
if (framework === "nextjs") {
vercelConfig.regions = ["iad1"]; // US East - good default
}
const configPath = path.join(projectPath, "vercel.json");
await fs.writeFile(configPath, JSON.stringify(vercelConfig, null, 2));
}
/**
* Create Netlify configuration
*/
async createNetlifyConfiguration(projectPath, framework, userChoices) {
let netlifyConfig = `# Netlify configuration
[build]
publish = "${framework === "nextjs" ? "out" : "dist"}"
command = "npm run build"
[build.environment]
NODE_VERSION = "18"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
`;
// Add framework-specific settings
if (framework === "nextjs") {
netlifyConfig += `
[[plugins]]
package = "@netlify/plugin-nextjs"
`;
// Create next.config.js for static export
await this.createNextjsStaticConfig(projectPath, userChoices);
}
const configPath = path.join(projectPath, "netlify.toml");
await fs.writeFile(configPath, netlifyConfig);
}
/**
* Create Next.js static export configuration for Netlify
*/
async createNextjsStaticConfig(projectPath, userChoices) {
const config = {
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
};
// Add TypeScript config if enabled
if (userChoices.typescript) {
config.typescript = {
ignoreBuildErrors: false,
};
}
const nextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = ${JSON.stringify(config, null, 2)};
module.exports = nextConfig;
`;
const configPath = path.join(projectPath, "next.config.js");
await fs.writeFile(configPath, nextConfig);
}
/**
* Step 7: Create framework-specific files - Must be implemented by subclasses
*/
async createFrameworkSpecificFiles(projectPath, userChoices) {
throw new Error("createFrameworkSpecificFiles must be implemented by subclass");
}
/**
* Utility method to check if a feature should be enabled
*/
isFeatureEnabled(userChoices, feature) {
return userChoices[feature] && userChoices[feature] !== "none";
}
/**
* Utility method for conditional feature setup
*/
async setupFeatureIf(condition, setupFunction, ...args) {
if (condition) {
await setupFunction(...args);
}
}
}