@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
JavaScript
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));
}
}