UNPKG

@reliverse/rse

Version:

@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power

309 lines (308 loc) 11.4 kB
import path from "@reliverse/pathkit"; import { ensuredir } from "@reliverse/relifso"; import fs from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { confirmPrompt, selectPrompt, multiselectPrompt, nextStepsPrompt, inputPrompt } from "@reliverse/rempts"; import { normalizeName } from "@reliverse/rempts"; import { installDependencies } from "nypm"; import open from "open"; import os from "os"; import { cliDomainDocs, homeDir, UNKNOWN_VALUE } from "../../constants.js"; import { experimental } from "../../utils/badgeNotifiers.js"; import { setupI18nFiles } from "../../utils/downloading/downloadI18nFiles.js"; import { isVSCodeInstalled } from "../../utils/handlers/isAppInstalled.js"; import { promptPackageJsonScripts } from "../../utils/handlers/promptPackageJsonScripts.js"; import { askOpenInIDE } from "../../utils/prompts/askOpenInIDE.js"; import { askProjectName } from "../../utils/prompts/askProjectName.js"; import { askUsernameFrontend } from "../../utils/prompts/askUsernameFrontend.js"; async function ensureUniqueProjectName(initialName, isDev, cwd, skipPrompts) { let projectName = initialName; let targetPath = isDev ? path.join(cwd, "tests-runtime", projectName) : path.join(cwd, projectName); let index = 1; while (await fs.pathExists(targetPath)) { if (skipPrompts) { projectName = `${initialName}-${index}`; index++; } else { projectName = await inputPrompt({ title: `Project directory '${projectName}' already exists. Please choose a different name:`, defaultValue: `${projectName}-${index}`, validate: (value) => { if (!value) return "Project name cannot be empty"; if (!/^[a-z0-9-_]+$/i.test(value)) { return "Project name can only contain letters, numbers, hyphens, and underscores"; } return true; } }); } targetPath = isDev ? path.join(cwd, "tests-runtime", projectName) : path.join(cwd, projectName); } return projectName; } export async function initializeProjectConfig(projectName, _memory, config, skipPrompts, isDev, cwd) { const frontendUsername = skipPrompts && config?.projectAuthor !== UNKNOWN_VALUE && config?.projectAuthor !== "" ? config.projectAuthor : await askUsernameFrontend(config, true) ?? "default-user"; if (!frontendUsername || frontendUsername.trim() === "") { throw new Error( "Failed to determine your frontend username. Please try again or notify the CLI developers." ); } if (skipPrompts) { if (projectName !== UNKNOWN_VALUE) { projectName = normalizeName(projectName); } else { projectName = await askProjectName({ repoName: "" }) ?? "my-app"; } } else { projectName = await askProjectName({ repoName: "" }) ?? "my-app"; } projectName = await ensureUniqueProjectName( projectName, isDev, cwd, skipPrompts ); const primaryDomain = skipPrompts && config?.projectDomain !== UNKNOWN_VALUE && config?.projectDomain !== "" ? config.projectDomain : `${projectName}.vercel.app`; return { frontendUsername: frontendUsername.trim(), projectName, primaryDomain: primaryDomain ?? `${projectName}.vercel.app` }; } export async function setupI18nSupport(projectPath, config) { const i18nFolderExists = await fs.pathExists(path.join(projectPath, "src/app/[locale]")) || await fs.pathExists(path.join(projectPath, "src/app/[lang]")); if (i18nFolderExists) { relinka( "verbose", "i18n is already enabled in the template. No changes needed." ); return true; } const i18nBehavior = config.i18nBehavior; let shouldEnableI18n = false; if (i18nBehavior !== "prompt") { shouldEnableI18n = i18nBehavior === "autoYes"; } else { shouldEnableI18n = await confirmPrompt({ title: "Do you want to enable i18n (internationalization)?", displayInstructions: true, content: "If `N`, i18n folder won't be created.", defaultValue: false }); } if (shouldEnableI18n) { await setupI18nFiles(projectPath); } return shouldEnableI18n; } export async function shouldInstallDependencies(behavior, isDev) { if (behavior === "autoYes") return true; if (behavior === "autoNo") return false; if (isDev) return false; return await confirmPrompt({ title: "Would you like to install dependencies now?", content: "- Recommended, but may take time.\n- Enables execution of scripts provided by the template.\n- Crucial if you've provided a fresh database API key.\n- Avoids potential Vercel build failures by ensuring `db:push` is run at least once.\n- Allows running additional necessary scripts after installation.", defaultValue: true }); } export async function handleDependencies(projectPath, config) { const depsBehavior = config?.depsBehavior ?? "prompt"; const shouldInstallDeps = await shouldInstallDependencies(depsBehavior, true); let shouldRunDbPush = false; if (shouldInstallDeps) { await installDependencies({ cwd: projectPath }); const scriptStatus = await promptPackageJsonScripts( projectPath, shouldRunDbPush, true ); shouldRunDbPush = scriptStatus.dbPush; } return { shouldInstallDeps, shouldRunDbPush }; } async function moveProjectFromTestsRuntime(projectName, sourceDir) { try { let getDefaultProjectPath = function() { const platform = os.platform(); return platform === "win32" ? "C:\\B\\S" : path.join(homeDir, "Projects"); }; const shouldUseProject = await confirmPrompt({ title: `Project bootstrapped in dev mode. Move to a permanent location? ${experimental}`, content: "If yes, I'll move it from the tests-runtime directory to a new location you specify.", defaultValue: false }); if (!shouldUseProject) { return null; } const defaultPath = getDefaultProjectPath(); const targetDir = await inputPrompt({ title: "Where should I move the project?", content: "Enter a desired path:", placeholder: `Press <Enter> to use default: ${defaultPath}`, defaultValue: defaultPath }); await ensuredir(targetDir); let effectiveProjectName = projectName; let effectivePath = path.join(targetDir, projectName); let counter = 1; while (await fs.pathExists(effectivePath)) { const newName = await inputPrompt({ title: `Directory '${effectiveProjectName}' already exists at ${targetDir}`, content: "Enter a new name for the project directory:", defaultValue: `${projectName}-${counter}`, validate: (value) => /^[a-zA-Z0-9-_]+$/.test(value) ? true : "Invalid directory name format" }); effectiveProjectName = newName; effectivePath = path.join(targetDir, effectiveProjectName); counter++; } await fs.move(sourceDir, effectivePath); relinka("success", `Project moved to ${effectivePath}`); return effectivePath; } catch (error) { relinka("error", "Failed to move project:", String(error)); return null; } } export async function showSuccessAndNextSteps(projectPath, selectedRepo, frontendUsername, isDeployed, primaryDomain, allDomains, skipPrompts, isDev) { let effectiveProjectPath = projectPath; if (isDev && !skipPrompts) { const newPath = await moveProjectFromTestsRuntime( path.basename(projectPath), projectPath ); if (newPath) { effectiveProjectPath = newPath; } } relinka( "info", `\u{1F389} Template '${selectedRepo}' was installed at ${effectiveProjectPath}` ); const vscodeInstalled = isVSCodeInstalled(); await nextStepsPrompt({ title: "\u{1F918} Project created successfully! Next steps:", titleColor: "cyanBright", content: [ `- To open in VSCode: code ${effectiveProjectPath}`, `- Or in terminal: cd ${effectiveProjectPath}`, "- Install dependencies manually if needed: bun i OR pnpm i", "- Apply linting & formatting: bun check OR pnpm check", "- Run the project: bun dev OR pnpm dev", "- Note: if some of bootstrapped file contains `<dler-...>` you probably need to run `dler magic`, or ask your template developer, or learn more here: https://github.com/reliverse/dler/?tab=readme-ov-file#14-magic" ] }); if (!skipPrompts) { await handleNextActions( isDev, effectiveProjectPath, vscodeInstalled, isDeployed, primaryDomain, allDomains ); } relinka( "success", "\u2728 By the way, one more thing you can try (highly experimental):", "\u{1F449} Run `rse cli` in your new project to add/remove features." ); relinka( "info", frontendUsername !== UNKNOWN_VALUE && frontendUsername !== "" ? `\u{1F44B} More features soon! See you, ${frontendUsername}!` : "\u{1F44B} All done for now!" ); } export async function handleNextActions(isDev, projectPath, vscodeInstalled, isDeployed, primaryDomain, allDomains) { const nextActions = await multiselectPrompt({ title: "What would you like to do next?", titleColor: "cyanBright", defaultValue: ["ide"], options: [ { label: "Open Your Default Code Editor", value: "ide", hint: vscodeInstalled ? "Detected: VSCode-based IDE" : "" }, ...isDeployed ? [ { label: "Open Deployed Project", value: "deployed", hint: `Visit ${primaryDomain}` } ] : [], { label: "Support Reliverse", value: "sponsors" }, { label: "Join Reliverse Discord", value: "discord" }, { label: "Open Reliverse docs", value: "docs" } ] }); await Promise.all( nextActions.map( (action) => handleNextAction(isDev, action, projectPath, primaryDomain, allDomains) ) ); } export async function handleNextAction(isDev, action, projectPath, primaryDomain, allDomains) { try { switch (action) { case "ide": { await askOpenInIDE({ projectPath, enforce: true, isDev }); break; } case "deployed": { if (allDomains && allDomains.length > 1) { const selectedDomain = await selectPrompt({ title: "Select domain to open:", options: allDomains.map((d) => ({ label: d, value: d, ...d === primaryDomain ? { hint: "(primary)" } : {} })) }); relinka( "verbose", `Opening deployed project at ${selectedDomain}...` ); await open(`https://${selectedDomain}`); } else { relinka("verbose", `Opening deployed project at ${primaryDomain}...`); await open(`https://${primaryDomain}`); } break; } case "sponsors": { relinka("verbose", "Opening GitHub Sponsors page..."); await open("https://github.com/sponsors/blefnk"); break; } case "discord": { relinka("verbose", "Opening Discord server..."); await open("https://discord.gg/Pb8uKbwpsJ"); break; } case "docs": { relinka("verbose", "Opening Reliverse Docs..."); await open(cliDomainDocs); break; } default: break; } } catch (error) { relinka("error", `Error handling action '${action}':`, String(error)); } }