From f022a023176049d0e2bb97ee924f186e4d4ca9dc Mon Sep 17 00:00:00 2001 From: Nazar Kornienko Date: Tue, 26 Nov 2024 23:57:36 +0100 Subject: [PATCH] =?UTF-8?q?reliverse=E2=86=92@reliverse/cli,=20fix=20all?= =?UTF-8?q?=20issues,=20etc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- .putout.json | 4 +- .vscode/extensions.json | 23 ++ README.md | 18 +- build.optim.ts | 92 +++++-- build.publish.ts | 111 +++++++++ bump.config.ts | 6 + bun.lockb | Bin 399960 -> 437272 bytes cspell.json | 25 ++ jsr.json => jsr.jsonc | 19 +- package.json | 81 +++--- reset.d.ts | 1 + src/app.ts | 104 ++++++++ src/main.ts | 8 +- src/menu/configs.ts | 3 +- src/menu/modules/00-showReliverseMenu.ts | 59 +++++ src/menu/modules/01-justInstallRelivator.ts | 11 + src/menu/modules/02-buildOwnRelivator.ts | 11 + src/menu/modules/03-installAnyGitRepo.ts | 121 +++++++++ src/menu/modules/04-askProjectDetails.ts | 79 ++++++ src/menu/modules/05-askAppName.ts | 18 ++ src/menu/modules/06-askUserName.ts | 18 ++ src/menu/modules/07-askAppDomain.ts | 17 ++ src/menu/modules/08-askGitInitialization.ts | 30 +++ src/menu/modules/09-askInstallDependencies.ts | 28 +++ src/menu/modules/10-askSummaryConfirmation.ts | 49 ++++ .../11-askInternationalizationSetup.ts | 15 ++ .../12-askToResolveProjectConflicts.ts | 88 +++++++ .../modules/13-askCheckAndDownloadFiles.ts | 119 +++++++++ src/menu/modules/14-askCodemodUserCodebase.ts | 45 ++++ src/menu/modules/15-showCongratulationMenu.ts | 122 +++++++++ .../modules/16-constructEnvVariablesFile.ts | 115 +++++++++ src/menu/modules/17-showUpdateCloneMenu.ts | 96 +++++++ .../modules/18-showRelivatorFeatEditor.ts | 37 +++ src/menu/prompts.ts | 115 ++++----- src/menu/schema.ts | 2 +- src/menu/utils.ts | 6 +- src/mods/replaceImportSymbol.ts | 29 +++ src/mods/replaceWithModern.ts | 50 ++++ src/temp/14-askCodemodUserCodebase.ts | 46 ++++ src/temp/16-showUpdateCloneMenu.ts | 71 ++++++ src/temp/menu/askPackageManager.ts | 64 +++++ src/temp/menu/utils/dependenciesInstall.ts | 40 +++ src/temp/menu/utils/packageManager.ts | 92 +++++++ src/temp/mod.ts | 28 +++ src/temp/replaceImportSymbol.ts | 32 +++ src/tests/codemod.ts | 19 ++ src/tests/update-config.json | 12 + src/tests/updater.ts | 19 ++ src/utils/app.ts | 234 ++++++++++++++++++ src/utils/appts.ts | 223 +++++++++++++++++ src/utils/biome.ts | 68 +++++ src/utils/choosePackageManager.ts | 40 +++ src/utils/cloneAndCopyFiles.ts | 104 ++++++++ src/utils/cmds/add.ts | 0 src/utils/cmds/diff.ts | 0 src/utils/cmds/init.ts | 0 src/utils/configure.ts | 151 +++++++++++ src/utils/console.ts | 30 +++ src/utils/downloadGitRepo.ts | 35 +++ src/utils/downloadI18nFiles.ts | 118 +++++++++ src/utils/env.ts | 211 ++++++++++++++++ src/utils/envjs.ts | 104 ++++++++ src/utils/eslint.ts | 96 +++++++ src/utils/extractRepoInfo.ts | 23 ++ src/utils/fileUtils.ts | 19 ++ src/utils/fs.ts | 24 ++ src/utils/git.ts | 47 ++++ src/utils/handleStringReplacements.ts | 21 ++ src/utils/isAppInstalled.ts | 26 ++ src/utils/knip.ts | 68 +++++ src/utils/metadata.ts | 13 + src/utils/moveAppToLocale.ts | 54 ++++ src/utils/nav.ts | 34 +++ src/utils/nextjs.ts | 114 +++++++++ src/{menu => }/utils/pkg.ts | 3 + src/utils/products.ts | 221 +++++++++++++++++ src/utils/putout.ts | 73 ++++++ src/utils/replaceStringsInFiles.ts | 65 +++++ src/utils/shadcnComponents.ts | 15 ++ src/utils/string.ts | 14 ++ src/utils/types.ts | 39 +++ src/utils/validate.ts | 34 +++ src/utils/with.ts | 34 +++ tsconfig.json | 14 +- 85 files changed, 4425 insertions(+), 148 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 build.publish.ts create mode 100644 bump.config.ts rename jsr.json => jsr.jsonc (61%) create mode 100644 reset.d.ts create mode 100644 src/app.ts create mode 100644 src/menu/modules/00-showReliverseMenu.ts create mode 100644 src/menu/modules/01-justInstallRelivator.ts create mode 100644 src/menu/modules/02-buildOwnRelivator.ts create mode 100644 src/menu/modules/03-installAnyGitRepo.ts create mode 100644 src/menu/modules/04-askProjectDetails.ts create mode 100644 src/menu/modules/05-askAppName.ts create mode 100644 src/menu/modules/06-askUserName.ts create mode 100644 src/menu/modules/07-askAppDomain.ts create mode 100644 src/menu/modules/08-askGitInitialization.ts create mode 100644 src/menu/modules/09-askInstallDependencies.ts create mode 100644 src/menu/modules/10-askSummaryConfirmation.ts create mode 100644 src/menu/modules/11-askInternationalizationSetup.ts create mode 100644 src/menu/modules/12-askToResolveProjectConflicts.ts create mode 100644 src/menu/modules/13-askCheckAndDownloadFiles.ts create mode 100644 src/menu/modules/14-askCodemodUserCodebase.ts create mode 100644 src/menu/modules/15-showCongratulationMenu.ts create mode 100644 src/menu/modules/16-constructEnvVariablesFile.ts create mode 100644 src/menu/modules/17-showUpdateCloneMenu.ts create mode 100644 src/menu/modules/18-showRelivatorFeatEditor.ts create mode 100644 src/mods/replaceImportSymbol.ts create mode 100644 src/mods/replaceWithModern.ts create mode 100644 src/temp/14-askCodemodUserCodebase.ts create mode 100644 src/temp/16-showUpdateCloneMenu.ts create mode 100644 src/temp/menu/askPackageManager.ts create mode 100644 src/temp/menu/utils/dependenciesInstall.ts create mode 100644 src/temp/menu/utils/packageManager.ts create mode 100644 src/temp/mod.ts create mode 100644 src/temp/replaceImportSymbol.ts create mode 100644 src/tests/codemod.ts create mode 100644 src/tests/update-config.json create mode 100644 src/tests/updater.ts create mode 100644 src/utils/app.ts create mode 100644 src/utils/appts.ts create mode 100644 src/utils/biome.ts create mode 100644 src/utils/choosePackageManager.ts create mode 100644 src/utils/cloneAndCopyFiles.ts create mode 100644 src/utils/cmds/add.ts create mode 100644 src/utils/cmds/diff.ts create mode 100644 src/utils/cmds/init.ts create mode 100644 src/utils/configure.ts create mode 100644 src/utils/console.ts create mode 100644 src/utils/downloadGitRepo.ts create mode 100644 src/utils/downloadI18nFiles.ts create mode 100644 src/utils/env.ts create mode 100644 src/utils/envjs.ts create mode 100644 src/utils/eslint.ts create mode 100644 src/utils/extractRepoInfo.ts create mode 100644 src/utils/fileUtils.ts create mode 100644 src/utils/fs.ts create mode 100644 src/utils/git.ts create mode 100644 src/utils/handleStringReplacements.ts create mode 100644 src/utils/isAppInstalled.ts create mode 100644 src/utils/knip.ts create mode 100644 src/utils/metadata.ts create mode 100644 src/utils/moveAppToLocale.ts create mode 100644 src/utils/nav.ts create mode 100644 src/utils/nextjs.ts rename src/{menu => }/utils/pkg.ts (58%) create mode 100644 src/utils/products.ts create mode 100644 src/utils/putout.ts create mode 100644 src/utils/replaceStringsInFiles.ts create mode 100644 src/utils/shadcnComponents.ts create mode 100644 src/utils/string.ts create mode 100644 src/utils/types.ts create mode 100644 src/utils/validate.ts create mode 100644 src/utils/with.ts diff --git a/.gitignore b/.gitignore index 4115daf..49615ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -dist/ -output/ +.venv/ +dist-jsr/ +dist-npm/ .DS_Store +merged.txt .eslintcache node_modules/ addons/premium/ diff --git a/.putout.json b/.putout.json index 30a1676..0ebedce 100644 --- a/.putout.json +++ b/.putout.json @@ -209,7 +209,9 @@ "nodejs/remove-useless-strict-mode": "off", "browserlist": "off", "filesystem": "off", - "conditions/apply-consistent-blocks": "off" + "conditions/apply-consistent-blocks": "off", + "sort-imports-by-specifiers": "off", + "group-imports-by-source": "off" }, "plugins": [ "apply-at", diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..65d46c9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,23 @@ +{ + "recommendations": [ + "aaron-bond.better-comments", + "astro-build.houston", + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "charliermarsh.ruff", + "chunsen.bracket-select", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "fabiospampinato.vscode-open-multiple-files", + "github.github-vscode-theme", + "lokalise.i18n-ally", + "mikekscholz.pop-icon-theme", + "ms-python.python", + "neptunedesign.vs-sequential-number", + "streetsidesoftware.code-spell-checker", + "unifiedjs.vscode-mdx", + "usernamehw.errorlens", + "usernamehw.remove-empty-lines", + "yzhang.markdown-all-in-one" + ] +} diff --git a/README.md b/README.md index 32696d8..f13c131 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Reliverse -[GitHub](https://github.com/reliverse/cli), [npmjs](https://npmjs.com/package/reliverse), [Discord](https://discord.gg/Pb8uKbwpsJ) +[npmjs](https://npmjs.com/package/@reliverse/cli), [GitHub](https://github.com/reliverse/cli), [Discord](https://discord.gg/Pb8uKbwpsJ) + +![Reliverse Cover Image](./reliverse.webp) + +## Introduction πŸ‘‹ Welcome! This tool can help you easily create new web projects and automatically make advanced codebase modifications, with more features coming soon. **Reliverse** is a CLI tool designed to streamline the setup of JavaScript, TypeScript, and other types of projects, with a primary focus on Next.js templates, though it is not limited to them. -![Reliverse Cover Image](./reliverse.webp) - It allows you to effortlessly bootstrap projects, including the [Relivator Next.js template](https://github.com/blefnk/relivator-nextjs-template) or any other template from GitHub or other Git-based sources. Additionally, Reliverse assists in managing configuration files and resolving potential conflicts between tools like ESLint, Prettier, and Biome. Reliverse is more than just the easiest way to install Relivator. It’s also the most convenient new way to download any repository from GitHub and automatically prepare it for work. Especially if it’s a project from the JavaScript ecosystem. @@ -44,10 +46,10 @@ By the way, you might think that a CLI doing so many things would become bloated You should install [Git](https://git-scm.com), [VSCode](https://code.visualstudio.com), and [Node.js LTS](https://nodejs.org/en/download/package-manager) first. Then use one of the following commands to install **Reliverse**: -- With [bun ]( .sh): `bun i -g reliverse` -- With [pnpm](https://pnpm.io/installation#using-corepack): `bun add -g reliverse` -- With [yarn](https://yarnpkg.com): `yarn global add reliverse` -- With [npm](https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager): `npm i -g reliverse` +- With [bun](https://bun.sh): `bun i -g @reliverse/cli` +- With [pnpm](https://pnpm.io/installation#using-corepack): `pnpm add -g @reliverse/cli` +- With [yarn](https://yarnpkg.com): `yarn global add @reliverse/cli` +- With [npm](https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager): `npm i -g @reliverse/cli` ## Usage @@ -255,7 +257,7 @@ This project is licensed under the MIT Licenseβ€”see the [LICENSE](LICENSE) file This project wouldn’t exist without the amazing work of the following projects: -[@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js#readme) | [terkelg/prompts](https://github.com/lu-jiejie/prompts-plus#readme#readme) | [@clack/prompts](https://github.com/bombshell-dev/clack#readme) | [create-t3-app](https://github.com/t3-oss/create-t3-app#readme) | [create-astro](https://github.com/withastro/astro/tree/main/packages/create-astro#readme) | [cronvel/terminal-kit](https://github.com/cronvel/terminal-kit#readme) | [unjs/consola](https://github.com/unjs/consola#readme) +[@reliverse/relinka](https://github.com/SBoudrias/Inquirer.js#readme) | [terkelg/prompts](https://github.com/lu-jiejie/prompts-plus#readme#readme) | [@reliverse/relinka](https://github.com/bombshell-dev/clack#readme) | [create-t3-app](https://github.com/t3-oss/create-t3-app#readme) | [create-astro](https://github.com/withastro/astro/tree/main/packages/create-astro#readme) | [cronvel/terminal-kit](https://github.com/cronvel/terminal-kit#readme) | [unjs/relinka](https://github.com/unjs/relinka#readme) ## Wrap-Up diff --git a/build.optim.ts b/build.optim.ts index 99abf99..f6f1b0b 100644 --- a/build.optim.ts +++ b/build.optim.ts @@ -1,11 +1,11 @@ -import { errorHandler, spinner } from "@reliverse/relinka"; +import { errorHandler, msg, spinner } from "@reliverse/prompts"; import glob from "fast-glob"; import fs from "fs-extra"; import path from "pathe"; import strip from "strip-comments"; // Verbose logging -const debug = true; +const debug = false; // Parse command-line arguments to check for '--jsr' flag const args: string[] = process.argv.slice(2); @@ -26,7 +26,7 @@ const npmFilesToDelete: string[] = [ "types/internal.d.ts", ]; -const jsrFilesToDelete: string[] = ["**/*.test.ts", "types/internal.ts"]; +const jsrFilesToDelete: string[] = ["**/*.test.ts"]; /** * Deletes files matching the provided patterns within the base directory. @@ -62,17 +62,20 @@ async function deleteFiles(patterns: string[], baseDir: string): Promise { /** * Replaces import paths that use '~/' with relative paths. + * If `isJSR` is true, also replaces '.js' extensions with '.ts'. * @param content - The file content. * @param fileDir - The directory of the current file. * @param rootDir - The root directory to resolve relative paths. + * @param isJSR - Flag indicating whether to apply JSR-specific transformations. * @returns The updated file content with modified import paths. */ function replaceImportPaths( content: string, fileDir: string, rootDir: string, + isJSR: boolean, ): string { - return content.replace( + let updatedContent = content.replace( // Matches both static and dynamic imports /(from\s+['"]|import\s*\(\s*['"])(~\/?[^'"]*)(['"]\s*\)?)/g, ( @@ -94,28 +97,39 @@ function replaceImportPaths( return `${prefix}${newPath}${suffix}`; }, ); + + if (isJSR) { + // Replace '.js' extensions with '.ts' in import paths + // @see https://jsr.io/docs/publishing-packages#relative-imports + updatedContent = updatedContent.replace(/(\.js)(?=['";])/g, ".ts"); + + if (debug) { + console.log("Replaced '.js' with '.ts' in import paths."); + } + } + + return updatedContent; } /** - * Removes comments from the given content string using strip-comments. + * Removes comments from the given content string. + * - Strips block comments using `strip-comments`. * @param content - The file content. * @param filePath - The path of the file being processed. - * @returns The content without comments. + * @returns The content without unwanted comments. */ function removeComments(content: string, filePath: string): string { - const stripped: string = strip(content); + // When not in JSR mode, strip all comments using strip-comments + const stripped = strip(content, { + line: true, + block: true, + keepProtected: true, + preserveNewlines: false, + }); if (debug) { - // Extract comments for comparison - const originalComments: string = ( - content.match(/\/\*[\s\S]*?\*\/|\/\/.*/g) || [] - ).join("\n"); - const strippedComments: string = ( - stripped.match(/\/\*[\s\S]*?\*\/|\/\/.*/g) || [] - ).join("\n"); console.log(`\nProcessing file: ${filePath}`); - console.log("Original Comments:\n", originalComments); - console.log("Stripped Comments:\n", strippedComments); + console.log("Stripped all comments."); } return stripped; @@ -156,9 +170,12 @@ async function processFiles(dir: string): Promise { content, path.dirname(filePath), outputDir, + isJSR, ); - updatedContent = removeComments(updatedContent, filePath); + if (!isJSR) { + updatedContent = removeComments(updatedContent, filePath); + } if (content !== updatedContent) { await fs.writeFile(filePath, updatedContent, "utf8"); @@ -242,9 +259,38 @@ async function optimizeBuildForProduction(dir: string): Promise { }); } -await optimizeBuildForProduction(outputDir).catch((error: Error) => - errorHandler( - error, - "If this issue is related to Reliverse CLI itself, please\nβ”‚ report the details at https://github.com/blefnk/reliverse", - ), -); +async function getDirectorySize(dirPath: string): Promise { + const files = await fs.readdir(dirPath); + let totalSize = 0; + + for (const file of files) { + const filePath = path.join(dirPath, file); + const stats = await fs.stat(filePath); + + if (stats.isDirectory()) { + totalSize += await getDirectorySize(filePath); + } else { + totalSize += stats.size; + } + } + + return totalSize; +} + +await optimizeBuildForProduction(outputDir) + .then(() => { + getDirectorySize(outputDir) + .then((size) => { + msg({ + type: "M_INFO", + title: `Total size of ${outputDir}: ${size} bytes`, + }); + }) + .catch((error) => { + msg({ + type: "M_ERROR", + title: `Error calculating directory size for ${outputDir}: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + }); + }) + .catch((error: Error) => errorHandler(error)); diff --git a/build.publish.ts b/build.publish.ts new file mode 100644 index 0000000..ab6b5c6 --- /dev/null +++ b/build.publish.ts @@ -0,0 +1,111 @@ +import relinka from "@reliverse/relinka"; +import { execa } from "execa"; +import fs from "fs-extra"; +import mri from "mri"; + +function showHelp() { + relinka.info(`Usage: bun build.publish.ts [options] + +Options: + no options Publish to npm registry + --jsr Publish to JSR registry + --dry-run Perform a dry run of the publish process + -h, --help Show help +`); +} + +const argv = mri(process.argv.slice(2), { + alias: { + h: "help", + }, + boolean: ["jsr", "dry-run", "help"], + default: { + jsr: false, + "dry-run": false, + help: false, + }, +}); + +// If help flag is present, display help and exit +if (argv.help) { + showHelp(); + process.exit(0); +} + +// Handle flags +const validFlags = ["jsr", "dry-run", "help", "h"]; +const unknownFlags = Object.keys(argv).filter( + (key) => !validFlags.includes(key) && key !== "_", +); + +if (unknownFlags.length > 0) { + relinka.error(`❌ Unknown flag(s): ${unknownFlags.join(", ")}`); + showHelp(); + process.exit(1); +} + +async function publishNpm(dryRun: boolean) { + try { + if (dryRun) { + await execa("bun publish --dry-run", { stdio: "inherit" }); + } else { + await execa("bun build:npm", { stdio: "inherit" }); + await execa("bun publish", { stdio: "inherit" }); + } + relinka.success("Published to npm successfully."); + } catch (error) { + relinka.error("❌ Failed to publish to npm:", error); + process.exit(1); + } +} + +async function publishJsr(dryRun: boolean) { + try { + if (dryRun) { + await execa( + "bunx jsr publish --allow-slow-types --allow-dirty --dry-run", + { stdio: "inherit" }, + ); + } else { + await execa("bun build:jsr", { stdio: "inherit" }); + await execa("bunx jsr publish --allow-slow-types --allow-dirty", { + stdio: "inherit", + }); + } + relinka.success("Published to JSR successfully."); + } catch (error) { + relinka.error("❌ Failed to publish to JSR:", error); + process.exit(1); + } +} + +async function bumpJsrVersion(disable?: boolean) { + if (disable) { + return; + } + const pkg = JSON.parse(await fs.readFile("package.json", "utf-8")); + const jsrConfig = JSON.parse(await fs.readFile("jsr.jsonc", "utf-8")); + // @ts-expect-error TODO: fix ts + jsrConfig.version = pkg.version; + await fs.writeFile("jsr.jsonc", JSON.stringify(jsrConfig, null, 2)); +} + +async function bumpNpmVersion() { + await execa("bun bumpp", { stdio: "inherit" }); +} + +async function main() { + const { jsr, "dry-run": dryRun } = argv; + if (jsr) { + // await bumpJsrVersion(); + await publishJsr(dryRun); + } else { + // await bumpNpmVersion(); + await publishNpm(dryRun); + } +} + +main().catch((error) => { + relinka.error("❌ An unexpected error occurred:", error); + process.exit(1); +}); diff --git a/bump.config.ts b/bump.config.ts new file mode 100644 index 0000000..ac124b2 --- /dev/null +++ b/bump.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + push: false, + commit: false, +}); diff --git a/bun.lockb b/bun.lockb index 1f6ca5681891b54dbf2405b53333d26011117027..ab3d0f3b773ffb4688bec48e7b944db0bcd36fe2 100644 GIT binary patch delta 104422 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 Q5`AY9-3x~}**vrUKiqZz0ssI2 delta 84022 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π*wLZ9V)yO`Dn63 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 diff --git a/cspell.json b/cspell.json index 20ae7ad..c134031 100644 --- a/cspell.json +++ b/cspell.json @@ -5,6 +5,9 @@ "@cspell/dict-npm/cspell-ext.json" ], "ignorePaths": [ + "**/*.lock", + "**/*.txt", + "pnpm-lock.yaml", "examples/deprecated", "dist-jsr", "dist-npm" @@ -18,11 +21,14 @@ "arethetypeswrong", "Arryn", "astro", + "astrowind", "attw", "ausgewΓ€hlt", + "authjs", "avez", "Baratheon", "blefnk", + "bleverse", "bootstrapper", "bootstrappers", "browserlist", @@ -34,6 +40,7 @@ "choisi", "Clooney", "cmder", + "Codemod", "confirmtoggle", "continuar", "cristal", @@ -50,6 +57,7 @@ "Dont", "Downey", "emojify", + "envjs", "eslintcache", "favourite", "figliolia", @@ -57,11 +65,14 @@ "Focusable", "forgetme", "Geralt", + "giget", "goroutines", "Greyjoy", + "Griptape", "Gyllenhaal", "haben", "Haha", + "headlessly", "hoverable", "iife", "invertir", @@ -70,17 +81,21 @@ "jiejie", "jumpstart", "keypresses", + "Kornienko", "Korniienko", "Lannister", "lazyness", "lockb", + "LOGLIB", "madrun", + "magicast", "Martell", "Mikey", "Misrefactored", "mkdist", "Mkey", "montag", + "Nazar", "Nazarii", "nenc", "nocheck", @@ -88,18 +103,24 @@ "Noninteractive", "npmjs", "ntqry", + "nuqs", "nypm", + "ofetch", + "onwidget", "optim", "optionception", "outro", "pagedown", "pageup", "picocolors", + "planetscale", + "pmndrs", "polski", "Preconfigured", "printj", "rawlist", "redrun", + "reli", "relinka", "Relivator", "reliverse", @@ -117,10 +138,12 @@ "subchoices", "Targ", "Targaryen", + "tasuku", "terkelg", "termkit", "tseslint", "Tully", + "turso", "typebox", "typecheck", "typesafe", @@ -130,8 +153,10 @@ "unjs", "unpub", "unstub", + "UPLOADTHING", "valign", "venv", + "versator", "Vous", "vsprintf", "Whoo", diff --git a/jsr.json b/jsr.jsonc similarity index 61% rename from jsr.json rename to jsr.jsonc index ce1972b..36cfce0 100644 --- a/jsr.json +++ b/jsr.jsonc @@ -1,7 +1,9 @@ { - "name": "@reliverse/reliverse", - "version": "1.0.0", - "exports": "./dist-jsr/mod.ts", + "name": "@reliverse/cli", + "version": "1.1.0", + "author": "blefnk", + "license": "MIT", + "exports": "./dist-jsr/main.ts", "publish": { "exclude": [ "!.", @@ -15,17 +17,22 @@ ".venv/**", ".github/**", ".gitignore", + ".eslintcache", "biome.jsonc", - "build.optim.ts", "build.config.ts", + "build.optim.ts", + "build.publish.ts", + "bump.config.ts", ".putout.json", "bun.lockb", "knip.jsonc", "merged.txt", "typestat.json", "vitest.config.ts", - "eslint.config.ts", - "cspell.json" + "eslint.config.js", + "tsconfig.json", + "cspell.json", + "reset.d.ts" ] } } diff --git a/package.json b/package.json index 5ce166a..dae65c3 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,30 @@ { - "name": "reliverse", - "version": "1.0.64", + "name": "@reliverse/cli", + "version": "1.1.0", + "author": "blefnk", + "type": "module", "description": "A CLI tool that offers a convenient way to bootstrap a new web project and prepare it for work.", "scripts": { - "appts": "bun typecheck && bun lint && bun format && bun optimize", - "build": "unbuild && bun build.optim.ts", - "unpub": "npm unpublish", - "bunp": "bun publish", - "pub:jsr": "bun build.optim.ts --jsr && bun optimize && bunx jsr publish --allow-slow-types --allow-dirty", - "pub:jsr-dry": "bun pub:jsr --dry-run", - "pub:npm": "redrun build bunp", - "pub": "redrun pub:jsr pub:npm", + "dev": "bun src/main.ts", + "check": "redrun typecheck lint format attw", + "release": "bumpp && bun check && bun pub", + "build:jsr": "bun build.optim.ts --jsr", + "build:npm": "unbuild && bun build.optim.ts", + "build": "redrun build:jsr build:npm optimize", + "pub:jsr": "bun build.publish.ts --jsr", + "pub:npm": "bun build.publish.ts", + "pub:dry": "bun build.publish.ts --dry-run", + "pub": "redrun pub:npm pub:jsr", + "typecheck": "tsc --noEmit", "lint": "eslint --cache --fix .", + "lint:i": "eslint --inspect-config", "format": "biome check --write .", "optimize": "putout dist-npm --fix", - "check": "bun test", - "attw": "attw --pack", - "tsc": "tshy", - "knip": "knip", - "start": "node dist-npm/main.js", - "dev": "bun src/main.ts", - "dex": "tsx src/main.ts", - "test:tape": "tape test/*.js | tap-spec", - "release": "bumpp & bun pub", - "typecheck": "tsc --noEmit" + "attw": "bunx @arethetypeswrong/cli", + "unpub": "npm unpublish", + "test": "vitest", + "knip": "knip" }, - "type": "module", "publishConfig": { "access": "public" }, @@ -49,35 +48,53 @@ "keywords": ["cli", "reliverse"], "license": "MIT", "dependencies": { - "@reliverse/relinka": "^1.1.3", - "@sinclair/typebox": "^0.34.8", + "@reliverse/core": "^0.1.0", + "@reliverse/fs": "^0.6.0", + "@reliverse/prompts": "^1.2.4", + "@reliverse/relinka": "^1.2.2", + "@sinclair/typebox": "^0.34.9", + "c12": "^2.0.1", "cli-spinners": "^3.2.0", "detect-package-manager": "^3.0.2", + "execa": "^9.5.1", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", + "giget": "^1.2.3", + "glob": "^11.0.0", + "magic-regexp": "^0.8.0", + "node-emoji": "^2.1.3", + "node-fetch": "^3.3.2", "nypm": "^0.3.12", + "ofetch": "^1.4.1", + "open": "^10.1.0", "pathe": "^1.1.2", - "picocolors": "^1.1.1" + "picocolors": "^1.1.1", + "random-words": "^2.0.1", + "simple-git": "^3.27.0", + "tasuku": "^2.0.1" }, "devDependencies": { - "@biomejs/biome": "1.9.4", - "@cspell/dict-npm": "^5.1.13", + "@biomejs/biome": "^1.9.4", + "@cspell/dict-npm": "^5.1.14", "@eslint/js": "^9.15.0", - "@types/bun": "^1.1.13", + "@total-typescript/ts-reset": "^0.6.1", + "@types/bun": "^1.1.14", "@types/eslint__js": "^8.42.3", "@types/fs-extra": "^11.0.4", - "@types/node": "^22.9.3", + "@types/node": "^22.10.0", "@types/strip-comments": "^2.0.4", + "bumpp": "^9.8.1", "eslint": "^9.15.0", - "eslint-plugin-perfectionist": "^4.0.3", + "eslint-plugin-perfectionist": "^4.1.2", "globals": "^15.12.0", - "knip": "^5.37.2", + "knip": "^5.38.1", + "magicast": "^0.3.5", "putout": "^36.13.1", "redrun": "^11.0.5", "strip-comments": "^2.0.1", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0", + "typescript-eslint": "^8.16.0", "unbuild": "^2.0.0", - "vitest": "^2.1.5" + "vitest": "^2.1.6" } } diff --git a/reset.d.ts b/reset.d.ts new file mode 100644 index 0000000..a3d4a03 --- /dev/null +++ b/reset.d.ts @@ -0,0 +1 @@ +import "@total-typescript/ts-reset"; diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..4f00fa6 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,104 @@ +// Debug settings to control verbose logging and temp clone cleanup behavior +export const DEBUG = { + alphaFeaturesEnabled: true, + disableTempCloneRemoving: true, // Control whether the temp clone folder is removed + enableVerboseLogging: false, // Toggle verbose logging on or off +}; + +// File conflict settings, useful for prompting user to resolve conflicts during project setup +export const FILE_CONFLICTS = [ + { + description: "ESLint config", + fileName: ".eslintrc.cjs", + shouldCopy: false, // Deprecated file, don't copy + }, + { + customMessage: + "Biome will be installed, so Prettier is not necessary. What would you like to do?", + description: "Prettier config", + fileName: "prettier.config.js", + }, + { + description: "TypeScript config", + fileName: "tsconfig.json", + }, +]; + +// Command-line arguments to check if we are in development mode +const args = process.argv.slice(2); + +export const isDev = args.includes("--dev"); + +export const REPO_FULL_URLS = { + relivatorGithubLink: "https://github.com/blefnk/relivator-nextjs-template", +}; + +export const REPO_SHORT_URLS = { + relivatorGithubLink: "blefnk/relivator-nextjs-template", + versatorGithubLink: "blefnk/versator-nextjs-template", +}; + +// Path settings for important files and directories +export const FILE_PATHS = { + layoutFile: "src/app/layout.tsx", // Path to layout file in the repo + pageFile: "src/app/page.tsx", // Path to page file in the repo + tempRepoClone: "temp-repo-clone", // Default temp clone folder name +}; + +// Files required for i18n setup +export const FILES_TO_DOWNLOAD = [FILE_PATHS.layoutFile, FILE_PATHS.pageFile]; + +// File categories used in conflict resolution or file download operations +export const fileCategories: Record = { + biome: ["biome.json"], + eslint: [".eslintrc.cjs", "eslint.config.js"], + GitHub: [".github", "README.md"], + IDE: [".vscode"], + putout: [".putout.json"], + "Reliverse configs": ["reliverse.config.ts", "reliverse.info.ts"], +}; + +// ======================================================================= +// FUTURE CONFIGURATIONS FOR RELIVATOR BUILDS (Placeholders for future features) +// ======================================================================= + +// Example of a configuration for future use +// export const RELIVATOR_CONFIG = { +// authProvider: "clerk" as "authjs" | "clerk" | "none", // Authentication provider options +// databaseDialect: "postgresql" as "mysql" | "postgresql" | "sqlite", // Database dialect options +// databaseProvider: "neon" as "neon" | "planetscale" | "turso", // Database providers for Reliverse +// disableDonateButton: false, // Option to disable donation button +// frameworkVersion: "1.2.6", // Framework version +// hideEnvInfo: false, // Toggle to hide environment info +// packageManager: "bun" as "bun" | "pnpm", // Future: default package manager +// }; + +// ======================================================================= +// Helper types for various configurations +// ======================================================================= + +// export type PackageManager = "bun" | "pnpm"; + +// export type AuthProvider = "authjs" | "clerk" | "none"; + +// export type DatabaseDialect = "mysql" | "postgresql" | "sqlite"; + +// export type DatabaseProvider = +// | "neon" +// | "planetscale" +// | "private-mysql" +// | "private-pg" +// | "railway-mysql" +// | "railway-pg" +// | "turso" +// | "vercel"; + +// ======================================================================= +// Example configurations for potential usage in the future +// ======================================================================= + +// export const databaseConfig = { +// provider: "neon" as DatabaseProvider, +// dialect: "postgresql" as DatabaseDialect, +// prefix: process.env.NEXT_PUBLIC_DATABASE_PREFIX || "bleverse", +// }; diff --git a/src/main.ts b/src/main.ts index 1ae40fd..d0861d8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ // πŸ“š Docs: https://docs.reliverse.org/relinka -import { errorHandler } from "@reliverse/relinka"; +import { errorHandler } from "@reliverse/prompts"; import { askDir, @@ -20,7 +20,7 @@ import { showResults, showSelectPrompt, showStartPrompt, - showTextPrompt, + showInputPrompt, showTogglePrompt, } from "./menu/prompts.js"; import { type UserInput } from "./menu/schema.js"; @@ -28,7 +28,7 @@ import { type UserInput } from "./menu/schema.js"; export default async function main() { await showStartPrompt(); await showAnykeyPrompt("privacy"); - const username = await showTextPrompt(); + const username = await showInputPrompt(); const dir = await askDir(username); const age = await showNumberPrompt(); const password = await showPasswordPrompt(); @@ -60,7 +60,7 @@ export default async function main() { await showEndPrompt(); } -await main().catch((error) => +await main().catch((error: Error) => errorHandler( error, "If this issue is related to Reliverse CLI itself, please\nβ”‚ report the details at https://github.com/blefnk/reliverse", diff --git a/src/menu/configs.ts b/src/menu/configs.ts index 718a5de..de48f0c 100644 --- a/src/menu/configs.ts +++ b/src/menu/configs.ts @@ -1,4 +1,4 @@ -import type { OptionalPromptOptions } from "@reliverse/relinka"; +import type { OptionalPromptOptions } from "@reliverse/prompts"; import { emojify } from "node-emoji"; @@ -12,7 +12,6 @@ export const extendedConfig = { ...basicConfig, contentTypography: "italic", contentColor: "dim", - answerColor: "none", } satisfies OptionalPromptOptions; export const experimentalConfig = { diff --git a/src/menu/modules/00-showReliverseMenu.ts b/src/menu/modules/00-showReliverseMenu.ts new file mode 100644 index 0000000..4197db9 --- /dev/null +++ b/src/menu/modules/00-showReliverseMenu.ts @@ -0,0 +1,59 @@ +import { selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { pkg } from "~/utils/pkg.js"; + +import { justInstallRelivator } from "./01-justInstallRelivator.js"; + +// import { installAnyGitRepo } from "./03-installAnyGitRepo"; +// import { shadcnComponents } from "~/utils/shadcnComponents"; +// import { buildOwnRelivator } from "./02-buildOwnRelivator"; + +// export async function showReliverseMenu(program: Command) { +export async function showReliverseMenu() { + await webProjectMenu(); +} + +async function webProjectMenu() { + console.log(""); + relinka.success(`✨ Reliverse CLI ${pkg.version}`); + + const option = await selectPrompt({ + title: "How would you like to proceed?", + options: [ + { label: "1. Install the pre-configured Relivator", value: "1" }, + { label: "2. Build your own Relivator from scratch", value: "2" }, + { + label: "3. Install any web-related repository from GitHub", + value: "3", + }, + // "4. Add shadcn/ui components to your React/Vue/Svelte project", + // "5. Run code modifications on the existing codebase", + // "6. Update your GitHub clone with the latest changes", + // "7. Add, remove, or replace the Relivator's features", + ], + }); + + if (option === "1") { + await justInstallRelivator(); + } else if (option === "2") { + // await buildOwnRelivator(); + relinka.warn("Not implemented yet 2."); + } else if (option === "3") { + // await installAnyGitRepo(); + relinka.warn("Not implemented yet 3."); + } + // else if ( option === "4. Add shadcn/ui components to your React/Vue/Svelte project" ) { + // await shadcnComponents(program); + // } + // else if (option === "5. Run code modifications on the existing codebase") { + // await askCodemodUserCodebase(); + // } else if (option === "6. Update your GitHub clone with the latest changes") { + // await showUpdateCloneMenu(); + // } else if (option === "7. Add, remove, or replace the Relivator's features") { + // await showRelivatorFeatEditor(); + // } + else { + relinka.error("Invalid option selected. Exiting."); + } +} diff --git a/src/menu/modules/01-justInstallRelivator.ts b/src/menu/modules/01-justInstallRelivator.ts new file mode 100644 index 0000000..73c0a3d --- /dev/null +++ b/src/menu/modules/01-justInstallRelivator.ts @@ -0,0 +1,11 @@ +import { REPO_SHORT_URLS } from "~/app.js"; + +import { askProjectDetails } from "./04-askProjectDetails.js"; + +export async function justInstallRelivator() { + const template = REPO_SHORT_URLS.relivatorGithubLink; + + const message = `Let's create your brand-new web app using the ${template} starter! After that, you can customize everything however you like.`; + + await askProjectDetails(template, message, "justInstallRelivator", false); +} diff --git a/src/menu/modules/02-buildOwnRelivator.ts b/src/menu/modules/02-buildOwnRelivator.ts new file mode 100644 index 0000000..bfd9744 --- /dev/null +++ b/src/menu/modules/02-buildOwnRelivator.ts @@ -0,0 +1,11 @@ +import { REPO_SHORT_URLS } from "~/app.js"; + +import { askProjectDetails } from "./04-askProjectDetails.js"; + +export async function buildOwnRelivator() { + const template = REPO_SHORT_URLS.versatorGithubLink; + + const message = `Let's build your own Relivator from scratch! We'll use the ${template} as a starting point.`; + + await askProjectDetails(template, message, "buildOwnRelivator", true); +} diff --git a/src/menu/modules/03-installAnyGitRepo.ts b/src/menu/modules/03-installAnyGitRepo.ts new file mode 100644 index 0000000..1093dcf --- /dev/null +++ b/src/menu/modules/03-installAnyGitRepo.ts @@ -0,0 +1,121 @@ +import { selectPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { REPO_SHORT_URLS } from "~/app.js"; +import { validate } from "~/utils/validate.js"; + +import { justInstallRelivator } from "./01-justInstallRelivator.js"; +import { askProjectDetails } from "./04-askProjectDetails.js"; + +export async function installAnyGitRepo() { + relinka.info( + "At the moment, the current mode is optimized for installing any package.json-based projects from GitHub. Support for other types of projects and git providers will be added in the future.", + ); + + const projectCategory = await selectPrompt({ + title: "Choose an installation category:", + options: [ + { + label: + "Install any template maintained or created by the Reliverse team", + value: "1", + }, + { + label: + "Install any external template by providing a custom GitHub link", + value: "2", + }, + { + label: "Install any other JS/TS repo project such as a React library", + value: "3", + }, + ], + }); + validate(projectCategory, "string", "Project category selection canceled."); + + let repoToInstall = ""; + + if (projectCategory === "1") { + const reliverseTemplate = await selectPrompt({ + title: "Free Reliverse Templates Collection", + options: [ + { + label: "blefnk/all-in-one-nextjs-template", + value: "blefnk/all-in-one-nextjs-template", + }, + { label: "blefnk/create-t3-app", value: "blefnk/create-t3-app" }, + { label: "blefnk/create-next-app", value: "blefnk/create-next-app" }, + { + label: "blefnk/astro-starlight-template", + value: "blefnk/astro-starlight-template", + }, + { + label: "reliverse/versator", + value: REPO_SHORT_URLS.versatorGithubLink, + }, + { + label: "reliverse/template-browser-extension", + value: "reliverse/template-browser-extension", + }, + ], + }); + validate(reliverseTemplate, "string", "Template selection canceled."); + repoToInstall = reliverseTemplate; + } else if (projectCategory === "2") { + const defaultLinks = [ + "reliverse/cli", + "shadcn-ui/taxonomy", + "onwidget/astrowind", + ]; + const randomDefaultLink = + defaultLinks[Math.floor(Math.random() * defaultLinks.length)]; + const customLink = await selectPrompt({ + title: "Enter the GitHub repository link:", + options: [ + { + label: randomDefaultLink, + value: randomDefaultLink, + }, + ], + }); + validate(customLink, "string", "Custom template providing canceled."); + repoToInstall = customLink; + } else if (projectCategory === "3") { + const defaultLinks = [ + "reliverse/acme", + "pmndrs/zustand", + "reliverse/core", + "biomejs/biome", + "reliverse/fs", + "blefnk/knip", + "47ng/nuqs", + ]; + const randomDefaultLink = + defaultLinks[Math.floor(Math.random() * defaultLinks.length)]; + const customLink = await inputPrompt({ + title: "Enter the GitHub repository link:", + defaultValue: randomDefaultLink, + placeholder: randomDefaultLink, + }); + validate(customLink, "string", "Custom template providing canceled."); + repoToInstall = + customLink.toString() ?? "https://github.com/reliverse/acme"; + } else { + relinka.error("Invalid option selected. Exiting."); + throw new Error("Unexpected template selection error."); + } + + if ( + repoToInstall === REPO_SHORT_URLS.relivatorGithubLink || + repoToInstall === "blefnk/relivator" + ) { + return justInstallRelivator(); + } + + await askProjectDetails( + repoToInstall, + `Setting up the repository: ${repoToInstall}...`, + "installAnyGitRepo", + false, + ); +} diff --git a/src/menu/modules/04-askProjectDetails.ts b/src/menu/modules/04-askProjectDetails.ts new file mode 100644 index 0000000..961fac8 --- /dev/null +++ b/src/menu/modules/04-askProjectDetails.ts @@ -0,0 +1,79 @@ +import { relinka } from "@reliverse/relinka"; +// import path from "pathe"; +// import { askAppName } from "~/menu/05-askAppName.js"; +// import { askUserName } from "~/menu/06-askUserName.js"; +// import { askAppDomain } from "~/menu/07-askAppDomain.js"; +// import { askGitInitialization } from "~/menu/08-askGitInitialization.js"; +// import { askInstallDependencies } from "~/menu/09-askInstallDependencies.js"; +// import { askSummaryConfirmation } from "~/menu/10-askSummaryConfirmation.js"; +// import { askInternationalizationSetup } from "~/menu/11-askInternationalizationSetup.js"; +// import { askCheckAndDownloadFiles } from "~/menu/13-askCheckAndDownloadFiles.js"; +// import { showCongratulationMenu } from "~/menu/15-showCongratulationMenu.js"; +// import { downloadI18nFiles } from "~/utils/downloadI18nFiles.js"; +// import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +// import { handleStringReplacements } from "~/utils/handleStringReplacements.js"; +// import { downloadGitRepo } from "~/utils/downloadGitRepo.js"; +// import { moveAppToLocale } from "~/utils/moveAppToLocale.js"; +// import { isDev } from "~/app.js"; +// import { verbose } from "~/utils/console.js"; + +export async function askProjectDetails( + template: string, + message: string, + mode: "buildOwnRelivator" | "installAnyGitRepo" | "justInstallRelivator", + allowI18nPrompt: boolean, +) { + relinka.info(message); + + /* const appName = await askAppName(); + const username = await askUserName(); + const domain = await askAppDomain(); + const git = await askGitInitialization(); + const deps = await askInstallDependencies(mode); + + const confirmed = await askSummaryConfirmation( + template, + appName, + username, + domain, + git, + deps, + ); + + verbose("info", "Installation confirmed by the user (3)."); + + if (!confirmed) { + relinka.info("Project creation process was canceled."); + return; + } + + verbose("info", "Installation confirmed by the user (4)."); */ + + // await downloadGitRepo(appName, template, deps, git); + + // const cwd = getCurrentWorkingDirectory(); + // const targetDir = isDev + // ? path.join(cwd, "..", appName) + // : path.join(cwd, appName); + + // await handleStringReplacements( + // targetDir, + // template, + // appName, + // username, + // domain, + // ); + + // if (allowI18nPrompt) { + // const i18nShouldBeEnabled = await askInternationalizationSetup(); + // if (i18nShouldBeEnabled) { + // await moveAppToLocale(targetDir); + // await downloadI18nFiles(targetDir, isDev); + // } + // } + + // await askCheckAndDownloadFiles(targetDir, appName); + // await showCongratulationMenu(targetDir, deps, template, targetDir); + + console.log("πŸŽ‰ Project created successfully!"); +} diff --git a/src/menu/modules/05-askAppName.ts b/src/menu/modules/05-askAppName.ts new file mode 100644 index 0000000..b82900c --- /dev/null +++ b/src/menu/modules/05-askAppName.ts @@ -0,0 +1,18 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { generate } from "random-words"; + +import { validate } from "~/utils/validate.js"; + +export async function askAppName(): Promise { + const placeholder = generate({ exactly: 3, join: "-" }); + + const name = await inputPrompt({ + title: "Enter the project name:", + defaultValue: placeholder, + placeholder, + }); + + validate(name, "string", "Project creation canceled."); + + return name.toString(); +} diff --git a/src/menu/modules/06-askUserName.ts b/src/menu/modules/06-askUserName.ts new file mode 100644 index 0000000..9599179 --- /dev/null +++ b/src/menu/modules/06-askUserName.ts @@ -0,0 +1,18 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { generate } from "random-words"; + +import { validate } from "~/utils/validate.js"; + +export async function askUserName(): Promise { + const placeholder = generate({ exactly: 1, join: "-" }); + const username = await inputPrompt({ + title: "Enter your GitHub username:", + defaultValue: placeholder, + placeholder, + }); + + validate(username, "string", "GitHub username prompt canceled."); + + return username; +} diff --git a/src/menu/modules/07-askAppDomain.ts b/src/menu/modules/07-askAppDomain.ts new file mode 100644 index 0000000..ebb7e27 --- /dev/null +++ b/src/menu/modules/07-askAppDomain.ts @@ -0,0 +1,17 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { validate } from "~/utils/validate.js"; + +export async function askAppDomain(): Promise { + const placeholder = "relivator.com"; + const website = await inputPrompt({ + title: "Enter your website:", + defaultValue: placeholder, + placeholder, + }); + + validate(website, "string", "Website prompt canceled."); + + return website; +} diff --git a/src/menu/modules/08-askGitInitialization.ts b/src/menu/modules/08-askGitInitialization.ts new file mode 100644 index 0000000..ca5f0bc --- /dev/null +++ b/src/menu/modules/08-askGitInitialization.ts @@ -0,0 +1,30 @@ +import { selectPrompt } from "@reliverse/prompts"; + +export type GitOption = + | "initializeNewGitRepository" + | "keepExistingGitFolder" + | "doNothing"; + +export async function askGitInitialization(): Promise { + const gitOption = await selectPrompt({ + title: + "Do you want to initialize a Git repository, keep the existing .git folder, or do nothing?", + options: [ + { + label: "Initialize new Git repository", + value: "initializeNewGitRepository", + }, + { + label: + "Keep existing .git folder (for forking later) [🚨 option is under development, may not work]", + value: "keepExistingGitFolder", + }, + { + label: "Do nothing", + value: "doNothing", + }, + ], + }); + + return gitOption; +} diff --git a/src/menu/modules/09-askInstallDependencies.ts b/src/menu/modules/09-askInstallDependencies.ts new file mode 100644 index 0000000..35f2dcc --- /dev/null +++ b/src/menu/modules/09-askInstallDependencies.ts @@ -0,0 +1,28 @@ +import { confirmPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { choosePackageManager } from "~/utils/choosePackageManager.js"; +import { validate } from "~/utils/validate.js"; + +export async function askInstallDependencies( + mode: "buildOwnRelivator" | "installAnyGitRepo" | "justInstallRelivator", +): Promise { + if (mode === "installAnyGitRepo") { + const cwd = process.cwd(); + const pkgManager = await choosePackageManager(cwd); + + relinka.info( + `In installAnyGitRepo mode, dependencies may not be installed automatically. If something, after project is created, you can run the following command to manually install deps: ${pkgManager} i`, + ); + } + + const deps = await confirmPrompt({ + title: + "Do you want to install the project dependencies? [🚨 Select No if issues occur]", + defaultValue: false, + }); + + validate(deps, "boolean", "Installation canceled by the user."); + + return deps; +} diff --git a/src/menu/modules/10-askSummaryConfirmation.ts b/src/menu/modules/10-askSummaryConfirmation.ts new file mode 100644 index 0000000..056ece2 --- /dev/null +++ b/src/menu/modules/10-askSummaryConfirmation.ts @@ -0,0 +1,49 @@ +import { relinka } from "@reliverse/relinka"; + +import { verbose } from "~/utils/console.js"; +import { validate } from "~/utils/validate.js"; + +import type { GitOption } from "./08-askGitInitialization.js"; +import { confirmPrompt } from "@reliverse/prompts"; + +export async function askSummaryConfirmation( + template: string, + projectName: string, + githubUser: string, + website: string, + gitOption: GitOption, + deps: boolean, +): Promise { + const depsMessage = deps + ? "Yes, install dependencies" + : "No, skip dependencies"; + + const message = `You have chosen the following options for your project: + - Template: ${template} + - Project Name: ${projectName} + - GitHub Username: ${githubUser} + - Website: ${website} + - Git Option: ${gitOption} + - Install Dependencies: ${depsMessage} + + Do you want to proceed?`; + + const confirmed = await confirmPrompt({ + title: message, + defaultValue: false, + }); + + verbose("info", "Installation confirmed by the user (1)."); // TODO: remove if random bun crash is fixed + + validate(confirmed, "boolean"); + + if (!confirmed) { + relinka.info("Installation canceled by the user."); + + return false; + } + + verbose("info", "Installation confirmed by the user (2)."); + + return true; +} diff --git a/src/menu/modules/11-askInternationalizationSetup.ts b/src/menu/modules/11-askInternationalizationSetup.ts new file mode 100644 index 0000000..a9b5d75 --- /dev/null +++ b/src/menu/modules/11-askInternationalizationSetup.ts @@ -0,0 +1,15 @@ +import { confirm } from "@reliverse/prompts"; + +import { validate } from "~/utils/validate.js"; + +export async function askInternationalizationSetup(): Promise { + const useI18n = await confirm({ + default: true, + message: + "Do you want to enable i18n (internationalization) for this project?", + }); + + validate(useI18n, "boolean", "i18n setup canceled."); + + return useI18n; +} diff --git a/src/menu/modules/12-askToResolveProjectConflicts.ts b/src/menu/modules/12-askToResolveProjectConflicts.ts new file mode 100644 index 0000000..5b6362a --- /dev/null +++ b/src/menu/modules/12-askToResolveProjectConflicts.ts @@ -0,0 +1,88 @@ +import { confirm, select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; + +import { DEBUG, FILE_CONFLICTS } from "~/app.js"; +import { removeFile, renameFile } from "~/utils/fileUtils.js"; + +export const resolveProjectConflicts = async (targetDir: string) => { + // Ask user if they want to decide what to do with each file conflict + const manualHandling = await confirm({ + default: false, // Default to 'No' + message: + "Do you want to manually handle file conflicts? \n If you choose 'N' then all the conflicting files will be automatically removed.", + }); + + await handleFileConflicts({ + files: FILE_CONFLICTS, + manualHandling, // Pass this flag to the handler + targetDir, + }); +}; + +type FileConflict = { + customMessage?: string; // Optional custom message for user prompt + description?: string; // Optional custom description for user-facing messages + fileName: string; // Name of the file (e.g., '.eslintrc.cjs') +}; + +type ConflictHandlerOptions = { + files: FileConflict[]; // List of files to check for conflicts + manualHandling: boolean; // Whether to ask the user or automatically remove files + targetDir: string; // Directory where the conflicts may happen +}; + +// Universal conflict handler function +const handleFileConflicts = async ({ + files, + manualHandling, + targetDir, +}: ConflictHandlerOptions): Promise => { + for (const { customMessage, description, fileName } of files) { + const filePath = path.join(targetDir, fileName); + + if (fs.pathExistsSync(filePath)) { + const fileDescription = description || fileName; + + DEBUG.enableVerboseLogging && + relinka.info(`${fileDescription} file exists at ${targetDir}.`); + + if (!manualHandling) { + // Automatically remove file without asking the user + await removeFile(filePath); + DEBUG.enableVerboseLogging && + relinka.success(`${fileDescription} removed automatically.`); + continue; // Skip to the next file + } + + const message = + customMessage || + `Do you want to remove or rename the ${fileDescription} file by adding .txt?`; + + const action = await selectPrompt({ + title: message, + options: [ + { label: `Remove ${fileDescription}`, value: "remove" }, + { label: `Rename to ${fileDescription}.txt`, value: "rename" }, + { label: "Do nothing", value: "nothing" }, + ], + }); + + if (action === "remove") { + await removeFile(filePath); + DEBUG.enableVerboseLogging && + relinka.success(`${fileDescription} removed.`); + } else if (action === "rename") { + const renamedFilePath = `${filePath}.txt`; + + await renameFile(filePath, renamedFilePath); + relinka.success( + `${fileDescription} renamed to ${fileDescription}.txt.`, + ); + } else { + relinka.info(`No changes made to ${fileDescription}.`); + } + } + } +}; diff --git a/src/menu/modules/13-askCheckAndDownloadFiles.ts b/src/menu/modules/13-askCheckAndDownloadFiles.ts new file mode 100644 index 0000000..0b4d5c5 --- /dev/null +++ b/src/menu/modules/13-askCheckAndDownloadFiles.ts @@ -0,0 +1,119 @@ +import { checkbox, confirm, multiselectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import path from "pathe"; + +import { DEBUG, FILE_PATHS, fileCategories } from "~/app.js"; +import { cloneAndCopyFiles } from "~/utils/cloneAndCopyFiles.js"; +import { verbose } from "~/utils/console.js"; +import { checkFileExists } from "~/utils/fileUtils.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; + +import { resolveProjectConflicts } from "./12-askToResolveProjectConflicts.js"; + +export const askCheckAndDownloadFiles = async ( + targetDir: string, + projectName: string, +): Promise => { + const missingFiles: string[] = []; + const existingFiles: string[] = []; + + verbose( + "info", + `Checking if all required files are present in ${projectName} located in ${targetDir}`, + ); + + // Check if any files in each category are missing or already exist + for (const category in fileCategories) { + const filesInCategory = fileCategories[category]; + + if (!filesInCategory) { + continue; + } + + for (const file of filesInCategory) { + const filePath = path.join(targetDir, file); + + if (!checkFileExists(filePath)) { + missingFiles.push(file); + } else { + existingFiles.push(file); + } + } + } + + // Handle project files conflicts + await resolveProjectConflicts(targetDir); + + // If there are missing files, prompt the user to download them + if (missingFiles.length > 0) { + DEBUG.enableVerboseLogging && + relinka.info( + `The following files are missing in ${targetDir}: ${missingFiles.join(", ")}`, + ); + + const categoriesToDownload = await multiselectPrompt({ + title: "Select the file categories you want to download:", + options: Object.keys(fileCategories).map((category) => ({ + label: category, + value: category, + hint: category, + })), + }); + + const filesToDownload = categoriesToDownload + .flatMap((category) => fileCategories[category] || []) + .filter(Boolean); + + const cwd = getCurrentWorkingDirectory(); + const tempCloneRepo = "https://github.com/blefnk/relivator"; + const tempRepoDir = path.resolve(cwd, `../${FILE_PATHS.tempRepoClone}`); + + // Handle conflicts for already existing files + if (existingFiles.length > 0) { + const replaceAll = await confirm({ + default: true, + message: + "Some files already exist. Do you want to replace all existing files? (N opens Conflict Management menu)", + }); + + if (!replaceAll) { + const filesToReplace = await multiselectPrompt({ + title: "Select the files you want to replace:", + options: existingFiles.map((file) => ({ + label: file, + value: file, + hint: file, + })), + }); + + await cloneAndCopyFiles( + [...filesToDownload, ...filesToReplace].filter(Boolean), + targetDir, + false, + tempCloneRepo, + tempRepoDir, + ); + } else { + await cloneAndCopyFiles( + filesToDownload.filter(Boolean), + targetDir, + true, + tempCloneRepo, + tempRepoDir, + ); // Replace all existing files + } + } else { + await cloneAndCopyFiles( + filesToDownload.filter(Boolean), + targetDir, + false, + tempCloneRepo, + tempRepoDir, + ); // No conflicts, just download the files + } + } else { + relinka.success( + `All required files are present in ${targetDir}. Codemod is finished.`, + ); + } +}; diff --git a/src/menu/modules/14-askCodemodUserCodebase.ts b/src/menu/modules/14-askCodemodUserCodebase.ts new file mode 100644 index 0000000..0213641 --- /dev/null +++ b/src/menu/modules/14-askCodemodUserCodebase.ts @@ -0,0 +1,45 @@ +import { selectPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { replaceImportSymbol } from "~/mods/replaceImportSymbol.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +import { validate } from "~/utils/validate.js"; + +export async function askCodemodUserCodebase() { + relinka.info("The code modification process will start now."); + + // Prompt for project path or use the current working directory + const projectPath = await inputPrompt({ + title: + "Enter the path to your project or press Enter to use the current directory", + defaultValue: getCurrentWorkingDirectory(), + }); + + // Validate project path + validate(projectPath, "string", "Invalid project path provided. Exiting."); + + const action = await selectPrompt({ + title: "Select the action to perform", + options: [ + { + label: "Replace import symbol with another", + value: "replaceImportSymbol", + }, + ], + }); + + validate(action, "string", "Invalid option selected. Exiting."); + + if (action === "replaceImportSymbol") { + const from = await inputPrompt({ + title: "Enter the import symbol to replace", + defaultValue: "@", + }); + const to = await inputPrompt({ + title: "Enter the import symbol to replace with", + defaultValue: "~", + }); + + await replaceImportSymbol(projectPath, from, to); + } +} diff --git a/src/menu/modules/15-showCongratulationMenu.ts b/src/menu/modules/15-showCongratulationMenu.ts new file mode 100644 index 0000000..8cdcc3e --- /dev/null +++ b/src/menu/modules/15-showCongratulationMenu.ts @@ -0,0 +1,122 @@ +import { checkbox, multiselectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "pathe"; + +import { DEBUG, FILE_PATHS, isDev } from "~/app.js"; +import { choosePackageManager } from "~/utils/choosePackageManager.js"; +import { isVSCodeInstalled } from "~/utils/isAppInstalled.js"; + +export async function showCongratulationMenu( + targetDir: string, + deps: boolean, + source: string, + dir: string, +) { + const cwd = process.cwd(); + const pkgManager = await choosePackageManager(cwd); + + console.info(""); + relinka.success("🀘 Project created successfully!"); + relinka.info("✨ Next steps to get started:"); + relinka.info(`- Open the project: cd ${targetDir}`); + + if (!deps) { + relinka.info(`- Install dependencies manually: ${pkgManager} i`); + } + + relinka.info(`- Apply linting and formatting: ${pkgManager} appts`); + relinka.info(`- Run the project: ${pkgManager} dev`); + relinka.info(""); + relinka.success(`πŸŽ‰ ${source} was successfully installed to ${dir}.`); + relinka.info(`- If you have VSCode installed, run: code ${targetDir}`); + + console.info(""); + + const vscodeInstalled = isVSCodeInstalled(); + + const nextActions = await multiselectPrompt({ + title: "What would you like to do next?", + options: [ + { + label: "Close Reliverse CLI", + value: "close", + hint: "Close Reliverse CLI", + }, + ...(DEBUG.alphaFeaturesEnabled + ? [ + { + label: "Open Documentation", + value: "docs", + hint: "Open Reliverse Documentation", + }, + ] + : []), + ...(DEBUG.alphaFeaturesEnabled + ? [ + { + label: "Join Discord", + value: "discord", + hint: "Join Reliverse Discord Server", + }, + ] + : []), + ...(vscodeInstalled && DEBUG.alphaFeaturesEnabled + ? [ + { + label: "Open in VSCode", + value: "vscode", + hint: "Open Project in VSCode", + }, + ] + : []), + { + label: "Clean Up", + value: "removeTemp", + hint: "Remove temp-repo-clone folder", + }, + ], + }); + + for (const action of nextActions) { + if (action === "docs") { + relinka.info("Opening Reliverse Documentation..."); + try { + await execa("firefox", ["https://reliverse.org/docs"]); + } catch (error) { + relinka.error("Error opening documentation:", error); + } + } else if (action === "discord") { + relinka.info("Joining Reliverse Discord server..."); + try { + await execa("firefox", ["https://discord.gg/Pb8uKbwpsJ"]); + } catch (error) { + relinka.error("Error opening Discord:", error); + } + } else if (action === "vscode") { + relinka.info("Opening the project in VSCode..."); + try { + await execa("code", [targetDir]); + } catch (error) { + relinka.error("Error opening VSCode:", error); + } + } else if (action === "removeTemp") { + const tempRepoDir = isDev + ? path.join(cwd, "..", FILE_PATHS.tempRepoClone) + : path.join(cwd, FILE_PATHS.tempRepoClone); + + if (await fs.pathExists(tempRepoDir)) { + await fs.remove(tempRepoDir); + relinka.success("Temporary clone folder removed."); + } else { + relinka.warn("Temporary clone folder not found."); + } + } + } + + relinka.success( + "πŸ‘‹ Closing the CLI... Thanks for using Reliverse! See you next time!\n", + ); + process.exit(0); +} diff --git a/src/menu/modules/16-constructEnvVariablesFile.ts b/src/menu/modules/16-constructEnvVariablesFile.ts new file mode 100644 index 0000000..d33169d --- /dev/null +++ b/src/menu/modules/16-constructEnvVariablesFile.ts @@ -0,0 +1,115 @@ +import { selectPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "pathe"; + +const PROJECT_ROOT = path.resolve(); +const EXAMPLE_ENV_PATH = path.join(PROJECT_ROOT, ".env.example"); +const ENV_PATH = path.join(PROJECT_ROOT, ".env"); + +type CharsetOptions = { + length?: number; + charset?: string; +}; + +function generateSecureString({ + length = 44, + charset = "alphanumeric", +}: CharsetOptions = {}): string { + const chars = + { + alphanumeric: + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + numeric: "0123456789", + alphabetic: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + }[charset] || charset; + + const bytes = new Uint8Array(length); + crypto.getRandomValues(bytes); + + return Array.from(bytes) + .map((byte) => chars.charAt(byte % chars.length)) + .join(""); +} + +async function constructEnvFile() { + if (!(await fs.pathExists(EXAMPLE_ENV_PATH))) { + relinka.warn( + `No .env.example file found in project root at ${EXAMPLE_ENV_PATH}`, + ); + return; + } + + if (!(await fs.pathExists(ENV_PATH))) { + await fs.copy(EXAMPLE_ENV_PATH, ENV_PATH); + relinka.success(".env file created based on .env.example."); + } else { + const exampleContent = (await fs.readFile(EXAMPLE_ENV_PATH, "utf8")).split( + "\n", + ); + const envContent = (await fs.readFile(ENV_PATH, "utf8")).split("\n"); + + const missingKeys: Record = {}; + exampleContent.forEach((line) => { + const [key, defaultValue] = line.split("="); + if (key && !envContent.some((envLine) => envLine.startsWith(`${key}=`))) { + missingKeys[key] = defaultValue ?? ""; + } + }); + + if (Object.keys(missingKeys).length > 0) { + const response = await selectPrompt({ + title: "Some keys are missing in .env. Edit manually or auto-fill?", + options: [ + { + label: "Edit manually", + value: "manual", + }, + { + label: "Auto-fill", + value: "auto", + }, + ], + }); + + if (response === "manual") { + relinka.info("Opening .env file for manual editing..."); + await execa("code", [ENV_PATH]); + } else { + for (const [key, defaultValue] of Object.entries(missingKeys)) { + const value = await inputPrompt({ + title: `Provide value for ${key}`, + defaultValue: defaultValue, + }); + envContent.push(`${key}=${value}`); + } + await fs.writeFile(ENV_PATH, envContent.join("\n")); + relinka.success(".env file updated with missing keys."); + } + } + } + + // Generate AUTH_SECRET if necessary + const envVars = (await fs.readFile(ENV_PATH, "utf8")).split("\n"); + const authSecretIndex = envVars.findIndex((line) => + line.startsWith("AUTH_SECRET="), + ); + + if (authSecretIndex !== -1) { + const line = envVars[authSecretIndex]; + if (line) { + const [key, value] = line.split("=") as [string, string | undefined]; + if (!value || value === "EnsureUseSomethingRandomHere44CharactersLong") { + const secureAuthSecret = generateSecureString(); + envVars[authSecretIndex] = `${key}=${secureAuthSecret}`; + await fs.writeFile(ENV_PATH, envVars.join("\n")); + relinka.success("Generated and updated AUTH_SECRET in .env."); + } + } + } +} + +constructEnvFile().catch((error) => { + relinka.error("Failed to construct or update .env file:", error); +}); diff --git a/src/menu/modules/17-showUpdateCloneMenu.ts b/src/menu/modules/17-showUpdateCloneMenu.ts new file mode 100644 index 0000000..f4d0314 --- /dev/null +++ b/src/menu/modules/17-showUpdateCloneMenu.ts @@ -0,0 +1,96 @@ +import { selectPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; + +import { isDev, REPO_SHORT_URLS } from "~/app.js"; +import { replaceImportSymbol } from "~/mods/replaceImportSymbol.js"; +import { downloadGitRepo } from "~/utils/downloadGitRepo.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +import { validate } from "~/utils/validate.js"; + +const cwd = getCurrentWorkingDirectory(); + +export async function showUpdateCloneMenu() { + relinka.info( + "πŸ”₯ The current mode is in active development and may not be stable. ✨ Select the supported repository you have cloned from GitHub to update it with the latest changes.", + ); + + const options = [ + REPO_SHORT_URLS.relivatorGithubLink, + ...(isDev ? ["🚧 relivator-nextjs-template (local dev only)"] : []), + ]; + + const option = await selectPrompt({ + title: "Select the repository to update", + options: options.map((option) => ({ + label: option, + value: option, + })), + }); + + validate(option, "string", "Invalid option selected. Exiting."); + + // For test development purposes only + if (option === "🚧 relivator-nextjs-template (local dev only)") { + relinka.warn( + "Make sure to run this script from the root folder of your reliverse/cli clone.", + ); + const projectPath = await downloadGitRepo( + "relivator-dev-test", + "relivator-nextjs-template", + false, + "doNothing", + ); + if (projectPath) { + await loadAndRunConfig( + path.join(projectPath, "src/prompts/tests/update-config.json"), + ); + } + } else { + await downloadAndRunConfig(option); + } + + relinka.success("The repository has been updated successfully."); +} + +async function downloadAndRunConfig(repoShortUrl: string) { + const configUrl = `https://raw.githubusercontent.com/${repoShortUrl}/main/scripts/update-config.json`; + const configPath = path.join(cwd, "update-config.json"); + + await downloadFileFromUrl(configUrl, configPath); + await loadAndRunConfig(configPath); +} + +async function downloadFileFromUrl(url: string, destinationPath: string) { + const response = await fetch(url); + const fileBuffer = await response.arrayBuffer(); + await fs.writeFile(destinationPath, Buffer.from(fileBuffer)); + relinka.info(`Downloaded the update configuration to ${destinationPath}`); +} + +async function loadAndRunConfig(configPath: string) { + if (!(await fs.pathExists(configPath))) { + relinka.error("The configuration file is missing."); + return; + } + + const config = await fs.readJson(configPath); + await executeActions(config.actions); +} + +async function executeActions(actions: any[]) { + for (const action of actions) { + switch (action.type) { + case "replaceImportSymbol": + await replaceImportSymbol( + action.params.repo, + action.params.from, + action.params.to, + ); + break; + default: + relinka.warn(`Unknown action type: ${action.type}`); + } + } +} diff --git a/src/menu/modules/18-showRelivatorFeatEditor.ts b/src/menu/modules/18-showRelivatorFeatEditor.ts new file mode 100644 index 0000000..ed04eb9 --- /dev/null +++ b/src/menu/modules/18-showRelivatorFeatEditor.ts @@ -0,0 +1,37 @@ +import { selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +export async function showRelivatorFeatEditor() { + relinka.info("Relivator feature editor"); + relinka.info("--------------------------------"); + relinka.info("This feature is not yet implemented."); + relinka.info("Note: This is an advanced feature. Use with caution."); + const option = await selectPrompt({ + title: "What would you like to do?", + options: [ + { label: "1. Add a new feature", value: "addNewFeature" }, + { label: "2. Remove a feature", value: "removeFeature" }, + { label: "3. Replace a feature", value: "replaceFeature" }, + ], + }); + + if (option === "addNewFeature") { + await addNewRelivatorFeature(); + } else if (option === "removeFeature") { + await removeRelivatorFeature(); + } else if (option === "replaceFeature") { + await replaceRelivatorFeature(); + } +} + +async function addNewRelivatorFeature() { + relinka.info("Add a new feature"); +} + +async function removeRelivatorFeature() { + relinka.info("Remove a feature"); +} + +async function replaceRelivatorFeature() { + relinka.info("Replace a feature"); +} diff --git a/src/menu/prompts.ts b/src/menu/prompts.ts index a73d09d..a43a2ad 100644 --- a/src/menu/prompts.ts +++ b/src/menu/prompts.ts @@ -1,7 +1,7 @@ -import { msg } from "@reliverse/relinka"; -import { anykeyPrompt } from "@reliverse/relinka"; -import { multiselectPrompt } from "@reliverse/relinka"; -import { progressbar } from "@reliverse/relinka"; +import { msg } from "@reliverse/prompts"; +import { anykeyPrompt } from "@reliverse/prompts"; +import { multiselectPrompt } from "@reliverse/prompts"; +import { progressbar } from "@reliverse/prompts"; import { animateText, confirmPrompt, @@ -12,23 +12,23 @@ import { numberPrompt, passwordPrompt, startPrompt, - textPrompt, + inputPrompt, togglePrompt, -} from "@reliverse/relinka"; -import { promptsDisplayResults } from "@reliverse/relinka"; -import { numSelectPrompt } from "@reliverse/relinka"; -import { selectPrompt } from "@reliverse/relinka"; -import { spinner } from "@reliverse/relinka"; +} from "@reliverse/prompts"; +import { promptsDisplayResults } from "@reliverse/prompts"; +import { numSelectPrompt } from "@reliverse/prompts"; +import { selectPrompt } from "@reliverse/prompts"; +import { spinner } from "@reliverse/prompts"; import { detect } from "detect-package-manager"; import { emojify } from "node-emoji"; import { bold } from "picocolors"; -import pkg from "~/../package.json" assert { type: "json" }; import { basicConfig, experimentalConfig, extendedConfig, } from "~/menu/configs.js"; +import { pm, pmv, pkg } from "~/utils/pkg.js"; import { IDs, schema, type UserInput } from "./schema.js"; import { @@ -38,11 +38,9 @@ import { hashPassword, validateAge, } from "./utils.js"; -import { pm, pmv } from "./utils/pkg.js"; export async function showStartPrompt() { await startPrompt({ - id: IDs.start, title: `reliverse v${pkg.version} | ${pm} v${pmv}`, ...basicConfig, titleColor: "inverse", @@ -64,12 +62,15 @@ export async function showAnykeyPrompt( await anykeyPrompt(notification); } -export async function showTextPrompt(): Promise { - const username = await textPrompt({ - id: IDs.username, +export async function showInputPrompt(): Promise { + const username = await inputPrompt({ + // 'id' is the key in the userInput result object. + // Choose any name for it, but ensure it’s unique. + // Intellisense will show you all available IDs. title: "We're glad you're testing our interactive prompts library!", content: "Let's get to know each other!\nWhat's your username?", - hint: "Press to use the default value. [Default: johnny911]", + hint: "Press to use the default value.", + placeholder: "[Default: johnny911]", defaultValue: "johnny911", schema: schema.properties.username, ...extendedConfig, @@ -78,8 +79,7 @@ export async function showTextPrompt(): Promise { } export async function askDir(username: string): Promise { - const dir = await textPrompt({ - id: IDs.dir, + const dir = await inputPrompt({ title: `Great! Nice to meet you, ${username}!`, content: "Where should we create your project?", @@ -94,7 +94,6 @@ export async function askDir(username: string): Promise { export async function showNumberPrompt(): Promise { const age = await numberPrompt({ - id: IDs.age, ...extendedConfig, title: "Enter your age", @@ -119,7 +118,6 @@ export async function showPasswordPrompt(): Promise { try { password = await passwordPrompt({ - id: IDs.password, title: "Imagine a password", schema: schema.properties.password, defaultValue: "silverHand2077", @@ -143,7 +141,6 @@ export async function showPasswordPrompt(): Promise { export async function showDatePrompt(): Promise { const birthdayDate = await datePrompt({ - id: IDs.birthday, dateKind: "birthday", dateFormat: "DD.MM.YYYY", title: "Enter your birthday", @@ -159,15 +156,14 @@ export async function showSelectPrompt(): Promise { const lang = await selectPrompt({ title: "Choose your language", options: [ - { label: "English", value: "en" }, - { label: "Ukrainian", value: "uk" }, - { label: "Polish", value: "pl" }, - { label: "French", value: "fr" }, - { label: "German", value: "de" }, - { label: "Other", value: "else" }, + { label: "English", value: "en", hint: "English" }, + { label: "Ukrainian", value: "uk", hint: "Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°" }, + { label: "Polish", value: "pl", hint: "Polski" }, + { label: "French", value: "fr", hint: "FranΓ§ais" }, + { label: "German", value: "de", hint: "Deutsch" }, + { label: "Other", value: "else", hint: "Other" }, ], - hints: ["English", "Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°", "Polski", "FranΓ§ais", "Deutsch", "Other"], - initial: "en", + defaultValue: "en", ...experimentalConfig, }); @@ -220,29 +216,37 @@ export async function showMultiselectPrompt(): Promise { const selectedOptions = await multiselectPrompt({ title: "Select your favorite programming languages", options: [ - "TypeScript", - "JavaScript", - "CoffeeScript", - "Python", - "Java", - "CSharp", - "Go", - "Rust", - "Swift", - ], - hints: [ - emojify(":blue_heart: Type-safe and scalable"), - emojify(":yellow_heart: Versatile and widely-used"), - emojify(":coffee: Elegant and concise"), - emojify(":snake: Powerful and easy to learn"), - emojify(":coffee: Robust and portable"), - emojify(":hash: Modern and object-oriented"), - emojify(":dolphin: Simple and efficient"), - emojify(":crab: Fast and memory-safe"), - emojify(":apple: Safe and performant"), + { + value: "ts", + label: "TypeScript", + hint: "πŸ’™ Type-safe and scalable", + }, + { + value: "js", + label: "JavaScript", + hint: "πŸ’› Versatile and widely-used", + }, + { + value: "cs", + label: "CoffeeScript", + hint: "β˜• Elegant and concise", + }, + { + value: "py", + label: "Python", + hint: "🐍 Powerful and easy to learn", + }, + { value: "java", label: "Java", hint: "β˜• Robust and portable" }, + { + value: "csharp", + label: "CSharp", + hint: "🟣 Modern and object-oriented", + }, + { value: "go", label: "Go", hint: "🐟 Simple and efficient" }, + { value: "rust", label: "Rust", hint: "πŸ¦€ Fast and memory-safe" }, + { value: "swift", label: "Swift", hint: "🍎 Safe and performant" }, ], required: true, - initial: ["TypeScript", "JavaScript"], ...experimentalConfig, }); @@ -280,7 +284,6 @@ export async function showNumSelectPrompt(): Promise { const choices = createColorChoices(); const color = await numSelectPrompt({ - id: IDs.color, title: "Choose your favorite color", content: "You are free to customize everything in your prompts using the following color palette.", @@ -298,7 +301,6 @@ export async function showNumMultiselectPrompt(): Promise< UserInput["features"] > { const features = await numMultiSelectPrompt({ - id: IDs.features, title: "What web technologies do you like?", defaultValue: ["react", "typescript"], choices: [ @@ -348,16 +350,11 @@ export async function showConfirmPrompt( await showAnykeyPrompt("pm", username); const spinner = await confirmPrompt({ - id: IDs.spinner, title: "Do you want to see spinner in action?", - titleColor: "red", titleVariant: "doubleBox", - schema: schema.properties.spinner, - content: "Spinners are helpful for long-running tasks.", ...extendedConfig, - defaultValue: true, }); @@ -415,7 +412,6 @@ export async function doSomeFunStuff(userInput: UserInput) { export async function showNextStepsPrompt() { await nextStepsPrompt({ - id: "nextSteps", title: "Next Steps", content: "- Set up your profile\n- Review your dashboard\n- Add tasks", ...extendedConfig, @@ -437,7 +433,6 @@ export async function showAnimatedText() { export async function showEndPrompt() { await endPrompt({ - id: "end", title: emojify( "β„Ή :books: Learn the docs here: https://docs.reliverse.org/relinka", ), diff --git a/src/menu/schema.ts b/src/menu/schema.ts index 03c30dd..f065d54 100644 --- a/src/menu/schema.ts +++ b/src/menu/schema.ts @@ -1,4 +1,4 @@ -import { colorMap } from "@reliverse/relinka"; +import { colorMap } from "@reliverse/prompts"; import { Type, type Static } from "@sinclair/typebox"; export const IDs = { diff --git a/src/menu/utils.ts b/src/menu/utils.ts index 44efb7e..72a62c7 100644 --- a/src/menu/utils.ts +++ b/src/menu/utils.ts @@ -1,7 +1,7 @@ -import type { ChoiceOptions, ColorName } from "@reliverse/relinka"; +import type { ChoiceOptions, ColorName } from "@reliverse/prompts"; -import { msg } from "@reliverse/relinka"; -import { colorMap } from "@reliverse/relinka"; +import { msg } from "@reliverse/prompts"; +import { colorMap } from "@reliverse/prompts"; import type { UserInput } from "./schema.js"; diff --git a/src/mods/replaceImportSymbol.ts b/src/mods/replaceImportSymbol.ts new file mode 100644 index 0000000..dc0194a --- /dev/null +++ b/src/mods/replaceImportSymbol.ts @@ -0,0 +1,29 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { glob } from "glob"; +import path from "pathe"; + +export async function replaceImportSymbol( + repo: string, + from: string, + to: string, +) { + relinka.info(`Replacing import symbols from "${from}" to "${to}" in ${repo}`); + + // Find files in the specified repo folder + const files = await glob("**/*.{js,ts,tsx}", { + cwd: path.resolve(repo), + }); + + // Update the imports in each file + for (const file of files) { + const filePath = path.join(repo, file); + const content = await fs.readFile(filePath, "utf-8"); + const updatedContent = content.replace( + new RegExp(`from '${from}`, "g"), + `from '${to}`, + ); + await fs.writeFile(filePath, updatedContent, "utf-8"); + relinka.info(`Updated imports in ${filePath}`); + } +} diff --git a/src/mods/replaceWithModern.ts b/src/mods/replaceWithModern.ts new file mode 100644 index 0000000..64ab3f5 --- /dev/null +++ b/src/mods/replaceWithModern.ts @@ -0,0 +1,50 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { glob } from "glob"; +import path from "pathe"; + +type ReplaceWithModernOptions = { + projectPath: string; +}; + +export async function replaceWithModern({ + projectPath, +}: ReplaceWithModernOptions) { + relinka.info("Starting replacement of 'fs' and 'path' imports..."); + + const files = await glob("**/*.{js,ts,tsx}", { + cwd: projectPath, + absolute: true, + ignore: ["node_modules/**", "dist/**"], + }); + + for (const file of files) { + const content = await fs.readFile(file, "utf8"); + + const updatedContent = content + .replace(/import fs from ["']fs["'];/g, 'import fs from "fs-extra";') + .replace(/import path from ["']path["'];/g, 'import path from "pathe";'); + + if (updatedContent !== content) { + await fs.writeFile(file, updatedContent, "utf8"); + relinka.success(`Updated imports in ${file}`); + } + } + + relinka.info("Replacement process completed."); +} + +async function runCodemod() { + const projectPath = await inputPrompt({ + title: + "Enter the path to your project or press Enter to use the current directory", + defaultValue: path.resolve(), + }); + + await replaceWithModern({ projectPath }); +} + +runCodemod().catch((err) => { + relinka.error("Codemod failed:", err); +}); diff --git a/src/temp/14-askCodemodUserCodebase.ts b/src/temp/14-askCodemodUserCodebase.ts new file mode 100644 index 0000000..c404316 --- /dev/null +++ b/src/temp/14-askCodemodUserCodebase.ts @@ -0,0 +1,46 @@ +import { selectPrompt } from "@reliverse/prompts"; +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { replaceImportSymbol } from "~/mods/replaceImportSymbol.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +import { validate } from "~/utils/validate.js"; + +export async function askCodemodUserCodebase() { + relinka.info("The code modification process will start now."); + + // Prompt for project path or use the current working directory + const projectPath = await inputPrompt({ + title: + "Enter the path to your project or press Enter to use the current directory", + defaultValue: getCurrentWorkingDirectory(), + }); + + // Validate project path + validate(projectPath, "string", "Invalid project path provided. Exiting."); + + const action = await selectPrompt({ + title: "Select the action to perform", + options: [ + { + label: "Replace import symbol with another", + value: "replaceImportSymbol", + }, + ], + }); + + validate(action, "string", "Invalid option selected. Exiting."); + + if (action === "replaceImportSymbol") { + const from = await inputPrompt({ + title: "Enter the import symbol to replace", + defaultValue: "@", + }); + const to = await inputPrompt({ + title: "Enter the import symbol to replace with", + defaultValue: "~", + }); + + await replaceImportSymbol(projectPath, from, to); + } +} diff --git a/src/temp/16-showUpdateCloneMenu.ts b/src/temp/16-showUpdateCloneMenu.ts new file mode 100644 index 0000000..99dbdd3 --- /dev/null +++ b/src/temp/16-showUpdateCloneMenu.ts @@ -0,0 +1,71 @@ +import { selectPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "pathe"; + +import { isDev, REPO_SHORT_URLS } from "~/app.js"; +import { verbose } from "~/utils/console.js"; +import { downloadGitRepo } from "~/utils/downloadGitRepo.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +import { validate } from "~/utils/validate.js"; + +const cwd = getCurrentWorkingDirectory(); + +export async function showUpdateCloneMenu() { + relinka.info( + "πŸ”₯ The current mode is in active development and may not be stable. ✨ Select the supported repository you have cloned from GitHub to update it with the latest changes.", + ); + + const options = [ + REPO_SHORT_URLS.relivatorGithubLink, + ...(isDev ? ["🚧 relivator-nextjs-template (local dev only)"] : []), + ]; + + const option = await selectPrompt({ + title: "Select the repository to update", + options: options.map((repo) => ({ label: repo, value: repo })), + }); + + validate(option, "string", "Invalid option selected. Exiting."); + + // for test development purposes only + if (option === "🚧 relivator-nextjs-template (local dev only)") { + relinka.warn( + "Make sure to run this script from the root folder of your reliverse/cli clone.", + ); + const projectPath = await downloadGitRepo( + "relivator-dev-test", + "relivator-nextjs-template", + false, + "doNothing", + ); + if (projectPath) { + await runScript(path.join(projectPath, "src/prompts/tests/updater.ts")); + } + } else { + await downloadRunUpdaterTSScript(option); + } + + relinka.success("The repository has been updated successfully."); +} + +async function downloadRunUpdaterTSScript(repoShortUrl: string) { + const updaterScriptUrl = `https://raw.githubusercontent.com/${repoShortUrl}/main/scripts/update.ts`; + const updaterScriptPath = path.join(cwd, "updater.ts"); + + await downloadFileFromUrl(updaterScriptUrl, updaterScriptPath); + await runScript(updaterScriptPath); +} + +async function downloadFileFromUrl(url: string, path: string) { + const response = await fetch(url); + const fileBuffer = await response.arrayBuffer(); + await fs.writeFile(path, Buffer.from(fileBuffer)); + relinka.info(`Downloaded the updater script to ${path}`); +} + +async function runScript(path: string) { + verbose("info", `Running the updater script at ${path}`); + await execa(`tsx ${path}`); +} diff --git a/src/temp/menu/askPackageManager.ts b/src/temp/menu/askPackageManager.ts new file mode 100644 index 0000000..9dd1ab7 --- /dev/null +++ b/src/temp/menu/askPackageManager.ts @@ -0,0 +1,64 @@ +import { prompt } from "@reliverse/prompts"; + +import { + PACKAGE_MANAGERS, + getBunVersion, + getPackageManagerVersion, +} from "./utils/packageManager.js"; + +export async function promptForPackageManager(): Promise { + try { + const choices = await Promise.all( + Object.keys(PACKAGE_MANAGERS).map(async (pm) => { + try { + const version = + pm === "bun" + ? await getBunVersion() + : await getPackageManagerVersion(pm); + + const versionDisplay = + version === "not installed" + ? "(not installed)" + : version + ? `v${version.replace(/^v/, "")}` + : ""; + + return { + title: `${pm} ${versionDisplay}`, + value: pm, + disabled: version === "not installed", + id: pm, + }; + } catch (err) { + console.warn(`Failed to get version for ${pm}:`, err); + return { + title: `${pm} (version unknown)`, + value: pm, + id: pm, + }; + } + }), + ); + + const { npmClient } = await prompt({ + id: "npmClient", + type: "select", + title: "Select package manager to use:", + choices: choices.filter((choice) => choice !== null), + defaultValue: + choices.findIndex((c) => !c.disabled) !== -1 + ? choices.findIndex((c) => !c.disabled) + : 0, + }); + + if (typeof npmClient !== "string") { + throw new Error("Package manager selection was cancelled"); + } + + return npmClient; + } catch (error) { + console.error("Failed to prompt for package manager:", error); + // Default to npm as fallback + return "npm"; + } +} diff --git a/src/temp/menu/utils/dependenciesInstall.ts b/src/temp/menu/utils/dependenciesInstall.ts new file mode 100644 index 0000000..c5846ff --- /dev/null +++ b/src/temp/menu/utils/dependenciesInstall.ts @@ -0,0 +1,40 @@ +import { prompt } from "@reliverse/prompts"; +import { execaCommand } from "execa"; + +export async function installWithPackageManager( + npmClient: string, + devMode: boolean, + cwd: string, +) { + if (devMode) { + const { confirmInstall } = await prompt({ + type: "confirm", + id: "confirmInstall", + title: + "You are in dev mode. Are you sure you want to install dependencies?", + defaultValue: false, + }); + + if (!confirmInstall) { + console.log("Installation cancelled."); + return; + } + } + + try { + console.log(`Installing dependencies with ${npmClient}...`); + await execaCommand(`${npmClient} install`, { cwd, stdio: "inherit" }); + } catch (error) { + if (error instanceof Error) { + console.error( + `Error using ${npmClient} for installation:`, + error.message, + ); + } else { + console.error( + `An unknown error occurred using ${npmClient} for installation:`, + error, + ); + } + } +} diff --git a/src/temp/menu/utils/packageManager.ts b/src/temp/menu/utils/packageManager.ts new file mode 100644 index 0000000..32f4e53 --- /dev/null +++ b/src/temp/menu/utils/packageManager.ts @@ -0,0 +1,92 @@ +import { execa, execaCommand } from "execa"; +import { spawnSync } from "node:child_process"; +import { detectPackageManager } from "nypm"; + +import { promptForPackageManager } from "../askPackageManager.js"; + +const isBun = typeof globalThis.Bun !== "undefined"; + +// Supported package managers +export const PACKAGE_MANAGERS = { + bun: "bun", + pnpm: "pnpm", + yarn: "yarn", + npm: "npm", +} as const; + +export type PackageManagerKey = keyof typeof PACKAGE_MANAGERS; + +export async function getBunVersion(): Promise { + try { + if (isBun) { + const result = Bun.spawnSync({ cmd: ["bun", "--version"] }); + const output = new TextDecoder().decode(result.stdout); + const versionMatch = /(\d+\.\d+\.\d+)/.exec(output); + return versionMatch?.[1] ?? "unknown"; + } + const output = spawnSync("bun", ["--version"], { encoding: "utf-8" }); + const versionMatch = /(\d+\.\d+\.\d+)/.exec(output.stdout); + return versionMatch?.[1] ?? "unknown"; + } catch { + return "not installed"; + } +} + +export async function getPnpmVersion(): Promise { + const result = await execa("pnpm", ["--version"]); + return result.stdout; +} + +// A placeholder function for installing dependencies +export async function installDeps({ + npmClient, + cwd, +}: { npmClient: string; cwd: string }) { + await execaCommand(`${npmClient} install`, { cwd, stdio: "inherit" }); +} + +export async function installWithNpmClient({ + npmClient, + cwd, +}: { npmClient: string; cwd: string }) { + if (npmClient === "pnpm" && /^8\.[0-6]\./.test(await getPnpmVersion())) { + // to avoid pnpm 8.0 ~ 8.6 installing minimal versions of dependencies + await execaCommand("pnpm up -L", { cwd, stdio: "inherit" }); + } else { + await installDeps({ npmClient, cwd }); + } +} + +export async function getPackageManagerVersion(pm: string): Promise { + try { + const result = await execa(pm, ["--version"]); + const versionMatch = /(\d+\.\d+\.\d+)/.exec(result.stdout); + return versionMatch?.[1] ?? "unknown"; + } catch { + return "not installed"; + } +} + +export async function getPackageManager(args: string[], cwd: string) { + const preferredPMFlag = args.find((arg) => arg.startsWith("--use-")); + const preferredPM = preferredPMFlag + ? (preferredPMFlag.replace("--use-", "") as PackageManagerKey) + : await promptForPackageManager(); + + const pmInfo = await detectPackageManager(cwd); + const pmName = preferredPM || pmInfo?.name || "unknown"; + let pmVersion = pmInfo?.version?.slice(0, 6) || ""; + + if (pmName === "bun" && isBun) { + pmVersion = pmVersion || (await getBunVersion()); + } else if (pmName === "pnpm") { + pmVersion = pmVersion || (await getPnpmVersion()); + } + + // TODO: fix this temporary hardcoded version check + if ((pmVersion === "9.12.2" || pmVersion === "v9.12.2") && pmName === "bun") { + pmVersion = await getBunVersion(); + } + + return { pmName, pmVersion }; +} diff --git a/src/temp/mod.ts b/src/temp/mod.ts new file mode 100644 index 0000000..808efd7 --- /dev/null +++ b/src/temp/mod.ts @@ -0,0 +1,28 @@ +import { pkg } from "~/utils/pkg.js"; + +import { installWithPackageManager } from "./menu/utils/dependenciesInstall.js"; +import { getPackageManager } from "./menu/utils/packageManager.js"; + +const cwd = process.cwd(); + +async function main() { + const args = process.argv.slice(2); + const devMode = args.includes("--dev"); + const { pmName, pmVersion } = await getPackageManager(args, cwd); + + console.log( + `\n✨ reliverse ${pkg.version ? `v${pkg.version}` : ""} | 🧩 ${pmName} ${pmVersion ? `v${pmVersion}` : ""}\n`, + ); + + if (pmName !== "unknown") { + await installWithPackageManager(pmName, devMode, cwd); + } else { + console.error( + "Unknown package manager. Please contact support: https://discord.gg/Pb8uKbwpsJ", + ); + } +} + +main().catch((error) => { + console.error("An error occurred:", error); +}); diff --git a/src/temp/replaceImportSymbol.ts b/src/temp/replaceImportSymbol.ts new file mode 100644 index 0000000..51b3322 --- /dev/null +++ b/src/temp/replaceImportSymbol.ts @@ -0,0 +1,32 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { glob } from "glob"; +import path from "pathe"; + +type ReplaceImportSymbolOptions = { + projectPath: string; + from: string; + to: string; +}; + +export async function replaceImportSymbol({ + projectPath, + from, + to, +}: ReplaceImportSymbolOptions) { + relinka.info(`Replacing ${from} with ${to} in files under ${projectPath}`); + + const files = await glob("**/*.{js,ts,tsx}", { + cwd: projectPath, + }); + + for (const file of files) { + const filePath = path.join(projectPath, file); + const content = await fs.readFile(filePath, "utf8"); + const updatedContent = content.replace( + new RegExp(`from '${from}`, "g"), + `from '${to}`, + ); + await fs.writeFile(filePath, updatedContent, "utf8"); + } +} diff --git a/src/tests/codemod.ts b/src/tests/codemod.ts new file mode 100644 index 0000000..baaba27 --- /dev/null +++ b/src/tests/codemod.ts @@ -0,0 +1,19 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { replaceImportSymbol } from "~/mods/replaceImportSymbol.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; + +async function runReliverseCodemod() { + const projectPath = await inputPrompt({ + title: + "Enter the path to your project or press Enter to use the current directory", + defaultValue: getCurrentWorkingDirectory(), + }); + + await replaceImportSymbol(projectPath, "@", "~"); +} + +runReliverseCodemod().catch((err) => + relinka.error("Failed to update project with reliverse updater", err), +); diff --git a/src/tests/update-config.json b/src/tests/update-config.json new file mode 100644 index 0000000..7bbadcc --- /dev/null +++ b/src/tests/update-config.json @@ -0,0 +1,12 @@ +{ + "actions": [ + { + "type": "replaceImportSymbol", + "params": { + "repo": "blefnk/relivator-nextjs-template", + "from": "@", + "to": "~" + } + } + ] +} diff --git a/src/tests/updater.ts b/src/tests/updater.ts new file mode 100644 index 0000000..c0d0b88 --- /dev/null +++ b/src/tests/updater.ts @@ -0,0 +1,19 @@ +import { inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; + +import { replaceImportSymbol } from "~/mods/replaceImportSymbol.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; + +async function runReliverseUpdater() { + const projectPath = await inputPrompt({ + title: + "Enter the path to your project or press Enter to use the current directory", + defaultValue: getCurrentWorkingDirectory(), + }); + + await replaceImportSymbol(projectPath, "@", "~"); +} + +runReliverseUpdater().catch((err) => + relinka.error("Failed to update project with reliverse updater", err), +); diff --git a/src/utils/app.ts b/src/utils/app.ts new file mode 100644 index 0000000..0bdac0e --- /dev/null +++ b/src/utils/app.ts @@ -0,0 +1,234 @@ +import type { FooterItem } from "~/utils/nav.js"; +import type { MainMenuItem } from "~/utils/with.js"; + +import metadata from "~/utils/metadata.js"; +import { productCategories } from "~/utils/products.js"; +import { REPO_SHORT_URLS } from "~/app.js"; + +export function slugify(str: string) { + return str + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w-]+/g, "") + .replace(/-{2,}/g, "-"); +} + +// Define available icon names as a union type +type IconName = + | "billing" + | "discord" + | "dollarSign" + | "laptop" + | "settings" + | "store" + | "terminal" + | "user" + | "view"; + +const socialLinks = { + discord: "https://discord.gg/Pb8uKbwpsJ", + facebook: "https://facebook.com/groups/bleverse", + github: REPO_SHORT_URLS.relivatorGithubLink, + githubAccount: "https://github.com/blefnk", + twitter: "https://x.com/blefnk", +}; + +// Did you know that you can edit some settings of this file headlessly? +// Just run bun reli:setup and configure the advanced settings. Perfect! +export const siteConfig = { + name: metadata.name, + siteNameDesc: metadata.siteNameDesc, + appPublisher: metadata.appPublisher, + appVersion: metadata.appVersion, + author: { + email: metadata.author.email, + fullName: metadata.author.fullName, + handle: metadata.author.handle, + handleAt: metadata.author.handleAt, + url: metadata.author.url, + }, + footerNav: [ + { + items: [ + { + external: false, + href: "/contact", + title: "Contact", + }, + { + external: false, + href: "/privacy", + title: "Privacy", + }, + { + external: false, + href: "/terms", + title: "Terms", + }, + { + external: false, + href: "/about", + title: "About", + }, + ], + title: "Help", + }, + { + items: [ + { + external: true, + href: socialLinks.githubAccount, + title: "Github", + }, + { + external: true, + href: socialLinks.discord, + title: "Discord", + }, + { + external: true, + href: socialLinks.twitter, + title: "Twitter", + }, + { + external: true, + href: socialLinks.facebook, + title: "Facebook", + }, + ], + title: "Social", + }, + { + items: [ + { + external: true, + href: "https://github.com/orgs/reliverse/repositories", + title: "@reliverse", + }, + { + external: true, + href: socialLinks.githubAccount, + title: "@blefnk", + }, + { + external: true, + href: socialLinks.github, + title: "Relivator", + }, + { + external: true, + href: "https://github.com/blefnk/reliverse-website-builder", + title: "Reliverse", + }, + ], + title: "Github", + }, + { + items: [ + { + external: true, + href: "https://github.com/sponsors/blefnk", + title: "GitHub Sponsors", + }, + { + external: true, + href: "https://buymeacoffee.com/blefnk", + title: "Buy Me a Coffee", + }, + { + external: true, + href: "https://patreon.com/blefnk", + title: "Patreon", + }, + { + external: true, + href: "https://paypal.me/blefony", + title: "PayPal", + }, + ], + title: "Support", + }, + ] satisfies FooterItem[], + images: [ + { + alt: "Shows the cover image of Relivator Next.js template", + url: "/og.png", + }, + ], + keywords: ["Freelance Marketplace", "Hire Freelancer"] as string[], + links: socialLinks, + mainNav: [ + { + href: "/", + items: [ + { + description: "All the products we have to offer", + href: "/products", + items: [], + title: "Products", + }, + { + description: "Build your own custom clothes", + href: "/custom/clothing", + items: [], + title: "Build a Look", + }, + { + description: "Read our latest blog posts", + href: "/blog", + items: [], + title: "Blog", + }, + ], + title: "Catalogue", + }, + ...productCategories.map((category) => ({ + href: `/categories/${slugify(category.title)}`, + items: [ + { + description: `All ${category.title}.`, + href: `/categories/${slugify(category.title)}`, + items: [], + title: "All", + }, + ...category.subcategories.map((subcategory) => ({ + description: subcategory.description, + href: `/categories/${slugify(category.title)}/${subcategory.slug}`, + items: [], + title: subcategory.title, + })), + ], + title: category.title, + })), + ] satisfies MainMenuItem[], + socialLinks: { + discord: "https://discord.gg/Pb8uKbwpsJ", + facebook: "https://facebook.com/groups/bleverse", + github: REPO_SHORT_URLS.relivatorGithubLink, + githubAccount: "https://github.com/blefnk", + twitter: "https://x.com/blefnk", + }, + themeToggleEnabled: true, +} as const; + +export const oauthProvidersClerk = [ + { + name: "Google", + icon: "view", + strategy: "oauth_google", + }, + { + name: "Discord", + icon: "discord", + strategy: "oauth_discord", + }, +] satisfies { + strategy: + | "oauth_discord" + | "oauth_facebook" + | "oauth_github" + | "oauth_google" + | "oauth_microsoft"; + icon: IconName; + name: string; +}[]; diff --git a/src/utils/appts.ts b/src/utils/appts.ts new file mode 100644 index 0000000..a97eca5 --- /dev/null +++ b/src/utils/appts.ts @@ -0,0 +1,223 @@ +import { config } from "@reliverse/core"; +import { fileExists } from "@reliverse/fs"; +import { confirmPrompt, inputPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { + loadFile as loadFileUsingMagicast, + writeFile as writeFileUsingMagicast, +} from "magicast"; +import process from "node:process"; +import { join } from "pathe"; +import pc from "picocolors"; + +import type { ApptsConfig } from "~/utils/types.js"; + +import metadata from "~/utils/metadata.js"; + +export async function configureAppts({ apptsConfig }: ApptsConfig) { + const apptsConfigPath = join(apptsConfig, "app.ts"); + const metadataConfigPath = join(apptsConfig, "constants/metadata.ts"); + + if (!(await fileExists(apptsConfigPath))) { + relinka.error( + "Oops! It seems like the configuration file `src/app.ts` has gone missing! β›”", + ); + + return process.exit(0); + } + + if (!(await fileExists(metadataConfigPath))) { + relinka.error( + `Uh-oh! We couldn't find the configuration file! (${metadataConfigPath}) β›”`, + ); + + return process.exit(0); + } + + let currentConfig: Record = {}; + + try { + const mod = await loadFileUsingMagicast(metadataConfigPath); + + currentConfig = mod.exports.default || {}; + } catch (error) { + relinka.error( + "Whoops! Something went wrong while loading the configuration file:", + error, + ); + + return process.exit(0); + } + + const proceed = await confirmPrompt({ + defaultValue: false, + title: pc.cyan( + "[βš™οΈ Advanced]: Do you want to configure the app metadata stored in the src/app.ts file?", + ), + }); + + if (typeof proceed !== "boolean" || !proceed) { + return process.exit(0); + } + + let handle = await askForHandle(metadata.author.handle || "blefnk"); + + // If the user skips the handle question, use the current handle from metadataConfig + if (!handle) { + handle = metadata.author.handle || "blefnk"; + } + + // If !handle for any other reason, use the fallback + if (!handle) { + handle = "blefnk"; + } + + const prompts = [ + { + default: "Relivator", + key: "name", + message: "What's the short name for your app?", + }, + { + default: "Relivator: Next.js 15 and React 19 template by Reliverse", + key: "siteNameDesc", + message: "Enter the full name for your app:", + }, + { + default: "Reliverse", + key: "appPublisher", + message: "Who is the publisher of your app?", + }, + { + default: "1.2.6", + key: "appVersion", + message: "What's the current version of your app?", + }, + { + default: "blefnk@gmail.com", + key: "authorEmail", + message: "Author's email address?", + }, + { + default: "Nazar Kornienko", + key: "authorFullName", + message: "Author's full name?", + }, + { + default: "https://github.com/blefnk", + key: "authorUrl", + message: "Author's URL?", + }, + ]; + + const results: Record = {}; + + for (const prompt of prompts) { + results[prompt.key] = await askForText( + prompt.message, + currentConfig[prompt.key] || prompt.default, + ); + } + + const { + name, + siteNameDesc, + appPublisher, + appVersion, + authorEmail, + authorFullName, + authorUrl, + } = results; + + if ( + [ + handle, + name, + siteNameDesc, + appPublisher, + appVersion, + authorEmail, + authorFullName, + authorUrl, + ].some((value) => typeof value !== "string") + ) { + return process.exit(0); + } + + try { + await updateFile(metadataConfigPath, { + name: name, + siteNameDesc: siteNameDesc, + appPublisher: appPublisher, + appVersion: appVersion, + authorEmail: authorEmail, + authorFullName: authorFullName, + authorUrl: authorUrl, + handle: handle, + }); + + relinka.success( + pc.italic( + "πŸŽ‰ Advanced configuration complete! Visit `src/app.ts` to fine-tune your settings further.", + ), + ); + } catch (error) { + relinka.error("Error updating configuration file content:", error); + } +} + +async function askForHandle(currentHandle: string): Promise { + return await inputPrompt({ + title: `${pc.bold(`Let's customize the ${config.framework.name} template to your needs. The 'src/app.ts' file holds the main configuration.`)} \nπŸš€ First of all, what's your username handle? (πŸ’‘ Type something or just press ${pc.cyan("")} to use the suggested value)`, + placeholder: currentHandle, + validate: (value) => { + if (value && !/^[\da-z]+$/i.test(value)) { + return "Please use only letters and numbers."; + } + }, + }); +} + +async function askForText( + message: string, + placeholder: string, +): Promise { + return ( + (await inputPrompt({ + title: message, + placeholder, + validate: (value) => { + if (value === undefined || value === null) { + return `Please enter ${message.toLowerCase()}.`; + } + }, + })) || placeholder + ); +} + +async function updateFile(filePath: string, config: Record) { + try { + const mod = await loadFileUsingMagicast(filePath); + + mod.exports.default = mod.exports.default || {}; + mod.exports.default.author = mod.exports.default.author || {}; + + mod.exports.default.name = config.name; + mod.exports.default.siteNameDesc = config.siteNameDesc; + mod.exports.default.appPublisher = config.appPublisher; + mod.exports.default.appVersion = config.appVersion; + mod.exports.default.author.email = config.authorEmail; + mod.exports.default.author.fullName = config.authorFullName; + mod.exports.default.author.handle = config.handle; + mod.exports.default.author.handleAt = `@${config.handle}`; + mod.exports.default.author.url = config.authorUrl; + + await writeFileUsingMagicast(mod, filePath); + + // Adding a blank new line at the end of the file + await fs.appendFile(filePath, "\n"); + } catch (error) { + relinka.error("Error updating configuration file content:", error); + } +} diff --git a/src/utils/biome.ts b/src/utils/biome.ts new file mode 100644 index 0000000..b05d14f --- /dev/null +++ b/src/utils/biome.ts @@ -0,0 +1,68 @@ +import { fileExists, removeFile } from "@reliverse/fs"; +import { select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import pc from "picocolors"; + +import type { BiomeConfig } from "./types.ts"; + +export async function configureBiome({ + biomeConfig, + biomeRecommendedConfig, + biomeRulesDisabledConfig, +}: BiomeConfig) { + const biomeConfigExists = await fileExists(biomeConfig); + const biomeRecommendedConfigExists = await fileExists(biomeRecommendedConfig); + const biomeRulesDisabledConfigExists = await fileExists( + biomeRulesDisabledConfig, + ); + + const biome: string | symbol = await selectPrompt({ + title: `Please select which type of Biome configuration you want to use.`, + options: [ + { + label: "Skip", + value: "Skip", + hint: "Skip Biome configuration", + }, + { + label: "biome.rules-disabled.json", + value: "RulesDisabled", + hint: "[βœ… Default] Disables almost all rules", + }, + { + label: "biome.recommended.json", + value: "Recommended", + hint: "[🐞 You'll encounter many issues on Relivator 1.3.0@canary]", + }, + ], + }); + + if (typeof biome !== "string") { + process.exit(0); + } + + if (biome === "Skip") { + relinka.success("Biome configuration was skipped."); + + return; + } + + if (biomeConfigExists) { + await removeFile(biomeConfig); + } + + if (biome === "Recommended" && biomeRecommendedConfigExists) { + await fs.copy(biomeRecommendedConfig, biomeConfig); + } else if (biome === "RulesDisabled" && biomeRulesDisabledConfigExists) { + await fs.copy(biomeRulesDisabledConfig, biomeConfig); + } + + if (await fileExists(biomeConfig)) { + relinka.success(`Biome configuration has been set to ${biome}`); + } else { + relinka.error( + "Something went wrong! Newly created `biome.json` file was not found!", + ); + } +} diff --git a/src/utils/choosePackageManager.ts b/src/utils/choosePackageManager.ts new file mode 100644 index 0000000..5d34214 --- /dev/null +++ b/src/utils/choosePackageManager.ts @@ -0,0 +1,40 @@ +import type { PackageManagerName } from "nypm"; + +import { selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { detectPackageManager } from "nypm"; + +export async function choosePackageManager( + cwd: string, +): Promise { + const detectedPkgManager = (await detectPackageManager(cwd))?.name || "pnpm"; + + let pkgManager: PackageManagerName = detectedPkgManager; + + if (pkgManager === "bun") { + relinka.warn("bun might not work for installing dependencies."); + const selectedPkgManager = await selectPrompt({ + title: + "bun was detected. Do you want to use `pnpm`, `npm`, `yarn`, or continue with bun ?", + defaultValue: "pnpm", + options: [ + { + label: "pnpm", + value: "pnpm", + hint: "The most popular package manager", + }, + { + label: "npm", + value: "npm", + hint: "The most compatible package manager", + }, + { label: "yarn", value: "yarn", hint: "The safest package manager" }, + { label: "bun", value: "bun", hint: "The fastest package manager" }, + ], + }); + + pkgManager = selectedPkgManager; + } + + return pkgManager; +} diff --git a/src/utils/cloneAndCopyFiles.ts b/src/utils/cloneAndCopyFiles.ts new file mode 100644 index 0000000..0f32a63 --- /dev/null +++ b/src/utils/cloneAndCopyFiles.ts @@ -0,0 +1,104 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; +import { simpleGit } from "simple-git"; + +import { DEBUG, FILE_CONFLICTS } from "../app.js"; + +// Function to clone and copy files from the repository +export async function cloneAndCopyFiles( + filesToDownload: string[], + targetDir: string, + overwrite: boolean, + repoUrl: string, + tempRepoDir: string, +): Promise { + DEBUG.enableVerboseLogging && + relinka.info(`Cloning from ${repoUrl} into ${tempRepoDir}...`); + + console.info( + "✨ Please wait while we make magic. It all depends on your internet speed.", + ); + + try { + const git = simpleGit(); + + DEBUG.enableVerboseLogging && relinka.log("tempRepoDir", tempRepoDir); + + // Step 0: Check if the tempRepoDir already exists and is not empty + if (await fs.pathExists(tempRepoDir)) { + const isDirEmpty = await fs.readdir(tempRepoDir); + + if (isDirEmpty.length > 0) { + // If directory is not empty and overwrite is not allowed + if (!overwrite) { + relinka.error( + `Error: Destination path '${tempRepoDir}' already exists and is not an empty directory.`, + ); + + return; + } + + // If overwrite is allowed, clean the directory + await fs.emptyDir(tempRepoDir); + DEBUG.enableVerboseLogging && + relinka.info( + `[cloneAndCopyFiles] Temp directory '${tempRepoDir}' is now empty to overwrite existing folder content.`, + ); + } + } + + // Step 1: Clone the repository into tempRepoDir + await git.clone(repoUrl, tempRepoDir, ["--depth", "1"]); + DEBUG.enableVerboseLogging && + relinka.success("Temporary repository cloned successfully."); + + // Step 2: Copy the necessary files to the target directory + for (const fileName of filesToDownload) { + // Find if the file is in FILE_CONFLICTS and has shouldCopy: false + const fileConflict = FILE_CONFLICTS.find( + (file) => file.fileName === fileName && !file.shouldCopy, + ); + + // Skip copying if shouldCopy is false + if (fileConflict) { + DEBUG.enableVerboseLogging && + relinka.info( + `Skipping ${fileName} as it is marked with shouldCopy: false.`, + ); + continue; + } + + const sourcePath = path.join(tempRepoDir, fileName); + const destPath = path.join(targetDir, fileName); + + // Log source and destination paths for debugging + DEBUG.enableVerboseLogging && + relinka.info(`Copying from '${sourcePath}' to '${destPath}'...`); + + // Check if source and destination are the same + if (path.resolve(sourcePath) === path.resolve(destPath)) { + relinka.error( + `Error: Source and destination paths must not be the same. Source: ${sourcePath}, Destination: ${destPath}`, + ); + continue; // Skip the file if source and destination are the same + } + + if ((await fs.pathExists(destPath)) && !overwrite) { + relinka.warn(`${fileName} already exists in ${targetDir}.`); + } else { + await fs.copy(sourcePath, destPath); + DEBUG.enableVerboseLogging && + relinka.success(`${fileName} copied to ${destPath}.`); + } + } + + // Step 3: Clean up the temporary clone directory if specified + if (!DEBUG.disableTempCloneRemoving) { + await fs.remove(tempRepoDir); + relinka.info("Temporary clone removed."); + } + } catch (error) { + relinka.error(`Error during file cloning: ${error}`); + } +} diff --git a/src/utils/cmds/add.ts b/src/utils/cmds/add.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/cmds/diff.ts b/src/utils/cmds/diff.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/cmds/init.ts b/src/utils/cmds/init.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/configure.ts b/src/utils/configure.ts new file mode 100644 index 0000000..1758015 --- /dev/null +++ b/src/utils/configure.ts @@ -0,0 +1,151 @@ +import { config } from "@reliverse/core"; +import { getCurrentDirname, getRootDirname } from "@reliverse/fs"; +import { confirmPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import { join } from "pathe"; +import pc from "picocolors"; + +import { siteConfig } from "~/utils/app.js"; +import { configureAppts } from "~/utils/appts.js"; +import { configureBiome } from "~/utils/biome.js"; +import { configureEnv } from "~/utils/envjs.js"; +import { configureEslint } from "~/utils/eslint.js"; +import { configureKnip } from "~/utils/knip.js"; +import { configureNext } from "~/utils/nextjs.js"; +import { configurePutout } from "~/utils/putout.js"; + +export async function runReliverseSetup() { + const currentDirname = getCurrentDirname(import.meta.url); + const rootDirectory = getRootDirname(import.meta.url, 5); + const srcDirectory = join(rootDirectory, "src"); + const configsFolder = join(currentDirname, "configs"); + + // Next.js configurations + const nextConfig = join(rootDirectory, "next.config.js"); + const nextMinimalConfig = join(configsFolder, "next.config.minimal.ts"); + + const nextRecommendedConfig = join( + configsFolder, + "next.config.recommended.ts", + ); + + // ESLint configurations + const eslintConfig = join(rootDirectory, "eslint.config.js"); + + const eslintUltimateConfig = join(configsFolder, "eslint.config.ultimate.ts"); + + const eslintRulesDisabledConfig = join( + configsFolder, + "eslint.config.rules-disabled.ts", + ); + + // Biome configurations + const biomeConfig = join(rootDirectory, "biome.json"); + const biomeRecommendedConfig = join(configsFolder, "biome.recommended.json"); + + const biomeRulesDisabledConfig = join( + configsFolder, + "biome.rules-disabled.json", + ); + + // Knip configurations + const knipConfig = join(rootDirectory, "knip.json"); + const knipRecommendedConfig = join(configsFolder, "knip.recommended.json"); + + const knipRulesDisabledConfig = join( + configsFolder, + "knip.rules-disabled.json", + ); + + // Putout configurations + const putoutConfig = join(rootDirectory, ".putout.json"); + + const putoutRecommendedConfig = join( + configsFolder, + ".putout.recommended.json", + ); + + const putoutRulesDisabledConfig = join( + configsFolder, + ".putout.rules-disabled.json", + ); + + // env.js configuration + const envConfig = join(srcDirectory, "env.js"); + const envRulesDisabledConfig = join(configsFolder, "env.rules-disabled.ts"); + const envRecommendedConfig = join(configsFolder, "env.recommended.ts"); + + // todo: consider ./canary/json.ts file which reads appts.json file + // todo: const apptsConfig = join(srcDirectory, "config/json/app"); + const apptsConfig = join(rootDirectory, "src"); + const { fullName } = siteConfig.author; + const [firstName] = fullName.split(" "); + + const welcomeCondition = `Hello, ${firstName !== "Nazar" ? firstName : "there"}!`; + + // Reliverse Config Setup + const accepted = await confirmPrompt({ + title: `${pc.bold(`${welcomeCondition} Welcome to the ${config.framework.name} 1.2.6 setup! This setup wizard will help you configure the main configuration of the app and let you choose the Next.js, ESLint, Biome, and Putout config presets.`)} \nπŸ‘‹ Are you ready to proceed? ${pc.dim("(πŸ’‘ You can press anywhere to close the setup)")}`, + defaultValue: true, + }); + + // Handle Cmd/Ctrl+C pressed by user or if unexpected things happen + if (typeof accepted !== "boolean") { + process.exit(0); + } + + if (accepted) { + await configureEslint({ + eslintConfig, + eslintRulesDisabledConfig, + eslintUltimateConfig, + }); + await configureNext({ + nextConfig, + nextMinimalConfig, + nextRecommendedConfig, + }); + await configureBiome({ + biomeConfig, + biomeRecommendedConfig, + biomeRulesDisabledConfig, + }); + await configureKnip({ + knipConfig, + knipRecommendedConfig, + knipRulesDisabledConfig, + }); + await configurePutout({ + putoutConfig, + putoutRecommendedConfig, + putoutRulesDisabledConfig, + }); + await configureEnv({ + envConfig, + envRecommendedConfig, + envRulesDisabledConfig, + }); + await configureAppts({ + apptsConfig, + }); + + relinka.success( + pc.green( + `πŸŽ‰ ${config.framework.name} setup completed successfully! Have a perfect day!`, + ), + ); + relinka.info( + "βš™οΈ By the way, run `bun reli:vscode` to choose VSCode settings preset!", + ); + relinka.info( + pc.blue( + "πŸ˜‰ It is recommended to open the desired configs and customize the specific options to your preferences, because it all belongs to you! Have fun and enjoy!", + ), + ); + relinka.info( + pc.magenta("πŸ”₯ Please restart your code editor to apply the changes!"), + ); + } else { + relinka.info("Setup was canceled by the user."); + } +} diff --git a/src/utils/console.ts b/src/utils/console.ts new file mode 100644 index 0000000..ef24a85 --- /dev/null +++ b/src/utils/console.ts @@ -0,0 +1,30 @@ +import { relinka } from "@reliverse/relinka"; + +import { DEBUG } from "~/app.js"; + +// Helper for conditional verbose logging +export const verbose = ( + kind: "error" | "info" | "success", + message: string, +) => { + if (DEBUG.enableVerboseLogging) { + if (kind === "success") { + relinka.success(message); + } else if (kind === "error") { + relinka.error(message); + } else { + relinka.info(message); + } + } +}; + +// General error handling +export const handleError = (error: unknown) => { + if (error instanceof Error) { + relinka.error(`πŸ€” Failed to set up the project: ${error.message}`); + } else { + relinka.error("πŸ€” An unknown error occurred."); + } + + process.exit(1); +}; diff --git a/src/utils/downloadGitRepo.ts b/src/utils/downloadGitRepo.ts new file mode 100644 index 0000000..53ae6a9 --- /dev/null +++ b/src/utils/downloadGitRepo.ts @@ -0,0 +1,35 @@ +import { downloadTemplate } from "giget"; +import path from "pathe"; +import type { GitOption } from "~/menu/modules/08-askGitInitialization.js"; + +import { handleError, verbose } from "~/utils/console.js"; +import { getCurrentWorkingDirectory } from "~/utils/fs.js"; +import { initializeGitRepository } from "~/utils/git.js"; +import { isDev } from "~/app.js"; + +export async function downloadGitRepo( + name: string, + template: string, + deps: boolean, + gitOption: GitOption, +): Promise { + try { + const cwd = getCurrentWorkingDirectory(); + const targetDir = path.join(cwd, isDev ? ".." : "", name); + + verbose("info", `Installing template in: ${targetDir}`); + + const { dir, source } = await downloadTemplate(`github:${template}`, { + dir: targetDir, + install: deps, + }); + + verbose("success", `${source} was downloaded to ${dir}.`); + + gitOption && (await initializeGitRepository(targetDir, gitOption)); + + return dir; + } catch (error) { + handleError(error); + } +} diff --git a/src/utils/downloadI18nFiles.ts b/src/utils/downloadI18nFiles.ts new file mode 100644 index 0000000..ab1c344 --- /dev/null +++ b/src/utils/downloadI18nFiles.ts @@ -0,0 +1,118 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; + +import { DEBUG, FILE_PATHS, FILES_TO_DOWNLOAD, REPO_FULL_URLS } from "~/app.js"; +import { cloneAndCopyFiles } from "~/utils/cloneAndCopyFiles.js"; + +// Function to download i18n layout.tsx and page.tsx files +export async function downloadI18nFiles( + targetDir: string, + isDev: boolean, +): Promise { + const tempRepoDir = isDev + ? path.join(targetDir, "..", FILE_PATHS.tempRepoClone) // Adjust for development + : path.join(targetDir, FILE_PATHS.tempRepoClone); // For production + + try { + // Step 0: Check if the tempRepoDir already exists and handle it + let isDirEmpty = true; + + if (await fs.pathExists(tempRepoDir)) { + const dirContents = await fs.readdir(tempRepoDir); + + isDirEmpty = dirContents.length === 0; + + if (!isDirEmpty) { + DEBUG.enableVerboseLogging && + relinka.info( + `Temp directory '${tempRepoDir}' already exists and is not empty.`, + ); + + // Prompt the user to decide if they want to skip or clean up the directory + const skipClone = true; + + // const skipClone = await relinka.prompt( + // "The directory already exists and contains files. Do you want to skip cloning and use the existing files?", + // { initial: true, type: "confirm" }, + // ); + + if (skipClone) { + DEBUG.enableVerboseLogging && + relinka.info( + `Skipping cloning and using the existing '${tempRepoDir}' folder.`, + ); + } else { + relinka.info("Cleaning up the temp directory and cloning again..."); + await fs.emptyDir(tempRepoDir); // Clean up the directory + DEBUG.enableVerboseLogging && + relinka.info( + `[downloadI18nFiles] Temp directory '${tempRepoDir}' is now empty to overwrite existing folder content.`, + ); + isDirEmpty = true; + } + } + } + + if (isDirEmpty) { + DEBUG.enableVerboseLogging && + relinka.info( + "Cloning repository for i18n-specific layout.tsx and page.tsx...", + ); + + // Step 1: Clone the repository + await cloneAndCopyFiles( + FILES_TO_DOWNLOAD, + targetDir, + true, + REPO_FULL_URLS.relivatorGithubLink, + tempRepoDir, + ); + } + + // Step 2: Check if the required files exist in the temp repo and copy them + for (const file of FILES_TO_DOWNLOAD) { + const tempFilePath = path.join(tempRepoDir, file); + const targetFilePath = path.join(targetDir, file); + + DEBUG.enableVerboseLogging && + relinka.info(`Checking if ${tempFilePath} exists...`); + + if (await fs.pathExists(tempFilePath)) { + DEBUG.enableVerboseLogging && + relinka.info(`File ${tempFilePath} exists. Copying to target...`); + + if (path.resolve(tempFilePath) !== path.resolve(targetFilePath)) { + await fs.copy(tempFilePath, targetFilePath); + DEBUG.enableVerboseLogging && + relinka.success(`Copied ${file} to ${targetFilePath}.`); + } else { + relinka.warn( + `Skipping copying file ${file} because source and destination are the same.`, + ); + } + } else { + relinka.error( + `File ${tempFilePath} not found in the cloned repository.`, + ); + + throw new Error(`File ${tempFilePath} not found.`); + } + } + + DEBUG.enableVerboseLogging && + relinka.success( + "i18n-specific files downloaded and copied successfully.", + ); + relinka.success("Internationalization (i18n) was successfully integrated."); + } catch (error) { + relinka.error("Error during file cloning or copying:", error); + } finally { + // Step 3: Conditionally clean up the temp directory after all operations are completed + if (!DEBUG.disableTempCloneRemoving && (await fs.pathExists(tempRepoDir))) { + relinka.info("Cleaning up temporary repository directory..."); + await fs.remove(tempRepoDir); // Ensure the temp folder is removed after all tasks + relinka.success("Temporary repository directory removed."); + } + } +} diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 0000000..06af292 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,211 @@ +import { getRootDirname } from "@reliverse/fs"; +import { confirm, input, password } from "@reliverse/prompts"; +import fs from "fs-extra"; +import { join } from "pathe"; + +// TODO: 🐞 Still in development! Please use at own risk! + +const rootDirname = getRootDirname(import.meta.url, 6); +const envFilePath = join(rootDirname, ".env"); + +type PromptType = "confirm" | "input" | "password"; + +type Question = { + default?: boolean | string; + key: string; + message: string; + type: PromptType; +}; + +function createPrompt( + type: PromptType, + message: string, + defaultValue?: boolean | string, +) { + const options: { default?: boolean | string; message: string } = { message }; + + if (defaultValue !== undefined) { + options.default = defaultValue; + } + + if (type === "input") { + return input(options as { default?: string; message: string }); + } else if (type === "password") { + return password(options as { default?: string; message: string }); + } else { + return confirm(options as { default?: boolean; message: string }); + } +} + +async function askQuestions() { + const questions: Question[] = [ + { + default: "http://localhost:3000", + key: "NEXT_PUBLIC_APP_URL", + message: "Specify the website domain in production", + type: "input", + }, + { key: "DATABASE_URL", message: "Database URL", type: "input" }, + { + key: "AUTH_SECRET", + message: + "Auth Secret (e.g.: EnsureUseSomethingRandomHere44CharactersLong)", + type: "password", + }, + { + key: "AUTH_DISCORD_SECRET", + message: "Auth Discord Secret", + type: "password", + }, + { key: "AUTH_DISCORD_ID", message: "Auth Discord ID", type: "input" }, + { + key: "AUTH_GITHUB_SECRET", + message: "Auth GitHub Secret", + type: "password", + }, + { key: "AUTH_GITHUB_ID", message: "Auth GitHub ID", type: "input" }, + { + key: "AUTH_GOOGLE_SECRET", + message: "Auth Google Secret", + type: "password", + }, + { key: "AUTH_GOOGLE_ID", message: "Auth Google ID", type: "input" }, + { + key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + message: "Clerk Publishable Key", + type: "input", + }, + { key: "CLERK_SECRET_KEY", message: "Clerk Secret Key", type: "password" }, + { + default: false, + key: "NEXT_PUBLIC_ORGANIZATIONS_ENABLED", + message: "Organizations Enabled", + type: "confirm", + }, + { + key: "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + message: "Stripe Publishable Key", + type: "input", + }, + { + key: "STRIPE_SECRET_KEY", + message: "Stripe Secret Key", + type: "password", + }, + { + key: "STRIPE_WEBHOOK_SIGNING_SECRET", + message: "Stripe Webhook Signing Secret", + type: "password", + }, + { + key: "STRIPE_PROFESSIONAL_SUBSCRIPTION_PRICE_ID", + message: "Stripe Professional Subscription Price ID", + type: "input", + }, + { + key: "STRIPE_ENTERPRISE_SUBSCRIPTION_PRICE_ID", + message: "Stripe Enterprise Subscription Price ID", + type: "input", + }, + { + default: false, + key: "PYTHON_INSTALLED", + message: "Python Installed", + type: "confirm", + }, + { + default: false, + key: "ENABLE_VERCEL_TOOLBAR", + message: "Enable Vercel Toolbar", + type: "confirm", + }, + { + default: false, + key: "ENABLE_VT_ON_PRODUCTION", + message: "Enable VT on Production", + type: "confirm", + }, + { + default: false, + key: "ENABLE_FEATURE_FLAGS", + message: "Enable Feature Flags", + type: "confirm", + }, + { key: "FLAGS_SECRET", message: "Flags Secret", type: "password" }, + { + key: "REMOTION_GITHUB_TOKEN", + message: "Remotion GitHub Token", + type: "password", + }, + { + key: "UPLOADTHING_SECRET", + message: "Uploadthing Secret", + type: "password", + }, + { key: "UPLOADTHING_APP_ID", message: "Uploadthing App ID", type: "input" }, + { + key: "NEXT_PUBLIC_RESEND_API_KEY", + message: "Resend API Key", + type: "input", + }, + { + default: "onboarding@resend.dev", + key: "NEXT_PUBLIC_RESEND_EMAIL_FROM", + message: "Resend Email From", + type: "input", + }, + { key: "LOGLIB_ID", message: "Loglib ID", type: "input" }, + { + key: "DISCORD_WEBHOOK_URL", + message: "Discord Webhook URL", + type: "input", + }, + ]; + + const answers: Record = {}; + + for (const question of questions) { + const answer = await createPrompt( + question.type, + question.message, + question.default, + ); + + answers[question.key] = answer; + } + + return answers; +} + +function generateEnvContent(answers: Record) { + const keys = Object.keys(answers); + + return keys.map((key) => `${key}="${answers[key]}"`).join("\n"); +} + +async function main() { + try { + const answers = await askQuestions(); + + console.log("\nPlease review your answers:"); + console.log(generateEnvContent(answers)); + + const confirmed = await confirm({ + default: true, + message: "Do you want to save these settings to .env file?", + }); + + if (confirmed) { + fs.writeFileSync(envFilePath, generateEnvContent(answers).trim()); + console.log(`.env file has been generated at ${envFilePath}`); + } else { + console.log("Aborted! The .env file was not generated."); + } + } catch (error) { + console.error("An error occurred:", error); + } +} + +main().catch((error) => { + console.error("An error occurred while generating the .env file:", error); +}); diff --git a/src/utils/envjs.ts b/src/utils/envjs.ts new file mode 100644 index 0000000..88fd32a --- /dev/null +++ b/src/utils/envjs.ts @@ -0,0 +1,104 @@ +import { fileExists, removeFile } from "@reliverse/fs"; +import { select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { readFile, writeFile } from "node:fs/promises"; +import pc from "picocolors"; + +import type { EnvJsConfig } from "~/utils/types.js"; + +export async function configureEnv({ + envConfig, + envRecommendedConfig, + envRulesDisabledConfig, +}: EnvJsConfig) { + const envConfigExists = await fileExists(envConfig); + const envRulesDisabledConfigExists = await fileExists(envRulesDisabledConfig); + const envRecommendedConfigExists = await fileExists(envRecommendedConfig); + + const env: string | symbol = await selectPrompt({ + maxItems: 5, + title: pc.cyan( + "Please select which type of env.js configuration you want to use.", + ), + options: [ + { + hint: "Skip src/env.js configuration", + label: "Skip", + value: "Skip", + }, + { + hint: "[βœ… Default] RulesDisabled: builds will NOT fail if env is not set", + label: "env.rules-disabled.ts", + value: "RulesDisabled", + }, + { + hint: "Recommended: builds WILL FAIL if specific env variables is not set", + label: "env.recommended.ts", + value: "Recommended", + }, + ], + }); + + if (typeof env !== "string") { + process.exit(0); + } + + if (env === "Skip") { + relinka.success("src/env.js configuration was skipped."); + + return; + } + + if (envConfigExists) { + await removeFile(envConfig); + } + + if (env === "RulesDisabled" && envRulesDisabledConfigExists) { + await fs.copy(envRulesDisabledConfig, envConfig); + } else if (env === "Recommended" && envRecommendedConfigExists) { + await fs.copy(envRecommendedConfig, envConfig); + } + + if (await fileExists(envConfig)) { + relinka.success(`env.js configuration has been set to ${env}`); + await updateFileToJs(envConfig); + } else { + relinka.error( + "Something went wrong! Newly created `src/env.js` file was not found!", + ); + } +} + +async function updateFileToJs(filePath: string) { + try { + let fileContent = await readFile(filePath, "utf8"); + + // Remove lines containing `ts-expect-error` + const lines = fileContent.split("\n"); + const filteredLines = lines.filter( + (line) => !line.includes("// @ts-expect-error TODO: fix"), + ); + + fileContent = filteredLines.join("\n"); + + // Replace const _knownVariables with export const knownVariables + fileContent = fileContent.replaceAll( + "const _knownVariables", + "export const knownVariables", + ); + + // Replace const _recommendedEnvVariables with export const recommendedEnvVariables + fileContent = fileContent.replaceAll( + "const _recommendedEnvVariables", + "export const recommendedEnvVariables", + ); + + // Replace const _env with export const env + fileContent = fileContent.replaceAll("const _env", "export const env"); + + await writeFile(filePath, fileContent, "utf8"); + } catch (error) { + relinka.error("Error updating file content:", error); + } +} diff --git a/src/utils/eslint.ts b/src/utils/eslint.ts new file mode 100644 index 0000000..8943e7a --- /dev/null +++ b/src/utils/eslint.ts @@ -0,0 +1,96 @@ +import { config } from "@reliverse/core"; +import { fileExists, removeFile } from "@reliverse/fs"; +import { selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import pc from "picocolors"; + +import type { EslintConfig } from "./types.js"; + +export async function configureEslint({ + eslintConfig, + eslintRulesDisabledConfig, + eslintUltimateConfig, +}: EslintConfig) { + const eslintConfigExists = await fileExists(eslintConfig); + const eslintUltimateConfigExists = await fileExists(eslintUltimateConfig); + + const eslintRulesDisabledConfigExists = await fileExists( + eslintRulesDisabledConfig, + ); + + const eslint: string | symbol = await selectPrompt({ + title: `${config.framework.name} uses various tools to automate code improvement. One of these tools is ESLint. Let's configure @reliverse/eslint-config to meet your needs.\n${pc.cyan("Please select the type of ESLint configuration you want to use.")} If you are a complete beginner, choose the "eslint.config.rules-disabled.ts" config preset, where almost all rules are disabled.\n${pc.dim("Please note that your current configurations will be replaced with the selected ones. If you do not want to lose anything, choose Skip or commit your changes to Git first.")}`, + options: [ + { + label: "Skip", + value: "Skip", + hint: "Skip ESLint configuration", + }, + { + label: "eslint.config.ultimate.ts", + value: "Ultimate", + hint: "[βœ… Default] Enables almost all rules", + }, + { + label: "eslint.config.rules-disabled.ts", + value: "RulesDisabled", + hint: "Disables almost all rules", + }, + ], + }); + + if (typeof eslint !== "string") { + process.exit(0); + } + + if (eslint === "Skip") { + relinka.success("ESLint configuration was skipped."); + + return; // Exit early if the user chose to skip + } + + if (eslintConfigExists) { + await removeFile(eslintConfig); + } + + if (eslint === "Ultimate" && eslintUltimateConfigExists) { + await fs.copy(eslintUltimateConfig, eslintConfig); + } else if (eslint === "RulesDisabled" && eslintRulesDisabledConfigExists) { + await fs.copy(eslintRulesDisabledConfig, eslintConfig); + } + + if (await fileExists(eslintConfig)) { + relinka.success(`ESLint configuration has been set to ${eslint}`); + await updateFileToJs(eslintConfig); + } else { + relinka.error( + "Something went wrong! Newly created `eslint.config.js` file was not found!", + ); + } +} + +async function updateFileToJs(filePath: string) { + try { + let fileContent = await fs.readFile(filePath, "utf8"); + + // Replace TypeScript type annotations in function parameters + const paramPattern = /(\w+): string/g; + const paramReplacement = "/** @type {string} */ $1"; + + fileContent = fileContent.replace(paramPattern, paramReplacement); + + // Remove lines containing `ts-expect-error` + const lines = fileContent.split("\n"); + + const filteredLines = lines.filter( + (line) => !line.includes("// @ts-expect-error TODO: fix"), + ); + + fileContent = filteredLines.join("\n"); + + await fs.writeFile(filePath, fileContent, "utf8"); + } catch (error) { + relinka.error("Error updating file content:", error); + } +} diff --git a/src/utils/extractRepoInfo.ts b/src/utils/extractRepoInfo.ts new file mode 100644 index 0000000..50483ad --- /dev/null +++ b/src/utils/extractRepoInfo.ts @@ -0,0 +1,23 @@ +// Utility to extract author and project name from template URL +export function extractRepoInfo(templateUrl: string): { + author: string; + projectName: string; +} { + // Ensure the template URL has the correct github: prefix without modifying the original parameter + const formattedTemplateUrl = templateUrl.startsWith("github:") + ? templateUrl + : `github:${templateUrl}`; + + const match = /^github:([^/]+)\/([^/]+)$/.exec(formattedTemplateUrl); + + if (!match) { + throw new Error(`Invalid GitHub URL format: ${templateUrl}`); + } + + const [, author, projectName] = match; + + return { + author: author ?? "", // Non-null assertion to assure TypeScript it's not undefined + projectName: projectName?.replace(".git", "") ?? "", // Non-null assertion and removing .git if present + }; +} diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts new file mode 100644 index 0000000..baa810d --- /dev/null +++ b/src/utils/fileUtils.ts @@ -0,0 +1,19 @@ +// fileUtils.ts +import fs from "fs-extra"; + +// Utility function to check if a file exists +export const checkFileExists = (filePath: string): boolean => + fs.pathExistsSync(filePath); + +// Function to rename a file +export const renameFile = async ( + oldPath: string, + newPath: string, +): Promise => { + await fs.rename(oldPath, newPath); +}; + +// Function to remove a file +export const removeFile = async (filePath: string): Promise => { + await fs.remove(filePath); +}; diff --git a/src/utils/fs.ts b/src/utils/fs.ts new file mode 100644 index 0000000..feafda3 --- /dev/null +++ b/src/utils/fs.ts @@ -0,0 +1,24 @@ +import { cwd } from "node:process"; +import { normalize } from "pathe"; + +let cachedCWD: null | string = null; + +export function getCurrentWorkingDirectory(useCache = true): string { + if (useCache && cachedCWD) { + return cachedCWD; + } + + try { + const currentDirectory = normalize(cwd()); + + if (useCache) { + cachedCWD = currentDirectory; + } + + return currentDirectory; + } catch (error) { + console.error("Error getting current working directory:", String(error)); + + throw error; + } +} diff --git a/src/utils/git.ts b/src/utils/git.ts new file mode 100644 index 0000000..159ad58 --- /dev/null +++ b/src/utils/git.ts @@ -0,0 +1,47 @@ +import type { SimpleGit } from "simple-git"; + +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; +import { simpleGit } from "simple-git"; + +// Initialize Git repository or keep existing .git folder +export async function initializeGitRepository( + dir: string, + gitOption: string, +): Promise { + try { + if (gitOption === "Initialize new Git repository") { + const git: SimpleGit = simpleGit({ baseDir: dir }); + + await git.init(); + await git.add("."); + await git.commit("Initial commit from Create Reliverse App"); + + console.info(""); + relinka.success("Git repository initialized and initial commit created."); + + // relinka.info("We recommend pushing your commits using GitHub Desktop."); + // relinka.info( + // "Learn more and find other tips by visiting https://reliverse.org.", + // ); + } else if ( + gitOption === + "Keep existing .git folder (for forking later) [🚨 option is under development, may not work]" + ) { + if (await fs.pathExists(path.join(dir, ".git"))) { + relinka.info( + "πŸ“‚ .git folder has been kept. You can make a fork from this repo later.", + ); + } else { + relinka.warn("No .git folder found in the template."); + } + } else { + relinka.info("No Git initialization performed."); + } + } catch (error) { + relinka.warn( + `πŸ€” Failed to initialize or manage the Git repository: ${String(error)}`, + ); + } +} diff --git a/src/utils/handleStringReplacements.ts b/src/utils/handleStringReplacements.ts new file mode 100644 index 0000000..6630565 --- /dev/null +++ b/src/utils/handleStringReplacements.ts @@ -0,0 +1,21 @@ +import { extractRepoInfo } from "~/utils/extractRepoInfo.js"; +import { replaceStringsInFiles } from "~/utils/replaceStringsInFiles.js"; + +export async function handleStringReplacements( + targetDir: string, + template: string, + projectName: string, + githubUser: string, + website: string, +): Promise { + const { author, projectName: oldProjectName } = extractRepoInfo(template); + + const replacements = { + [`${oldProjectName}.com`]: website, + [author]: githubUser, + [oldProjectName]: projectName, + ["relivator.com"]: website, + }; + + await replaceStringsInFiles(targetDir, replacements); +} diff --git a/src/utils/isAppInstalled.ts b/src/utils/isAppInstalled.ts new file mode 100644 index 0000000..7f7dbb2 --- /dev/null +++ b/src/utils/isAppInstalled.ts @@ -0,0 +1,26 @@ +import fs from "fs-extra"; +import os from "node:os"; + +export function isVSCodeInstalled(): boolean { + const platform = os.platform(); + const homeDir = os.homedir(); + + const commonVSCodeInstallPaths = { + darwin: [ + "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", + ], + linux: ["/usr/bin/code", "/snap/bin/code"], + win32: [ + `${homeDir}\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe`, + "C:/Program Files/Microsoft VS Code/Code.exe", + "C:/Program Files (x86)/Microsoft VS Code/Code.exe", + ], + }; + + const pathsToCheck = + commonVSCodeInstallPaths[platform as "darwin" | "linux" | "win32"] || []; + + return pathsToCheck.some((vsCodePath: string) => + fs.pathExistsSync(vsCodePath), + ); +} diff --git a/src/utils/knip.ts b/src/utils/knip.ts new file mode 100644 index 0000000..6ab8c23 --- /dev/null +++ b/src/utils/knip.ts @@ -0,0 +1,68 @@ +import { fileExists, removeFile } from "@reliverse/fs"; +import { select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import pc from "picocolors"; + +import type { KnipConfig } from "./types.js"; + +export async function configureKnip({ + knipConfig, + knipRecommendedConfig, + knipRulesDisabledConfig, +}: KnipConfig) { + const knipConfigExists = await fileExists(knipConfig); + const knipRecommendedConfigExists = await fileExists(knipRecommendedConfig); + const knipRulesDisabledConfigExists = await fileExists( + knipRulesDisabledConfig, + ); + + const knip: string | symbol = await selectPrompt({ + title: "Please select which type of Knip configuration you want to use.", + options: [ + { + label: "Skip", + value: "Skip", + hint: "Skip Knip configuration", + }, + { + label: "knip.recommended.json", + value: "Recommended", + hint: "[βœ… Default] Recommended configuration", + }, + { + label: "knip.rules-disabled.json", + value: "RulesDisabled", + hint: "Disables almost all rules", + }, + ], + }); + + if (typeof knip !== "string") { + process.exit(0); + } + + if (knip === "Skip") { + relinka.success("Knip configuration was skipped."); + + return; + } + + if (knipConfigExists) { + await removeFile(knipConfig); + } + + if (knip === "Recommended" && knipRecommendedConfigExists) { + await fs.copy(knipRecommendedConfig, knipConfig); + } else if (knip === "RulesDisabled" && knipRulesDisabledConfigExists) { + await fs.copy(knipRulesDisabledConfig, knipConfig); + } + + if (await fileExists(knipConfig)) { + relinka.success(`Knip configuration has been set to ${knip}`); + } else { + relinka.error( + "Something went wrong! Newly created `knip.json` file was not found!", + ); + } +} diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts new file mode 100644 index 0000000..cf7da89 --- /dev/null +++ b/src/utils/metadata.ts @@ -0,0 +1,13 @@ +export default { + name: "Relivator", + siteNameDesc: "Relivator 1.2.6: Next.js 15, React 19, TailwindCSS Template", + appPublisher: "Reliverse", + appVersion: "1.3.0", + author: { + email: "blefnk@gmail.com", + fullName: "Nazar Kornienko", + handle: "blefnk", + handleAt: "@blefnk", + url: "https://github.com/blefnk", + }, +}; diff --git a/src/utils/moveAppToLocale.ts b/src/utils/moveAppToLocale.ts new file mode 100644 index 0000000..b70f027 --- /dev/null +++ b/src/utils/moveAppToLocale.ts @@ -0,0 +1,54 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; + +import { DEBUG } from "~/app.js"; + +// Function to move the content of src/app to src/app/[locale] +export async function moveAppToLocale(targetDir: string): Promise { + const appDir = path.join(targetDir, "src", "app"); + const localeDir = path.join(appDir, "[locale]"); + + // Check if the app directory exists + if (await fs.pathExists(appDir)) { + DEBUG.enableVerboseLogging && + relinka.info("Moving src/app content to src/app/[locale]..."); + + // Ensure the [locale] directory exists + try { + await fs.ensureDir(localeDir); // Ensure locale directory + DEBUG.enableVerboseLogging && + relinka.success("Created src/app/[locale] directory."); + } catch (error) { + relinka.error("Failed to create [locale] directory:", error); + + return; + } + + // Move all files from src/app to src/app/[locale] + const files = await fs.readdir(appDir); + + for (const file of files) { + const oldPath = path.join(appDir, file); + const newPath = path.join(localeDir, file); + + // Skip moving the [locale] folder itself to prevent infinite recursion + if (file === "[locale]") { + continue; + } + + try { + await fs.move(oldPath, newPath); + DEBUG.enableVerboseLogging && + relinka.info(`Moved ${file} to ${newPath}`); + } catch (error) { + relinka.error(`Error moving ${file} to ${newPath}:`, error); + } + } + + DEBUG.enableVerboseLogging && + relinka.success("Files moved to src/app/[locale] successfully."); + } else { + relinka.warn("src/app directory not found."); + } +} diff --git a/src/utils/nav.ts b/src/utils/nav.ts new file mode 100644 index 0000000..55236a8 --- /dev/null +++ b/src/utils/nav.ts @@ -0,0 +1,34 @@ +export type FooterItem = { + items: { + external?: boolean; + href: string; + title: string; + }[]; + title: string; +}; + +type FooterConfig = { + link: string; + text: string; +}; + +type SocialConfig = { + alt?: string; + icon: string; + link: string; +}; + +export type NavigationKeys = "about" | "blog" | "docs" | "download" | "learn"; + +export type NavigationEntry = { + items?: Record; + label?: string; + link?: string; +}; + +export type SiteNavigation = { + footerLinks: FooterConfig[]; + sideNavigation: Record; + socialLinks: SocialConfig[]; + topNavigation: Record; +}; diff --git a/src/utils/nextjs.ts b/src/utils/nextjs.ts new file mode 100644 index 0000000..809d571 --- /dev/null +++ b/src/utils/nextjs.ts @@ -0,0 +1,114 @@ +import { fileExists, removeFile } from "@reliverse/fs"; +import { select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import { readFile, writeFile } from "node:fs/promises"; +import pc from "picocolors"; + +import type { NextJsConfig } from "~/utils/types.js"; + +const debugEnabled = false; + +export async function configureNext({ + nextConfig, + nextMinimalConfig, + nextRecommendedConfig, +}: NextJsConfig) { + const nextConfigExists = await fileExists(nextConfig); + const nextMinimalConfigExists = await fileExists(nextMinimalConfig); + const nextRecommendedConfigExists = await fileExists(nextRecommendedConfig); + + const next: string | symbol = await selectPrompt({ + maxItems: 3, + title: pc.cyan( + "Please select which type of Next.js configuration you want to use.", + ), + options: [ + { + hint: "Skip Next.js configuration", + label: "Skip", + value: "Skip", + }, + { + hint: "[βœ… Default] Recommended configuration: faster runtime (but slower builds); may be less stable", + label: "next.config.recommended.ts", + value: "Recommended", + }, + { + hint: "Minimal configuration: faster builds (but slower runtime); more stable", + label: "next.config.minimal.ts", + value: "Minimal", + }, + ], + }); + + if (typeof next !== "string") { + process.exit(0); + } + + if (next === "Skip") { + relinka.success("Next.js configuration was skipped."); + + return; + } + + if (nextConfigExists) { + await removeFile(nextConfig); + } + + if (next === "Minimal" && nextMinimalConfigExists) { + await fs.copy(nextMinimalConfig, nextConfig); + } else if (next === "Recommended" && nextRecommendedConfigExists) { + await fs.copy(nextRecommendedConfig, nextConfig); + } + + if (await fileExists(nextConfig)) { + relinka.success(`Next.js configuration has been set to ${next}`); + await updateFileToJs(nextConfig); + await replaceEnvImport(nextConfig); + } else { + relinka.error( + "Something went wrong! Newly created `next.config.js` file was not found!", + ); + } +} + +async function updateFileToJs(filePath: string) { + try { + let fileContent = await readFile(filePath, "utf8"); + + // Remove lines containing `ts-expect-error` + const lines = fileContent.split("\n"); + const filteredLines = lines.filter( + (line) => !line.includes("// @ts-expect-error TODO: fix"), + ); + + fileContent = filteredLines.join("\n"); + + await writeFile(filePath, fileContent, "utf8"); + } catch (error) { + relinka.error("Error updating file content:", error); + } +} + +async function replaceEnvImport(filePath: string) { + try { + let fileContent = await readFile(filePath, "utf8"); + + const oldImportStatement = `await import("~/env.js");`; + const newImportStatement = `await import("./src/env.js");`; + + if (fileContent.includes(oldImportStatement)) { + fileContent = fileContent.replace(oldImportStatement, newImportStatement); + await writeFile(filePath, fileContent, "utf8"); + + if (debugEnabled) { + relinka.success(`Replaced import statement in ${filePath}`); + } + } else { + relinka.info(`Import statement not found in ${filePath}`); + } + } catch (error) { + relinka.error("Error replacing import statement:", error); + } +} diff --git a/src/menu/utils/pkg.ts b/src/utils/pkg.ts similarity index 58% rename from src/menu/utils/pkg.ts rename to src/utils/pkg.ts index 90ba3f6..e7ebdd7 100644 --- a/src/menu/utils/pkg.ts +++ b/src/utils/pkg.ts @@ -1,4 +1,7 @@ import { detect, getNpmVersion } from "detect-package-manager"; +import packageJson from "~/../package.json" with { type: "json" }; + export const pm = await detect(); export const pmv = await getNpmVersion(pm); +export const pkg = packageJson; diff --git a/src/utils/products.ts b/src/utils/products.ts new file mode 100644 index 0000000..3cea8d8 --- /dev/null +++ b/src/utils/products.ts @@ -0,0 +1,221 @@ +export const sortOptions = [ + { + label: "Date: Old to new", + value: "createdAt.asc", + }, + { + label: "Date: New to old", + value: "createdAt.desc", + }, + { + label: "Price: Low to high", + value: "price.asc", + }, + { + label: "Price: High to low", + value: "price.desc", + }, + { + label: "Alphabetical: A to Z", + value: "name.asc", + }, + { + label: "Alphabetical: Z to A", + value: "name.desc", + }, +]; + +export const productCategories = [ + { + image: "/images/skateboard-one.webp", + subcategories: [ + { + description: "The board itself.", + image: "/images/deck-one.webp", + slug: "decks", + title: "Decks", + }, + { + description: "The wheels that go on the board.", + image: "/images/wheel-one.webp", + slug: "wheels", + title: "Wheels", + }, + { + description: "The trucks that go on the board.", + image: "/images/truck-one.webp", + slug: "trucks", + title: "Trucks", + }, + { + description: "The bearings that go in the wheels.", + image: "/images/bearing-one.webp", + slug: "bearings", + title: "Bearings", + }, + { + description: "The griptape that goes on the board.", + image: "/images/griptape-one.webp", + slug: "griptape", + title: "Griptape", + }, + { + description: "The hardware that goes on the board.", + image: "/images/hardware-one.webp", + slug: "hardware", + title: "Hardware", + }, + { + description: "The tools that go with the board.", + image: "/images/tool-one.webp", + slug: "tools", + title: "Tools", + }, + ], + title: "furniture", + }, + { + image: "/images/clothing-one.webp", + subcategories: [ + { + description: "Cool and comfy tees for effortless style.", + slug: "t-shirts", + title: "T-shirts", + }, + { + description: "Cozy up in trendy hoodies.", + slug: "hoodies", + title: "Hoodies", + }, + { + description: "Relaxed and stylish pants for everyday wear.", + slug: "pants", + title: "Pants", + }, + { + description: "Stay cool with casual and comfortable shorts.", + slug: "shorts", + title: "Shorts", + }, + { + description: "Top off the look with stylish and laid-back hats.", + slug: "hats", + title: "Hats", + }, + ], + title: "clothing", + }, + { + image: "/images/shoe-one.webp", + subcategories: [ + { + description: "Rad low tops shoes for a stylish low-profile look.", + slug: "low-tops", + title: "Low Tops", + }, + { + description: "Elevate the style with rad high top shoes.", + slug: "high-tops", + title: "High Tops", + }, + { + description: "Effortless style with rad slip-on shoes.", + slug: "slip-ons", + title: "Slip-ons", + }, + { + description: "Performance-driven rad shoes for the pros.", + slug: "pros", + title: "Pros", + }, + { + description: "Timeless style with rad classic shoes.", + slug: "classics", + title: "Classics", + }, + ], + title: "tech", + }, + { + image: "/images/backpack-one.webp", + subcategories: [ + { + description: "Essential tools for maintaining the skateboard, all rad.", + slug: "skate-tools", + title: "Skate Tools", + }, + { + description: "Upgrade the ride with our rad selection of bushings.", + slug: "bushings", + title: "Bushings", + }, + { + description: + "Enhance the skateboard's performance with rad shock and riser pads.", + slug: "shock-riser-pads", + title: "Shock & Riser Pads", + }, + { + description: + "Add creativity and style to the tricks with our rad skate rails.", + slug: "skate-rails", + title: "Skate Rails", + }, + { + description: "Keep the board gliding smoothly with our rad skate wax.", + slug: "wax", + title: "Wax", + }, + { + description: "Keep the feet comfy and stylish with our rad socks.", + slug: "socks", + title: "Socks", + }, + { + description: "Carry the gear in style with our rad backpacks.", + slug: "backpacks", + title: "Backpacks", + }, + ], + title: "accessories", + }, +] satisfies { + subcategories: { + description?: string; + image?: string; + slug: string; + title: string; + }[]; + image: string; + // @ts-expect-error TODO: Fix ts + title: Product["category"]; +}[]; + +export const productTags = [ + "new", + "sale", + "bestseller", + "featured", + "popular", + "trending", + "limited", + "exclusive", +]; + +export function getSubcategories(category?: string): { + label: string; + value: string; +}[] { + if (!category) { + return []; + } + + const categoryObject = productCategories.find((c) => c.title === category); + + return ( + categoryObject?.subcategories.map((s) => ({ + label: s.title, + value: s.slug, + })) || [] + ); +} diff --git a/src/utils/putout.ts b/src/utils/putout.ts new file mode 100644 index 0000000..9ba8e44 --- /dev/null +++ b/src/utils/putout.ts @@ -0,0 +1,73 @@ +import { fileExists, removeFile } from "@reliverse/fs"; +import { select, selectPrompt } from "@reliverse/prompts"; +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import pc from "picocolors"; + +import type { PutoutConfig } from "~/utils/types.js"; + +export async function configurePutout({ + putoutConfig, + putoutRecommendedConfig, + putoutRulesDisabledConfig, +}: PutoutConfig) { + const putoutConfigExists = await fileExists(putoutConfig); + const putoutRecommendedConfigExists = await fileExists( + putoutRecommendedConfig, + ); + + const putoutRulesDisabledConfigExists = await fileExists( + putoutRulesDisabledConfig, + ); + + const putout: string | symbol = await selectPrompt({ + title: pc.cyan( + "Please select which type of Putout configuration you want to use.", + ), + options: [ + { + label: "Skip", + value: "Skip", + hint: "Skip Putout configuration", + }, + { + label: ".putout.recommended.json", + value: "Recommended", + hint: "[βœ… Default] Recommended configuration", + }, + { + label: ".putout.rules-disabled.json", + value: "RulesDisabled", + hint: "Disables almost all rules", + }, + ], + }); + + if (typeof putout !== "string") { + process.exit(0); + } + + if (putout === "Skip") { + relinka.success("Putout configuration was skipped."); + + return; + } + + if (putoutConfigExists) { + await removeFile(putoutConfig); + } + + if (putout === "Recommended" && putoutRecommendedConfigExists) { + await fs.copy(putoutRecommendedConfig, putoutConfig); + } else if (putout === "RulesDisabled" && putoutRulesDisabledConfigExists) { + await fs.copy(putoutRulesDisabledConfig, putoutConfig); + } + + if (await fileExists(putoutConfig)) { + relinka.success(`Putout configuration has been set to ${putout}`); + } else { + relinka.error( + "Something went wrong! Newly created `.putout.json` file was not found!", + ); + } +} diff --git a/src/utils/replaceStringsInFiles.ts b/src/utils/replaceStringsInFiles.ts new file mode 100644 index 0000000..594c664 --- /dev/null +++ b/src/utils/replaceStringsInFiles.ts @@ -0,0 +1,65 @@ +import { relinka } from "@reliverse/relinka"; +import fs from "fs-extra"; +import path from "pathe"; + +import { DEBUG } from "~/app.js"; + +export async function replaceStringsInFiles( + targetDir: string, + oldValues: Record, +): Promise { + const fileExtensions = [ + ".js", + ".ts", + ".json", + ".md", + ".html", + ".jsx", + ".tsx", + ]; + + function shouldReplaceInFile(filename: string): boolean { + return fileExtensions.some((ext) => filename.endsWith(ext)); + } + + async function replaceInFile(filePath: string) { + const fileContent = await fs.promises.readFile(filePath, "utf8"); + + let newContent = fileContent; + + for (const key of Object.keys(oldValues)) { + const value = oldValues[key]; + + if (value !== undefined) { + // Ensure value is not undefined + const regex = new RegExp(key, "g"); // Replace all occurrences + + newContent = newContent.replace(regex, value); + } + } + + if (newContent !== fileContent) { + await fs.promises.writeFile(filePath, newContent, "utf8"); + DEBUG.enableVerboseLogging && + relinka.info(`Replaced strings in ${filePath}`); + } + } + + async function traverseDirectory(dir: string) { + const files = await fs.promises.readdir(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = await fs.promises.lstat(fullPath); + + if (stat.isDirectory()) { + await traverseDirectory(fullPath); + } else if (shouldReplaceInFile(file)) { + await replaceInFile(fullPath); + } + } + } + + await traverseDirectory(targetDir); + relinka.success("All string replacements have been made."); +} diff --git a/src/utils/shadcnComponents.ts b/src/utils/shadcnComponents.ts new file mode 100644 index 0000000..200db94 --- /dev/null +++ b/src/utils/shadcnComponents.ts @@ -0,0 +1,15 @@ +import type { Command } from "commander"; + +import { relinka } from "@reliverse/relinka"; + +// import { add } from "~/utils/cmds/add"; +// import { diff } from "~/utils/cmds/diff"; +// import { init } from "~/utils/cmds/init"; + +export async function shadcnComponents(program: Command) { + relinka.success( + "✨ Using: shadcn@2.1.3, shadcn-vue@0.11.0, shadcn-svelte@0.14.0", + ); + + // program.addCommand(init).addCommand(add).addCommand(diff); +} diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..5d73831 --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,14 @@ +// export type PackageManager = "bun" | "pnpm" | "yarn" | "npm"; +// export type SemanticVersionFramework = "1.2.6"; +// export type SemanticVersionEngine = "0.4.0"; +export type CamelCase = T extends `${infer U}${infer V}` + ? `${Uppercase}${V}` + : T; + +export type HyphenatedStringToCamelCase = + S extends `${infer T}-${infer U}` + ? `${T}${HyphenatedStringToCamelCase>}` + : CamelCase; + +export type HyphenatedDataStringToCamelCase = + S extends `data-${infer U}` ? HyphenatedStringToCamelCase : S; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..d5b8ed4 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,39 @@ +export type ApptsConfig = { + apptsConfig: string; +}; + +export type NextJsConfig = { + nextConfig: string; + nextMinimalConfig: string; + nextRecommendedConfig: string; +}; + +export type EnvJsConfig = { + envConfig: string; + envRecommendedConfig: string; + envRulesDisabledConfig: string; +}; + +export type EslintConfig = { + eslintConfig: string; + eslintRulesDisabledConfig: string; + eslintUltimateConfig: string; +}; + +export type BiomeConfig = { + biomeConfig: string; + biomeRecommendedConfig: string; + biomeRulesDisabledConfig: string; +}; + +export type KnipConfig = { + knipConfig: string; + knipRecommendedConfig: string; + knipRulesDisabledConfig: string; +}; + +export type PutoutConfig = { + putoutConfig: string; + putoutRecommendedConfig: string; + putoutRulesDisabledConfig: string; +}; diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 0000000..317c89e --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,34 @@ +import { relinka } from "@reliverse/relinka"; + +function handlePromptCancellation(input: unknown, exitMessage: string): void { + if (typeof input === "symbol" && String(input) === "Symbol(clack:cancel)") { + relinka.info(exitMessage); + process.exit(0); + } +} + +export function validate( + input: unknown, + type: + | "bigint" + | "boolean" + | "function" + | "number" + | "object" + | "string" + | "symbol" + | "undefined", + exitMessage = `Invalid input: Expected a ${type}, but got ${String(input)}`, +): void { + handlePromptCancellation(input, exitMessage); + + if ( + typeof input !== type || + input === undefined || + input === null || + (type === "string" && input === "") + ) { + relinka.error(exitMessage); + process.exit(0); + } +} diff --git a/src/utils/with.ts b/src/utils/with.ts new file mode 100644 index 0000000..49bdeed --- /dev/null +++ b/src/utils/with.ts @@ -0,0 +1,34 @@ +type IconName = + | "billing" + | "dollarSign" + | "laptop" + | "settings" + | "store" + | "terminal" + | "user"; + +type NavItem = { + description?: string; + disabled?: boolean; + external?: boolean; + href: string; + icon?: IconName; + label?: string; + title: string; +}; + +type NavItemWithChildren = { + items: NavItemWithChildren[]; +} & NavItem; + +type NavItemWithOptionalChildren = { + items?: NavItemWithChildren[]; +} & NavItem; + +export type MainMenuItem = NavItemWithOptionalChildren; + +export type SidebarNavItem = NavItemWithChildren; + +export type GeneralShellProps = { + header?: any; +}; diff --git a/tsconfig.json b/tsconfig.json index 41710e6..c22f897 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { + "rootDir": ".", "baseUrl": ".", "paths": { "~/*": ["./src/*"], - "#/*": ["./addons/*"], - "@/*": ["./examples/*"] + "#/*": ["./addons/*"] }, "outDir": "dist-npm", "module": "NodeNext", @@ -26,13 +26,13 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": false, - "noEmitOnError": true, - "useDefineForClassFields": true, "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, "verbatimModuleSyntax": true, "strictNullChecks": false, "isolatedModules": true, "noImplicitAny": false, + "noEmitOnError": true, "skipLibCheck": true, "allowJs": true, "noEmit": true @@ -47,7 +47,9 @@ "vitest.config.ts", "src/**/*.ts", "addons/**/*.ts", - "examples/**/*.ts" + "reset.d.ts", + "build.publish.ts", + "bump.config.ts" ], - "exclude": ["dist-npm", "dist-jsr", "node_modules"] + "exclude": ["node_modules", "dist-jsr", "dist-npm"] }