feat: add command (#337)

This commit is contained in:
Aman Varshney
2025-06-22 03:20:05 +05:30
committed by GitHub
parent 198d0e7434
commit 9c7a0f0110
29 changed files with 1015 additions and 255 deletions

View File

@@ -0,0 +1,104 @@
import path from "node:path";
import { cancel, log } from "@clack/prompts";
import pc from "picocolors";
import type { AddInput, Addons, ProjectConfig } from "../../types";
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
import { updateBtsConfig } from "../../utils/bts-config";
import { setupAddons } from "../setup/addons-setup";
import {
detectProjectConfig,
isBetterTStackProject,
} from "./detect-project-config";
import { installDependencies } from "./install-dependencies";
import { setupAddonsTemplate } from "./template-manager";
function exitWithError(message: string): never {
cancel(pc.red(message));
process.exit(1);
}
export async function addAddonsToProject(
input: AddInput & { addons: Addons[] },
): Promise<void> {
try {
const projectDir = input.projectDir || process.cwd();
const isBetterTStack = await isBetterTStackProject(projectDir);
if (!isBetterTStack) {
exitWithError(
"This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.",
);
}
const detectedConfig = await detectProjectConfig(projectDir);
if (!detectedConfig) {
exitWithError(
"Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.",
);
}
const config: ProjectConfig = {
projectName: detectedConfig.projectName || path.basename(projectDir),
projectDir,
relativePath: ".",
database: detectedConfig.database || "none",
orm: detectedConfig.orm || "none",
backend: detectedConfig.backend || "none",
runtime: detectedConfig.runtime || "none",
frontend: detectedConfig.frontend || [],
addons: input.addons,
examples: detectedConfig.examples || [],
auth: detectedConfig.auth || false,
git: false,
packageManager:
input.packageManager || detectedConfig.packageManager || "npm",
install: input.install || false,
dbSetup: detectedConfig.dbSetup || "none",
api: detectedConfig.api || "none",
};
for (const addon of input.addons) {
const { isCompatible, reason } = validateAddonCompatibility(
addon,
config.frontend,
);
if (!isCompatible) {
exitWithError(
reason ||
`${addon} addon is not compatible with current frontend configuration`,
);
}
}
log.info(
pc.green(
`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`,
),
);
await setupAddonsTemplate(projectDir, config);
await setupAddons(config, true);
const currentAddons = detectedConfig.addons || [];
const mergedAddons = [...new Set([...currentAddons, ...input.addons])];
await updateBtsConfig(projectDir, { addons: mergedAddons });
if (config.install) {
await installDependencies({
projectDir,
packageManager: config.packageManager,
});
} else {
log.info(
pc.yellow(
`Run ${pc.bold(
`${config.packageManager} install`,
)} to install dependencies`,
),
);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
exitWithError(`Error adding addons: ${message}`);
}
}

View File

@@ -2,6 +2,7 @@ import { cancel, log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { writeBtsConfig } from "../../utils/bts-config";
import { setupAddons } from "../setup/addons-setup";
import { setupApi } from "../setup/api-setup";
import { setupAuth } from "../setup/auth-setup";
@@ -71,6 +72,9 @@ export async function createProject(options: ProjectConfig) {
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
await writeBtsConfig(options);
await initializeGit(projectDir, options.git);
log.success("Project template successfully scaffolded!");

View File

@@ -0,0 +1,43 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { readBtsConfig } from "../../utils/bts-config";
export async function detectProjectConfig(
projectDir: string,
): Promise<Partial<ProjectConfig> | null> {
try {
const btsConfig = await readBtsConfig(projectDir);
if (btsConfig) {
return {
projectDir,
projectName: path.basename(projectDir),
database: btsConfig.database,
orm: btsConfig.orm,
backend: btsConfig.backend,
runtime: btsConfig.runtime,
frontend: btsConfig.frontend,
addons: btsConfig.addons,
examples: btsConfig.examples,
auth: btsConfig.auth,
packageManager: btsConfig.packageManager,
dbSetup: btsConfig.dbSetup,
api: btsConfig.api,
};
}
return null;
} catch (_error) {
return null;
}
}
export async function isBetterTStackProject(
projectDir: string,
): Promise<boolean> {
try {
return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
} catch (_error) {
return false;
}
}

View File

@@ -1,11 +1,13 @@
import path from "node:path";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { Frontend, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { setupStarlight } from "./starlight-setup";
import { setupTauri } from "./tauri-setup";
export async function setupAddons(config: ProjectConfig) {
export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
const { addons, frontend, projectDir } = config;
const hasReactWebFrontend =
frontend.includes("react-router") ||
@@ -21,6 +23,20 @@ export async function setupAddons(config: ProjectConfig) {
devDependencies: ["turbo"],
projectDir,
});
if (isAddCommand) {
log.info(`${pc.yellow("Update your package.json scripts:")}
${pc.dim("Replace:")} ${pc.yellow('"pnpm -r dev"')} ${pc.dim("→")} ${pc.green(
'"turbo dev"',
)}
${pc.dim("Replace:")} ${pc.yellow('"pnpm --filter web dev"')} ${pc.dim(
"→",
)} ${pc.green('"turbo -F web dev"')}
${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
`);
}
}
if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) {