@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
216 lines (215 loc) • 7.86 kB
JavaScript
import path from "@reliverse/pathkit";
import fs from "@reliverse/relifso";
import { relinka } from "@reliverse/relinka";
import { destr } from "destr";
import { readPackageJSON } from "pkg-types";
import { glob } from "tinyglobby";
import { extractRepoInfo, replaceStringsInFiles } from "./reps-impl.js";
import { CommonPatterns, HardcodedStrings } from "./reps-keys.js";
async function getPackageNames(projectPath) {
try {
const pkgPath = path.join(projectPath, "package.json");
if (!await fs.pathExists(pkgPath)) return [];
const pkg = await readPackageJSON(pkgPath);
const allDeps = {
...pkg.dependencies || {},
...pkg.devDependencies || {},
...pkg.peerDependencies || {}
};
return Object.keys(allDeps);
} catch (error) {
relinka("warn", "Failed to read package.json:", String(error));
return [];
}
}
async function getImportPaths(projectPath) {
const importPaths = /* @__PURE__ */ new Set();
try {
const files = await glob("**/*.{js,ts,jsx,tsx}", {
cwd: projectPath,
ignore: ["node_modules/**", "dist/**", ".next/**", "build/**"]
});
for (const file of files) {
const content = await fs.readFile(path.join(projectPath, file), "utf-8");
const importMatches = [
...content.matchAll(/(?:import|from)\s+['"]([^'"]+)['"]/g),
...content.matchAll(/import\(['"]([^'"]+)['"]\)/g)
];
for (const match of importMatches) {
const importPath = match[1];
if (importPath && !importPath.startsWith(".")) {
importPaths.add(importPath);
}
}
}
} catch (error) {
relinka("warn", "Failed to gather import paths:", String(error));
}
return [...importPaths];
}
async function getRepositoryUrl(projectPath) {
try {
const pkgPath = path.join(projectPath, "package.json");
if (!await fs.pathExists(pkgPath)) return "";
const pkg = await readPackageJSON(pkgPath);
if (pkg.repository) {
if (typeof pkg.repository === "string") {
if (!pkg.repository.includes("://")) {
return `https://github.com/${pkg.repository}`;
}
return pkg.repository;
}
if (typeof pkg.repository === "object" && pkg.repository.url) {
return pkg.repository.url;
}
}
if (pkg.name?.startsWith("@")) {
const [scope, name] = pkg.name.slice(1).split("/");
return `https://github.com/${scope}/${name}`;
}
return "";
} catch (error) {
relinka("warn", "Failed to read package.json:", String(error));
return "";
}
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function capitalizeWithDashes(str) {
return str.split("-").map((word) => capitalize(word)).join(" ");
}
function createCaseVariations(oldValue, newValue) {
return {
[oldValue.toLowerCase()]: newValue.toLowerCase(),
[capitalize(oldValue)]: capitalize(newValue),
[oldValue.toUpperCase()]: newValue.toUpperCase(),
[capitalizeWithDashes(oldValue)]: capitalizeWithDashes(newValue)
};
}
export async function handleReplacements(projectPath, selectedRepo, externalrseth, config, existingRepo, showSuccessMessage, isTemplateDownload) {
const term = isTemplateDownload ? "template" : "repo";
relinka("verbose", "Personalizing texts in the initialized files...");
const [packageNames, importPaths] = await Promise.all([
getPackageNames(projectPath),
getImportPaths(projectPath)
]);
let externalConfig;
if (existingRepo && externalrseth && await fs.pathExists(externalrseth)) {
try {
const externalConfigContent = await fs.readFile(externalrseth, "utf-8");
const parsed = destr(externalConfigContent);
if (parsed && typeof parsed === "object") {
externalConfig = parsed;
relinka(
"info",
"Found external config from existing repo, will use its values for replacements"
);
}
} catch (error) {
relinka("warn", "Failed to parse external config:", String(error));
}
}
const { inputRepoAuthor, inputRepoName } = extractRepoInfo(selectedRepo);
let templateUrl = await getRepositoryUrl(projectPath);
if (!templateUrl) {
templateUrl = HardcodedStrings.RelivatorDomain;
}
const replacementsMap = {
...createCaseVariations(HardcodedStrings.GeneralTemplate, "project"),
[HardcodedStrings.RelivatorDomain]: config.primaryDomain,
[templateUrl]: config.primaryDomain,
[`${inputRepoName}.vercel.app`]: `${config.projectName}.vercel.app`,
...createCaseVariations(inputRepoName, config.projectName),
...createCaseVariations(
HardcodedStrings.RelivatorShort,
config.projectName
),
...createCaseVariations(inputRepoAuthor, config.frontendUsername),
...createCaseVariations(
HardcodedStrings.DefaultAuthor,
config.frontendUsername
),
[CommonPatterns.githubUrl(inputRepoAuthor, inputRepoName)]: CommonPatterns.githubUrl(config.frontendUsername, config.projectName),
[CommonPatterns.githubUrl(
HardcodedStrings.DefaultAuthor,
HardcodedStrings.RelivatorLower
)]: CommonPatterns.githubUrl(config.frontendUsername, config.projectName),
[CommonPatterns.packageName(inputRepoName)]: CommonPatterns.packageName(
config.projectName
),
[CommonPatterns.packageName(HardcodedStrings.RelivatorLower)]: CommonPatterns.packageName(config.projectName),
[HardcodedStrings.RelivatorTitle]: config.projectDescription ? `${capitalizeWithDashes(config.projectName)} - ${config.projectDescription}` : `${capitalizeWithDashes(config.projectName)} - A modern web application for your business needs`,
[HardcodedStrings.DefaultEmail]: config.frontendUsername.includes("@") ? config.frontendUsername : `${config.frontendUsername}@${config.primaryDomain}`
};
if (externalConfig) {
if (externalConfig.projectName && externalConfig.projectName !== config.projectName) {
replacementsMap[externalConfig.projectName] = config.projectName;
replacementsMap[externalConfig.projectName.toLowerCase()] = config.projectName.toLowerCase();
replacementsMap[capitalize(externalConfig.projectName)] = capitalizeWithDashes(config.projectName);
}
if (externalConfig.projectAuthor && externalConfig.projectAuthor !== config.frontendUsername) {
replacementsMap[externalConfig.projectAuthor] = config.frontendUsername;
}
if (externalConfig.projectDescription) {
replacementsMap[externalConfig.projectDescription] = config.projectDescription ?? "";
}
}
const validReplacements = Object.fromEntries(
Object.entries(replacementsMap).filter(
([key, value]) => key && value && key !== value && key.length > 1
)
);
try {
await replaceStringsInFiles(projectPath, validReplacements, {
verbose: true,
fileExtensions: [
".js",
".ts",
".jsx",
".tsx",
".json",
"package.json",
".jsonc",
".md",
".mdx",
".html",
".css",
".scss",
".env",
".env.example",
"README.md"
],
excludedDirs: [
"node_modules",
".git",
"build",
".next",
"dist",
"coverage",
".cache",
".vercel",
".github"
],
stringExclusions: [
"@types/",
/^(?:https?:\/\/)?[^\s/$.?#].[^\s]*\.[a-z]{2,}(?:\/[^\s]*)?$/i.source,
...packageNames,
...importPaths
],
dryRun: false,
skipBinaryFiles: true,
maxConcurrency: 10,
stopOnError: false
});
if (showSuccessMessage) {
relinka("success", `Successfully personalized ${term} files!`);
}
} catch (error) {
relinka(
"error",
`\u274C Failed to personalize ${term} files:`,
error instanceof Error ? error.message : String(error)
);
}
}