feat(cli): add alchemy and improve cli tooling and structure (#520)

This commit is contained in:
Aman Varshney
2025-08-20 23:43:58 +05:30
committed by GitHub
parent c5430ae4fd
commit 5788876c47
152 changed files with 5804 additions and 2264 deletions

View File

@@ -0,0 +1,96 @@
import path from "node:path";
import { 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 { exitWithError } from "../../utils/errors";
import { setupAddons } from "../addons/addons-setup";
import {
detectProjectConfig,
isBetterTStackProject,
} from "./detect-project-config";
import { installDependencies } from "./install-dependencies";
import { setupAddonsTemplate } from "./template-manager";
export async function addAddonsToProject(
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
) {
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",
webDeploy: detectedConfig.webDeploy || "none",
serverDeploy: detectedConfig.serverDeploy || "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`,
);
}
}
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 if (!input.suppressInstallMessage) {
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

@@ -0,0 +1,121 @@
import path from "node:path";
import { log } from "@clack/prompts";
import pc from "picocolors";
import type {
AddInput,
ProjectConfig,
ServerDeploy,
WebDeploy,
} from "../../types";
import { updateBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { setupServerDeploy } from "../deployment/server-deploy-setup";
import { setupWebDeploy } from "../deployment/web-deploy-setup";
import {
detectProjectConfig,
isBetterTStackProject,
} from "./detect-project-config";
import { installDependencies } from "./install-dependencies";
import { setupDeploymentTemplates } from "./template-manager";
export async function addDeploymentToProject(
input: AddInput & {
webDeploy?: WebDeploy;
serverDeploy?: ServerDeploy;
suppressInstallMessage?: boolean;
},
) {
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.",
);
}
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) {
exitWithError(
`${input.webDeploy} web deployment is already configured for this project.`,
);
}
if (
input.serverDeploy &&
detectedConfig.serverDeploy === input.serverDeploy
) {
exitWithError(
`${input.serverDeploy} server deployment is already configured for this 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: detectedConfig.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",
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none",
};
if (input.webDeploy && input.webDeploy !== "none") {
log.info(
pc.green(
`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`,
),
);
}
if (input.serverDeploy && input.serverDeploy !== "none") {
log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
}
await setupDeploymentTemplates(projectDir, config);
await setupWebDeploy(config);
await setupServerDeploy(config);
await updateBtsConfig(projectDir, {
webDeploy: input.webDeploy || config.webDeploy,
serverDeploy: input.serverDeploy || config.serverDeploy,
});
if (config.install) {
await installDependencies({
projectDir,
packageManager: config.packageManager,
});
} else if (!input.suppressInstallMessage) {
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 deployment: ${message}`);
}
}

View File

@@ -0,0 +1,303 @@
import path from "node:path";
import fs from "fs-extra";
import type { AvailableDependencies } from "../../constants";
import type { Frontend, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
async function addBackendWorkspaceDependency(
projectDir: string,
backendPackageName: string,
workspaceVersion: string,
) {
const pkgJsonPath = path.join(projectDir, "package.json");
try {
const pkgJson = await fs.readJson(pkgJsonPath);
if (!pkgJson.dependencies) {
pkgJson.dependencies = {};
}
pkgJson.dependencies[backendPackageName] = workspaceVersion;
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
} catch (_error) {}
}
function getFrontendType(frontend: Frontend[]): {
hasReactWeb: boolean;
hasNuxtWeb: boolean;
hasSvelteWeb: boolean;
hasSolidWeb: boolean;
hasNative: boolean;
} {
const reactBasedFrontends = [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
];
const nativeFrontends = ["native-nativewind", "native-unistyles"];
return {
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
hasNuxtWeb: frontend.includes("nuxt"),
hasSvelteWeb: frontend.includes("svelte"),
hasSolidWeb: frontend.includes("solid"),
hasNative: frontend.some((f) => nativeFrontends.includes(f)),
};
}
function getApiDependencies(
api: string,
frontendType: ReturnType<typeof getFrontendType>,
) {
const deps: Record<
string,
{ dependencies: string[]; devDependencies?: string[] }
> = {};
if (api === "orpc") {
deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
} else if (api === "trpc") {
deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
}
if (frontendType.hasReactWeb) {
if (api === "orpc") {
deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
} else if (api === "trpc") {
deps.web = {
dependencies: [
"@trpc/tanstack-react-query",
"@trpc/client",
"@trpc/server",
],
};
}
} else if (frontendType.hasNuxtWeb && api === "orpc") {
deps.web = {
dependencies: [
"@tanstack/vue-query",
"@orpc/tanstack-query",
"@orpc/client",
],
devDependencies: ["@tanstack/vue-query-devtools"],
};
} else if (frontendType.hasSvelteWeb && api === "orpc") {
deps.web = {
dependencies: [
"@orpc/tanstack-query",
"@orpc/client",
"@tanstack/svelte-query",
],
devDependencies: ["@tanstack/svelte-query-devtools"],
};
} else if (frontendType.hasSolidWeb && api === "orpc") {
deps.web = {
dependencies: [
"@orpc/tanstack-query",
"@orpc/client",
"@tanstack/solid-query",
],
devDependencies: [
"@tanstack/solid-query-devtools",
"@tanstack/solid-router-devtools",
],
};
}
if (api === "trpc") {
deps.native = {
dependencies: [
"@trpc/tanstack-react-query",
"@trpc/client",
"@trpc/server",
],
};
} else if (api === "orpc") {
deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
}
return deps;
}
function getQueryDependencies(frontend: Frontend[]) {
const reactBasedFrontends: Frontend[] = [
"react-router",
"tanstack-router",
"tanstack-start",
"next",
"native-nativewind",
"native-unistyles",
];
const deps: Record<
string,
{ dependencies: string[]; devDependencies?: string[] }
> = {};
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
if (needsReactQuery) {
const hasReactWeb = frontend.some(
(f) =>
f !== "native-nativewind" &&
f !== "native-unistyles" &&
reactBasedFrontends.includes(f),
);
const hasNative =
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles");
if (hasReactWeb) {
deps.web = {
dependencies: ["@tanstack/react-query"],
devDependencies: ["@tanstack/react-query-devtools"],
};
}
if (hasNative) {
deps.native = { dependencies: ["@tanstack/react-query"] };
}
}
if (frontend.includes("solid")) {
deps.web = {
dependencies: ["@tanstack/solid-query"],
devDependencies: [
"@tanstack/solid-query-devtools",
"@tanstack/solid-router-devtools",
],
};
}
return deps;
}
function getConvexDependencies(frontend: Frontend[]) {
const deps: Record<string, { dependencies: string[] }> = {
web: { dependencies: ["convex"] },
native: { dependencies: ["convex"] },
};
if (frontend.includes("tanstack-start")) {
deps.web.dependencies.push("@convex-dev/react-query");
}
if (frontend.includes("svelte")) {
deps.web.dependencies.push("convex-svelte");
}
if (frontend.includes("nuxt")) {
deps.web.dependencies.push("convex-nuxt", "convex-vue");
}
return deps;
}
export async function setupApi(config: ProjectConfig) {
const { api, projectName, frontend, backend, packageManager, projectDir } =
config;
const isConvex = backend === "convex";
const webDir = path.join(projectDir, "apps/web");
const nativeDir = path.join(projectDir, "apps/native");
const serverDir = path.join(projectDir, "apps/server");
const webDirExists = await fs.pathExists(webDir);
const nativeDirExists = await fs.pathExists(nativeDir);
const serverDirExists = await fs.pathExists(serverDir);
const frontendType = getFrontendType(frontend);
if (!isConvex && api !== "none") {
const apiDeps = getApiDependencies(api, frontendType);
if (serverDirExists && apiDeps.server) {
await addPackageDependency({
dependencies: apiDeps.server.dependencies as AvailableDependencies[],
projectDir: serverDir,
});
if (api === "trpc") {
if (backend === "hono") {
await addPackageDependency({
dependencies: ["@hono/trpc-server"],
projectDir: serverDir,
});
} else if (backend === "elysia") {
await addPackageDependency({
dependencies: ["@elysiajs/trpc"],
projectDir: serverDir,
});
}
}
}
if (webDirExists && apiDeps.web) {
await addPackageDependency({
dependencies: apiDeps.web.dependencies as AvailableDependencies[],
devDependencies: apiDeps.web.devDependencies as AvailableDependencies[],
projectDir: webDir,
});
}
if (nativeDirExists && apiDeps.native) {
await addPackageDependency({
dependencies: apiDeps.native.dependencies as AvailableDependencies[],
projectDir: nativeDir,
});
}
}
if (!isConvex) {
const queryDeps = getQueryDependencies(frontend);
if (webDirExists && queryDeps.web) {
await addPackageDependency({
dependencies: queryDeps.web.dependencies as AvailableDependencies[],
devDependencies: queryDeps.web
.devDependencies as AvailableDependencies[],
projectDir: webDir,
});
}
if (nativeDirExists && queryDeps.native) {
await addPackageDependency({
dependencies: queryDeps.native.dependencies as AvailableDependencies[],
projectDir: nativeDir,
});
}
}
if (isConvex) {
const convexDeps = getConvexDependencies(frontend);
if (webDirExists) {
await addPackageDependency({
dependencies: convexDeps.web.dependencies as AvailableDependencies[],
projectDir: webDir,
});
}
if (nativeDirExists) {
await addPackageDependency({
dependencies: convexDeps.native.dependencies as AvailableDependencies[],
projectDir: nativeDir,
});
}
const backendPackageName = `@${projectName}/backend`;
const backendWorkspaceVersion =
packageManager === "npm" ? "*" : "workspace:*";
if (webDirExists) {
await addBackendWorkspaceDependency(
webDir,
backendPackageName,
backendWorkspaceVersion,
);
}
if (nativeDirExists) {
await addBackendWorkspaceDependency(
nativeDir,
backendPackageName,
backendWorkspaceVersion,
);
}
}
}

View File

@@ -0,0 +1,64 @@
import path from "node:path";
import type { AvailableDependencies } from "../../constants";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupBackendDependencies(config: ProjectConfig) {
const { backend, runtime, api, projectDir } = config;
if (backend === "convex") {
return;
}
const framework = backend;
const serverDir = path.join(projectDir, "apps/server");
const dependencies: AvailableDependencies[] = [];
const devDependencies: AvailableDependencies[] = [];
if (framework === "hono") {
dependencies.push("hono");
if (api === "trpc") {
dependencies.push("@hono/trpc-server");
}
if (runtime === "node") {
dependencies.push("@hono/node-server");
devDependencies.push("tsx", "@types/node");
}
} else if (framework === "elysia") {
dependencies.push("elysia", "@elysiajs/cors");
if (api === "trpc") {
dependencies.push("@elysiajs/trpc");
}
if (runtime === "node") {
dependencies.push("@elysiajs/node");
devDependencies.push("tsx", "@types/node");
}
} else if (framework === "express") {
dependencies.push("express", "cors");
devDependencies.push("@types/express", "@types/cors");
if (runtime === "node") {
devDependencies.push("tsx", "@types/node");
}
} else if (framework === "fastify") {
dependencies.push("fastify", "@fastify/cors");
if (runtime === "node") {
devDependencies.push("tsx", "@types/node");
}
}
if (runtime === "bun") {
devDependencies.push("@types/bun");
}
if (dependencies.length > 0 || devDependencies.length > 0) {
await addPackageDependency({
dependencies,
devDependencies,
projectDir: serverDir,
});
}
}

View File

@@ -0,0 +1,372 @@
import path from "node:path";
import { intro, log, outro } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { getDefaultConfig } from "../../constants";
import { getAddonsToAdd } from "../../prompts/addons";
import { gatherConfig } from "../../prompts/config-prompts";
import { getProjectName } from "../../prompts/project-name";
import { getServerDeploymentToAdd } from "../../prompts/server-deploy";
import { getDeploymentToAdd } from "../../prompts/web-deploy";
import type {
AddInput,
CreateInput,
DirectoryConflict,
InitResult,
ProjectConfig,
} from "../../types";
import { trackProjectCreation } from "../../utils/analytics";
import { coerceBackendPresets } from "../../utils/compatibility-rules";
import { displayConfig } from "../../utils/display-config";
import { exitWithError, handleError } from "../../utils/errors";
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
import {
handleDirectoryConflict,
setupProjectDirectory,
} from "../../utils/project-directory";
import { renderTitle } from "../../utils/render-title";
import {
getProvidedFlags,
processAndValidateFlags,
processProvidedFlagsWithoutValidation,
validateConfigCompatibility,
} from "../../validation";
import { addAddonsToProject } from "./add-addons";
import { addDeploymentToProject } from "./add-deployment";
import { createProject } from "./create-project";
import { detectProjectConfig } from "./detect-project-config";
import { installDependencies } from "./install-dependencies";
export async function createProjectHandler(
input: CreateInput & { projectName?: string },
): Promise<InitResult> {
const startTime = Date.now();
const timeScaffolded = new Date().toISOString();
if (input.renderTitle !== false) {
renderTitle();
}
intro(pc.magenta("Creating a new Better-T Stack project"));
if (input.yolo) {
consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
}
let currentPathInput: string;
if (input.yes && input.projectName) {
currentPathInput = input.projectName;
} else if (input.yes) {
const defaultConfig = getDefaultConfig();
let defaultName = defaultConfig.relativePath;
let counter = 1;
while (
(await fs.pathExists(path.resolve(process.cwd(), defaultName))) &&
(await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0
) {
defaultName = `${defaultConfig.projectName}-${counter}`;
counter++;
}
currentPathInput = defaultName;
} else {
currentPathInput = await getProjectName(input.projectName);
}
let finalPathInput: string;
let shouldClearDirectory: boolean;
try {
if (input.directoryConflict) {
const result = await handleDirectoryConflictProgrammatically(
currentPathInput,
input.directoryConflict,
);
finalPathInput = result.finalPathInput;
shouldClearDirectory = result.shouldClearDirectory;
} else {
const result = await handleDirectoryConflict(currentPathInput);
finalPathInput = result.finalPathInput;
shouldClearDirectory = result.shouldClearDirectory;
}
} catch (error) {
const elapsedTimeMs = Date.now() - startTime;
return {
success: false,
projectConfig: {
projectName: "",
projectDir: "",
relativePath: "",
database: "none",
orm: "none",
backend: "none",
runtime: "none",
frontend: [],
addons: [],
examples: [],
auth: false,
git: false,
packageManager: "npm",
install: false,
dbSetup: "none",
api: "none",
webDeploy: "none",
serverDeploy: "none",
} satisfies ProjectConfig,
reproducibleCommand: "",
timeScaffolded,
elapsedTimeMs,
projectDirectory: "",
relativePath: "",
error: error instanceof Error ? error.message : String(error),
};
}
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
finalPathInput,
shouldClearDirectory,
);
const cliInput = {
...input,
projectDirectory: input.projectName,
};
const providedFlags = getProvidedFlags(cliInput);
let config: ProjectConfig;
if (input.yes) {
const flagConfig = processProvidedFlagsWithoutValidation(
cliInput,
finalBaseName,
);
config = {
...getDefaultConfig(),
...flagConfig,
projectName: finalBaseName,
projectDir: finalResolvedPath,
relativePath: finalPathInput,
};
coerceBackendPresets(config);
validateConfigCompatibility(config, providedFlags, cliInput);
if (config.backend === "convex") {
log.info(
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
);
} else if (config.backend === "none") {
log.info(
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
);
}
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
log.message(displayConfig(config));
log.message("");
} else {
const flagConfig = processAndValidateFlags(
cliInput,
providedFlags,
finalBaseName,
);
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
if (Object.keys(otherFlags).length > 0) {
log.info(pc.yellow("Using these pre-selected options:"));
log.message(displayConfig(otherFlags));
log.message("");
}
config = await gatherConfig(
flagConfig,
finalBaseName,
finalResolvedPath,
finalPathInput,
);
}
await createProject(config);
const reproducibleCommand = generateReproducibleCommand(config);
log.success(
pc.blue(
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
),
);
await trackProjectCreation(config, input.disableAnalytics);
const elapsedTimeMs = Date.now() - startTime;
const elapsedTimeInSeconds = (elapsedTimeMs / 1000).toFixed(2);
outro(
pc.magenta(
`Project created successfully in ${pc.bold(
elapsedTimeInSeconds,
)} seconds!`,
),
);
return {
success: true,
projectConfig: config,
reproducibleCommand,
timeScaffolded,
elapsedTimeMs,
projectDirectory: config.projectDir,
relativePath: config.relativePath,
};
}
async function handleDirectoryConflictProgrammatically(
currentPathInput: string,
strategy: DirectoryConflict,
): Promise<{ finalPathInput: string; shouldClearDirectory: boolean }> {
const currentPath = path.resolve(process.cwd(), currentPathInput);
if (!(await fs.pathExists(currentPath))) {
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
}
const dirContents = await fs.readdir(currentPath);
const isNotEmpty = dirContents.length > 0;
if (!isNotEmpty) {
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
}
switch (strategy) {
case "overwrite":
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
case "merge":
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
case "increment": {
let counter = 1;
const baseName = currentPathInput;
let finalPathInput = `${baseName}-${counter}`;
while (
(await fs.pathExists(path.resolve(process.cwd(), finalPathInput))) &&
(await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length >
0
) {
counter++;
finalPathInput = `${baseName}-${counter}`;
}
return { finalPathInput, shouldClearDirectory: false };
}
case "error":
throw new Error(
`Directory "${currentPathInput}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`,
);
default:
throw new Error(`Unknown directory conflict strategy: ${strategy}`);
}
}
export async function addAddonsHandler(input: AddInput) {
try {
const projectDir = input.projectDir || process.cwd();
const detectedConfig = await detectProjectConfig(projectDir);
if (!detectedConfig) {
exitWithError(
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
);
}
if (!input.addons || input.addons.length === 0) {
const addonsPrompt = await getAddonsToAdd(
detectedConfig.frontend || [],
detectedConfig.addons || [],
);
if (addonsPrompt.length > 0) {
input.addons = addonsPrompt;
}
}
if (!input.webDeploy) {
const deploymentPrompt = await getDeploymentToAdd(
detectedConfig.frontend || [],
detectedConfig.webDeploy,
);
if (deploymentPrompt !== "none") {
input.webDeploy = deploymentPrompt;
}
}
if (!input.serverDeploy) {
const serverDeploymentPrompt = await getServerDeploymentToAdd(
detectedConfig.runtime,
detectedConfig.serverDeploy,
);
if (serverDeploymentPrompt !== "none") {
input.serverDeploy = serverDeploymentPrompt;
}
}
const packageManager =
input.packageManager || detectedConfig.packageManager || "npm";
let somethingAdded = false;
if (input.addons && input.addons.length > 0) {
await addAddonsToProject({
...input,
install: false,
suppressInstallMessage: true,
addons: input.addons,
});
somethingAdded = true;
}
if (input.webDeploy && input.webDeploy !== "none") {
await addDeploymentToProject({
...input,
install: false,
suppressInstallMessage: true,
webDeploy: input.webDeploy,
});
somethingAdded = true;
}
if (input.serverDeploy && input.serverDeploy !== "none") {
await addDeploymentToProject({
...input,
install: false,
suppressInstallMessage: true,
serverDeploy: input.serverDeploy,
});
somethingAdded = true;
}
if (!somethingAdded) {
outro(pc.yellow("No addons or deployment configurations to add."));
return;
}
if (input.install) {
await installDependencies({
projectDir,
packageManager,
});
} else {
log.info(
`Run ${pc.bold(`${packageManager} install`)} to install dependencies`,
);
}
outro("Add command completed successfully!");
} catch (error) {
handleError(error, "Failed to add addons or deployment");
}
}

View File

@@ -0,0 +1,13 @@
import path from "node:path";
import { execa } from "execa";
import type { PackageManager } from "../../types";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export async function runConvexCodegen(
projectDir: string,
packageManager: PackageManager | null | undefined,
) {
const backendDir = path.join(projectDir, "packages/backend");
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
await execa(cmd, { cwd: backendDir, shell: true });
}

View File

@@ -0,0 +1,120 @@
import { log } from "@clack/prompts";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { writeBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { formatProjectWithBiome } from "../../utils/format-with-biome";
import { setupAddons } from "../addons/addons-setup";
import { setupAuth } from "../addons/auth-setup";
import { setupExamples } from "../addons/examples-setup";
import { setupApi } from "../core/api-setup";
import { setupBackendDependencies } from "../core/backend-setup";
import { setupDatabase } from "../core/db-setup";
import { setupRuntime } from "../core/runtime-setup";
import { setupServerDeploy } from "../deployment/server-deploy-setup";
import { setupWebDeploy } from "../deployment/web-deploy-setup";
import { runConvexCodegen } from "./convex-codegen";
import { createReadme } from "./create-readme";
import { setupEnvironmentVariables } from "./env-setup";
import { initializeGit } from "./git";
import { installDependencies } from "./install-dependencies";
import { displayPostInstallInstructions } from "./post-installation";
import { updatePackageConfigurations } from "./project-config";
import {
copyBaseTemplate,
handleExtras,
setupAddonsTemplate,
setupAuthTemplate,
setupBackendFramework,
setupDbOrmTemplates,
setupDeploymentTemplates,
setupDockerComposeTemplates,
setupExamplesTemplate,
setupFrontendTemplates,
} from "./template-manager";
export async function createProject(options: ProjectConfig) {
const projectDir = options.projectDir;
const isConvex = options.backend === "convex";
try {
await fs.ensureDir(projectDir);
await copyBaseTemplate(projectDir, options);
await setupFrontendTemplates(projectDir, options);
await setupBackendFramework(projectDir, options);
if (!isConvex) {
await setupDbOrmTemplates(projectDir, options);
await setupDockerComposeTemplates(projectDir, options);
await setupAuthTemplate(projectDir, options);
}
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamplesTemplate(projectDir, options);
}
await setupAddonsTemplate(projectDir, options);
await setupDeploymentTemplates(projectDir, options);
await setupApi(options);
if (!isConvex) {
await setupBackendDependencies(options);
await setupDatabase(options);
await setupRuntime(options);
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamples(options);
}
}
if (options.addons.length > 0 && options.addons[0] !== "none") {
await setupAddons(options);
}
if (!isConvex && options.auth) {
await setupAuth(options);
}
await handleExtras(projectDir, options);
await setupWebDeploy(options);
await setupServerDeploy(options);
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
await writeBtsConfig(options);
await formatProjectWithBiome(projectDir);
if (isConvex) {
await runConvexCodegen(projectDir, options.packageManager);
}
log.success("Project template successfully scaffolded!");
if (options.install) {
await installDependencies({
projectDir,
packageManager: options.packageManager,
});
}
await initializeGit(projectDir, options.git);
await displayPostInstallInstructions({
...options,
depsInstalled: options.install,
});
return projectDir;
} catch (error) {
if (error instanceof Error) {
console.error(error.stack);
exitWithError(`Error during project creation: ${error.message}`);
} else {
console.error(error);
exitWithError(`An unexpected error occurred: ${String(error)}`);
}
}
}

View File

@@ -0,0 +1,634 @@
import path from "node:path";
import consola from "consola";
import fs from "fs-extra";
import type {
Addons,
API,
Database,
DatabaseSetup,
Frontend,
ORM,
ProjectConfig,
Runtime,
} from "../../types";
export async function createReadme(projectDir: string, options: ProjectConfig) {
const readmePath = path.join(projectDir, "README.md");
const content = generateReadmeContent(options);
try {
await fs.writeFile(readmePath, content);
} catch (error) {
consola.error("Failed to create README.md file:", error);
}
}
function generateReadmeContent(options: ProjectConfig): string {
const {
projectName,
packageManager,
database,
auth,
addons = [],
orm = "drizzle",
runtime = "bun",
frontend = ["tanstack-router"],
backend = "hono",
api = "trpc",
} = options;
const isConvex = backend === "convex";
const hasReactRouter = frontend.includes("react-router");
const hasNative =
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles");
const hasSvelte = frontend.includes("svelte");
const packageManagerRunCmd =
packageManager === "npm" ? "npm run" : packageManager;
let webPort = "3001";
if (hasReactRouter || hasSvelte) {
webPort = "5173";
}
const stackDescription = generateStackDescription(
frontend,
backend,
api,
isConvex,
);
return `# ${projectName}
This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack${
stackDescription ? ` that combines ${stackDescription}` : ""
}.
## Features
${generateFeaturesList(
database,
auth,
addons,
orm,
runtime,
frontend,
backend,
api,
)}
## Getting Started
First, install the dependencies:
\`\`\`bash
${packageManager} install
\`\`\`
${
isConvex
? `
## Convex Setup
This project uses Convex as a backend. You'll need to set up Convex before running the app:
\`\`\`bash
${packageManagerRunCmd} dev:setup
\`\`\`
Follow the prompts to create a new Convex project and connect it to your application.`
: generateDatabaseSetup(
database,
auth,
packageManagerRunCmd,
orm,
options.dbSetup,
)
}
Then, run the development server:
\`\`\`bash
${packageManagerRunCmd} dev
\`\`\`
${generateRunningInstructions(frontend, backend, webPort, hasNative, isConvex)}
${
addons.includes("pwa") && hasReactRouter
? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n"
: ""
}
## Project Structure
\`\`\`
${generateProjectStructure(
projectName,
frontend,
backend,
addons,
isConvex,
api,
)}
\`\`\`
## Available Scripts
${generateScriptsList(
packageManagerRunCmd,
database,
orm,
auth,
hasNative,
addons,
backend,
)}
`;
}
function generateStackDescription(
frontend: Frontend[],
backend: string,
api: API,
isConvex: boolean,
): string {
const parts: string[] = [];
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasReactRouter = frontend.includes("react-router");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
const hasNuxt = frontend.includes("nuxt");
const hasSolid = frontend.includes("solid");
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
if (!hasFrontendNone) {
if (hasTanstackRouter) {
parts.push("React, TanStack Router");
} else if (hasReactRouter) {
parts.push("React, React Router");
} else if (hasNext) {
parts.push("Next.js");
} else if (hasTanstackStart) {
parts.push("React, TanStack Start");
} else if (hasSvelte) {
parts.push("SvelteKit");
} else if (hasNuxt) {
parts.push("Nuxt");
} else if (hasSolid) {
parts.push("SolidJS");
}
}
if (backend !== "none") {
parts.push(backend[0].toUpperCase() + backend.slice(1));
}
if (!isConvex && api !== "none") {
parts.push(api.toUpperCase());
}
return parts.length > 0 ? `${parts.join(", ")}, and more` : "";
}
function generateRunningInstructions(
frontend: Frontend[],
backend: string,
webPort: string,
hasNative: boolean,
isConvex: boolean,
): string {
const instructions: string[] = [];
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
const isBackendNone = backend === "none";
if (!hasFrontendNone) {
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasReactRouter = frontend.includes("react-router");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
const hasNuxt = frontend.includes("nuxt");
const hasSolid = frontend.includes("solid");
if (
hasTanstackRouter ||
hasReactRouter ||
hasNext ||
hasTanstackStart ||
hasSvelte ||
hasNuxt ||
hasSolid
) {
instructions.push(
`Open [http://localhost:${webPort}](http://localhost:${webPort}) in your browser to see the web application.`,
);
}
}
if (hasNative) {
instructions.push("Use the Expo Go app to run the mobile application.");
}
if (isConvex) {
instructions.push(
"Your app will connect to the Convex cloud backend automatically.",
);
} else if (!isBackendNone) {
instructions.push(
"The API is running at [http://localhost:3000](http://localhost:3000).",
);
}
return instructions.join("\n");
}
function generateProjectStructure(
projectName: string,
frontend: Frontend[],
backend: string,
addons: Addons[],
isConvex: boolean,
api: API,
): string {
const structure: string[] = [`${projectName}/`, "├── apps/"];
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
const isBackendNone = backend === "none";
if (!hasFrontendNone) {
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasReactRouter = frontend.includes("react-router");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
const hasNuxt = frontend.includes("nuxt");
const hasSolid = frontend.includes("solid");
if (
hasTanstackRouter ||
hasReactRouter ||
hasNext ||
hasTanstackStart ||
hasSvelte ||
hasNuxt ||
hasSolid
) {
let frontendType = "";
if (hasTanstackRouter) frontendType = "React + TanStack Router";
else if (hasReactRouter) frontendType = "React + React Router";
else if (hasNext) frontendType = "Next.js";
else if (hasTanstackStart) frontendType = "React + TanStack Start";
else if (hasSvelte) frontendType = "SvelteKit";
else if (hasNuxt) frontendType = "Nuxt";
else if (hasSolid) frontendType = "SolidJS";
structure.push(
`│ ├── web/ # Frontend application (${frontendType})`,
);
}
}
const hasNative =
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles");
if (hasNative) {
structure.push(
"│ ├── native/ # Mobile application (React Native, Expo)",
);
}
if (addons.includes("starlight")) {
structure.push(
"│ ├── docs/ # Documentation site (Astro Starlight)",
);
}
if (isConvex) {
structure.push("├── packages/");
structure.push(
"│ └── backend/ # Convex backend functions and schema",
);
} else if (!isBackendNone) {
const backendName = backend[0].toUpperCase() + backend.slice(1);
const apiName = api !== "none" ? api.toUpperCase() : "";
const backendDesc = apiName ? `${backendName}, ${apiName}` : backendName;
structure.push(`│ └── server/ # Backend API (${backendDesc})`);
}
return structure.join("\n");
}
function generateFeaturesList(
database: Database,
auth: boolean,
addons: Addons[],
orm: ORM,
runtime: Runtime,
frontend: Frontend[],
backend: string,
api: API,
): string {
const isConvex = backend === "convex";
const isBackendNone = backend === "none";
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasReactRouter = frontend.includes("react-router");
const hasNative =
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
const hasNuxt = frontend.includes("nuxt");
const hasSolid = frontend.includes("solid");
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
const addonsList = [
"- **TypeScript** - For type safety and improved developer experience",
];
if (!hasFrontendNone) {
if (hasTanstackRouter) {
addonsList.push(
"- **TanStack Router** - File-based routing with full type safety",
);
} else if (hasReactRouter) {
addonsList.push("- **React Router** - Declarative routing for React");
} else if (hasNext) {
addonsList.push("- **Next.js** - Full-stack React framework");
} else if (hasTanstackStart) {
addonsList.push(
"- **TanStack Start** - SSR framework with TanStack Router",
);
} else if (hasSvelte) {
addonsList.push(
"- **SvelteKit** - Web framework for building Svelte apps",
);
} else if (hasNuxt) {
addonsList.push("- **Nuxt** - The Intuitive Vue Framework");
} else if (hasSolid) {
addonsList.push("- **SolidJS** - Simple and performant reactivity");
}
}
if (hasNative) {
addonsList.push("- **React Native** - Build mobile apps using React");
addonsList.push("- **Expo** - Tools for React Native development");
}
if (!hasFrontendNone) {
addonsList.push(
"- **TailwindCSS** - Utility-first CSS for rapid UI development",
"- **shadcn/ui** - Reusable UI components",
);
}
if (isConvex) {
addonsList.push("- **Convex** - Reactive backend-as-a-service platform");
} else if (!isBackendNone) {
if (backend === "hono") {
addonsList.push("- **Hono** - Lightweight, performant server framework");
} else if (backend === "express") {
addonsList.push("- **Express** - Fast, unopinionated web framework");
} else if (backend === "fastify") {
addonsList.push("- **Fastify** - Fast, low-overhead web framework");
} else if (backend === "elysia") {
addonsList.push("- **Elysia** - Type-safe, high-performance framework");
} else if (backend === "next") {
addonsList.push("- **Next.js** - Full-stack React framework");
}
if (api === "trpc") {
addonsList.push("- **tRPC** - End-to-end type-safe APIs");
} else if (api === "orpc") {
addonsList.push(
"- **oRPC** - End-to-end type-safe APIs with OpenAPI integration",
);
}
if (runtime !== "none") {
addonsList.push(
`- **${
runtime === "bun" ? "Bun" : runtime === "node" ? "Node.js" : runtime
}** - Runtime environment`,
);
}
}
if (database !== "none" && !isConvex) {
const ormName =
orm === "drizzle"
? "Drizzle"
: orm === "prisma"
? "Prisma"
: orm === "mongoose"
? "Mongoose"
: "ORM";
const dbName =
database === "sqlite"
? "SQLite/Turso"
: database === "postgres"
? "PostgreSQL"
: database === "mysql"
? "MySQL"
: database === "mongodb"
? "MongoDB"
: "Database";
addonsList.push(
`- **${ormName}** - TypeScript-first ORM`,
`- **${dbName}** - Database engine`,
);
}
if (auth && !isConvex) {
addonsList.push(
"- **Authentication** - Email & password authentication with Better Auth",
);
}
for (const addon of addons) {
if (addon === "pwa") {
addonsList.push("- **PWA** - Progressive Web App support");
} else if (addon === "tauri") {
addonsList.push("- **Tauri** - Build native desktop applications");
} else if (addon === "biome") {
addonsList.push("- **Biome** - Linting and formatting");
} else if (addon === "husky") {
addonsList.push("- **Husky** - Git hooks for code quality");
} else if (addon === "starlight") {
addonsList.push("- **Starlight** - Documentation site with Astro");
} else if (addon === "turborepo") {
addonsList.push("- **Turborepo** - Optimized monorepo build system");
}
}
return addonsList.join("\n");
}
function generateDatabaseSetup(
database: Database,
_auth: boolean,
packageManagerRunCmd: string,
orm: ORM,
dbSetup: DatabaseSetup,
): string {
if (database === "none") {
return "";
}
let setup = "## Database Setup\n\n";
if (database === "sqlite") {
setup += `This project uses SQLite${
orm === "drizzle"
? " with Drizzle ORM"
: orm === "prisma"
? " with Prisma"
: ` with ${orm}`
}.
1. Start the local SQLite database:
${
dbSetup === "d1"
? "Local development for a Cloudflare D1 database will already be running as part of the `wrangler dev` command."
: `\`\`\`bash
cd apps/server && ${packageManagerRunCmd} db:local
\`\`\`
`
}
2. Update your \`.env\` file in the \`apps/server\` directory with the appropriate connection details if needed.
`;
} else if (database === "postgres") {
setup += `This project uses PostgreSQL${
orm === "drizzle"
? " with Drizzle ORM"
: orm === "prisma"
? " with Prisma"
: ` with ${orm}`
}.
1. Make sure you have a PostgreSQL database set up.
2. Update your \`apps/server/.env\` file with your PostgreSQL connection details.
`;
} else if (database === "mysql") {
setup += `This project uses MySQL${
orm === "drizzle"
? " with Drizzle ORM"
: orm === "prisma"
? " with Prisma"
: ` with ${orm}`
}.
1. Make sure you have a MySQL database set up.
2. Update your \`apps/server/.env\` file with your MySQL connection details.
`;
} else if (database === "mongodb") {
setup += `This project uses MongoDB ${
orm === "mongoose"
? "with Mongoose"
: orm === "prisma"
? "with Prisma ORM"
: `with ${orm}`
}.
1. Make sure you have MongoDB set up.
2. Update your \`apps/server/.env\` file with your MongoDB connection URI.
`;
}
setup += `
3. ${
orm === "prisma"
? `Generate the Prisma client and push the schema:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
: orm === "drizzle"
? `Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
: `Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
}
`;
return setup;
}
function generateScriptsList(
packageManagerRunCmd: string,
database: Database,
orm: ORM,
_auth: boolean,
hasNative: boolean,
addons: Addons[],
backend: string,
): string {
const isConvex = backend === "convex";
const isBackendNone = backend === "none";
let scripts = `- \`${packageManagerRunCmd} dev\`: Start all applications in development mode
- \`${packageManagerRunCmd} build\`: Build all applications`;
scripts += `
- \`${packageManagerRunCmd} dev:web\`: Start only the web application`;
if (isConvex) {
scripts += `
- \`${packageManagerRunCmd} dev:setup\`: Setup and configure your Convex project`;
} else if (!isBackendNone) {
scripts += `
- \`${packageManagerRunCmd} dev:server\`: Start only the server`;
}
scripts += `
- \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all apps`;
if (hasNative) {
scripts += `
- \`${packageManagerRunCmd} dev:native\`: Start the React Native/Expo development server`;
}
if (database !== "none" && !isConvex) {
scripts += `
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
- \`${packageManagerRunCmd} db:studio\`: Open database studio UI`;
if (database === "sqlite" && orm === "drizzle") {
scripts += `
- \`cd apps/server && ${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
}
}
if (addons.includes("biome")) {
scripts += `
- \`${packageManagerRunCmd} check\`: Run Biome formatting and linting`;
}
if (addons.includes("pwa")) {
scripts += `
- \`cd apps/web && ${packageManagerRunCmd} generate-pwa-assets\`: Generate PWA assets`;
}
if (addons.includes("tauri")) {
scripts += `
- \`cd apps/web && ${packageManagerRunCmd} desktop:dev\`: Start Tauri desktop app in development
- \`cd apps/web && ${packageManagerRunCmd} desktop:build\`: Build Tauri desktop app`;
}
if (addons.includes("starlight")) {
scripts += `
- \`cd apps/docs && ${packageManagerRunCmd} dev\`: Start documentation site
- \`cd apps/docs && ${packageManagerRunCmd} build\`: Build documentation site`;
}
return scripts;
}

View File

@@ -0,0 +1,103 @@
import path from "node:path";
import { spinner } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { setupCloudflareD1 } from "../database-providers/d1-setup";
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
import { setupNeonPostgres } from "../database-providers/neon-setup";
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
import { setupSupabase } from "../database-providers/supabase-setup";
import { setupTurso } from "../database-providers/turso-setup";
export async function setupDatabase(config: ProjectConfig) {
const { database, orm, dbSetup, backend, projectDir } = config;
if (backend === "convex" || database === "none") {
if (backend !== "convex") {
const serverDir = path.join(projectDir, "apps/server");
const serverDbDir = path.join(serverDir, "src/db");
if (await fs.pathExists(serverDbDir)) {
await fs.remove(serverDbDir);
}
}
return;
}
const s = spinner();
const serverDir = path.join(projectDir, "apps/server");
if (!(await fs.pathExists(serverDir))) {
return;
}
try {
if (orm === "prisma") {
await addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
} else if (orm === "drizzle") {
if (database === "sqlite") {
await addPackageDependency({
dependencies: ["drizzle-orm", "@libsql/client"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else if (database === "postgres") {
if (dbSetup === "neon") {
await addPackageDependency({
dependencies: ["drizzle-orm", "@neondatabase/serverless", "ws"],
devDependencies: ["drizzle-kit", "@types/ws"],
projectDir: serverDir,
});
} else {
await addPackageDependency({
dependencies: ["drizzle-orm", "pg"],
devDependencies: ["drizzle-kit", "@types/pg"],
projectDir: serverDir,
});
}
} else if (database === "mysql") {
await addPackageDependency({
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
}
} else if (orm === "mongoose") {
await addPackageDependency({
dependencies: ["mongoose"],
devDependencies: [],
projectDir: serverDir,
});
}
if (dbSetup === "docker") {
await setupDockerCompose(config);
} else if (database === "sqlite" && dbSetup === "turso") {
await setupTurso(config);
} else if (database === "sqlite" && dbSetup === "d1") {
await setupCloudflareD1(config);
} else if (database === "postgres") {
if (dbSetup === "prisma-postgres") {
await setupPrismaPostgres(config);
} else if (dbSetup === "neon") {
await setupNeonPostgres(config);
} else if (dbSetup === "supabase") {
await setupSupabase(config);
}
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
await setupMongoDBAtlas(config);
}
} catch (error) {
s.stop(pc.red("Failed to set up database"));
if (error instanceof Error) {
consola.error(pc.red(error.message));
}
}
}

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,
webDeploy: btsConfig.webDeploy,
serverDeploy: btsConfig.serverDeploy,
};
}
return null;
} catch (_error) {
return null;
}
}
export async function isBetterTStackProject(projectDir: string) {
try {
return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
} catch (_error) {
return false;
}
}

View File

@@ -0,0 +1,298 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { generateAuthSecret } from "../addons/auth-setup";
export interface EnvVariable {
key: string;
value: string | null | undefined;
condition: boolean;
}
export async function addEnvVariablesToFile(
filePath: string,
variables: EnvVariable[],
) {
await fs.ensureDir(path.dirname(filePath));
let envContent = "";
if (await fs.pathExists(filePath)) {
envContent = await fs.readFile(filePath, "utf8");
}
let modified = false;
let contentToAdd = "";
const exampleVariables: string[] = [];
for (const { key, value, condition } of variables) {
if (condition) {
const regex = new RegExp(`^${key}=.*$`, "m");
const valueToWrite = value ?? "";
exampleVariables.push(`${key}=`);
if (regex.test(envContent)) {
const existingMatch = envContent.match(regex);
if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
modified = true;
}
} else {
contentToAdd += `${key}=${valueToWrite}\n`;
modified = true;
}
}
}
if (contentToAdd) {
if (envContent.length > 0 && !envContent.endsWith("\n")) {
envContent += "\n";
}
envContent += contentToAdd;
}
if (modified) {
await fs.writeFile(filePath, envContent.trimEnd());
}
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
let exampleEnvContent = "";
if (await fs.pathExists(exampleFilePath)) {
exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
}
let exampleModified = false;
let exampleContentToAdd = "";
for (const exampleVar of exampleVariables) {
const key = exampleVar.split("=")[0];
const regex = new RegExp(`^${key}=.*$`, "m");
if (!regex.test(exampleEnvContent)) {
exampleContentToAdd += `${exampleVar}\n`;
exampleModified = true;
}
}
if (exampleContentToAdd) {
if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) {
exampleEnvContent += "\n";
}
exampleEnvContent += exampleContentToAdd;
}
if (exampleModified || !(await fs.pathExists(exampleFilePath))) {
await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
}
}
export async function setupEnvironmentVariables(config: ProjectConfig) {
const {
backend,
frontend,
database,
auth,
examples,
dbSetup,
projectDir,
webDeploy,
serverDeploy,
} = config;
const hasReactRouter = frontend.includes("react-router");
const hasTanStackRouter = frontend.includes("tanstack-router");
const hasTanStackStart = frontend.includes("tanstack-start");
const hasNextJs = frontend.includes("next");
const hasNuxt = frontend.includes("nuxt");
const hasSvelte = frontend.includes("svelte");
const hasSolid = frontend.includes("solid");
const hasWebFrontend =
hasReactRouter ||
hasTanStackRouter ||
hasTanStackStart ||
hasNextJs ||
hasNuxt ||
hasSolid ||
hasSvelte;
if (hasWebFrontend) {
const clientDir = path.join(projectDir, "apps/web");
if (await fs.pathExists(clientDir)) {
let envVarName = "VITE_SERVER_URL";
let serverUrl = "http://localhost:3000";
if (hasNextJs) {
envVarName = "NEXT_PUBLIC_SERVER_URL";
} else if (hasNuxt) {
envVarName = "NUXT_PUBLIC_SERVER_URL";
} else if (hasSvelte) {
envVarName = "PUBLIC_SERVER_URL";
}
if (backend === "convex") {
if (hasNextJs) envVarName = "NEXT_PUBLIC_CONVEX_URL";
else if (hasNuxt) envVarName = "NUXT_PUBLIC_CONVEX_URL";
else if (hasSvelte) envVarName = "PUBLIC_CONVEX_URL";
else envVarName = "VITE_CONVEX_URL";
serverUrl = "https://<YOUR_CONVEX_URL>";
}
const clientVars: EnvVariable[] = [
{
key: envVarName,
value: serverUrl,
condition: true,
},
];
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
}
}
if (
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles")
) {
const nativeDir = path.join(projectDir, "apps/native");
if (await fs.pathExists(nativeDir)) {
let envVarName = "EXPO_PUBLIC_SERVER_URL";
let serverUrl = "http://localhost:3000";
if (backend === "convex") {
envVarName = "EXPO_PUBLIC_CONVEX_URL";
serverUrl = "https://<YOUR_CONVEX_URL>";
}
const nativeVars: EnvVariable[] = [
{
key: envVarName,
value: serverUrl,
condition: true,
},
];
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
}
}
if (backend === "convex") {
return;
}
const serverDir = path.join(projectDir, "apps/server");
if (!(await fs.pathExists(serverDir))) {
return;
}
const envPath = path.join(serverDir, ".env");
let corsOrigin = "http://localhost:3001";
if (hasReactRouter || hasSvelte) {
corsOrigin = "http://localhost:5173";
}
let databaseUrl: string | null = null;
const specializedSetup =
dbSetup === "turso" ||
dbSetup === "prisma-postgres" ||
dbSetup === "mongodb-atlas" ||
dbSetup === "neon" ||
dbSetup === "supabase" ||
dbSetup === "d1" ||
dbSetup === "docker";
if (database !== "none" && !specializedSetup) {
switch (database) {
case "postgres":
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
break;
case "mysql":
databaseUrl = "mysql://root:password@localhost:3306/mydb";
break;
case "mongodb":
databaseUrl = "mongodb://localhost:27017/mydatabase";
break;
case "sqlite":
if (config.runtime === "workers") {
databaseUrl = "http://127.0.0.1:8080";
} else {
databaseUrl = "file:./local.db";
}
break;
}
}
const serverVars: EnvVariable[] = [
{
key: "CORS_ORIGIN",
value: corsOrigin,
condition: true,
},
{
key: "BETTER_AUTH_SECRET",
value: generateAuthSecret(),
condition: !!auth,
},
{
key: "BETTER_AUTH_URL",
value: "http://localhost:3000",
condition: !!auth,
},
{
key: "DATABASE_URL",
value: databaseUrl,
condition: database !== "none" && !specializedSetup,
},
{
key: "GOOGLE_GENERATIVE_AI_API_KEY",
value: "",
condition: examples?.includes("ai") || false,
},
];
await addEnvVariablesToFile(envPath, serverVars);
const isUnifiedAlchemy =
webDeploy === "alchemy" && serverDeploy === "alchemy";
const isIndividualAlchemy =
webDeploy === "alchemy" || serverDeploy === "alchemy";
if (isUnifiedAlchemy) {
const rootEnvPath = path.join(projectDir, ".env");
const rootAlchemyVars: EnvVariable[] = [
{
key: "ALCHEMY_PASSWORD",
value: "please-change-this",
condition: true,
},
];
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
} else if (isIndividualAlchemy) {
if (webDeploy === "alchemy") {
const webDir = path.join(projectDir, "apps/web");
if (await fs.pathExists(webDir)) {
const webAlchemyVars: EnvVariable[] = [
{
key: "ALCHEMY_PASSWORD",
value: "please-change-this",
condition: true,
},
];
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
}
}
if (serverDeploy === "alchemy") {
const serverDir = path.join(projectDir, "apps/server");
if (await fs.pathExists(serverDir)) {
const serverAlchemyVars: EnvVariable[] = [
{
key: "ALCHEMY_PASSWORD",
value: "please-change-this",
condition: true,
},
];
await addEnvVariablesToFile(
path.join(serverDir, ".env"),
serverAlchemyVars,
);
}
}
}
}

View File

@@ -0,0 +1,31 @@
import { log } from "@clack/prompts";
import { $ } from "execa";
import pc from "picocolors";
export async function initializeGit(projectDir: string, useGit: boolean) {
if (!useGit) return;
const gitVersionResult = await $({
cwd: projectDir,
reject: false,
stderr: "pipe",
})`git --version`;
if (gitVersionResult.exitCode !== 0) {
log.warn(pc.yellow("Git is not installed"));
return;
}
const result = await $({
cwd: projectDir,
reject: false,
stderr: "pipe",
})`git init`;
if (result.exitCode !== 0) {
throw new Error(`Git initialization failed: ${result.stderr}`);
}
await $({ cwd: projectDir })`git add -A`;
await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
}

View File

@@ -0,0 +1,32 @@
import { spinner } from "@clack/prompts";
import consola from "consola";
import { $ } from "execa";
import pc from "picocolors";
import type { Addons, PackageManager } from "../../types";
export async function installDependencies({
projectDir,
packageManager,
}: {
projectDir: string;
packageManager: PackageManager;
addons?: Addons[];
}) {
const s = spinner();
try {
s.start(`Running ${packageManager} install...`);
await $({
cwd: projectDir,
stderr: "inherit",
})`${packageManager} install`;
s.stop("Dependencies installed successfully");
} catch (error) {
s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) {
consola.error(pc.red(`Installation error: ${error.message}`));
}
}
}

View File

@@ -0,0 +1,410 @@
import { consola } from "consola";
import pc from "picocolors";
import type {
Database,
DatabaseSetup,
ORM,
ProjectConfig,
Runtime,
} from "../../types";
import { getDockerStatus } from "../../utils/docker-utils";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export async function displayPostInstallInstructions(
config: ProjectConfig & { depsInstalled: boolean },
) {
const {
database,
relativePath,
packageManager,
depsInstalled,
orm,
addons,
runtime,
frontend,
backend,
dbSetup,
webDeploy,
serverDeploy,
} = config;
const isConvex = backend === "convex";
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${relativePath}`;
const hasHuskyOrBiome =
addons?.includes("husky") || addons?.includes("biome");
const databaseInstructions =
!isConvex && database !== "none"
? await getDatabaseInstructions(
database,
orm,
runCmd,
runtime,
dbSetup,
serverDeploy,
)
: "";
const tauriInstructions = addons?.includes("tauri")
? getTauriInstructions(runCmd)
: "";
const lintingInstructions = hasHuskyOrBiome
? getLintingInstructions(runCmd)
: "";
const nativeInstructions =
frontend?.includes("native-nativewind") ||
frontend?.includes("native-unistyles")
? getNativeInstructions(isConvex)
: "";
const pwaInstructions =
addons?.includes("pwa") && frontend?.includes("react-router")
? getPwaInstructions()
: "";
const starlightInstructions = addons?.includes("starlight")
? getStarlightInstructions(runCmd)
: "";
const wranglerDeployInstructions = getWranglerDeployInstructions(
runCmd,
webDeploy,
serverDeploy,
);
const alchemyDeployInstructions = getAlchemyDeployInstructions(
runCmd,
webDeploy,
serverDeploy,
);
const hasWeb = frontend?.some((f) =>
[
"tanstack-router",
"react-router",
"next",
"tanstack-start",
"nuxt",
"svelte",
"solid",
].includes(f),
);
const hasNative =
frontend?.includes("native-nativewind") ||
frontend?.includes("native-unistyles");
const bunWebNativeWarning =
packageManager === "bun" && hasNative && hasWeb
? getBunWebNativeWarning()
: "";
const noOrmWarning =
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
const hasReactRouter = frontend?.includes("react-router");
const hasSvelte = frontend?.includes("svelte");
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
let stepCounter = 2;
if (!depsInstalled) {
output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
}
if (isConvex) {
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(
" (this will guide you through Convex project setup)",
)}\n`;
output += `${pc.cyan(
`${stepCounter++}.`,
)} Copy environment variables from\n${pc.white(
" packages/backend/.env.local",
)} to ${pc.white("apps/*/.env")}\n`;
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
} else {
if (runtime !== "workers") {
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
}
if (runtime === "workers") {
if (dbSetup === "d1") {
output += `${pc.yellow(
"IMPORTANT:",
)} Complete D1 database setup first\n (see Database commands below)\n`;
}
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
if (serverDeploy === "wrangler") {
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
}
}
}
output += `${pc.bold("Your project will be available at:")}\n`;
if (hasWeb) {
output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
} else if (!hasNative && !addons?.includes("starlight")) {
output += `${pc.yellow(
"NOTE:",
)} You are creating a backend-only app\n (no frontend selected)\n`;
}
if (!isConvex) {
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
}
if (addons?.includes("starlight")) {
output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
}
if (addons?.includes("fumadocs")) {
output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
}
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
if (wranglerDeployInstructions)
output += `\n${wranglerDeployInstructions.trim()}\n`;
if (alchemyDeployInstructions)
output += `\n${alchemyDeployInstructions.trim()}\n`;
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(
tazeCommand,
)}\n\n`;
output += `${pc.bold(
"Like Better-T Stack?",
)} Please consider giving us a star\n on GitHub:\n`;
output += pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack");
consola.box(output);
}
function getNativeInstructions(isConvex: boolean): string {
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
const exampleUrl = isConvex
? "https://<YOUR_CONVEX_URL>"
: "http://<YOUR_LOCAL_IP>:3000";
const envFileName = ".env";
const ipNote = isConvex
? "your Convex deployment URL (find after running 'dev:setup')"
: "your local IP address";
let instructions = `${pc.yellow(
"NOTE:",
)} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
if (isConvex) {
instructions += `\n${pc.yellow(
"IMPORTANT:",
)} When using local development with Convex and native apps,\n ensure you use your local IP address instead of localhost or 127.0.0.1\n for proper connectivity.\n`;
}
return instructions;
}
function getLintingInstructions(runCmd?: string): string {
return `${pc.bold("Linting and formatting:")}\n${pc.cyan(
"•",
)} Format and lint fix: ${`${runCmd} check`}\n`;
}
async function getDatabaseInstructions(
database: Database,
orm?: ORM,
runCmd?: string,
_runtime?: Runtime,
dbSetup?: DatabaseSetup,
serverDeploy?: string,
): Promise<string> {
const instructions: string[] = [];
if (dbSetup === "docker") {
const dockerStatus = await getDockerStatus(database);
if (dockerStatus.message) {
instructions.push(dockerStatus.message);
instructions.push("");
}
}
if (serverDeploy === "wrangler" && dbSetup === "d1") {
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
instructions.push(
`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(
`${packageManager} wrangler login`,
)}`,
);
instructions.push(
`${pc.cyan("2.")} Create D1 database: ${pc.white(
`${packageManager} wrangler d1 create your-database-name`,
)}`,
);
instructions.push(
`${pc.cyan(
"3.",
)} Update apps/server/wrangler.jsonc with database_id and database_name`,
);
instructions.push(
`${pc.cyan("4.")} Generate migrations: ${pc.white(
`cd apps/server && ${packageManager} db:generate`,
)}`,
);
instructions.push(
`${pc.cyan("5.")} Apply migrations locally: ${pc.white(
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`,
)}`,
);
instructions.push(
`${pc.cyan("6.")} Apply migrations to production: ${pc.white(
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`,
)}`,
);
}
if (dbSetup === "d1" && serverDeploy === "alchemy") {
}
if (orm === "prisma") {
if (dbSetup === "turso") {
instructions.push(
`${pc.yellow(
"NOTE:",
)} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`,
);
}
if (database === "mongodb" && dbSetup === "docker") {
instructions.push(
`${pc.yellow(
"WARNING:",
)} Prisma + MongoDB + Docker combination\n may not work.`,
);
}
if (dbSetup === "docker") {
instructions.push(
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
);
}
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
} else if (orm === "drizzle") {
if (dbSetup === "docker") {
instructions.push(
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
);
}
if (dbSetup !== "d1") {
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
}
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
if (database === "sqlite" && dbSetup !== "d1") {
instructions.push(
`${pc.cyan(
"•",
)} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
);
}
} else if (orm === "mongoose") {
if (dbSetup === "docker") {
instructions.push(
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
);
}
} else if (orm === "none") {
instructions.push(
`${pc.yellow("NOTE:")} Manual database schema setup\n required.`,
);
}
return instructions.length
? `${pc.bold("Database commands:")}\n${instructions.join("\n")}`
: "";
}
function getTauriInstructions(runCmd?: string): string {
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan(
"•",
)} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan(
"•",
)} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow(
"NOTE:",
)} Tauri requires Rust and platform-specific dependencies.\n See: ${"https://v2.tauri.app/start/prerequisites/"}`;
}
function getPwaInstructions(): string {
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
"NOTE:",
)} There is a known compatibility issue between VitePWA\n and React Router v7. See:\n https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
}
function getStarlightInstructions(runCmd?: string): string {
return `\n${pc.bold("Documentation with Starlight:")}\n${pc.cyan(
"•",
)} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan(
"•",
)} Build docs site: ${`cd apps/docs && ${runCmd} build`}`;
}
function getNoOrmWarning(): string {
return `\n${pc.yellow(
"WARNING:",
)} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
}
function getBunWebNativeWarning(): string {
return `\n${pc.yellow(
"WARNING:",
)} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
}
function getWranglerDeployInstructions(
runCmd?: string,
webDeploy?: string,
serverDeploy?: string,
): string {
const instructions: string[] = [];
if (webDeploy === "wrangler") {
instructions.push(
`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`,
);
}
if (serverDeploy === "wrangler") {
instructions.push(
`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`,
);
}
return instructions.length ? `\n${instructions.join("\n")}` : "";
}
function getAlchemyDeployInstructions(
runCmd?: string,
webDeploy?: string,
serverDeploy?: string,
): string {
const instructions: string[] = [];
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") {
instructions.push(
`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`,
);
} else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") {
instructions.push(
`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`,
);
} else if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
instructions.push(
`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`,
);
}
return instructions.length ? `\n${instructions.join("\n")}` : "";
}

View File

@@ -0,0 +1,294 @@
import path from "node:path";
import { log } from "@clack/prompts";
import { execa } from "execa";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
export async function updatePackageConfigurations(
projectDir: string,
options: ProjectConfig,
) {
await updateRootPackageJson(projectDir, options);
if (options.backend !== "convex") {
await updateServerPackageJson(projectDir, options);
} else {
await updateConvexPackageJson(projectDir, options);
}
}
async function updateRootPackageJson(
projectDir: string,
options: ProjectConfig,
) {
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (!(await fs.pathExists(rootPackageJsonPath))) return;
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
if (!packageJson.scripts) {
packageJson.scripts = {};
}
const scripts = packageJson.scripts;
const backendPackageName =
options.backend === "convex" ? `@${options.projectName}/backend` : "server";
let serverDevScript = "";
if (options.addons.includes("turborepo")) {
serverDevScript = `turbo -F ${backendPackageName} dev`;
} else if (options.packageManager === "bun") {
serverDevScript = `bun run --filter ${backendPackageName} dev`;
} else if (options.packageManager === "pnpm") {
serverDevScript = `pnpm --filter ${backendPackageName} dev`;
} else if (options.packageManager === "npm") {
serverDevScript = `npm run dev --workspace ${backendPackageName}`;
}
let devScript = "";
if (options.packageManager === "pnpm") {
devScript = "pnpm -r dev";
} else if (options.packageManager === "npm") {
devScript = "npm run dev --workspaces";
} else if (options.packageManager === "bun") {
devScript = "bun run --filter '*' dev";
}
const needsDbScripts =
options.backend !== "convex" &&
options.database !== "none" &&
options.orm !== "none" &&
options.orm !== "mongoose";
if (options.addons.includes("turborepo")) {
scripts.dev = "turbo dev";
scripts.build = "turbo build";
scripts["check-types"] = "turbo check-types";
scripts["dev:native"] = "turbo -F native dev";
scripts["dev:web"] = "turbo -F web dev";
scripts["dev:server"] = serverDevScript;
if (options.backend === "convex") {
scripts["dev:setup"] = `turbo -F ${backendPackageName} dev:setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
if (options.orm === "prisma") {
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
}
}
if (options.dbSetup === "docker") {
scripts["db:start"] = `turbo -F ${backendPackageName} db:start`;
scripts["db:watch"] = `turbo -F ${backendPackageName} db:watch`;
scripts["db:stop"] = `turbo -F ${backendPackageName} db:stop`;
scripts["db:down"] = `turbo -F ${backendPackageName} db:down`;
}
} else if (options.packageManager === "pnpm") {
scripts.dev = devScript;
scripts.build = "pnpm -r build";
scripts["check-types"] = "pnpm -r check-types";
scripts["dev:native"] = "pnpm --filter native dev";
scripts["dev:web"] = "pnpm --filter web dev";
scripts["dev:server"] = serverDevScript;
if (options.backend === "convex") {
scripts["dev:setup"] = `pnpm --filter ${backendPackageName} dev:setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
if (options.orm === "prisma") {
scripts["db:generate"] =
`pnpm --filter ${backendPackageName} db:generate`;
scripts["db:migrate"] =
`pnpm --filter ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`pnpm --filter ${backendPackageName} db:generate`;
scripts["db:migrate"] =
`pnpm --filter ${backendPackageName} db:migrate`;
}
}
if (options.dbSetup === "docker") {
scripts["db:start"] = `pnpm --filter ${backendPackageName} db:start`;
scripts["db:watch"] = `pnpm --filter ${backendPackageName} db:watch`;
scripts["db:stop"] = `pnpm --filter ${backendPackageName} db:stop`;
scripts["db:down"] = `pnpm --filter ${backendPackageName} db:down`;
}
} else if (options.packageManager === "npm") {
scripts.dev = devScript;
scripts.build = "npm run build --workspaces";
scripts["check-types"] = "npm run check-types --workspaces";
scripts["dev:native"] = "npm run dev --workspace native";
scripts["dev:web"] = "npm run dev --workspace web";
scripts["dev:server"] = serverDevScript;
if (options.backend === "convex") {
scripts["dev:setup"] =
`npm run dev:setup --workspace ${backendPackageName}`;
}
if (needsDbScripts) {
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
scripts["db:studio"] =
`npm run db:studio --workspace ${backendPackageName}`;
if (options.orm === "prisma") {
scripts["db:generate"] =
`npm run db:generate --workspace ${backendPackageName}`;
scripts["db:migrate"] =
`npm run db:migrate --workspace ${backendPackageName}`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`npm run db:generate --workspace ${backendPackageName}`;
scripts["db:migrate"] =
`npm run db:migrate --workspace ${backendPackageName}`;
}
}
if (options.dbSetup === "docker") {
scripts["db:start"] =
`npm run db:start --workspace ${backendPackageName}`;
scripts["db:watch"] =
`npm run db:watch --workspace ${backendPackageName}`;
scripts["db:stop"] = `npm run db:stop --workspace ${backendPackageName}`;
scripts["db:down"] = `npm run db:down --workspace ${backendPackageName}`;
}
} else if (options.packageManager === "bun") {
scripts.dev = devScript;
scripts.build = "bun run --filter '*' build";
scripts["check-types"] = "bun run --filter '*' check-types";
scripts["dev:native"] = "bun run --filter native dev";
scripts["dev:web"] = "bun run --filter web dev";
scripts["dev:server"] = serverDevScript;
if (options.backend === "convex") {
scripts["dev:setup"] = `bun run --filter ${backendPackageName} dev:setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
if (options.orm === "prisma") {
scripts["db:generate"] =
`bun run --filter ${backendPackageName} db:generate`;
scripts["db:migrate"] =
`bun run --filter ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`bun run --filter ${backendPackageName} db:generate`;
scripts["db:migrate"] =
`bun run --filter ${backendPackageName} db:migrate`;
}
}
if (options.dbSetup === "docker") {
scripts["db:start"] = `bun run --filter ${backendPackageName} db:start`;
scripts["db:watch"] = `bun run --filter ${backendPackageName} db:watch`;
scripts["db:stop"] = `bun run --filter ${backendPackageName} db:stop`;
scripts["db:down"] = `bun run --filter ${backendPackageName} db:down`;
}
}
try {
const { stdout } = await execa(options.packageManager, ["-v"], {
cwd: projectDir,
});
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
} catch (_e) {
log.warn(`Could not determine ${options.packageManager} version.`);
}
if (!packageJson.workspaces) {
packageJson.workspaces = [];
}
const workspaces = packageJson.workspaces;
if (options.backend === "convex") {
if (!workspaces.includes("packages/*")) {
workspaces.push("packages/*");
}
const needsAppsDir =
options.frontend.length > 0 || options.addons.includes("starlight");
if (needsAppsDir && !workspaces.includes("apps/*")) {
workspaces.push("apps/*");
}
} else {
if (!workspaces.includes("apps/*")) {
workspaces.push("apps/*");
}
if (!workspaces.includes("packages/*")) {
workspaces.push("packages/*");
}
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
}
async function updateServerPackageJson(
projectDir: string,
options: ProjectConfig,
) {
const serverPackageJsonPath = path.join(
projectDir,
"apps/server/package.json",
);
if (!(await fs.pathExists(serverPackageJsonPath))) return;
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (!serverPackageJson.scripts) {
serverPackageJson.scripts = {};
}
const scripts = serverPackageJson.scripts;
if (options.database !== "none") {
if (
options.database === "sqlite" &&
options.orm === "drizzle" &&
options.dbSetup !== "d1"
) {
scripts["db:local"] = "turso dev --db-file local.db";
}
if (options.orm === "prisma") {
scripts["db:push"] = "prisma db push";
scripts["db:studio"] = "prisma studio";
scripts["db:generate"] = "prisma generate";
scripts["db:migrate"] = "prisma migrate dev";
} else if (options.orm === "drizzle") {
scripts["db:push"] = "drizzle-kit push";
scripts["db:studio"] = "drizzle-kit studio";
scripts["db:generate"] = "drizzle-kit generate";
scripts["db:migrate"] = "drizzle-kit migrate";
}
}
if (options.dbSetup === "docker") {
scripts["db:start"] = "docker compose up -d";
scripts["db:watch"] = "docker compose up";
scripts["db:stop"] = "docker compose stop";
scripts["db:down"] = "docker compose down";
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
async function updateConvexPackageJson(
projectDir: string,
options: ProjectConfig,
) {
const convexPackageJsonPath = path.join(
projectDir,
"packages/backend/package.json",
);
if (!(await fs.pathExists(convexPackageJsonPath))) return;
const convexPackageJson = await fs.readJson(convexPackageJsonPath);
convexPackageJson.name = `@${options.projectName}/backend`;
if (!convexPackageJson.scripts) {
convexPackageJson.scripts = {};
}
await fs.writeJson(convexPackageJsonPath, convexPackageJson, { spaces: 2 });
}

View File

@@ -0,0 +1,76 @@
import path from "node:path";
import fs from "fs-extra";
import type { Backend, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupRuntime(config: ProjectConfig) {
const { runtime, backend, projectDir } = config;
if (backend === "convex" || backend === "next" || runtime === "none") {
return;
}
const serverDir = path.join(projectDir, "apps/server");
if (!(await fs.pathExists(serverDir))) {
return;
}
if (runtime === "bun") {
await setupBunRuntime(serverDir, backend);
} else if (runtime === "node") {
await setupNodeRuntime(serverDir, backend);
}
}
async function setupBunRuntime(serverDir: string, _backend: Backend) {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
dev: "bun run --hot src/index.ts",
start: "bun run dist/index.js",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
await addPackageDependency({
devDependencies: ["@types/bun"],
projectDir: serverDir,
});
}
async function setupNodeRuntime(serverDir: string, backend: Backend) {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
dev: "tsx watch src/index.ts",
start: "node dist/index.js",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
await addPackageDependency({
devDependencies: ["tsx", "@types/node"],
projectDir: serverDir,
});
if (backend === "hono") {
await addPackageDependency({
dependencies: ["@hono/node-server"],
projectDir: serverDir,
});
} else if (backend === "elysia") {
await addPackageDependency({
dependencies: ["@elysiajs/node"],
projectDir: serverDir,
});
}
}

View File

@@ -0,0 +1,950 @@
import path from "node:path";
import fs from "fs-extra";
import { glob } from "tinyglobby";
import { PKG_ROOT } from "../../constants";
import type { ProjectConfig } from "../../types";
import { processTemplate } from "../../utils/template-processor";
export async function processAndCopyFiles(
sourcePattern: string | string[],
baseSourceDir: string,
destDir: string,
context: ProjectConfig,
overwrite = true,
ignorePatterns?: string[],
) {
const sourceFiles = await glob(sourcePattern, {
cwd: baseSourceDir,
dot: true,
onlyFiles: true,
absolute: false,
ignore: ignorePatterns,
});
for (const relativeSrcPath of sourceFiles) {
const srcPath = path.join(baseSourceDir, relativeSrcPath);
let relativeDestPath = relativeSrcPath;
if (relativeSrcPath.endsWith(".hbs")) {
relativeDestPath = relativeSrcPath.slice(0, -4);
}
const basename = path.basename(relativeDestPath);
if (basename === "_gitignore") {
relativeDestPath = path.join(
path.dirname(relativeDestPath),
".gitignore",
);
} else if (basename === "_npmrc") {
relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
}
const destPath = path.join(destDir, relativeDestPath);
await fs.ensureDir(path.dirname(destPath));
if (!overwrite && (await fs.pathExists(destPath))) {
continue;
}
if (srcPath.endsWith(".hbs")) {
await processTemplate(srcPath, destPath, context);
} else {
await fs.copy(srcPath, destPath, { overwrite: true });
}
}
}
export async function copyBaseTemplate(
projectDir: string,
context: ProjectConfig,
) {
const templateDir = path.join(PKG_ROOT, "templates/base");
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
}
export async function setupFrontendTemplates(
projectDir: string,
context: ProjectConfig,
) {
const hasReactWeb = context.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasSolidWeb = context.frontend.includes("solid");
const hasNativeWind = context.frontend.includes("native-nativewind");
const hasUnistyles = context.frontend.includes("native-unistyles");
const _hasNative = hasNativeWind || hasUnistyles;
const isConvex = context.backend === "convex";
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
const webAppDir = path.join(projectDir, "apps/web");
await fs.ensureDir(webAppDir);
if (hasReactWeb) {
const webBaseDir = path.join(
PKG_ROOT,
"templates/frontend/react/web-base",
);
if (await fs.pathExists(webBaseDir)) {
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
} else {
}
const reactFramework = context.frontend.find((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
f,
),
);
if (reactFramework) {
const frameworkSrcDir = path.join(
PKG_ROOT,
`templates/frontend/react/${reactFramework}`,
);
if (await fs.pathExists(frameworkSrcDir)) {
await processAndCopyFiles(
"**/*",
frameworkSrcDir,
webAppDir,
context,
);
} else {
}
if (!isConvex && context.api !== "none") {
const apiWebBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/react/base`,
);
if (await fs.pathExists(apiWebBaseDir)) {
await processAndCopyFiles(
"**/*",
apiWebBaseDir,
webAppDir,
context,
);
} else {
}
}
}
} else if (hasNuxtWeb) {
const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
if (await fs.pathExists(nuxtBaseDir)) {
await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
} else {
}
if (!isConvex && context.api === "orpc") {
const apiWebNuxtDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/nuxt`,
);
if (await fs.pathExists(apiWebNuxtDir)) {
await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
} else {
}
}
} else if (hasSvelteWeb) {
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
if (await fs.pathExists(svelteBaseDir)) {
await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
} else {
}
if (!isConvex && context.api === "orpc") {
const apiWebSvelteDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/svelte`,
);
if (await fs.pathExists(apiWebSvelteDir)) {
await processAndCopyFiles(
"**/*",
apiWebSvelteDir,
webAppDir,
context,
);
} else {
}
}
} else if (hasSolidWeb) {
const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
if (await fs.pathExists(solidBaseDir)) {
await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
} else {
}
if (!isConvex && context.api === "orpc") {
const apiWebSolidDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/solid`,
);
if (await fs.pathExists(apiWebSolidDir)) {
await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
} else {
}
}
}
}
if (hasNativeWind || hasUnistyles) {
const nativeAppDir = path.join(projectDir, "apps/native");
await fs.ensureDir(nativeAppDir);
const nativeBaseCommonDir = path.join(
PKG_ROOT,
"templates/frontend/native/native-base",
);
if (await fs.pathExists(nativeBaseCommonDir)) {
await processAndCopyFiles(
"**/*",
nativeBaseCommonDir,
nativeAppDir,
context,
);
} else {
}
let nativeFrameworkPath = "";
if (hasNativeWind) {
nativeFrameworkPath = "nativewind";
} else if (hasUnistyles) {
nativeFrameworkPath = "unistyles";
}
const nativeSpecificDir = path.join(
PKG_ROOT,
`templates/frontend/native/${nativeFrameworkPath}`,
);
if (await fs.pathExists(nativeSpecificDir)) {
await processAndCopyFiles(
"**/*",
nativeSpecificDir,
nativeAppDir,
context,
true,
);
}
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
const apiNativeSrcDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/native`,
);
if (await fs.pathExists(apiNativeSrcDir)) {
await processAndCopyFiles(
"**/*",
apiNativeSrcDir,
nativeAppDir,
context,
);
}
}
}
}
export async function setupBackendFramework(
projectDir: string,
context: ProjectConfig,
) {
if (context.backend === "none") {
return;
}
const serverAppDir = path.join(projectDir, "apps/server");
if (context.backend === "convex") {
if (await fs.pathExists(serverAppDir)) {
await fs.remove(serverAppDir);
}
const convexBackendDestDir = path.join(projectDir, "packages/backend");
const convexSrcDir = path.join(
PKG_ROOT,
"templates/backend/convex/packages/backend",
);
await fs.ensureDir(convexBackendDestDir);
if (await fs.pathExists(convexSrcDir)) {
await processAndCopyFiles(
"**/*",
convexSrcDir,
convexBackendDestDir,
context,
);
}
return;
}
await fs.ensureDir(serverAppDir);
const serverBaseDir = path.join(
PKG_ROOT,
"templates/backend/server/server-base",
);
if (await fs.pathExists(serverBaseDir)) {
await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
} else {
}
const frameworkSrcDir = path.join(
PKG_ROOT,
`templates/backend/server/${context.backend}`,
);
if (await fs.pathExists(frameworkSrcDir)) {
await processAndCopyFiles(
"**/*",
frameworkSrcDir,
serverAppDir,
context,
true,
);
} else {
}
if (context.api !== "none") {
const apiServerBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/base`,
);
if (await fs.pathExists(apiServerBaseDir)) {
await processAndCopyFiles(
"**/*",
apiServerBaseDir,
serverAppDir,
context,
true,
);
} else {
}
const apiServerFrameworkDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/${context.backend}`,
);
if (await fs.pathExists(apiServerFrameworkDir)) {
await processAndCopyFiles(
"**/*",
apiServerFrameworkDir,
serverAppDir,
context,
true,
);
} else {
}
}
}
export async function setupDbOrmTemplates(
projectDir: string,
context: ProjectConfig,
) {
if (
context.backend === "convex" ||
context.orm === "none" ||
context.database === "none"
)
return;
const serverAppDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverAppDir);
const dbOrmSrcDir = path.join(
PKG_ROOT,
`templates/db/${context.orm}/${context.database}`,
);
if (await fs.pathExists(dbOrmSrcDir)) {
await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
} else {
}
}
export async function setupAuthTemplate(
projectDir: string,
context: ProjectConfig,
) {
if (context.backend === "convex" || !context.auth) return;
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
const nativeAppDir = path.join(projectDir, "apps/native");
const serverAppDirExists = await fs.pathExists(serverAppDir);
const webAppDirExists = await fs.pathExists(webAppDir);
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
const hasReactWeb = context.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasSolidWeb = context.frontend.includes("solid");
const hasNativeWind = context.frontend.includes("native-nativewind");
const hasUnistyles = context.frontend.includes("native-unistyles");
const hasNative = hasNativeWind || hasUnistyles;
if (serverAppDirExists) {
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
if (await fs.pathExists(authServerBaseSrc)) {
await processAndCopyFiles(
"**/*",
authServerBaseSrc,
serverAppDir,
context,
);
} else {
}
if (context.backend === "next") {
const authServerNextSrc = path.join(
PKG_ROOT,
"templates/auth/server/next",
);
if (await fs.pathExists(authServerNextSrc)) {
await processAndCopyFiles(
"**/*",
authServerNextSrc,
serverAppDir,
context,
);
} else {
}
}
if (context.orm !== "none" && context.database !== "none") {
const orm = context.orm;
const db = context.database;
let authDbSrc = "";
if (orm === "drizzle") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/drizzle/${db}`,
);
} else if (orm === "prisma") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/prisma/${db}`,
);
} else if (orm === "mongoose") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/mongoose/${db}`,
);
}
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
} else if (authDbSrc) {
}
}
}
if (
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) &&
webAppDirExists
) {
if (hasReactWeb) {
const authWebBaseSrc = path.join(
PKG_ROOT,
"templates/auth/web/react/base",
);
if (await fs.pathExists(authWebBaseSrc)) {
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
} else {
}
const reactFramework = context.frontend.find((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
f,
),
);
if (reactFramework) {
const authWebFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/web/react/${reactFramework}`,
);
if (await fs.pathExists(authWebFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
authWebFrameworkSrc,
webAppDir,
context,
);
} else {
}
}
} else if (hasNuxtWeb) {
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
if (await fs.pathExists(authWebNuxtSrc)) {
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
} else {
}
} else if (hasSvelteWeb) {
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
if (await fs.pathExists(authWebSvelteSrc)) {
await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
} else {
}
} else if (hasSolidWeb) {
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
if (await fs.pathExists(authWebSolidSrc)) {
await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
} else {
}
}
}
if (hasNative && nativeAppDirExists) {
const authNativeBaseSrc = path.join(
PKG_ROOT,
"templates/auth/native/native-base",
);
if (await fs.pathExists(authNativeBaseSrc)) {
await processAndCopyFiles(
"**/*",
authNativeBaseSrc,
nativeAppDir,
context,
);
}
let nativeFrameworkAuthPath = "";
if (hasNativeWind) {
nativeFrameworkAuthPath = "nativewind";
} else if (hasUnistyles) {
nativeFrameworkAuthPath = "unistyles";
}
if (nativeFrameworkAuthPath) {
const authNativeFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/native/${nativeFrameworkAuthPath}`,
);
if (await fs.pathExists(authNativeFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
authNativeFrameworkSrc,
nativeAppDir,
context,
);
}
}
}
}
export async function setupAddonsTemplate(
projectDir: string,
context: ProjectConfig,
) {
if (!context.addons || context.addons.length === 0) return;
for (const addon of context.addons) {
if (addon === "none") continue;
if (addon === "ruler") continue;
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
let addonDestDir = projectDir;
if (addon === "pwa") {
const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) {
continue;
}
addonDestDir = webAppDir;
if (context.frontend.includes("next")) {
addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
} else if (
context.frontend.some((f) =>
["tanstack-router", "react-router", "solid"].includes(f),
)
) {
addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
} else {
continue;
}
}
if (await fs.pathExists(addonSrcDir)) {
await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
} else {
}
}
}
export async function setupExamplesTemplate(
projectDir: string,
context: ProjectConfig,
) {
if (
!context.examples ||
context.examples.length === 0 ||
context.examples[0] === "none"
) {
return;
}
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
const serverAppDirExists = await fs.pathExists(serverAppDir);
const webAppDirExists = await fs.pathExists(webAppDir);
const nativeAppDir = path.join(projectDir, "apps/native");
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
const hasReactWeb = context.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasSolidWeb = context.frontend.includes("solid");
for (const example of context.examples) {
if (example === "none") continue;
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
if (
serverAppDirExists &&
context.backend !== "convex" &&
context.backend !== "none"
) {
const exampleServerSrc = path.join(exampleBaseDir, "server");
if (example === "ai" && context.backend === "next") {
const aiNextServerSrc = path.join(exampleServerSrc, "next");
if (await fs.pathExists(aiNextServerSrc)) {
await processAndCopyFiles(
"**/*",
aiNextServerSrc,
serverAppDir,
context,
false,
);
}
}
if (context.orm !== "none" && context.database !== "none") {
const exampleOrmBaseSrc = path.join(
exampleServerSrc,
context.orm,
"base",
);
if (await fs.pathExists(exampleOrmBaseSrc)) {
await processAndCopyFiles(
"**/*",
exampleOrmBaseSrc,
serverAppDir,
context,
false,
);
}
const exampleDbSchemaSrc = path.join(
exampleServerSrc,
context.orm,
context.database,
);
if (await fs.pathExists(exampleDbSchemaSrc)) {
await processAndCopyFiles(
"**/*",
exampleDbSchemaSrc,
serverAppDir,
context,
false,
);
}
}
}
if (webAppDirExists) {
if (hasReactWeb) {
const exampleWebSrc = path.join(exampleBaseDir, "web/react");
if (await fs.pathExists(exampleWebSrc)) {
const reactFramework = context.frontend.find((f) =>
[
"next",
"react-router",
"tanstack-router",
"tanstack-start",
].includes(f),
);
if (reactFramework) {
const exampleWebFrameworkSrc = path.join(
exampleWebSrc,
reactFramework,
);
if (await fs.pathExists(exampleWebFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebFrameworkSrc,
webAppDir,
context,
false,
);
} else {
}
}
}
} else if (hasNuxtWeb) {
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
if (await fs.pathExists(exampleWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebNuxtSrc,
webAppDir,
context,
false,
);
} else {
}
} else if (hasSvelteWeb) {
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
if (await fs.pathExists(exampleWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSvelteSrc,
webAppDir,
context,
false,
);
} else {
}
} else if (hasSolidWeb) {
const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
if (await fs.pathExists(exampleWebSolidSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSolidSrc,
webAppDir,
context,
false,
);
} else {
}
}
}
if (nativeAppDirExists) {
const hasNativeWind = context.frontend.includes("native-nativewind");
const hasUnistyles = context.frontend.includes("native-unistyles");
if (hasNativeWind || hasUnistyles) {
let nativeFramework = "";
if (hasNativeWind) {
nativeFramework = "nativewind";
} else if (hasUnistyles) {
nativeFramework = "unistyles";
}
const exampleNativeSrc = path.join(
exampleBaseDir,
`native/${nativeFramework}`,
);
if (await fs.pathExists(exampleNativeSrc)) {
await processAndCopyFiles(
"**/*",
exampleNativeSrc,
nativeAppDir,
context,
false,
);
}
}
}
}
}
export async function handleExtras(projectDir: string, context: ProjectConfig) {
const extrasDir = path.join(PKG_ROOT, "templates/extras");
const hasNativeWind = context.frontend.includes("native-nativewind");
const hasUnistyles = context.frontend.includes("native-unistyles");
const hasNative = hasNativeWind || hasUnistyles;
if (context.packageManager === "pnpm") {
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
if (await fs.pathExists(pnpmWorkspaceSrc)) {
await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
}
}
if (context.packageManager === "bun") {
const bunfigSrc = path.join(extrasDir, "bunfig.toml.hbs");
if (await fs.pathExists(bunfigSrc)) {
await processAndCopyFiles(
"bunfig.toml.hbs",
extrasDir,
projectDir,
context,
);
}
}
if (
context.packageManager === "pnpm" &&
(hasNative || context.frontend.includes("nuxt"))
) {
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
if (await fs.pathExists(npmrcTemplateSrc)) {
await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
}
}
}
export async function setupDockerComposeTemplates(
projectDir: string,
context: ProjectConfig,
) {
if (context.dbSetup !== "docker" || context.database === "none") {
return;
}
const serverAppDir = path.join(projectDir, "apps/server");
const dockerSrcDir = path.join(
PKG_ROOT,
`templates/db-setup/docker-compose/${context.database}`,
);
if (await fs.pathExists(dockerSrcDir)) {
await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
} else {
}
}
export async function setupDeploymentTemplates(
projectDir: string,
context: ProjectConfig,
) {
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") {
if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
const alchemyTemplateSrc = path.join(
PKG_ROOT,
"templates/deploy/alchemy",
);
if (await fs.pathExists(alchemyTemplateSrc)) {
await processAndCopyFiles(
"alchemy.run.ts.hbs",
alchemyTemplateSrc,
projectDir,
context,
);
const serverAppDir = path.join(projectDir, "apps/server");
if (await fs.pathExists(serverAppDir)) {
await processAndCopyFiles(
"env.d.ts.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
await processAndCopyFiles(
"wrangler.jsonc.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
}
}
} else {
if (context.webDeploy === "alchemy") {
const alchemyTemplateSrc = path.join(
PKG_ROOT,
"templates/deploy/alchemy",
);
const webAppDir = path.join(projectDir, "apps/web");
if (
(await fs.pathExists(alchemyTemplateSrc)) &&
(await fs.pathExists(webAppDir))
) {
await processAndCopyFiles(
"alchemy.run.ts.hbs",
alchemyTemplateSrc,
webAppDir,
context,
);
}
}
if (context.serverDeploy === "alchemy") {
const alchemyTemplateSrc = path.join(
PKG_ROOT,
"templates/deploy/alchemy",
);
const serverAppDir = path.join(projectDir, "apps/server");
if (
(await fs.pathExists(alchemyTemplateSrc)) &&
(await fs.pathExists(serverAppDir))
) {
await processAndCopyFiles(
"alchemy.run.ts.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
await processAndCopyFiles(
"env.d.ts.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
await processAndCopyFiles(
"wrangler.jsonc.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
}
}
}
}
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
const webAppDir = path.join(projectDir, "apps/web");
if (await fs.pathExists(webAppDir)) {
const frontends = context.frontend;
const templateMap: Record<string, string> = {
"tanstack-router": "react/tanstack-router",
"tanstack-start": "react/tanstack-start",
"react-router": "react/react-router",
solid: "solid",
next: "react/next",
nuxt: "nuxt",
svelte: "svelte",
};
for (const f of frontends) {
if (templateMap[f]) {
const deployTemplateSrc = path.join(
PKG_ROOT,
`templates/deploy/${context.webDeploy}/web/${templateMap[f]}`,
);
if (await fs.pathExists(deployTemplateSrc)) {
await processAndCopyFiles(
"**/*",
deployTemplateSrc,
webAppDir,
context,
);
}
}
}
}
}
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
const serverAppDir = path.join(projectDir, "apps/server");
if (await fs.pathExists(serverAppDir)) {
const deployTemplateSrc = path.join(
PKG_ROOT,
`templates/deploy/${context.serverDeploy}/server`,
);
if (await fs.pathExists(deployTemplateSrc)) {
await processAndCopyFiles(
"**/*",
deployTemplateSrc,
serverAppDir,
context,
);
}
}
}
}