create-links-base
Version:
CLI tool to create a new Links Base project
472 lines (459 loc) • 14.2 kB
JavaScript
// src/index.ts
import { Command } from "commander";
// src/commands/init.ts
import boxen2 from "boxen";
import chalk2 from "chalk";
import fs3 from "fs/promises";
import ora from "ora";
import prompts from "prompts";
import { fileURLToPath as fileURLToPath2 } from "url";
// src/utils/banner.ts
import boxen from "boxen";
import figlet from "figlet";
import gradient from "gradient-string";
async function showBanner() {
const gradientColors = gradient([
{ color: "#00ff00", pos: 0 },
{ color: "#00ffff", pos: 0.5 },
{ color: "#ff00ff", pos: 1 }
]);
const text = await new Promise((resolve) => {
figlet(
"Links Base",
{
font: "Standard",
horizontalLayout: "default",
verticalLayout: "default"
},
(err, data) => {
if (err) {
resolve("Links Base");
return;
}
resolve(data || "Links Base");
}
);
});
const gradientText = gradientColors(text);
const info = boxen(
"Create a new Links Base project\nA modern, static link management application",
{
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "cyan"
}
);
console.log(gradientText);
console.log(info);
}
// src/utils/copy.ts
import { exec } from "child_process";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
// src/constants/packages.ts
var CORE_PACKAGES = [
"@links-base/ui",
"@links-base/tsconfig",
"@links-base/tailwind-config"
];
var PACKAGE_NAME_MAP = {
tsconfig: "tsconfig",
ui: "ui",
"tailwind-config": "tailwind"
};
// src/utils/copy.ts
var execAsync = promisify(exec);
var __dirname = path.dirname(fileURLToPath(import.meta.url));
var isDevelopment = () => process.env.NODE_ENV === "development";
var getProjectRoot = () => {
return path.join(__dirname, "../../..");
};
async function copyFiles(targetDir) {
if (isDevelopment()) {
await copyLocalFiles(targetDir);
} else {
await downloadRemoteFiles(targetDir);
}
}
async function copyLocalFiles(targetDir) {
const projectRoot = getProjectRoot();
const appDir = path.join(projectRoot, "apps/app");
const excludeDirs = [
"node_modules",
".next",
"out",
".turbo",
"coverage",
".git",
"dist",
".cache",
".vercel",
".env*"
];
await copyDir(appDir, targetDir, excludeDirs);
const packagesDir = path.join(projectRoot, "packages");
await copyPackages(packagesDir, targetDir);
await copyRootFiles(projectRoot, targetDir);
}
async function copyRootFiles(projectRoot, targetDir) {
const rootFiles = [
".gitignore",
".editorconfig",
".npmrc",
".nvmrc",
"LICENCE"
];
for (const file of rootFiles) {
const srcPath = path.join(projectRoot, file);
const destPath = path.join(targetDir, file);
try {
await fs.copyFile(srcPath, destPath);
} catch (error) {
if (error instanceof Error) {
console.warn(`Warning: Could not copy ${file}: ${error.message}`);
} else {
console.warn(`Warning: Could not copy ${file}: Unknown error occurred`);
}
}
}
}
async function downloadRemoteFiles(targetDir) {
await execAsync(`npx degit thedaviddias/links-base/apps/app ${targetDir}`);
const packagesDir = path.join(targetDir, "packages");
await fs.mkdir(packagesDir, { recursive: true });
for (const pkg of CORE_PACKAGES) {
const pkgName = pkg.split("/")[1];
await execAsync(
`npx degit thedaviddias/links-base/packages/${pkgName} ${packagesDir}/${pkgName}`
);
}
}
async function copyPackages(packagesDir, targetDir) {
for (const pkg of CORE_PACKAGES) {
const pkgName = pkg.split("/")[1];
const folderName = PACKAGE_NAME_MAP[pkgName] || pkgName;
const srcDir = path.join(packagesDir, folderName);
const destDir = path.join(targetDir, "packages", pkgName);
await fs.mkdir(destDir, { recursive: true });
await copyPackageJson(srcDir, destDir);
await copyDir(srcDir, destDir, [
"node_modules",
".turbo",
"dist",
"coverage",
".git",
".cache",
".env*",
".vercel",
"codecov.yml"
]);
}
}
async function copyDir(src, dest, excludeDirs = []) {
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (excludeDirs.includes(entry.name)) continue;
if (entry.isDirectory()) {
await fs.mkdir(destPath, { recursive: true });
await copyDir(srcPath, destPath, excludeDirs);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
async function copyPackageJson(srcDir, destDir) {
const srcPackageJson = path.join(srcDir, "package.json");
const destPackageJson = path.join(destDir, "package.json");
const pkgContent = JSON.parse(await fs.readFile(srcPackageJson, "utf8"));
updateWorkspaceDependencies(pkgContent);
await fs.writeFile(destPackageJson, JSON.stringify(pkgContent, null, 2));
}
function updateWorkspaceDependencies(pkgContent) {
;
["dependencies", "devDependencies"].forEach((depType) => {
const deps = pkgContent[depType];
if (deps) {
Object.keys(deps).forEach((dep) => {
if (dep.startsWith("@links-base/")) {
deps[dep] = "workspace:*";
}
});
}
});
}
// src/utils/dependencies.ts
import { exec as execCallback } from "child_process";
import { promisify as promisify2 } from "util";
var exec2 = promisify2(execCallback);
async function installDependencies(targetDir) {
await exec2("pnpm install", { cwd: targetDir });
}
// src/utils/logger.ts
import chalk from "chalk";
var logger = {
info: (...args) => console.log(chalk.blue(...args)),
success: (...args) => console.log(chalk.green(...args)),
warning: (...args) => console.log(chalk.yellow(...args)),
error: (...args) => console.log(chalk.red(...args)),
subtle: (...args) => console.log(chalk.gray(...args))
};
// src/utils/package-json.ts
import fs2 from "fs/promises";
import path2 from "path";
async function updatePackageJson(targetDir) {
const pkgPath = path2.join(targetDir, "package.json");
const pkg = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
removeUnwantedDependencies(pkg);
updateWorkspaceReferences(pkg);
await writePackageFiles(targetDir, pkg);
}
function removeUnwantedDependencies(pkg) {
delete pkg.dependencies["@links-base/web"];
delete pkg.dependencies["@links-base/e2e"];
delete pkg.dependencies["@links-base/keystatic"];
delete pkg.workspaces;
}
function updateWorkspaceReferences(pkg) {
for (const pkgName of CORE_PACKAGES) {
if (pkg.dependencies?.[pkgName]) {
pkg.dependencies[pkgName] = "workspace:*";
}
}
}
async function writePackageFiles(targetDir, pkg) {
const pkgPath = path2.join(targetDir, "package.json");
await fs2.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
const workspaceConfig = `packages:
- 'packages/*'
`;
await fs2.writeFile(
path2.join(targetDir, "pnpm-workspace.yaml"),
workspaceConfig
);
}
// src/commands/init.ts
async function init(projectName, options = {}) {
await showBanner();
const spinner = ora({
spinner: "dots",
color: "cyan"
});
try {
const targetDir = await getProjectName(projectName);
if (options.dryRun) {
await handleDryRun(targetDir, spinner);
return;
}
const steps = [
{
text: "Creating project directory",
action: async () => {
await fs3.mkdir(targetDir, { recursive: true });
}
},
{
text: "Copying project files",
action: async () => {
await copyFiles(targetDir);
await updatePackageJson(targetDir);
}
},
{
text: "Installing dependencies",
action: async () => {
await installDependencies(targetDir);
}
}
];
for (const step of steps) {
spinner.start(step.text);
await step.action();
spinner.succeed();
}
logger.success(
boxen2(`\u{1F389} Successfully created Links Base app at ${targetDir}!`, {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "green"
})
);
logger.info("\n\u{1F4E6} Available commands:\n");
const commands = [
["pnpm dev", "Start the development server"],
["pnpm build", "Build for production"],
["pnpm start", "Run production build"]
];
commands.forEach(([command, description]) => {
logger.info(` ${chalk2.cyan(command)}`);
logger.subtle(` ${description}
`);
});
logger.info("\n\u{1F680} Get started with:\n");
logger.info(` ${chalk2.cyan(`cd ${targetDir}`)}`);
logger.info(` ${chalk2.cyan("pnpm dev")}
`);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error occurred";
spinner.fail(`Failed to create project: ${errorMessage}`);
process.exit(1);
}
}
async function getProjectName(projectName) {
if (projectName) return projectName;
const response = await prompts({
type: "text",
name: "projectName",
message: "What is your project named?",
initial: "my-links-app"
});
if (!response.projectName) {
logger.error("Project name is required");
process.exit(1);
}
return response.projectName;
}
async function handleDryRun(targetDir, spinner) {
await Promise.resolve();
try {
spinner.start("Simulating project creation...");
spinner.info(`Would create directory: ${targetDir}`);
spinner.info(`Would copy app files from apps/app to ${targetDir}`);
spinner.info(`Would copy packages to ${targetDir}/packages`);
spinner.info("Would update package.json");
spinner.info("Would install dependencies using pnpm");
spinner.succeed("Dry run completed - no changes made");
} catch (err) {
const error = err;
spinner.fail(`Dry run failed: ${error.message}`);
throw error;
}
}
if (import.meta.url === fileURLToPath2(import.meta.url)) {
init().catch((error) => {
console.error("Failed to initialize project:", error);
process.exit(1);
});
}
// src/commands/update.ts
import { exec as execCallback2 } from "child_process";
import fs4 from "fs/promises";
import ora2 from "ora";
import path3 from "path";
import { promisify as promisify3 } from "util";
var exec3 = promisify3(execCallback2);
async function checkIsLinksBaseProject() {
try {
const pkg = JSON.parse(
await fs4.readFile(path3.join(process.cwd(), "package.json"), "utf8")
);
return pkg.dependencies?.["@links-base/ui"] !== void 0;
} catch {
return false;
}
}
async function getCurrentVersion() {
const pkg = JSON.parse(
await fs4.readFile(path3.join(process.cwd(), "package.json"), "utf8")
);
return pkg.version || "0.0.0";
}
async function getLatestVersion() {
const { stdout } = await exec3("npm view @links-base/ui version");
return stdout.trim();
}
async function hasLocalChanges() {
try {
const { stdout } = await exec3("git status --porcelain");
return stdout.trim().length > 0;
} catch {
return false;
}
}
async function performUpdate() {
const targetDir = process.cwd();
await backupUserConfigs();
await downloadRemoteFiles(targetDir);
await exec3("pnpm install");
await restoreUserConfigs();
}
async function backupUserConfigs() {
const filesToBackup = [
".env",
".env.local",
"next.config.js",
"tailwind.config.js"
];
for (const file of filesToBackup) {
try {
await fs4.copyFile(
path3.join(process.cwd(), file),
path3.join(process.cwd(), `${file}.backup`)
);
} catch {
}
}
}
async function restoreUserConfigs() {
const backupFiles = await fs4.readdir(process.cwd());
for (const file of backupFiles) {
if (file.endsWith(".backup")) {
const originalFile = file.replace(".backup", "");
await fs4.rename(
path3.join(process.cwd(), file),
path3.join(process.cwd(), originalFile)
);
}
}
}
async function update(options = {}) {
await showBanner();
const spinner = ora2({ spinner: "dots", color: "cyan" });
try {
const isLinksBaseProject = await checkIsLinksBaseProject();
if (!isLinksBaseProject) {
logger.error("Not in a Links Base project directory");
process.exit(1);
}
spinner.start("Checking for updates...");
const currentVersion = await getCurrentVersion();
const latestVersion = await getLatestVersion();
if (currentVersion === latestVersion) {
spinner.succeed("You're already using the latest version!");
return;
}
if (options.dryRun) {
spinner.info(`Would update from ${currentVersion} to ${latestVersion}`);
return;
}
if (!options.force && await hasLocalChanges()) {
spinner.fail("Local changes detected. Use --force to override");
process.exit(1);
}
spinner.start(`Updating to version ${latestVersion}...`);
await performUpdate();
spinner.succeed(`Successfully updated to version ${latestVersion}`);
logger.info("\n\u{1F389} Update complete!");
logger.info("\nPlease review any changes and test your application.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error occurred";
spinner.fail(`Update failed: ${errorMessage}`);
process.exit(1);
}
}
// src/index.ts
var program = new Command();
program.name("links-base").description("CLI tool to create and manage Links Base projects").version("1.0.0");
program.command("init").description("Create a new Links Base project").argument("[name]", "Project name").option("--dry-run", "Show what would be done without making changes", false).action(async (name, options) => {
await init(name, options);
});
program.command("update").description("Update Links Base to the latest version").option("--dry-run", "Show what would be done without making changes", false).option("--force", "Force update even if there are local changes", false).action(async (options) => {
await update(options);
});
program.parse();