UNPKG

create-crack

Version:

CLI tool for creating crack projects

775 lines (754 loc) 27.1 kB
#!/usr/bin/env node import { Command } from 'commander'; import kleur from 'kleur'; import { resolveApp } from '@verve-kit/utils'; import { select, confirm, intro } from '@clack/prompts'; import fs from 'fs-extra'; import { dirname, join } from 'node:path'; import { fileURLToPath as fileURLToPath$1 } from 'node:url'; import { createSpinner } from 'nanospinner'; import os from 'node:os'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'url'; import { execa } from 'execa'; import { execSync } from 'node:child_process'; import boxen from 'boxen'; /** * 支持的项目模板类型 */ const SUPPORTED_TEMPLATES = [ 'react-web-js', 'react-web-ts' ]; /** * 支持的包管理器 */ const SUPPORTED_PACKAGE_MANAGERS = [ 'npm', 'yarn', 'pnpm', 'cnpm' ]; /** * 项目类型选择项配置 */ const PROJECT_TYPE_OPTIONS = [ { value: 'react-web-js', label: 'react-web-js', hint: 'React + JavaScript Web应用程序 🚀' }, { value: 'react-web-ts', label: 'react-web-ts', hint: 'React + TypeScript Web应用程序 🚀' } ]; /** * 包管理器选择项配置 */ const PACKAGE_MANAGER_OPTIONS = [ { value: 'npm', label: 'npm' }, { value: 'yarn', label: 'yarn' }, { value: 'pnpm', label: 'pnpm' }, { value: 'cnpm', label: 'cnpm' } ]; /** * 用户界面文本常量 */ const UI_MESSAGES = { INTRO: ' 🚧 Create Your App - 项目脚手架工具 ', SELECT_PROJECT_TYPE: '🎯 选择项目类型:', SELECT_PACKAGE_MANAGER: '📦 选择包管理器:', ENABLE_ESLINT: '🔍 是否启用 ESLint 代码检查?', ENABLE_COMMIT_LINT: '📝 是否启用 Commit Lint 配置?', OVERWRITE_CONFIRM: 'Target directory already exists. Overwrite?', CONFIG_SELECTION: '\n📋 请选择项目配置:\n', CREATING_PROJECT: '\n🔧 正在创建项目...', CTRL_C_EXIT: '⌨️ Ctrl+C pressed - Exiting the program' }; /** * 错误信息常量 */ const ERROR_MESSAGES = { INVALID_TEMPLATE: (template)=>`❌ 无效的模板类型: ${template}`, INVALID_PACKAGE_MANAGER: (manager)=>`❌ 无效的包管理器: ${manager}`, AVAILABLE_TEMPLATES: '可用的模板: react-web-js, react-web-ts', AVAILABLE_PACKAGE_MANAGERS: '可用的包管理器: npm, yarn, pnpm, cnpm' }; /** * 成功信息常量 */ const SUCCESS_MESSAGES = { PROJECT_CREATED: '🎉 项目创建成功!\n', FEATURES_INSTALLED: '📦 已安装的功能:', ESLINT_CREATED: '✅ ESLint 配置文件已创建', ESLINT_WARNING: '⚠️ ESLint 配置模板文件未找到', TEMPLATE_COPY_SUCCESS: '✅ 项目模板复制成功', DEPENDENCY_INSTALL_SUCCESS: '✅ Project initialization complete' }; /** * 验证项目模板类型是否有效 */ function validateTemplate(template) { return SUPPORTED_TEMPLATES.includes(template); } /** * 验证包管理器是否有效 */ function validatePackageManager(manager) { return SUPPORTED_PACKAGE_MANAGERS.includes(manager); } /** * 验证并处理模板选项 */ function validateTemplateOption(template) { if (!template) return undefined; if (!validateTemplate(template)) { console.error(kleur.red(ERROR_MESSAGES.INVALID_TEMPLATE(template))); console.error(kleur.yellow(ERROR_MESSAGES.AVAILABLE_TEMPLATES)); process.exit(1); } return template; } /** * 验证并处理包管理器选项 */ function validatePackageManagerOption(manager) { if (!manager) return undefined; if (!validatePackageManager(manager)) { console.error(kleur.red(ERROR_MESSAGES.INVALID_PACKAGE_MANAGER(manager))); console.error(kleur.yellow(ERROR_MESSAGES.AVAILABLE_PACKAGE_MANAGERS)); process.exit(1); } return manager; } /** * 验证并规范化所有选项 */ function validateOptions(options) { const validated = { force: Boolean(options.force) }; // 只有在明确提供了值时才设置属性 const template = validateTemplateOption(options.template); if (template) { validated.template = template; } const packageManager = validatePackageManagerOption(options.packageManager); if (packageManager) { validated.packageManager = packageManager; } if (options.eslint !== undefined) { validated.eslint = Boolean(options.eslint); } if (options.commitLint !== undefined) { validated.commitLint = Boolean(options.commitLint); } return validated; } /** * 收集用户的项目配置选择 */ async function collectUserConfiguration(options) { let projectType; let packageManager; let enableEslint; let commitLint; // 获取项目类型 if (options.template) { projectType = options.template; } else { projectType = await select({ message: UI_MESSAGES.SELECT_PROJECT_TYPE, options: PROJECT_TYPE_OPTIONS }); } // 获取包管理器 if (options.packageManager) { packageManager = options.packageManager; } else { packageManager = await select({ message: UI_MESSAGES.SELECT_PACKAGE_MANAGER, options: PACKAGE_MANAGER_OPTIONS }); } // 获取 ESLint 选项 if (options.eslint !== undefined) { enableEslint = options.eslint; } else { enableEslint = await confirm({ message: UI_MESSAGES.ENABLE_ESLINT }); } // 获取 Commit Lint 选项 if (options.commitLint !== undefined) { commitLint = options.commitLint; } else { commitLint = await confirm({ message: UI_MESSAGES.ENABLE_COMMIT_LINT }); } return { projectType, packageManager, enableEslint, commitLint }; } /** * 检查是否为非交互模式 */ function isNonInteractiveMode(options) { return !!(options.template || options.packageManager || options.eslint !== undefined || options.commitLint !== undefined); } /** * 创建项目根目录 * 如果目录已存在且未强制覆盖,则询问用户是否覆盖 */ async function createProjectDirectory(name, options) { const root = resolveApp(name); if (await fs.pathExists(root) && !options.force) { const shouldOverwrite = await confirm({ message: UI_MESSAGES.OVERWRITE_CONFIRM }); if (!shouldOverwrite) { console.log(kleur.yellow('项目创建已取消')); process.exit(1); } await fs.remove(root); } await fs.ensureDir(root); return root; } const __filename$3 = fileURLToPath(import.meta.url); const __dirname$3 = dirname(__filename$3); /** * 获取并解析指定路径下的 package.json 文件。 * * @param relativePath - 相对于当前模块目录的路径(当 `isFromCurrentDir` 为 `true` 时) * @param isFromCurrentDir - 如果为 `true`,则路径基于当前文件目录;否则视为绝对路径或调用方自定义路径 * @returns 返回解析后的 package.json 内容对象 * * @example * ```ts * const pkg = await getPackageJsonInfo('../package.json', true); * console.log(pkg.name); * ``` */ async function getPackageJsonInfo(relativePath, isFromCurrentDir) { const filePath = isFromCurrentDir ? join(__dirname$3, relativePath) : relativePath; return await fs.readJson(filePath); } /** * 包版本管理配置 */ // 需要动态更新版本的自有包列表 const PACKAGES_TO_UPDATE = [ '@verve-kit/react-script' ]; const __filename$2 = fileURLToPath$1(import.meta.url); const __dirname$2 = dirname(__filename$2); /** * 获取 npm 包的最新版本 * * @param packageName - 包名 * @returns Promise<string> - 最新版本号,如果获取失败则返回默认版本 */ async function getLatestPackageVersion(packageName) { try { const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return `^${data.version}`; } catch (error) { console.warn(kleur.yellow(`⚠️ 无法获取 ${packageName} 的最新版本,跳过更新`)); console.warn(kleur.gray(`错误信息: ${error}`)); return null; } } /** * 更新包依赖的版本号为最新版本 * * @param packageJson - package.json 对象 * @returns Promise<PackageJsonType> - 更新后的 package.json 对象 */ async function updatePackageVersions(packageJson) { console.log(kleur.cyan('🔄 正在获取最新包版本...')); const packagesToUpdate = PACKAGES_TO_UPDATE; // 并发获取所有包的最新版本 const versionPromises = packagesToUpdate.map(async (packageName)=>{ const version = await getLatestPackageVersion(packageName); return { packageName, version }; }); try { const versionResults = await Promise.all(versionPromises); // 更新 dependencies if (packageJson.dependencies) { for (const { packageName, version } of versionResults){ if (version && packageJson.dependencies[packageName]) { packageJson.dependencies[packageName] = version; console.log(kleur.green(`✅ 更新 ${packageName}: ${version}`)); } } } // 更新 devDependencies if (packageJson.devDependencies) { for (const { packageName, version } of versionResults){ if (version && packageJson.devDependencies[packageName]) { packageJson.devDependencies[packageName] = version; console.log(kleur.green(`✅ 更新 ${packageName}: ${version}`)); } } } console.log(kleur.green('🎉 包版本更新完成')); } catch (error) { console.error(kleur.red('❌ 更新包版本时出错:'), error); } return packageJson; } /** * 合并 ESLint 配置到 package.json 中 * * @param packageJson - 基础的 package.json 对象 * @returns 合并了 ESLint 配置的 package.json 对象 */ async function mergeEslintConfig(packageJson) { try { const eslintConfigPath = join(__dirname$2, './package/eslint.json'); const eslintConfig = await getPackageJsonInfo(eslintConfigPath, false); if (!eslintConfig) { console.warn('⚠️ ESLint 配置文件未找到,跳过 ESLint 配置合并'); return packageJson; } // 合并 scripts if (eslintConfig.scripts) { packageJson.scripts = { ...packageJson.scripts, ...eslintConfig.scripts }; } // 合并 devDependencies if (eslintConfig.devDependencies) { packageJson.devDependencies = { ...packageJson.devDependencies, ...eslintConfig.devDependencies }; } // 合并 lint-staged if (eslintConfig['lint-staged']) { packageJson['lint-staged'] = eslintConfig['lint-staged']; } console.log('✅ ESLint 配置已成功合并到 package.json'); return packageJson; } catch (error) { console.error('❌ 合并 ESLint 配置时出错:', error); return packageJson; } } /** * 创建指定类型项目的 `package.json` 对象。 * * @param projectType - 模板类型(如:react、vue、node 等) * @param projectName - 项目名称,会被写入到 `package.json.name` * @param enableEslint - 是否启用 ESLint 配置 * @returns 返回已定制的 `package.json` 对象 * * @example * ```ts * const pkg = createPackageJson('react-web-ts', 'my-app', true); * console.log(pkg.name); // 'my-app' * ``` */ async function createPackageJson(projectType, projectName, enableEslint = false) { try { // 从 package 目录读取对应项目类型的 JSON 文件 const templatePath = join(__dirname$2, `./package/${projectType}.json`); console.log(`尝试读取模板: ${templatePath}`); const packageInfo = await getPackageJsonInfo(templatePath, false); if (!packageInfo) throw new Error('Package info is undefined'); packageInfo.author = os.userInfo().username; packageInfo.name = projectName; // 更新包版本为最新版本 const updatedPackageInfo = await updatePackageVersions(packageInfo); // 如果启用了 ESLint,合并 ESLint 配置 if (enableEslint) { return await mergeEslintConfig(updatedPackageInfo); } return updatedPackageInfo; } catch (error) { console.error(`❌ Failed to create package.json for "${projectType}"`); console.error(error); process.exit(1); } } /** * 读取模板目录中的任意 JSON 文件为字符串内容。 * * @param fileName - 模板文件名(例如:`config.json`) * @returns 返回文件内容的字符串 * * @example * ```ts * const config = createTemplateFile('vite.config.json'); * console.log(JSON.parse(config)); * ``` */ function createTemplateFile(fileName) { const filePath = join(__dirname$2, `./package/${fileName}`); return readFileSync(filePath, 'utf-8'); } const __filename$1 = fileURLToPath$1(import.meta.url); const __dirname$1 = dirname(__filename$1); // 获取模板目录路径 - 总是相对于包根目录 const getTemplateDir$1 = async ()=>{ // 向上查找到包含 package.json 的目录(包根目录) let currentDir = __dirname$1; while(!await fs.pathExists(join(currentDir, 'package.json'))){ const parent = dirname(currentDir); if (parent === currentDir) { throw new Error('无法找到包根目录'); } currentDir = parent; } return join(currentDir, 'template'); }; // 移除自己实现的复制函数,使用 fs-extra 的 copy 方法 /** * 为指定项目集成 commitlint 和相关 husky 配置。 * * @param projectName - 项目目录名 * * @example * ```ts * await createCommitlint('my-app'); * ``` */ async function createCommitlint(projectName) { try { // 复制 husky 模板文件 const huskyTemplateSource = join(await getTemplateDir$1(), 'template-husky'); if (await fs.pathExists(huskyTemplateSource)) { await fs.copy(huskyTemplateSource, projectName); console.log('✅ Husky 模板文件已复制'); } else { console.warn('⚠️ Husky 模板目录未找到'); return; } // 使用绝对路径 const targetPackagePath = join(projectName, 'package.json'); const huskyTemplatePath = join(__dirname$1, './package/husky.json'); console.log(`读取 husky 模板: ${huskyTemplatePath}`); console.log(`读取项目 package.json: ${targetPackagePath}`); const huskyConfig = await fs.readJson(huskyTemplatePath); const projectPackageJson = await fs.readJson(targetPackagePath); // 合并 husky 配置到项目的 package.json 中 for(const key in huskyConfig){ const sourceValue = huskyConfig[key]; const targetValue = projectPackageJson[key]; if (typeof sourceValue === 'object' && !Array.isArray(sourceValue)) { projectPackageJson[key] = { ...targetValue, ...sourceValue }; } else if (Array.isArray(sourceValue)) { projectPackageJson[key] = [ ...sourceValue, ...targetValue ?? [] ]; } else { projectPackageJson[key] = sourceValue; } } await fs.writeJson(targetPackagePath, projectPackageJson, { spaces: 2 }); console.log('✅ Commit Lint 配置已成功合并到 package.json'); } catch (error) { console.error('❌ 创建 Commit Lint 配置时出错:', error); process.exit(1); } } // 获取当前文件的目录路径 const __filename = fileURLToPath$1(import.meta.url); const __dirname = dirname(__filename); // 获取模板目录路径 - 总是相对于包根目录 const getTemplateDir = async ()=>{ // 向上查找到包含 package.json 的目录(包根目录) let currentDir = __dirname; while(!await fs.pathExists(join(currentDir, 'package.json'))){ const parent = dirname(currentDir); if (parent === currentDir) { throw new Error('无法找到包根目录'); } currentDir = parent; } return join(currentDir, 'template'); }; /** * 复制项目模板到目标目录 */ async function copyProjectTemplate(projectType, projectRoot) { const spinner = createSpinner(kleur.bold().cyan('正在复制项目模板...')).start(); try { const templateSource = join(await getTemplateDir(), `template-${projectType}`); if (!await fs.pathExists(templateSource)) { throw new Error(`模板目录不存在: ${templateSource}`); } await fs.copy(templateSource, projectRoot); spinner.success({ text: kleur.bold().green(SUCCESS_MESSAGES.TEMPLATE_COPY_SUCCESS) }); } catch (error) { spinner.error({ text: kleur.bold().red('❌ 项目模板复制失败') }); console.error('Error:', error); throw error; } } /** * 创建 ESLint 配置文件 */ async function createEslintConfig(root) { try { const eslintConfigSource = join(await getTemplateDir(), 'eslint/eslint.config.mjs'); const eslintConfigDest = join(root, 'eslint.config.mjs'); if (await fs.pathExists(eslintConfigSource)) { await fs.copy(eslintConfigSource, eslintConfigDest); console.log(SUCCESS_MESSAGES.ESLINT_CREATED); } else { console.warn(SUCCESS_MESSAGES.ESLINT_WARNING); } } catch (error) { console.error('❌ 创建 ESLint 配置文件时出错:', error); throw error; } } /** * 创建项目的基础文件 */ async function createProjectFiles(context) { const { root, config, name } = context; try { // 创建 package.json const pkg = await createPackageJson(config.projectType, name, config.enableEslint); await fs.writeJson(join(root, 'package.json'), pkg, { spaces: 2 }); // 创建 .gitignore await fs.writeFile(join(root, '.gitignore'), createTemplateFile('gitignore')); // 复制项目模板 await copyProjectTemplate(config.projectType, root); // 创建 commit lint 配置 if (config.commitLint) { await createCommitlint(root); } // 创建 ESLint 配置 if (config.enableEslint) { await createEslintConfig(root); } } catch (error) { console.error(kleur.red('❌ 创建项目文件时出错:'), error); throw error; } } /** * 检查当前系统是否已安装 Git。 * * @returns `true` 表示 Git 已安装,`false` 表示未安装。 * * @example * ```ts * if (checkGitInstallation()) { * console.log('Git is available.'); * } else { * console.log('Please install Git.'); * } * ``` */ function checkGitInstallation() { try { // 尝试静默执行 git --version,如果命令执行失败将抛出异常 execSync('git --version', { stdio: 'ignore' }); return true; } catch { return false; } } function createSuccessInfo(name, packageManage) { const END_MSG = `${kleur.blue('🎉 created project ' + kleur.green(name) + ' Successfully')}\n\n 🙏 Thanks for using Create-Crack !`; const BOXEN_CONFIG = { padding: 1, margin: { top: 1, bottom: 1 }, borderColor: 'cyan', align: 'center', borderStyle: 'double', title: '🚀 Congratulations', titleAlignment: 'center' }; process.stdout.write(boxen(END_MSG, BOXEN_CONFIG)); console.log('👉 Get started with the following commands:'); console.log(`\n\r\r cd ${kleur.cyan(name)}`); console.log(`\r\r ${kleur.cyan(packageManage)} start \r\n`); } /** * 安装项目依赖 */ async function installDependencies(packageManager, projectRoot) { const spinner = createSpinner(kleur.bold('Installing dependencies...')).start(); try { await execa(packageManager, [ 'install' ], { cwd: projectRoot }); spinner.success({ text: kleur.green(SUCCESS_MESSAGES.DEPENDENCY_INSTALL_SUCCESS) }); } catch (error) { spinner.error({ text: kleur.red('Failed to install dependencies') }); console.error(error); throw error; } } /** * 初始化 Git 仓库 */ async function initializeGitRepository(projectRoot) { try { if (checkGitInstallation()) { await execa('git', [ 'init' ], { cwd: projectRoot, stdio: 'ignore' }); console.log(kleur.green('✅ Git 仓库初始化成功')); } else { console.warn(kleur.yellow('⚠️ Git 未安装,跳过 Git 仓库初始化')); } } catch (error) { console.warn(kleur.yellow('⚠️ Git 仓库初始化失败:'), error); } } /** * 显示项目创建成功的信息 */ function displaySuccessInfo(context) { const { name, config } = context; console.log(kleur.green(SUCCESS_MESSAGES.PROJECT_CREATED)); console.log(kleur.cyan(SUCCESS_MESSAGES.FEATURES_INSTALLED)); console.log(kleur.gray(` • ${config.projectType === 'react-web-ts' ? 'React + TypeScript' : 'React + JavaScript'} 项目模板`)); if (config.enableEslint) { console.log(kleur.gray(' • ESLint 代码检查工具')); } if (config.commitLint) { console.log(kleur.gray(' • Commit Lint 提交规范')); } createSuccessInfo(name, config.packageManager); } /** * 执行后续设置:安装依赖、初始化Git、显示成功信息 */ async function performPostSetup(context) { try { // 安装依赖 await installDependencies(context.config.packageManager, context.root); // 初始化 Git 仓库 await initializeGitRepository(context.root); // 显示成功信息 displaySuccessInfo(context); } catch (error) { console.error(kleur.red('❌ 项目后续设置过程中发生错误:'), error); throw error; } } /** * 设置 Ctrl+C 退出监听(仅限终端环境) */ function setupExitHandler() { if (process.stdin.isTTY) { process.stdin.setRawMode(true); process.stdin.on('data', (key)=>{ if (key[0] === 3) { console.log(UI_MESSAGES.CTRL_C_EXIT); process.exit(1); } }); } } /** * 显示配置信息 */ function displayConfiguration(context) { const { name, config } = context; console.log(kleur.yellow(UI_MESSAGES.CREATING_PROJECT)); console.log(kleur.gray(`📁 项目名称: ${name}`)); console.log(kleur.gray(`🎯 项目类型: ${config.projectType}`)); console.log(kleur.gray(`📦 包管理器: ${config.packageManager}`)); console.log(kleur.gray(`🔍 ESLint: ${config.enableEslint ? '启用' : '禁用'}`)); console.log(kleur.gray(`📝 Commit Lint: ${config.commitLint ? '启用' : '禁用'}`)); } /** * 创建项目上下文 */ async function createProjectContext(name, rawOptions) { const options = validateOptions(rawOptions); const root = resolveApp(name); const config = await collectUserConfiguration(options); return { name, root, config, options }; } /** * 创建项目主流程 * * @param name - 项目名 * @param rawOptions - 控制参数 */ async function createApp(name, rawOptions) { try { // 设置退出处理器 setupExitHandler(); // 显示欢迎信息 intro(kleur.green(UI_MESSAGES.INTRO)); // 创建项目上下文 const context = await createProjectContext(name, rawOptions); // 如果是交互模式,显示配置选择提示 if (!isNonInteractiveMode(context.options)) { console.log(kleur.cyan(UI_MESSAGES.CONFIG_SELECTION)); } // 显示配置信息 displayConfiguration(context); // 创建项目目录 context.root = await createProjectDirectory(name, context.options); // 创建项目文件 await createProjectFiles(context); // 执行后续设置(安装依赖、初始化Git、显示成功信息) await performPostSetup(context); } catch (error) { console.error(kleur.red('❌ 项目创建过程中发生错误:'), error); process.exit(1); } } async function main() { const program = new Command(); // 异步获取版本号 const packageInfo = await getPackageJsonInfo('../package.json', true); program.version(kleur.green(packageInfo.version)).arguments('<project-name>').description(kleur.cyan('Create a directory for your project files')).option('-f, --force', 'Overwrite target directory if it exists').option('-t, --template <template>', 'Project template (react-web-js | react-web-ts)').option('-p, --package-manager <manager>', 'Package manager (npm | yarn | pnpm | cnpm)').option('-e, --eslint', 'Enable ESLint configuration').option('-c, --commit-lint', 'Enable Commit Lint configuration').option('--no-eslint', 'Disable ESLint configuration').option('--no-commit-lint', 'Disable Commit Lint configuration').addHelpText('after', ` ${kleur.yellow('Examples:')} ${kleur.gray('# Interactive mode (default)')} $ create-crack my-app ${kleur.gray('# Non-interactive mode with all options')} $ create-crack my-app -t react-web-ts -p pnpm -e -c ${kleur.gray('# Create React JS project with npm and ESLint')} $ create-crack my-app --template react-web-js --package-manager npm --eslint ${kleur.gray('# Create project without ESLint and Commit Lint')} $ create-crack my-app -t react-web-ts -p yarn --no-eslint --no-commit-lint ${kleur.yellow('Available Templates:')} ${kleur.cyan('react-web-js')} - React + JavaScript Web应用程序 ${kleur.cyan('react-web-ts')} - React + TypeScript Web应用程序 ${kleur.yellow('Available Package Managers:')} ${kleur.cyan('npm')} - Node Package Manager ${kleur.cyan('yarn')} - Yarn Package Manager ${kleur.cyan('pnpm')} - PNPM Package Manager ${kleur.cyan('cnpm')} - CNPM Package Manager `).action((name, options)=>{ createApp(name, options); }).parse(process.argv); } main().catch(console.error); //# sourceMappingURL=index.esm.js.map