setup-next-project
Version:
CLI to quickly create a pre-configured Next.js project with modular components and features
304 lines ⢠13.6 kB
JavaScript
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