UNPKG

setup-next-project

Version:

CLI to quickly create a pre-configured Next.js project with modular components and features

304 lines • 13.6 kB
import chalk from "chalk"; import fs from "fs-extra"; import ora from "ora"; import * as path from "path"; import { detectPackageManager, runCommand } from "./packageManager.js"; import { getFeature } from "./features.js"; import { dirname } from "path"; import { fileURLToPath } from "url"; // Folders and files to exclude when copying templates const EXCLUDED_ITEMS = [ "node_modules", ".git", ".next", "dist", "build", ".turbo", "pnpm-lock.yaml", "yarn.lock", "package-lock.json", ".DS_Store", "Thumbs.db", ]; async function copyTemplateFiles(sourcePath, targetPath) { const items = await fs.readdir(sourcePath); for (const item of items) { if (EXCLUDED_ITEMS.includes(item)) { continue; // Ignore excluded items } const sourceItemPath = path.join(sourcePath, item); const targetItemPath = path.join(targetPath, item); const stat = await fs.stat(sourceItemPath); if (stat.isDirectory()) { await fs.ensureDir(targetItemPath); await copyTemplateFiles(sourceItemPath, targetItemPath); } else { // Handle gitignore.template -> .gitignore rename if (item === "gitignore.template") { const gitignoreDestPath = path.join(targetPath, ".gitignore"); await fs.copy(sourceItemPath, gitignoreDestPath); } else { await fs.copy(sourceItemPath, targetItemPath); } } } } export async function createProject(options) { const { projectName, features, projectPath } = options; const spinner = ora("Creating project...").start(); try { // Get the basic template path const basicTemplatePath = getBasicTemplatePath(); spinner.text = "Copying base Next.js template..."; // Create project folder await fs.ensureDir(projectPath); // Copy basic template files if (await fs.pathExists(basicTemplatePath)) { await copyTemplateFiles(basicTemplatePath, projectPath); } else { throw new Error(`Basic template folder "${basicTemplatePath}" does not exist.`); } spinner.text = "Updating package.json..."; // Initialize package.json with project name const packageJsonPath = path.join(projectPath, "package.json"); let packageJson = {}; if (await fs.pathExists(packageJsonPath)) { packageJson = await fs.readJson(packageJsonPath); packageJson.name = projectName; } // Process selected features if (features.length > 0) { spinner.text = "Adding selected features..."; await addFeaturesToProject(projectPath, features, packageJson); } // Save updated package.json await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); spinner.text = "Personalizing project..."; // Update project files with project name await updateHeaderWithProjectName(projectPath, projectName); await updateLayoutMetadata(projectPath, projectName); await updateHomePageWithProjectName(projectPath, projectName); spinner.succeed(chalk.green("Project created successfully!")); // Git initialization await initializeGitRepository(projectPath); // Automatic dependency installation if requested if (options.autoInstall !== false) { await installDependencies(projectPath, projectName); } } catch (error) { spinner.fail(chalk.red("Error creating project")); throw error; } } async function updateHeaderWithProjectName(projectPath, projectName) { const headerPath = path.join(projectPath, "src", "components", "layout", "header.tsx"); if (await fs.pathExists(headerPath)) { try { let headerContent = await fs.readFile(headerPath, "utf-8"); // Convert project name to a nice title (capitalize each word, replace hyphens/underscores with spaces) const projectTitle = projectName .replace(/[-_]/g, " ") .split(" ") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); // Replace "My App" with the project title headerContent = headerContent.replace('<h1 className="text-lg font-bold">My App</h1>', `<h1 className="text-lg font-bold">${projectTitle}</h1>`); await fs.writeFile(headerPath, headerContent, "utf-8"); } catch (error) { // Don't fail the process if header update fails console.log(chalk.gray("Could not update header title")); } } } async function updateLayoutMetadata(projectPath, projectName) { const layoutPath = path.join(projectPath, "app", "layout.tsx"); if (await fs.pathExists(layoutPath)) { try { let layoutContent = await fs.readFile(layoutPath, "utf-8"); // Convert project name to a nice title const projectTitle = projectName .replace(/[-_]/g, " ") .split(" ") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); // Replace metadata title and description layoutContent = layoutContent.replace('title: "Create Next App"', `title: "${projectTitle}"`); layoutContent = layoutContent.replace('description: "Generated by create next app"', `description: "${projectTitle} - Built with setup-next-project"`); await fs.writeFile(layoutPath, layoutContent, "utf-8"); } catch (error) { // Don't fail the process if layout update fails console.log(chalk.gray("Could not update layout metadata")); } } } async function updateHomePageWithProjectName(projectPath, projectName) { const homePage = path.join(projectPath, "app", "page.tsx"); if (await fs.pathExists(homePage)) { try { let homeContent = await fs.readFile(homePage, "utf-8"); // Convert project name to a nice title (capitalize each word, replace hyphens/underscores with spaces) const projectTitle = projectName .replace(/[-_]/g, " ") .split(" ") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); // Replace "Welcome to My App" with the project title homeContent = homeContent.replace('<h1 className="text-2xl font-bold">Welcome to My App</h1>', `<h1 className="text-2xl font-bold">Welcome to ${projectTitle}</h1>`); await fs.writeFile(homePage, homeContent, "utf-8"); } catch (error) { // Don't fail the process if homepage update fails console.log(chalk.gray("Could not update homepage title")); } } } async function initializeGitRepository(projectPath) { const spinner = ora("Initializing Git repository...").start(); try { // Initialize Git repo await runCommand("git init", projectPath); // Add all files await runCommand("git add .", projectPath); // Make first commit await runCommand('git commit -m "Initial commit from setup-next-project"', projectPath); spinner.succeed(chalk.green("āœ… Git repository initialized!")); console.log(chalk.cyan("šŸ”— Ready for GitHub Desktop or manual push")); } catch (error) { spinner.warn(chalk.yellow("āš ļø Git initialization failed")); console.log(chalk.gray("You can initialize Git manually if needed")); // Don't fail the process for a Git error console.log(chalk.gray(`Error: ${error instanceof Error ? error.message : String(error)}`)); } } async function installDependencies(projectPath, projectName) { const spinner = ora("Detecting package manager...").start(); try { // Detect available package manager const packageManager = await detectPackageManager(); spinner.text = `Installing dependencies with ${packageManager.name}...`; spinner.color = "blue"; // Install dependencies await runCommand(packageManager.installCommand, projectPath); spinner.succeed(chalk.green(`āœ… Dependencies installed with ${packageManager.name}!`)); console.log(chalk.cyan("\nšŸŽ‰ Project setup complete! Available commands:")); console.log(chalk.white(` cd ${projectName}`)); console.log(chalk.white(` ${packageManager.runCommand} dev`)); } catch (error) { spinner.warn(chalk.yellow("āš ļø Automatic installation failed")); console.log(chalk.yellow("You can install dependencies manually with:")); console.log(chalk.white(` cd ${projectName}`)); console.log(chalk.white(" pnpm install # or npm install")); // Don't fail the entire process for an installation error console.log(chalk.gray(`Error: ${error instanceof Error ? error.message : String(error)}`)); } } function getBasicTemplatePath() { let templatesDir; // First, try to find the templates directory relative to the script location const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); templatesDir = path.join(__dirname, "../../templates"); // If templates doesn't exist relative to script, try current working directory method if (!fs.existsSync(templatesDir)) { if (process.cwd().includes("setup-next-project")) { templatesDir = path.join(process.cwd(), "templates"); } else { // Look for setup-next-project in parent directories let currentDir = process.cwd(); while (currentDir !== path.dirname(currentDir)) { const possibleTemplatesDir = path.join(currentDir, "templates"); if (fs.existsSync(possibleTemplatesDir)) { templatesDir = possibleTemplatesDir; break; } currentDir = path.dirname(currentDir); } } } return path.join(templatesDir, "basic"); } async function addFeaturesToProject(projectPath, selectedFeatures, packageJson) { for (const featureId of selectedFeatures) { const feature = await getFeature(featureId); if (!feature) { console.warn(chalk.yellow(`āš ļø Feature "${featureId}" not found, skipping...`)); continue; } console.log(chalk.cyan(` Adding ${feature.name}...`)); // Add dependencies if (feature.dependencies && feature.dependencies.length > 0) { if (!packageJson.dependencies) packageJson.dependencies = {}; feature.dependencies.forEach(dep => { // For simplicity, we'll add them with "latest" version // In a real implementation, you might want to specify exact versions packageJson.dependencies[dep] = "latest"; }); } // Add dev dependencies if (feature.devDependencies && feature.devDependencies.length > 0) { if (!packageJson.devDependencies) packageJson.devDependencies = {}; feature.devDependencies.forEach(dep => { packageJson.devDependencies[dep] = "latest"; }); } // Update package.json scripts and other updates if (feature.packageJsonUpdates) { for (const [key, value] of Object.entries(feature.packageJsonUpdates)) { if (typeof value === 'object' && value !== null) { // Merge objects (like scripts) if (!packageJson[key]) packageJson[key] = {}; Object.assign(packageJson[key], value); } else { packageJson[key] = value; } } } // Create feature files if (feature.files && feature.files.length > 0) { for (const file of feature.files) { const filePath = path.join(projectPath, file.path); const fileDir = path.dirname(filePath); // Ensure directory exists await fs.ensureDir(fileDir); // Handle different file modes switch (file.mode) { case 'append': if (await fs.pathExists(filePath)) { await fs.appendFile(filePath, '\n' + file.content); } else { await fs.writeFile(filePath, file.content, 'utf8'); } break; case 'prepend': if (await fs.pathExists(filePath)) { const existingContent = await fs.readFile(filePath, 'utf8'); await fs.writeFile(filePath, file.content + '\n' + existingContent, 'utf8'); } else { await fs.writeFile(filePath, file.content, 'utf8'); } break; case 'create': default: await fs.writeFile(filePath, file.content, 'utf8'); break; } } } } } //# sourceMappingURL=createProject.js.map