@solutioniser/create-next-starter
Version:
CLI tool to create Next.js starter applications with optional authentication providers and UI libraries
235 lines ⢠9.19 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
import { execa } from 'execa';
import chalk from 'chalk';
import ora from 'ora';
import { checkIfDirectoryExists, getTemplatePath, deepMerge } from './utils.js';
import { ensureTemplatesAvailable } from './github-fetcher.js';
export async function createProject(options) {
const { name, pm, provider, ui, storybook, templateTag } = options;
const projectPath = path.resolve(process.cwd(), name);
console.log(chalk.blue(`\nš Creating Next.js starter: ${chalk.bold(name)}\n`));
// Check if directory exists
if (checkIfDirectoryExists(projectPath)) {
throw new Error(`Directory "${name}" already exists. Please choose a different name.`);
}
// Ensure templates are available
const templatesDir = await ensureTemplatesAvailable(templateTag);
// Create project directory
await fs.mkdir(projectPath, { recursive: true });
// Copy base template
const baseTemplatePath = getTemplatePath('base', templatesDir);
await copyDirectory(baseTemplatePath, projectPath);
// Apply overlays
if (storybook) {
await applyOverlay(projectPath, 'storybook', templatesDir);
}
if (provider !== 'none') {
await applyOverlay(projectPath, provider, templatesDir);
}
if (ui !== 'none' && ui !== 'headless') {
await applyOverlay(projectPath, `ui-${ui}`, templatesDir);
}
// Merge package.json files
await mergePackageJson(projectPath);
// Create environment files
await createEnvFiles(projectPath, provider);
// Install dependencies
const installSpinner = ora('Installing dependencies...').start();
try {
await execa(pm, ['install'], { cwd: projectPath });
installSpinner.succeed('Dependencies installed');
}
catch (error) {
installSpinner.fail('Failed to install dependencies');
throw error;
}
// Initialize git
const gitSpinner = ora('Initializing git repository...').start();
try {
await execa('git', ['init', '-b', 'main'], { cwd: projectPath });
await execa('git', ['add', '-A'], { cwd: projectPath });
await execa('git', ['commit', '-m', 'chore: bootstrap from create-next-starter'], { cwd: projectPath });
gitSpinner.succeed('Git repository initialized');
}
catch (error) {
gitSpinner.fail('Failed to initialize git repository');
// Don't throw here, git is optional
}
// Print success message
printSuccessMessage(name, provider, ui, storybook);
}
async function copyDirectory(src, dest) {
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await fs.mkdir(destPath, { recursive: true });
await copyDirectory(srcPath, destPath);
}
else {
await fs.copyFile(srcPath, destPath);
}
}
}
async function applyOverlay(projectPath, overlayName, templatesDir) {
const overlayPath = getTemplatePath(`overlays/${overlayName}`, templatesDir);
if (!checkIfDirectoryExists(overlayPath)) {
throw new Error(`Overlay "${overlayName}" not found`);
}
await copyDirectory(overlayPath, projectPath);
}
async function mergePackageJson(projectPath) {
const packageJsonPath = path.join(projectPath, 'package.json');
// Find all package.json.partial files
const partialFiles = await findAllPartialFiles(projectPath);
if (partialFiles.length === 0) {
throw new Error('No package.json.partial files found in template');
}
// Process all partial files and replace PROJECT_NAME placeholder
const projectName = path.basename(projectPath);
const processedPartials = [];
for (const partialFile of partialFiles) {
const partialContent = await fs.readFile(partialFile, 'utf-8');
const processedContent = partialContent.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
const partialPackageJson = JSON.parse(processedContent);
processedPartials.push(partialPackageJson);
}
// Start with the first processed partial as the base
let mergedPackageJson = processedPartials[0];
// Merge remaining processed partials
for (let i = 1; i < processedPartials.length; i++) {
mergedPackageJson = deepMerge(mergedPackageJson, processedPartials[i]);
}
// Remove all partial files
for (const partialFile of partialFiles) {
await fs.unlink(partialFile);
}
// Project name is already set from placeholder replacement
// Reorder package.json properties to follow standard convention
const orderedPackageJson = reorderPackageJson(mergedPackageJson);
// Write merged package.json
await fs.writeFile(packageJsonPath, JSON.stringify(orderedPackageJson, null, 2));
}
function reorderPackageJson(packageJson) {
// Define the standard order for package.json properties
const standardOrder = [
'name',
'version',
'description',
'private',
'type',
'main',
'module',
'exports',
'bin',
'engines',
'scripts',
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
'bundledDependencies',
'os',
'cpu',
'preferGlobal',
'publishConfig',
'repository',
'bugs',
'homepage',
'keywords',
'author',
'contributors',
'license',
'files',
];
const ordered = {};
// Add properties in standard order if they exist
for (const key of standardOrder) {
if (packageJson.hasOwnProperty(key)) {
ordered[key] = packageJson[key];
}
}
// Add any remaining properties that weren't in the standard order
for (const key in packageJson) {
if (!ordered.hasOwnProperty(key)) {
ordered[key] = packageJson[key];
}
}
return ordered;
}
async function findAllPartialFiles(dir) {
const files = [];
async function search(currentDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
await search(fullPath);
}
else if (entry.name === 'package.json.partial' ||
entry.name.match(/^package\..*\.partial$/)) {
files.push(fullPath);
}
}
}
await search(dir);
return files;
}
async function findPartialFiles(dir, pattern) {
const files = [];
async function search(currentDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
await search(fullPath);
}
else if (entry.name === pattern) {
files.push(fullPath);
}
}
}
await search(dir);
return files;
}
async function createEnvFiles(projectPath, provider) {
const envExamplePath = path.join(projectPath, 'env.example');
const envLocalPath = path.join(projectPath, '.env.local');
let envContent = await fs.readFile(envExamplePath, 'utf-8');
// Add provider-specific environment variables
if (provider !== 'none') {
const providerEnvPath = path.join(projectPath, 'README.addon.md');
if (checkIfDirectoryExists(providerEnvPath)) {
// In a real implementation, you'd parse the README to extract env vars
// For now, we'll add a comment
envContent += `\n# ${provider} specific environment variables\n# See README.addon.md for details\n`;
}
}
// Create .env.local if it doesn't exist
if (!checkIfDirectoryExists(envLocalPath)) {
await fs.writeFile(envLocalPath, envContent);
}
}
function printSuccessMessage(projectName, provider, ui, storybook) {
console.log(chalk.green(`\nā
Successfully created ${chalk.bold(projectName)}!\n`));
console.log(chalk.blue('Next steps:'));
console.log(` ${chalk.cyan('cd')} ${projectName}`);
console.log(` ${chalk.cyan('pnpm dev')} - Start the development server`);
if (storybook) {
console.log(` ${chalk.cyan('pnpm storybook')} - Start Storybook`);
}
console.log(`\n${chalk.yellow("Don't forget to:")}`);
console.log(' ⢠Set up your environment variables in .env.local');
if (provider !== 'none') {
console.log(` ⢠Configure your ${provider} provider`);
console.log(` ⢠See README.addon.md for ${provider} setup instructions`);
}
if (ui !== 'none' && ui !== 'headless') {
console.log(` ⢠Configure your ${ui} UI library`);
console.log(` ⢠See README.addon.md for ${ui} setup instructions`);
}
console.log('\nHappy coding! š\n');
}
//# sourceMappingURL=create-project.js.map