UNPKG

@penjc/homepage

Version:

个人主页模板,支持博客、随笔等功能

445 lines (367 loc) 12.5 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { execSync, spawn } = require('child_process'); // 进度显示相关函数 function showProgress(current, total, message) { const percentage = Math.round((current / total) * 100); const filled = Math.floor(percentage / 5); const empty = 20 - filled; const progressBar = '█'.repeat(filled) + '░'.repeat(empty); // 使用不同颜色显示进度 const coloredBar = `\x1b[32m${'█'.repeat(filled)}\x1b[37m${'░'.repeat(empty)}\x1b[0m`; const coloredPercentage = percentage === 100 ? `\x1b[32m${percentage}%\x1b[0m` : `\x1b[33m${percentage}%\x1b[0m`; process.stdout.write(`\r[${coloredBar}] ${coloredPercentage} ${message}`); } function clearProgress() { process.stdout.write('\r' + ' '.repeat(100) + '\r'); } function logStep(step, total, message) { console.log(`\n📋 步骤 ${step}/${total}: \x1b[36m${message}\x1b[0m`); } function showSpinner(message) { const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; let i = 0; return setInterval(() => { process.stdout.write(`\r${spinners[i]} ${message}`); i = (i + 1) % spinners.length; }, 100); } function showHelp() { console.log(` Homepage 项目创建工具 用法: npm create homepage <项目名称> npx create-homepage <项目名称> 示例: npm create homepage my-blog npm create homepage my-website 选项: -h, --help 显示帮助信息 -v, --version 显示版本信息 `); } function showVersion() { const packagePath = path.join(__dirname, '../package.json'); const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); console.log(pkg.version); } async function createProject(projectName) { if (!projectName) { console.error('❌ 错误: 请提供项目名称'); console.log('用法: npm create homepage <project-name>'); process.exit(1); } const targetDir = path.join(process.cwd(), projectName); if (fs.existsSync(targetDir)) { console.error(`❌ 错误: 目录 "${projectName}" 已存在`); process.exit(1); } console.log(`🚀 正在创建项目 "${projectName}"...\n`); const totalSteps = 5; let currentStep = 0; try { // 步骤 1: 创建项目目录 currentStep++; logStep(currentStep, totalSteps, '创建项目目录'); showProgress(0, 100, '创建目录中...'); fs.mkdirSync(targetDir, { recursive: true }); showProgress(100, 100, '目录创建完成'); clearProgress(); console.log('✅ 项目目录创建完成'); // 步骤 2: 复制模板文件 currentStep++; logStep(currentStep, totalSteps, '复制模板文件'); const templateDir = path.join(__dirname, '..'); copyTemplateWithProgress(templateDir, targetDir); console.log('✅ 模板文件复制完成'); // 步骤 3: 进入项目目录并更新配置 currentStep++; logStep(currentStep, totalSteps, '配置项目文件'); showProgress(0, 100, '更新配置中...'); process.chdir(targetDir); updatePackageJson(projectName); showProgress(100, 100, '配置更新完成'); clearProgress(); console.log('✅ 项目配置完成'); // 步骤 4: 安装依赖 currentStep++; logStep(currentStep, totalSteps, '安装项目依赖'); await installDependenciesWithProgress(); // 步骤 5: 完成设置 currentStep++; logStep(currentStep, totalSteps, '完成项目设置'); showProgress(100, 100, '项目设置完成'); clearProgress(); console.log(`\n🎉 项目创建成功! 接下来的步骤: 1. cd ${projectName} 2. 复制 site.config.example.ts 为 site.config.ts 3. 编辑 site.config.ts 配置你的网站信息 4. 在 content/blog/ 目录添加你的博客文章 5. 在 content/thoughts/ 目录添加你的随笔 6. npm run dev 启动开发服务器 (http://localhost:4000) 7. npm run build 构建生产版本 更多信息请查看 README.md 文件 开发命令: npm run dev # 启动开发服务器 npm run build # 构建生产版本 npm run start # 启动生产服务器 npm run lint # 代码检查 `); } catch (error) { clearProgress(); console.error('\n❌ 创建项目时出错:', error.message); process.exit(1); } } function installDependenciesWithProgress() { return new Promise((resolve, reject) => { console.log('📦 正在安装依赖包...'); let startTime = Date.now(); let currentStep = '初始化'; let progress = 0; // 显示初始进度 showProgress(0, 100, '正在初始化安装...'); // 使用 spawn 来实时显示安装进度 const npmProcess = spawn('npm', ['install'], { stdio: ['inherit', 'pipe', 'inherit'], // 让stderr直接输出到终端 env: { ...process.env } }); let output = ''; let hasOutput = false; // 定时更新进度(模拟进度) const progressTimer = setInterval(() => { if (progress < 90) { progress += Math.random() * 10; progress = Math.min(progress, 90); let message = currentStep; if (progress < 20) { message = '正在解析依赖关系...'; currentStep = '解析依赖'; } else if (progress < 40) { message = '正在下载依赖包...'; currentStep = '下载包'; } else if (progress < 70) { message = '正在安装依赖包...'; currentStep = '安装包'; } else { message = '正在构建项目...'; currentStep = '构建项目'; } showProgress(Math.round(progress), 100, message); } }, 800); npmProcess.stdout.on('data', (data) => { const text = data.toString(); output += text; hasOutput = true; // 根据npm输出更新进度 if (text.includes('added') && text.includes('packages')) { clearInterval(progressTimer); progress = 95; showProgress(95, 100, '正在完成安装...'); // 提取安装的包数量 const match = text.match(/added (\d+) packages/); if (match) { const count = match[1]; showProgress(98, 100, `已安装 ${count} 个包`); } } if (text.includes('audited') && text.includes('packages')) { clearInterval(progressTimer); progress = 99; const match = text.match(/audited (\d+) packages/); if (match) { const count = match[1]; showProgress(99, 100, `正在审计 ${count} 个包...`); } } }); npmProcess.on('close', (code) => { clearInterval(progressTimer); clearProgress(); const duration = ((Date.now() - startTime) / 1000).toFixed(1); if (code === 0) { console.log(`\x1b[32m✅ 依赖安装完成\x1b[0m \x1b[90m(耗时 ${duration}s)\x1b[0m`); resolve(); } else { console.error('\n❌ 依赖安装失败'); console.error(`退出码: ${code}`); if (output) { console.error('输出信息:', output); } reject(new Error(`npm install 失败,退出码: ${code}`)); } }); npmProcess.on('error', (error) => { clearInterval(progressTimer); clearProgress(); console.error('\n❌ 启动 npm install 失败:', error.message); reject(error); }); // 超时保护(如果npm没有输出,显示通用进度) setTimeout(() => { if (!hasOutput) { clearInterval(progressTimer); showProgress(50, 100, '安装进行中,请稍候...'); // 继续显示通用进度 const fallbackTimer = setInterval(() => { if (progress < 85) { progress += 2; showProgress(Math.round(progress), 100, '安装进行中,请稍候...'); } }, 1000); npmProcess.on('close', () => { clearInterval(fallbackTimer); }); } }, 5000); }); } function copyTemplateWithProgress(src, dest) { const files = getAllFiles(src); const totalFiles = files.length; let copiedFiles = 0; files.forEach(file => { const relativePath = path.relative(src, file); const destPath = path.join(dest, relativePath); // 跳过不需要的文件 if (shouldSkipFile(file)) { return; } // 确保目标目录存在 const destDir = path.dirname(destPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); } // 复制文件 if (path.basename(file) === 'site.config.example.ts') { const newDestPath = path.join(path.dirname(destPath), 'site.config.ts'); fs.copyFileSync(file, newDestPath); } else { fs.copyFileSync(file, destPath); } copiedFiles++; const progress = Math.round((copiedFiles / totalFiles) * 100); showProgress(progress, 100, `复制文件中... (${copiedFiles}/${totalFiles})`); }); clearProgress(); } function getAllFiles(dir) { let files = []; function traverse(currentDir) { const items = fs.readdirSync(currentDir); items.forEach(item => { const fullPath = path.join(currentDir, item); const stats = fs.statSync(fullPath); if (stats.isDirectory()) { // 跳过不需要的目录 if (shouldSkipDirectory(item)) { return; } traverse(fullPath); } else { files.push(fullPath); } }); } traverse(dir); return files; } function shouldSkipDirectory(dirname) { return ['.git', 'node_modules', '.next', '.DS_Store', '.idea', 'bin'].includes(dirname) || dirname.startsWith('backup-'); } function shouldSkipFile(filepath) { const basename = path.basename(filepath); return ['.DS_Store', 'package-lock.json', '.gitignore', 'tsconfig.tsbuildinfo', '.npmignore'].includes(basename); } function updatePackageJson(projectName) { const packageJsonPath = path.join(process.cwd(), 'package.json'); const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // 更新项目信息 pkg.name = projectName; pkg.version = "0.1.0"; pkg.description = "基于 @penjc/homepage 模板创建的个人主页"; // 移除bin配置和files配置,这些是模板包特有的 delete pkg.bin; delete pkg.files; delete pkg.publishConfig; // 移除create脚本 delete pkg.scripts.create; delete pkg.scripts.prepublishOnly; // 更新repository信息 delete pkg.repository; delete pkg.bugs; delete pkg.homepage; // 更新关键词 pkg.keywords = [ "personal-website", "blog", "next.js", "react", "tailwindcss" ]; // 更新作者信息 pkg.author = "Your Name"; fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2)); } function copyTemplate(src, dest) { const stats = fs.statSync(src); if (stats.isDirectory()) { // 跳过不需要的目录 const basename = path.basename(src); if (['.git', 'node_modules', '.next', '.DS_Store', '.idea', 'bin'].includes(basename) || basename.startsWith('backup-')) { return; } // 防止复制目标目录本身造成递归 const srcAbs = path.resolve(src); const destAbs = path.resolve(dest); if (srcAbs.startsWith(destAbs) || destAbs.startsWith(srcAbs)) { return; } if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const items = fs.readdirSync(src); items.forEach(item => { const srcPath = path.join(src, item); const destPath = path.join(dest, item); copyTemplate(srcPath, destPath); }); } else { // 跳过不需要的文件 const basename = path.basename(src); if (['.DS_Store', 'package-lock.json', '.gitignore', 'tsconfig.tsbuildinfo', '.npmignore'].includes(basename)) { return; } // 复制文件,但重命名示例配置文件 if (basename === 'site.config.example.ts') { const newDest = path.join(path.dirname(dest), 'site.config.ts'); fs.copyFileSync(src, newDest); } else { fs.copyFileSync(src, dest); } } } // 处理命令行参数 const args = process.argv.slice(2); if (args.length === 0) { showHelp(); process.exit(1); } const command = args[0]; switch (command) { case '-h': case '--help': showHelp(); break; case '-v': case '--version': showVersion(); break; default: createProject(command); break; }