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