UNPKG

@reliverse/rse

Version:

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

446 lines (445 loc) 15.2 kB
import { generateRseConfig } from "@reliverse/cfg"; import path from "@reliverse/pathkit"; import { ensuredir } from "@reliverse/relifso"; import fs from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { defineCommand } from "@reliverse/rempts"; import { parseJSONC } from "confbox"; import { execaCommand } from "execa"; import { jsonrepair } from "jsonrepair"; import { loadFile, writeFile, builders } from "magicast"; import { UNKNOWN_VALUE } from "../../libs/sdk/constants.js"; import { downloadFileFromGitHub, ensureEnvCacheDir, getEnvCacheDir, getEnvCachePath, logVerbose } from "../../libs/sdk/mrse/mrse-impl.js"; export default defineCommand({ meta: { name: "mrse", description: "Generate rse config files for multiple projects", hidden: true }, args: { ts: { type: "boolean", description: "Generate TypeScript config files (default is JSONC)", default: false }, dev: { type: "boolean", description: "Generate configs in development mode", default: false }, jsonc: { type: "boolean", description: "Generate JSONC config files (default)", default: true }, nocache: { type: "boolean", description: "Disable caching of downloaded .env.example files", default: false }, fresh: { type: "boolean", description: "Redownload all cached .env.example files, ignoring existing cache", default: false }, typesPath: { type: "string", description: "Custom path to type definitions for TypeScript configs", default: "../src/libs/sdk/sdk-mod" }, _: { type: "array", description: "Names of projects to generate configs for", required: false } }, run: async ({ args }) => { const useJsonc = !args.ts; const projectNames = args._ || []; const devFlag = args.dev === true; const isDev = devFlag || process.env.NODE_ENV === "development"; const cacheFlag = !args.nocache; const freshFlag = args.fresh === true; logVerbose("Parsed arguments:", args); logVerbose("Project names:", projectNames); logVerbose("Format:", useJsonc ? "JSONC" : "TypeScript"); logVerbose("Dev mode:", isDev); logVerbose("Using cache:", cacheFlag); logVerbose("Fresh mode:", freshFlag); if (freshFlag) { relinka( "info", "Fresh mode enabled: Will redownload all cached .env.example files" ); } const cwd = process.cwd(); const mrseFolderPath = path.join(cwd, ".config", "mrse"); await ensuredir(mrseFolderPath); const mrseFileName = useJsonc ? "mrse.jsonc" : "mrse.ts"; const mrsePath = path.join(cwd, ".config", mrseFileName); const mrseExists = await fs.pathExists(mrsePath); const oppositeFormatFileName = useJsonc ? "mrse.ts" : "mrse.jsonc"; const oppositeFormatPath = path.join( cwd, ".config", oppositeFormatFileName ); const oppositeFormatExists = await fs.pathExists(oppositeFormatPath); if (!mrseExists && oppositeFormatExists) { relinka( "error", `Cannot generate ${mrseFileName} when ${oppositeFormatFileName} already exists. Please delete ${oppositeFormatFileName} first or use the appropriate format flag.` ); process.exit(1); } let genCfgData = []; const existingFiles = await fs.readdir(mrseFolderPath); const hasJsoncFiles = existingFiles.some( (file) => file.endsWith(".jsonc") && file !== "mrse.jsonc" ); const hasTsFiles = existingFiles.some( (file) => file.endsWith(".ts") && file !== "mrse.ts" ); if (useJsonc && hasTsFiles || !useJsonc && hasJsoncFiles) { const currentFormat = useJsonc ? "JSONC" : "TypeScript"; const existingFormat = useJsonc ? "TypeScript" : "JSONC"; relinka( "error", `Cannot generate ${currentFormat} files when ${existingFormat} files already exist in the mrse folder. Please use --${useJsonc ? "ts" : "jsonc"} flag to match your existing configuration format.` ); process.exit(1); } if (!mrseExists) { relinka("info", `Generating ${mrseFileName} file...`); let mrseContent = ""; if (useJsonc) { mrseContent = `{ // @reliverse/rse mrse mode // \u{1F449} ${isDev ? "`bun dev:mrse`" : "`rse mrse`"} "genCfg": [ { "projectName": "project1", "projectTemplate": "blefnk/relivator-nextjs-template", "getEnvExample": true }, { "projectName": "project2", "projectTemplate": "blefnk/relivator-nextjs-template", "getEnvExample": true }, { "projectName": "project3", "projectTemplate": "blefnk/relivator-nextjs-template", "getEnvExample": true } ] }`; } else { mrseContent = `// @reliverse/rse mrse mode // \u{1F449} ${isDev ? "`bun dev:mrse`" : "`rse mrse`"} type GenCfg = { projectName: string; projectTemplate: string; getEnvExample: boolean; projectPath?: string; }; export const genCfg: GenCfg[] = [ { projectName: "project1", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true, }, { projectName: "project2", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true, }, { projectName: "project3", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true, }, ]; `; } if (useJsonc) { await fs.writeFile(mrsePath, mrseContent); } else { try { const mod = builders.raw(mrseContent); await writeFile(mod, mrsePath); } catch { await fs.writeFile(mrsePath, mrseContent); } } relinka("success", `Generated ${mrseFileName} in .config folder`); genCfgData = [ { projectName: "project1", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true }, { projectName: "project2", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true }, { projectName: "project3", projectTemplate: "blefnk/relivator-nextjs-template", getEnvExample: true } ]; if (projectNames.length === 0) { relinka( "info", "No project names specified. Only generated the config file." ); return; } } else { relinka("info", `Using existing ${mrseFileName} file`); try { if (useJsonc) { const fileContent = await fs.readFile(mrsePath, "utf-8"); try { const parsedData = parseJSONC(fileContent); genCfgData = parsedData.genCfg || []; } catch (parseError) { relinka( "warn", `JSONC parsing failed, attempting to repair the file: ${parseError instanceof Error ? parseError.message : String(parseError)}` ); try { const commentStrippedContent = fileContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); const repairedJson = jsonrepair(commentStrippedContent); const parsedData = JSON.parse(repairedJson); genCfgData = parsedData.genCfg || []; relinka("success", "JSON repaired successfully"); } catch (repairError) { relinka( "error", `Failed to repair JSON: ${repairError instanceof Error ? repairError.message : String(repairError)}` ); throw new Error( "Unable to parse or repair the JSON configuration file" ); } } } else { try { const mod = await loadFile(mrsePath); if (mod.exports && mod.exports.genCfg) { genCfgData = JSON.parse( JSON.stringify(mod.exports.genCfg) ); } else { relinka( "warn", "The mrse.ts file does not export a 'genCfg' array" ); genCfgData = []; } } catch (error) { relinka( "error", `Error loading TypeScript file: ${error instanceof Error ? error.message : String(error)}` ); genCfgData = []; } } logVerbose("Loaded mrse data:", genCfgData); } catch (error) { relinka( "error", `Error parsing ${mrseFileName}: ${error instanceof Error ? error.message : String(error)}` ); relinka("warn", "Continuing with empty configuration"); genCfgData = []; } } let projectsToProcess = []; if (projectNames.length > 0) { const matchingProjects = projectNames.filter( (name) => genCfgData.some((cfg) => cfg.projectName === name) ); if (matchingProjects.length > 0) { projectsToProcess = matchingProjects; relinka( "info", `Found ${matchingProjects.length} matching projects in ${mrseFileName}` ); } else { projectsToProcess = projectNames; } } else if (mrseExists) { projectsToProcess = genCfgData.map((cfg) => cfg.projectName); relinka( "info", `Processing all ${projectsToProcess.length} projects from ${mrseFileName}` ); } if (projectsToProcess.length === 0) { relinka( "info", "No projects to process. Either specify project names or populate the gen.cfg file." ); return; } if (cacheFlag && genCfgData.some((cfg) => cfg.getEnvExample && cfg.projectTemplate)) { await ensureEnvCacheDir(); logVerbose(`Env cache directory: ${getEnvCacheDir()}`); } let generatedCount = 0; let envFilesDownloaded = 0; let envFilesFromCache = 0; let envFilesRefreshed = 0; for (const projectName of projectsToProcess) { relinka("verbose", `Generating config for project: ${projectName}`); try { const projectConfig = genCfgData.find( (cfg) => cfg.projectName === projectName ); logVerbose(`Found config for ${projectName}:`, projectConfig); if (projectConfig?.getEnvExample && projectConfig?.projectTemplate) { const cachePath = getEnvCachePath(projectConfig.projectTemplate); const cacheExists = cacheFlag && await fs.pathExists(cachePath); if (cacheExists && !freshFlag) { relinka( "info", `Using cached .env.example for ${projectName} from ${projectConfig.projectTemplate}` ); const envFilePath = path.join(mrseFolderPath, `${projectName}.env`); await fs.copy(cachePath, envFilePath); relinka("success", `Created ${projectName}.env from cache`); envFilesFromCache++; } else { const isRefreshing = freshFlag && cacheExists; if (isRefreshing) { relinka( "info", `Refreshing .env.example for ${projectName} from ${projectConfig.projectTemplate}` ); } else { relinka( "info", `Downloading .env.example for ${projectName} from ${projectConfig.projectTemplate}` ); } const envContent = await downloadFileFromGitHub( projectConfig.projectTemplate, ".env.example", "main", cacheFlag, freshFlag ); if (envContent) { const envFilePath = path.join( mrseFolderPath, `${projectName}.env` ); await fs.writeFile(envFilePath, envContent); if (isRefreshing) { relinka("success", `Refreshed and saved ${projectName}.env`); envFilesRefreshed++; } else { relinka("success", `Downloaded and saved ${projectName}.env`); envFilesDownloaded++; } } else { relinka( "warn", `Could not download .env.example for ${projectName}` ); } } } const configFileName = useJsonc ? `${projectName}.jsonc` : `${projectName}.ts`; const configPath = path.join(mrseFolderPath, configFileName); const fileExists = await fs.pathExists(configPath); logVerbose(`File ${configPath} already exists: ${fileExists}`); if (fileExists) { relinka( "warn", `Skipping ${projectName} - file already exists: ${configFileName}` ); continue; } const githubUsername = UNKNOWN_VALUE; const skipInstallPrompt = !useJsonc && isDev; let customTypeImportPath; if (!useJsonc && isDev) { customTypeImportPath = args.typesPath; } logVerbose("Using custom type import path:", customTypeImportPath); await generateRseConfig({ projectName, frontendUsername: UNKNOWN_VALUE, deployService: "vercel", primaryDomain: `https://${projectName}.vercel.app`, projectPath: projectConfig?.projectPath || cwd, githubUsername, isDev, customOutputPath: mrseFolderPath, customFilename: configFileName, skipInstallPrompt, ...customTypeImportPath ? { customPathToTypes: customTypeImportPath } : {}, // Override project config fields ...projectConfig?.projectTemplate ? { projectTemplate: projectConfig.projectTemplate } : {}, overrides: {} }); relinka( "success", `Generated ${configFileName} in .config/mrse folder` ); generatedCount++; } catch (error) { relinka( "error", `Failed to generate config for ${projectName}: ${error instanceof Error ? error.message : String(error)}` ); } } if (generatedCount > 0 || envFilesDownloaded > 0 || envFilesFromCache > 0 || envFilesRefreshed > 0) { if (generatedCount > 0) { relinka( "success", `Generated ${generatedCount} configs in the .config/mrse folder.` ); } if (envFilesDownloaded > 0) { relinka( "success", `Downloaded ${envFilesDownloaded} .env files from templates.` ); } if (envFilesFromCache > 0) { relinka("success", `Used ${envFilesFromCache} .env files from cache.`); } if (envFilesRefreshed > 0) { relinka( "success", `Refreshed ${envFilesRefreshed} .env files from templates.` ); } } else { relinka("info", "No new files were generated."); } if (isDev) { await fs.copy( path.join(cwd, "schema.json"), path.join(mrseFolderPath, "schema.json") ); await execaCommand("bunx biome check --write .", { cwd: mrseFolderPath, stdio: "inherit" }); } } });