UNPKG

scaffolder-toolkit

Version:

🚀 A universal command-line tool for developers to automate project scaffolding and streamline their workflows.

289 lines (277 loc) • 10.3 kB
import { e as execute, D as DevkitError, t, f as fs, G as GitError, F as FILE_NAMES, l as logger, a as executeCommand } from './main-CB-u5F8M.js'; import path from 'path'; import os from 'os'; import 'commander'; import 'fs'; import 'url'; import 'chalk'; import 'ora'; import 'node:util'; import 'node:child_process'; import 'execa'; import '@inquirer/prompts'; async function installDependencies(options) { const { projectName, packageManager } = options; const projectPath = path.join(process.cwd(), projectName); try { await execute(packageManager, ["install"], { cwd: projectPath, stdio: "inherit", }); } catch (error) { throw new DevkitError(t("errors.scaffolding.install_fail"), { cause: error, }); } } function getRepoNameFromUrl(url) { const parts = url.split("/"); let repoName = parts.pop() || ""; if (repoName.endsWith(".git")) { repoName = repoName.slice(0, -4); } return repoName; } async function cloneRepo(url, repoPath) { try { await fs.ensureDir(repoPath); await execute("git", ["clone", url, "."], { cwd: repoPath, stdio: "ignore", }); } catch (error) { throw new GitError(t("errors.cache.clone_fail"), url, { cause: error }); } } async function pullRepo(repoPath) { try { await execute("git", ["pull"], { cwd: repoPath, stdio: "ignore" }); } catch (error) { throw new GitError(t("errors.cache.refresh_fail"), undefined, { cause: error, }); } } async function isRepoFresh(repoPath, strategy) { if (strategy === "never-refresh") { return true; } if (strategy === "always-refresh") { return false; } try { const stat = await fs.stat(path.join(repoPath, ".git/FETCH_HEAD")); const oneDayInMs = 24 * 60 * 60 * 1000; return Date.now() - stat.mtime.getTime() < oneDayInMs; } catch { return false; } } async function doesRepoExist(repoPath) { try { await fs.stat(repoPath); return true; } catch { return false; } } async function updateJavascriptProjectName(projectPath, newProjectName) { const packageJsonPath = path.join(projectPath, FILE_NAMES.packageJson); if (!fs.existsSync(packageJsonPath)) { logger.error(t("errors.system.package_file_not_found"), "TEMPL"); return; } try { const packageJson = await fs.readJson(packageJsonPath); await fs.writeJson(packageJsonPath, { ...packageJson, name: newProjectName, }); } catch (error) { const errorMessage = t("errors.system.package_name_update_fail"); if (error instanceof Error) { logger.error(`${errorMessage}: ${error.message}`, "TEMPL"); } else { logger.error(errorMessage, "TEMPL"); } if (error instanceof Error && error.stack) { logger.dimmed(error.stack); } } } function getFilesToFilter(language) { const commonFiles = Object.values(FILE_NAMES.common); const languageSpecificFiles = FILE_NAMES[language].lockFiles; return [...commonFiles, ...languageSpecificFiles]; } async function copyJavascriptTemplate(repoPath, destination) { const filesToFilter = getFilesToFilter("javascript"); await fs.copy(repoPath, destination, { filter: (src) => { const isFiltered = filesToFilter.some((file) => src.includes(file)); return !isFiltered; }, }); } const CACHE_DIR = path.join(os.homedir(), ".devkit", "cache"); async function getTemplateFromCache(options) { const { url, projectName, spinner, strategy } = options; const destination = path.join(process.cwd(), projectName); try { const repoName = getRepoNameFromUrl(url); const repoPath = path.join(CACHE_DIR, repoName); spinner.text = logger.colors.cyan(logger.colors.bold(`Checking cache for: ${repoName}...`)); spinner.start(); const repoExists = await doesRepoExist(repoPath); if (!repoExists) { spinner.text = logger.colors.cyan(logger.colors.italic(t("messages.status.cache_clone_start", { url }))); await cloneRepo(url, repoPath); spinner.succeed(logger.colors.green(logger.colors.bold(t("messages.success.template_added")))); } else { const fresh = await isRepoFresh(repoPath, strategy); if (!fresh) { spinner.text = logger.colors.cyan(t("messages.status.cache_refresh_start")); await pullRepo(repoPath); spinner.succeed(logger.colors.green(t("messages.success.template_updated"))); } else { spinner.info(logger.colors.yellow(t("messages.status.cache_use_info", { repoName }))); } } spinner.text = logger.colors.cyan(t("messages.status.cache_copy_start")); await copyJavascriptTemplate(repoPath, destination); await updateJavascriptProjectName(destination, projectName); spinner.succeed(logger.colors.green(logger.colors.bold(t("messages.success.new_project")))); } catch (error) { spinner.fail(logger.colors.red(t("errors.cache.copy_fail"))); const message = t("errors.cache.copy_fail"); if (error instanceof Error) { logger.error(`${message}: ${error.message}`, "CACHE"); } else { logger.error(message, "CACHE"); } throw error; } } async function runCliCommand(options) { const { command, projectName, packageManager } = options; const finalCommand = command.replace("{pm}", packageManager); try { if (!finalCommand.trim()) { throw new DevkitError(t("errors.validation.invalid_command", { command: finalCommand })); } await executeCommand(`${finalCommand} ${projectName}`, { stdio: "inherit", }); } catch (error) { const cause = error.stderr || error.message; throw new DevkitError(t("errors.scaffolding.run_fail"), { cause }); } } async function copyLocalTemplate(options) { const { sourcePath, projectName } = options; const projectPath = path.join(process.cwd(), projectName); try { let finalSourcePath = sourcePath; if (finalSourcePath.startsWith("file://")) { finalSourcePath = finalSourcePath.substring(7); } if (!path.isAbsolute(finalSourcePath)) { finalSourcePath = path.join(process.cwd(), finalSourcePath); } await copyJavascriptTemplate(finalSourcePath, projectPath); await updateJavascriptProjectName(projectPath, projectName); } catch (error) { throw new DevkitError(t("errors.scaffolding.copy_fail"), { cause: error }); } } async function scaffoldTemplate(projectName, templateConfig, packageManager, cacheStrategy, spinner) { const { location } = templateConfig; if (location.includes("{pm}")) { spinner.text = logger.colors.cyan(logger.colors.bold(t("messages.scaffolding.run_start", { command: location, }))); spinner.stop(); await runCliCommand({ command: location, projectName, packageManager}); return { isOfficialCli: true, projectDirCreated: false }; } else if (location.startsWith("http") || location.startsWith("git@")) { await getTemplateFromCache({ url: location, projectName, spinner, strategy: cacheStrategy, }); return { isOfficialCli: false, projectDirCreated: true }; } else { spinner.text = logger.colors.cyan(t("messages.scaffolding.copy_start")); spinner.start(); await copyLocalTemplate({ sourcePath: location, projectName}); spinner.succeed(logger.colors.green(t("messages.scaffolding.copy_success"))); return { isOfficialCli: false, projectDirCreated: true }; } } function logSuccessMessages(projectName) { logger.log(logger.colors.green(logger.colors.bold(t("messages.success.scaffolding_complete")))); logger.log(logger.colors.white(logger.colors.bold(logger.colors.italic(t("messages.success.next_steps"))))); logger.log(logger.colors.green(logger.colors.bold(` cd ${projectName}\n git init && git add -A && git commit -m "Initial commit"\n`))); } async function scaffoldProject(options) { const { projectName, templateConfig, packageManager, cacheStrategy } = options; const spinner = logger.spinner(t("messages.status.scaffolding_project")); let projectDirCreated = false; let isOfficialCli = false; try { spinner.start(); const result = await scaffoldTemplate(projectName, templateConfig, packageManager, cacheStrategy, spinner); isOfficialCli = result.isOfficialCli; projectDirCreated = result.projectDirCreated; if (!isOfficialCli) { spinner.text = logger.colors.cyan(logger.colors.bold(`${t("messages.scaffolding.install_start", { pm: packageManager })}\n`)); spinner.stop(); await installDependencies({ projectName, packageManager, spinner }); } if (!isOfficialCli) { logSuccessMessages(projectName); } } catch (err) { spinner.fail(logger.colors.red(t("errors.scaffolding.unexpected"))); if (projectDirCreated) { try { await fs.remove(projectName); logger.warning(logger.colors.yellow(t("messages.status.project_removed", { project: projectName }))); // oxlint-disable-next-line no-unused-vars } catch (cleanupErr) { logger.error(t("errors.scaffolding.fail", { project: projectName }), "CLEANUP"); } } const message = t("errors.scaffolding.unexpected"); if (err instanceof Error) { logger.error(`${message}: ${err.cause}`, "UNKNOWN"); } else { logger.error(message, "UNKNOWN"); } } } export { scaffoldProject };