add convex

This commit is contained in:
Aman Varshney
2025-04-28 11:42:11 +05:30
parent 7ef3cfce9e
commit 2a5358a105
70 changed files with 2330 additions and 1139 deletions

View File

@@ -1,80 +1,94 @@
import * as path from "node:path";
import path from "node:path";
import consola from "consola"; // Import consola
import fs from "fs-extra";
import type { ProjectConfig } from "../types";
import type { AvailableDependencies } from "../constants";
import type { ProjectConfig, ProjectFrontend } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupApi(config: ProjectConfig): Promise<void> {
const { api, projectName, frontend } = config;
const { api, projectName, frontend, backend, packageManager } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const isConvex = backend === "convex";
const webDir = path.join(projectDir, "apps/web");
const serverDir = path.join(projectDir, "apps/server");
const nativeDir = path.join(projectDir, "apps/native");
const webDirExists = await fs.pathExists(webDir);
const hasReactWeb = frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = frontend.includes("nuxt");
const hasSvelteWeb = frontend.includes("svelte");
const nativeDirExists = await fs.pathExists(nativeDir);
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/server", "@orpc/client"],
projectDir: serverDir,
});
} else if (api === "trpc") {
await addPackageDependency({
dependencies: ["@trpc/server", "@trpc/client"],
projectDir: serverDir,
});
if (config.backend === "hono") {
await addPackageDependency({
dependencies: ["@hono/trpc-server"],
projectDir: serverDir,
});
} else if (config.backend === "elysia") {
await addPackageDependency({
dependencies: ["@elysiajs/trpc"],
projectDir: serverDir,
});
}
}
if (!isConvex && api !== "none") {
const serverDir = path.join(projectDir, "apps/server");
const serverDirExists = await fs.pathExists(serverDir);
if (webDirExists) {
if (hasReactWeb) {
const hasReactWeb = frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = frontend.includes("nuxt");
const hasSvelteWeb = frontend.includes("svelte");
if (serverDirExists) {
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/react-query", "@orpc/client", "@orpc/server"],
projectDir: webDir,
dependencies: ["@orpc/server", "@orpc/client"],
projectDir: serverDir,
});
} else if (api === "trpc") {
await addPackageDependency({
dependencies: [
"@trpc/tanstack-react-query",
"@trpc/client",
"@trpc/server",
],
projectDir: webDir,
dependencies: ["@trpc/server", "@trpc/client"],
projectDir: serverDir,
});
if (config.backend === "hono") {
await addPackageDependency({
dependencies: ["@hono/trpc-server"],
projectDir: serverDir,
});
} else if (config.backend === "elysia") {
await addPackageDependency({
dependencies: ["@elysiajs/trpc"],
projectDir: serverDir,
});
}
}
} else if (hasNuxtWeb) {
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/vue-query", "@orpc/client", "@orpc/server"],
projectDir: webDir,
});
}
} else if (hasSvelteWeb) {
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/svelte-query", "@orpc/client", "@orpc/server"],
projectDir: webDir,
});
} else {
}
if (webDirExists) {
if (hasReactWeb) {
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/react-query", "@orpc/client", "@orpc/server"],
projectDir: webDir,
});
} else if (api === "trpc") {
await addPackageDependency({
dependencies: [
"@trpc/tanstack-react-query",
"@trpc/client",
"@trpc/server",
],
projectDir: webDir,
});
}
} else if (hasNuxtWeb) {
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/vue-query", "@orpc/client", "@orpc/server"],
projectDir: webDir,
});
}
} else if (hasSvelteWeb) {
if (api === "orpc") {
await addPackageDependency({
dependencies: [
"@orpc/svelte-query",
"@orpc/client",
"@orpc/server",
],
projectDir: webDir,
});
}
}
}
}
if (frontend.includes("native")) {
const nativeDir = path.join(projectDir, "apps/native");
if (await fs.pathExists(nativeDir)) {
if (nativeDirExists) {
if (api === "trpc") {
await addPackageDependency({
dependencies: [
@@ -92,4 +106,131 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
}
}
}
const reactBasedFrontends: ProjectFrontend[] = [
"react-router",
"tanstack-router",
"tanstack-start",
"next",
"native",
];
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
if (needsReactQuery && !isConvex) {
const reactQueryDeps: AvailableDependencies[] = ["@tanstack/react-query"];
const reactQueryDevDeps: AvailableDependencies[] = [
"@tanstack/react-query-devtools",
];
const hasReactWeb = frontend.some(
(f) => f !== "native" && reactBasedFrontends.includes(f),
);
const hasNative = frontend.includes("native");
if (hasReactWeb && webDirExists) {
const webPkgJsonPath = path.join(webDir, "package.json");
if (await fs.pathExists(webPkgJsonPath)) {
try {
await addPackageDependency({
dependencies: reactQueryDeps,
devDependencies: reactQueryDevDeps,
projectDir: webDir,
});
} catch (error) {}
} else {
}
}
if (hasNative && nativeDirExists) {
const nativePkgJsonPath = path.join(nativeDir, "package.json");
if (await fs.pathExists(nativePkgJsonPath)) {
try {
await addPackageDependency({
dependencies: reactQueryDeps,
projectDir: nativeDir,
});
} catch (error) {}
} else {
}
}
} else if (needsReactQuery && isConvex) {
}
if (isConvex) {
if (webDirExists) {
const webPkgJsonPath = path.join(webDir, "package.json");
if (await fs.pathExists(webPkgJsonPath)) {
try {
const webDepsToAdd: AvailableDependencies[] = ["convex"];
if (frontend.includes("tanstack-start")) {
webDepsToAdd.push("@convex-dev/react-query");
}
await addPackageDependency({
dependencies: webDepsToAdd,
projectDir: webDir,
});
} catch (error) {}
} else {
}
}
if (nativeDirExists) {
const nativePkgJsonPath = path.join(nativeDir, "package.json");
if (await fs.pathExists(nativePkgJsonPath)) {
try {
await addPackageDependency({
dependencies: ["convex"],
projectDir: nativeDir,
});
} catch (error) {}
} else {
}
}
const backendPackageName = `@${projectName}/backend`;
const backendWorkspaceVersion =
packageManager === "npm" ? "*" : "workspace:*";
const addWorkspaceDepManually = async (
pkgJsonPath: string,
depName: string,
depVersion: string,
) => {
try {
const pkgJson = await fs.readJson(pkgJsonPath);
if (!pkgJson.dependencies) {
pkgJson.dependencies = {};
}
if (pkgJson.dependencies[depName] !== depVersion) {
pkgJson.dependencies[depName] = depVersion;
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
} else {
}
} catch (error) {}
};
if (webDirExists) {
const webPkgJsonPath = path.join(webDir, "package.json");
if (await fs.pathExists(webPkgJsonPath)) {
await addWorkspaceDepManually(
webPkgJsonPath,
backendPackageName,
backendWorkspaceVersion,
);
} else {
}
}
if (nativeDirExists) {
const nativePkgJsonPath = path.join(nativeDir, "package.json");
if (await fs.pathExists(nativePkgJsonPath)) {
await addWorkspaceDepManually(
nativePkgJsonPath,
backendPackageName,
backendWorkspaceVersion,
);
} else {
}
}
}
}

View File

@@ -6,8 +6,9 @@ import type { ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupAuth(config: ProjectConfig): Promise<void> {
const { projectName, auth, frontend } = config;
if (!auth) {
const { projectName, auth, frontend, backend } = config;
if (backend === "convex" || !auth) {
return;
}
@@ -18,12 +19,15 @@ export async function setupAuth(config: ProjectConfig): Promise<void> {
const clientDirExists = await fs.pathExists(clientDir);
const nativeDirExists = await fs.pathExists(nativeDir);
const serverDirExists = await fs.pathExists(serverDir);
try {
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: serverDir,
});
if (serverDirExists) {
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: serverDir,
});
}
const hasWebFrontend = frontend.some((f) =>
[
@@ -48,10 +52,12 @@ export async function setupAuth(config: ProjectConfig): Promise<void> {
dependencies: ["better-auth", "@better-auth/expo"],
projectDir: nativeDir,
});
await addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: serverDir,
});
if (serverDirExists) {
await addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: serverDir,
});
}
}
} catch (error) {
consola.error(pc.red("Failed to configure authentication dependencies"));

View File

@@ -8,6 +8,11 @@ export async function setupBackendDependencies(
config: ProjectConfig,
): Promise<void> {
const { projectName, backend, runtime, api } = config;
if (backend === "convex") {
return;
}
const projectDir = path.resolve(process.cwd(), projectName);
const framework = backend;
const serverDir = path.join(projectDir, "apps/server");
@@ -47,9 +52,11 @@ export async function setupBackendDependencies(
devDependencies.push("@types/bun");
}
await addPackageDependency({
dependencies,
devDependencies,
projectDir: serverDir,
});
if (dependencies.length > 0 || devDependencies.length > 0) {
await addPackageDependency({
dependencies,
devDependencies,
projectDir: serverDir,
});
}
}

View File

@@ -28,44 +28,46 @@ import {
export async function createProject(options: ProjectConfig) {
const projectDir = path.resolve(process.cwd(), options.projectName);
const isConvex = options.backend === "convex";
try {
await fs.ensureDir(projectDir);
await copyBaseTemplate(projectDir, options);
await setupFrontendTemplates(projectDir, options);
await setupBackendFramework(projectDir, options);
await setupBackendDependencies(options);
await setupDbOrmTemplates(projectDir, options);
await setupDatabase(options);
await setupAuthTemplate(projectDir, options);
await setupAuth(options);
if (!isConvex) {
await setupDbOrmTemplates(projectDir, options);
await setupAuthTemplate(projectDir, options);
}
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamplesTemplate(projectDir, options);
}
await setupAddonsTemplate(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);
}
await setupExamplesTemplate(projectDir, options);
await handleExtras(projectDir, options);
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamples(options);
if (!isConvex && options.auth) {
await setupAuth(options);
}
await setupApi(options);
await setupRuntime(options);
await handleExtras(projectDir, options);
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
await initializeGit(projectDir, options.git);
log.success("Project template successfully scaffolded!");
@@ -89,6 +91,10 @@ export async function createProject(options: ProjectConfig) {
cancel(pc.red(`Error during project creation: ${error.message}`));
console.error(error.stack);
process.exit(1);
} else {
cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
console.error(error);
process.exit(1);
}
}
}

View File

@@ -13,13 +13,25 @@ import { setupNeonPostgres } from "./neon-setup";
import type { ProjectConfig } from "../types";
export async function setupDatabase(config: ProjectConfig): Promise<void> {
const { projectName, database, orm, dbSetup } = config;
const { projectName, database, orm, dbSetup, backend } = config;
if (backend === "convex" || database === "none") {
if (backend !== "convex") {
const projectDir = path.resolve(process.cwd(), projectName);
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 projectDir = path.resolve(process.cwd(), projectName);
const s = spinner();
const serverDir = path.join(projectDir, "apps/server");
if (database === "none") {
await fs.remove(path.join(serverDir, "src/db"));
if (!(await fs.pathExists(serverDir))) {
return;
}

View File

@@ -5,7 +5,7 @@ import { generateAuthSecret } from "./auth-setup";
interface EnvVariable {
key: string;
value: string;
value: string | null | undefined;
condition: boolean;
}
@@ -21,41 +21,59 @@ async function addEnvVariablesToFile(
}
let modified = false;
let contentToAdd = "";
for (const { key, value, condition } of variables) {
if (condition) {
const regex = new RegExp(`^${key}=.*$`, "m");
const valueToWrite = value ?? "";
if (regex.test(envContent)) {
if (value) {
envContent = envContent.replace(regex, `${key}=${value}`);
const existingMatch = envContent.match(regex);
if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
modified = true;
}
} else {
envContent += `\n${key}=${value}`;
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.trim());
await fs.writeFile(filePath, envContent.trimEnd());
}
}
export async function setupEnvironmentVariables(
config: ProjectConfig,
): Promise<void> {
const { projectName } = config;
const {
projectName,
backend,
frontend,
database,
orm,
auth,
examples,
dbSetup,
} = config;
const projectDir = path.resolve(process.cwd(), projectName);
const options = config;
const serverDir = path.join(projectDir, "apps/server");
const envPath = path.join(serverDir, ".env");
const hasReactRouter = options.frontend.includes("react-router");
const hasTanStackRouter = options.frontend.includes("tanstack-router");
const hasTanStackStart = options.frontend.includes("tanstack-start");
const hasNextJs = options.frontend.includes("next");
const hasNuxt = options.frontend.includes("nuxt");
const hasSvelte = options.frontend.includes("svelte");
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 hasWebFrontend =
hasReactRouter ||
hasTanStackRouter ||
@@ -64,30 +82,99 @@ export async function setupEnvironmentVariables(
hasNuxt ||
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")) {
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";
} else if (hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt) {
corsOrigin = "http://localhost:3001";
}
let databaseUrl = "";
let databaseUrl: string | null = null;
const specializedSetup =
options.dbSetup === "turso" ||
options.dbSetup === "prisma-postgres" ||
options.dbSetup === "mongodb-atlas" ||
options.dbSetup === "neon";
dbSetup === "turso" ||
dbSetup === "prisma-postgres" ||
dbSetup === "mongodb-atlas" ||
dbSetup === "neon";
if (!specializedSetup) {
if (options.database === "postgres") {
databaseUrl =
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public";
} else if (options.database === "mysql") {
databaseUrl = "mysql://root:password@localhost:3306/mydb";
} else if (options.database === "mongodb") {
databaseUrl = "mongodb://localhost:27017/mydatabase";
} else if (options.database === "sqlite") {
databaseUrl = "file:./local.db";
if (database !== "none" && !specializedSetup) {
switch (database) {
case "postgres":
databaseUrl =
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public";
break;
case "mysql":
databaseUrl = "mysql://root:password@localhost:3306/mydb";
break;
case "mongodb":
databaseUrl = "mongodb://localhost:27017/mydatabase";
break;
case "sqlite":
databaseUrl = "file:./local.db";
break;
}
}
@@ -100,59 +187,24 @@ export async function setupEnvironmentVariables(
{
key: "BETTER_AUTH_SECRET",
value: generateAuthSecret(),
condition: !!options.auth,
condition: !!auth,
},
{
key: "BETTER_AUTH_URL",
value: "http://localhost:3000",
condition: !!options.auth,
condition: !!auth,
},
{
key: "DATABASE_URL",
value: databaseUrl,
condition:
options.database !== "none" && databaseUrl !== "" && !specializedSetup,
condition: database !== "none" && !specializedSetup,
},
{
key: "GOOGLE_GENERATIVE_AI_API_KEY",
value: "",
condition: options.examples?.includes("ai") || false,
condition: examples?.includes("ai") || false,
},
];
await addEnvVariablesToFile(envPath, serverVars);
if (hasWebFrontend) {
const clientDir = path.join(projectDir, "apps/web");
let envVarName = "VITE_SERVER_URL";
if (hasNextJs) {
envVarName = "NEXT_PUBLIC_SERVER_URL";
} else if (hasNuxt) {
envVarName = "NUXT_PUBLIC_SERVER_URL";
} else if (hasSvelte) {
envVarName = "PUBLIC_SERVER_URL";
}
const clientVars: EnvVariable[] = [
{
key: envVarName,
value: "http://localhost:3000",
condition: true,
},
];
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
}
if (options.frontend.includes("native")) {
const nativeDir = path.join(projectDir, "apps/native");
const nativeVars: EnvVariable[] = [
{
key: "EXPO_PUBLIC_SERVER_URL",
value: "http://localhost:3000",
condition: true,
},
];
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
}
}

View File

@@ -5,13 +5,24 @@ import type { ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupExamples(config: ProjectConfig): Promise<void> {
const { projectName, examples, frontend } = config;
const { projectName, examples, frontend, backend } = config;
if (
backend === "convex" ||
!examples ||
examples.length === 0 ||
examples[0] === "none"
) {
return;
}
const projectDir = path.resolve(process.cwd(), projectName);
if (examples.includes("ai")) {
const clientDir = path.join(projectDir, "apps/web");
const serverDir = path.join(projectDir, "apps/server");
const clientDirExists = await fs.pathExists(clientDir);
const serverDirExists = await fs.pathExists(serverDir);
const hasNuxt = frontend.includes("nuxt");
const hasSvelte = frontend.includes("svelte");
@@ -22,6 +33,7 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
dependencies.push("@ai-sdk/vue");
} else if (hasSvelte) {
dependencies.push("@ai-sdk/svelte");
} else {
}
await addPackageDependency({
dependencies,
@@ -29,9 +41,11 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
});
}
await addPackageDependency({
dependencies: ["ai", "@ai-sdk/google"],
projectDir: serverDir,
});
if (serverDirExists) {
await addPackageDependency({
dependencies: ["ai", "@ai-sdk/google"],
projectDir: serverDir,
});
}
}
}

View File

@@ -1,6 +1,11 @@
import { consola } from "consola";
import pc from "picocolors";
import type { ProjectDatabase, ProjectOrm, ProjectRuntime } from "../types";
import type {
ProjectBackend,
ProjectDatabase,
ProjectOrm,
ProjectRuntime,
} from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import type { ProjectConfig } from "../types";
@@ -17,16 +22,20 @@ export function displayPostInstallInstructions(
addons,
runtime,
frontend,
backend,
} = config;
const isConvex = backend === "convex";
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`;
const hasHuskyOrBiome =
addons?.includes("husky") || addons?.includes("biome");
const databaseInstructions =
database !== "none"
!isConvex && database !== "none"
? getDatabaseInstructions(database, orm, runCmd, runtime)
: "";
const tauriInstructions = addons?.includes("tauri")
? getTauriInstructions(runCmd)
: "";
@@ -34,7 +43,7 @@ export function displayPostInstallInstructions(
? getLintingInstructions(runCmd)
: "";
const nativeInstructions = frontend?.includes("native")
? getNativeInstructions()
? getNativeInstructions(isConvex)
: "";
const pwaInstructions =
addons?.includes("pwa") &&
@@ -45,6 +54,7 @@ export function displayPostInstallInstructions(
const starlightInstructions = addons?.includes("starlight")
? getStarlightInstructions(runCmd)
: "";
const hasWeb = frontend?.some((f) =>
[
"tanstack-router",
@@ -56,78 +66,86 @@ export function displayPostInstallInstructions(
].includes(f),
);
const hasNative = frontend?.includes("native");
const bunWebNativeWarning =
packageManager === "bun" && hasNative && hasWeb
? getBunWebNativeWarning()
: "";
const noOrmWarning =
database !== "none" && orm === "none" ? getNoOrmWarning() : "";
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
const hasTanstackRouter = frontend?.includes("tanstack-router");
const hasTanstackStart = frontend?.includes("tanstack-start");
const hasReactRouter = frontend?.includes("react-router");
const hasNuxt = frontend?.includes("nuxt");
const hasSvelte = frontend?.includes("svelte");
const hasWebFrontend =
hasTanstackRouter ||
hasReactRouter ||
hasTanstackStart ||
hasNuxt ||
hasSvelte;
const hasNativeFrontend = frontend?.includes("native");
const hasFrontend = hasWebFrontend || hasNativeFrontend;
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
consola.box(
`${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}
${
!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""
}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
${pc.bold("Your project will be available at:")}
${
hasFrontend
? `${
hasWebFrontend
? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`
: ""
}`
: `${pc.yellow(
"NOTE:",
)} You are creating a backend-only app (no frontend selected)\n`
}${pc.cyan("•")} Backend: http://localhost:3000
${
addons?.includes("starlight")
? `${pc.cyan("•")} Docs: http://localhost:4321\n`
: ""
}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${
databaseInstructions ? `\n${databaseInstructions.trim()}` : ""
}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${
lintingInstructions ? `\n${lintingInstructions.trim()}` : ""
}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${
starlightInstructions ? `\n${starlightInstructions.trim()}` : ""
}${noOrmWarning ? `\n${noOrmWarning.trim()}` : ""}${
bunWebNativeWarning ? `\n${bunWebNativeWarning.trim()}` : ""
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 ${pc.dim("(this will guide you through Convex project setup)")}\n`;
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
} else {
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\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 (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 (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 (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 on GitHub:\n`;
output += pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack");
consola.box(output);
}
${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}
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";
${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub:
${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
);
}
function getNativeInstructions(): string {
return `${pc.yellow(
"NOTE:",
)} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`;
)} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`;
}
function getLintingInstructions(runCmd?: string): string {
return `${pc.bold("Linting and formatting:")}\n${pc.cyan(
"•",
)} Format and lint fix: ${`${runCmd} check`}\n\n`;
)} Format and lint fix: ${`${runCmd} check`}\n`;
}
function getDatabaseInstructions(
@@ -161,10 +179,19 @@ function getDatabaseInstructions(
} else if (orm === "drizzle") {
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
if (database === "sqlite") {
instructions.push(
`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
);
}
} else if (orm === "none") {
instructions.push(
`${pc.yellow("NOTE:")} Manual database schema setup required.`,
);
}
return instructions.length
? `${pc.bold("Database commands:")}\n${instructions.join("\n")}\n\n`
? `${pc.bold("Database commands:")}\n${instructions.join("\n")}`
: "";
}
@@ -175,31 +202,31 @@ function getTauriInstructions(runCmd?: string): string {
"•",
)} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow(
"NOTE:",
)} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`;
)} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}`;
}
function getPwaInstructions(): string {
return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
"NOTE:",
)} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`;
)} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
}
function getStarlightInstructions(runCmd?: string): string {
return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan(
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`}\n`;
)} Build docs site: ${`cd apps/docs && ${runCmd} build`}`;
}
function getNoOrmWarning(): string {
return `\n${pc.yellow(
"WARNING:",
)} Database selected without an ORM. Features requiring database access (e.g., examples, auth) need manual setup.\n`;
)} Database selected without an ORM. Features requiring 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. Use 'pnpm' if problems arise.\n`;
)} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
}

View File

@@ -3,7 +3,6 @@ import { log } from "@clack/prompts";
import { $, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import { dependencyVersionMap } from "../constants";
import type { ProjectConfig } from "../types";
export async function updatePackageConfigurations(
@@ -11,7 +10,11 @@ export async function updatePackageConfigurations(
options: ProjectConfig,
): Promise<void> {
await updateRootPackageJson(projectDir, options);
await updateServerPackageJson(projectDir, options);
if (options.backend !== "convex") {
await updateServerPackageJson(projectDir, options);
} else {
await updateConvexPackageJson(projectDir, options);
}
}
async function updateRootPackageJson(
@@ -19,75 +22,148 @@ async function updateRootPackageJson(
options: ProjectConfig,
): Promise<void> {
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
if (!(await fs.pathExists(rootPackageJsonPath))) return;
const turboScripts = {
dev: "turbo dev",
build: "turbo build",
"check-types": "turbo check-types",
"dev:native": "turbo -F native dev",
"dev:web": "turbo -F web dev",
"dev:server": "turbo -F server dev",
"db:push": "turbo -F server db:push",
"db:studio": "turbo -F server db:studio",
};
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
const pnpmScripts = {
dev: "pnpm -r dev",
build: "pnpm -r build",
"check-types": "pnpm -r check-types",
"dev:native": "pnpm --filter native dev",
"dev:web": "pnpm --filter web dev",
"dev:server": "pnpm --filter server dev",
"db:push": "pnpm --filter server db:push",
"db:studio": "pnpm --filter server db:studio",
};
if (!packageJson.scripts) {
packageJson.scripts = {};
}
const scripts = packageJson.scripts;
const npmScripts = {
dev: "npm run dev --workspaces",
build: "npm run build --workspaces",
"check-types": "npm run check-types --workspaces",
"dev:native": "npm run dev --workspace native",
"dev:web": "npm run dev --workspace web",
"dev:server": "npm run dev --workspace server",
"db:push": "npm run db:push --workspace server",
"db:studio": "npm run db:studio --workspace server",
};
const backendPackageName =
options.backend === "convex" ? `@${options.projectName}/backend` : "server";
const bunScripts = {
dev: "bun run --filter '*' dev",
build: "bun run --filter '*' build",
"check-types": "bun run --filter '*' check-types",
"dev:native": "bun run --filter native dev",
"dev:web": "bun run --filter web dev",
"dev:server": "bun run --filter server dev",
"db:push": "bun run --filter server db:push",
"db:studio": "bun run --filter server db:studio",
};
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}`;
}
if (options.addons.includes("turborepo")) {
packageJson.scripts = turboScripts;
} else {
if (options.packageManager === "pnpm") {
packageJson.scripts = pnpmScripts;
} else if (options.packageManager === "npm") {
packageJson.scripts = npmScripts;
} else if (options.packageManager === "bun") {
packageJson.scripts = bunScripts;
} else {
packageJson.scripts = {};
}
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";
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} setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
}
} 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} setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
}
} 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 setup --workspace ${backendPackageName}`;
}
if (needsDbScripts) {
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
scripts["db:studio"] =
`npm run db:studio --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} setup`;
}
if (needsDbScripts) {
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
}
}
if (options.addons.includes("biome")) {
scripts.check = "biome check --write .";
}
if (options.addons.includes("husky")) {
scripts.prepare = "husky";
packageJson["lint-staged"] = {
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
"biome check --write .",
],
};
}
try {
const { stdout } = await execa(options.packageManager, ["-v"], {
cwd: projectDir,
});
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
} 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(
@@ -99,28 +175,53 @@ async function updateServerPackageJson(
"apps/server/package.json",
);
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (!(await fs.pathExists(serverPackageJsonPath))) return;
if (options.database !== "none") {
if (options.database === "sqlite" && options.orm === "drizzle") {
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
}
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (options.orm === "prisma") {
serverPackageJson.scripts["db:push"] =
"prisma db push --schema ./prisma/schema";
serverPackageJson.scripts["db:studio"] = "prisma studio";
} else if (options.orm === "drizzle") {
serverPackageJson.scripts["db:push"] = "drizzle-kit push";
serverPackageJson.scripts["db:studio"] = "drizzle-kit studio";
}
if (!serverPackageJson.scripts) {
serverPackageJson.scripts = {};
}
const scripts = serverPackageJson.scripts;
if (options.database !== "none") {
if (options.database === "sqlite" && options.orm === "drizzle") {
scripts["db:local"] = "turso dev --db-file local.db";
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
if (options.orm === "prisma") {
scripts["db:push"] = "prisma db push --schema ./prisma/schema.prisma";
scripts["db:studio"] = "prisma studio";
} else if (options.orm === "drizzle") {
scripts["db:push"] = "drizzle-kit push";
scripts["db:studio"] = "drizzle-kit studio";
}
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
async function updateConvexPackageJson(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
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 });
}
export async function initializeGit(

View File

@@ -5,13 +5,18 @@ import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime(config: ProjectConfig): Promise<void> {
const { projectName, runtime, backend } = config;
const projectDir = path.resolve(process.cwd(), projectName);
if (backend === "next") {
if (backend === "convex" || backend === "next" || runtime === "none") {
return;
}
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server");
if (!(await fs.pathExists(serverDir))) {
return;
}
if (runtime === "bun") {
await setupBunRuntime(serverDir, backend);
} else if (runtime === "node") {
@@ -24,6 +29,8 @@ async function setupBunRuntime(
backend: ProjectBackend,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
@@ -45,6 +52,8 @@ async function setupNodeRuntime(
backend: ProjectBackend,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {

View File

@@ -1,5 +1,4 @@
import path from "node:path";
import consola from "consola";
import fs from "fs-extra";
import { globby } from "globby";
import pc from "picocolors";
@@ -28,26 +27,28 @@ async function processAndCopyFiles(
if (relativeSrcPath.endsWith(".hbs")) {
relativeDestPath = relativeSrcPath.slice(0, -4);
}
if (path.basename(relativeSrcPath) === "_gitignore") {
const basename = path.basename(relativeSrcPath);
if (basename === "_gitignore") {
relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore");
}
if (path.basename(relativeSrcPath) === "_npmrc") {
} else if (basename === "_npmrc") {
relativeDestPath = path.join(path.dirname(relativeSrcPath), ".npmrc");
}
const destPath = path.join(destDir, relativeDestPath);
await fs.ensureDir(path.dirname(destPath));
try {
await fs.ensureDir(path.dirname(destPath));
if (!overwrite && (await fs.pathExists(destPath))) {
continue;
}
if (!overwrite && (await fs.pathExists(destPath))) {
continue;
}
if (srcPath.endsWith(".hbs")) {
await processTemplate(srcPath, destPath, context);
} else {
await fs.copy(srcPath, destPath, { overwrite: true });
}
if (srcPath.endsWith(".hbs")) {
await processTemplate(srcPath, destPath, context);
} else {
await fs.copy(srcPath, destPath, { overwrite: true });
}
} catch (error) {}
}
}
@@ -57,6 +58,7 @@ export async function copyBaseTemplate(
): Promise<void> {
const templateDir = path.join(PKG_ROOT, "templates/base");
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
await fs.ensureDir(path.join(projectDir, "packages"));
}
export async function setupFrontendTemplates(
@@ -69,6 +71,7 @@ export async function setupFrontendTemplates(
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasNative = context.frontend.includes("native");
const isConvex = context.backend === "convex";
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb) {
const webAppDir = path.join(projectDir, "apps/web");
@@ -81,6 +84,7 @@ export async function setupFrontendTemplates(
);
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(
@@ -99,33 +103,47 @@ export async function setupFrontendTemplates(
webAppDir,
context,
);
} else {
}
const apiWebBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/react/base`,
);
if (await fs.pathExists(apiWebBaseDir)) {
await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
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 {
}
const apiWebNuxtDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/nuxt`,
);
if (await fs.pathExists(apiWebNuxtDir)) {
await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
if (!isConvex && context.api !== "none") {
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 (context.api === "orpc") {
if (!isConvex && context.api === "orpc") {
const apiWebSvelteDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/svelte`,
@@ -137,6 +155,7 @@ export async function setupFrontendTemplates(
webAppDir,
context,
);
} else {
}
}
}
@@ -149,22 +168,10 @@ export async function setupFrontendTemplates(
const nativeBaseDir = path.join(PKG_ROOT, "templates/frontend/native");
if (await fs.pathExists(nativeBaseDir)) {
await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
} else {
}
if (context.api === "trpc") {
const apiNativeSrcDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/native`,
);
if (await fs.pathExists(apiNativeSrcDir)) {
await processAndCopyFiles(
"**/*",
apiNativeSrcDir,
nativeAppDir,
context,
);
}
} else if (context.api === "orpc") {
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
const apiNativeSrcDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/native`,
@@ -176,6 +183,7 @@ export async function setupFrontendTemplates(
nativeAppDir,
context,
);
} else {
}
}
}
@@ -185,53 +193,89 @@ export async function setupBackendFramework(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if ((context.backend as string) === "none") return;
if (context.backend === "convex") {
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,
);
} else {
}
const serverAppDir = path.join(projectDir, "apps/server");
if (await fs.pathExists(serverAppDir)) {
await fs.remove(serverAppDir);
}
return;
}
const serverAppDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverAppDir);
const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server-base");
const serverBaseDir = path.join(
PKG_ROOT,
"templates/backend/server/server-base",
);
if (await fs.pathExists(serverBaseDir)) {
await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(`Warning: server-base template not found at ${serverBaseDir}`),
);
}
const frameworkSrcDir = path.join(
PKG_ROOT,
`templates/backend/${context.backend}`,
`templates/backend/server/${context.backend}`,
);
if (await fs.pathExists(frameworkSrcDir)) {
await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Backend template directory not found, skipping: ${frameworkSrcDir}`,
),
);
}
const apiServerBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/base`,
);
if (await fs.pathExists(apiServerBaseDir)) {
await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context);
}
const apiServerFrameworkDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/${context.backend}`,
);
if (await fs.pathExists(apiServerFrameworkDir)) {
await processAndCopyFiles(
"**/*",
apiServerFrameworkDir,
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 {
}
}
}
@@ -239,7 +283,12 @@ export async function setupDbOrmTemplates(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (context.orm === "none" || context.database === "none") return;
if (
context.backend === "convex" ||
context.orm === "none" ||
context.database === "none"
)
return;
const serverAppDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverAppDir);
@@ -252,11 +301,6 @@ export async function setupDbOrmTemplates(
if (await fs.pathExists(dbOrmSrcDir)) {
await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Database/ORM template directory not found, skipping: ${dbOrmSrcDir}`,
),
);
}
}
@@ -264,7 +308,7 @@ export async function setupAuthTemplate(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (!context.auth) return;
if (context.backend === "convex" || !context.auth) return;
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
@@ -290,6 +334,7 @@ export async function setupAuthTemplate(
serverAppDir,
context,
);
} else {
}
if (context.backend === "next") {
@@ -304,6 +349,7 @@ export async function setupAuthTemplate(
serverAppDir,
context,
);
} else {
}
}
@@ -324,12 +370,7 @@ export async function setupAuthTemplate(
}
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Auth template for ${orm}/${db} not found at ${authDbSrc}`,
),
);
} else if (authDbSrc) {
}
}
}
@@ -342,6 +383,7 @@ export async function setupAuthTemplate(
);
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(
@@ -360,12 +402,14 @@ export async function setupAuthTemplate(
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) {
if (context.api === "orpc") {
@@ -380,6 +424,7 @@ export async function setupAuthTemplate(
webAppDir,
context,
);
} else {
}
}
}
@@ -390,11 +435,6 @@ export async function setupAuthTemplate(
if (await fs.pathExists(authNativeSrc)) {
await processAndCopyFiles("**/*", authNativeSrc, nativeAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Auth native template not found at ${authNativeSrc}`,
),
);
}
}
}
@@ -430,8 +470,6 @@ export async function setupExamplesTemplate(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (!context.examples || context.examples.length === 0) return;
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
@@ -445,6 +483,13 @@ export async function setupExamplesTemplate(
const hasSvelteWeb = context.frontend.includes("svelte");
for (const example of context.examples) {
if (
!context.examples ||
context.examples.length === 0 ||
context.examples[0] === "none"
)
continue;
if (example === "none") continue;
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
@@ -452,23 +497,23 @@ export async function setupExamplesTemplate(
if (serverAppDirExists) {
const exampleServerSrc = path.join(exampleBaseDir, "server");
if (await fs.pathExists(exampleServerSrc)) {
if (context.orm !== "none") {
const exampleOrmBaseSrc = path.join(
exampleServerSrc,
context.orm,
"base",
);
if (await fs.pathExists(exampleOrmBaseSrc)) {
await processAndCopyFiles(
"**/*",
exampleOrmBaseSrc,
serverAppDir,
context,
false,
if (context.backend !== "convex") {
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,
);
}
if (context.database !== "none") {
const exampleDbSchemaSrc = path.join(
exampleServerSrc,
context.orm,
@@ -484,63 +529,81 @@ export async function setupExamplesTemplate(
);
}
}
const generalServerFiles = await globby(["*.ts", "*.hbs"], {
cwd: exampleServerSrc,
onlyFiles: true,
deep: 1,
ignore: [`${context.orm}/**`],
});
for (const file of generalServerFiles) {
const srcPath = path.join(exampleServerSrc, file);
const destPath = path.join(serverAppDir, file.replace(".hbs", ""));
if (srcPath.endsWith(".hbs")) {
await processTemplate(srcPath, destPath, context);
} else {
await fs.copy(srcPath, destPath, { overwrite: false });
}
}
}
}
}
if (hasReactWeb && webAppDirExists) {
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 (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 (await fs.pathExists(exampleWebFrameworkSrc)) {
if (reactFramework) {
const exampleWebFrameworkSrc = path.join(
exampleWebSrc,
reactFramework,
);
if (await fs.pathExists(exampleWebFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebFrameworkSrc,
webAppDir,
context,
false,
);
} else {
}
}
}
} else if (hasNuxtWeb) {
if (context.api === "orpc") {
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
if (await fs.pathExists(exampleWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebFrameworkSrc,
exampleWebNuxtSrc,
webAppDir,
context,
false,
);
} else {
}
}
}
} else if (hasNuxtWeb && webAppDirExists) {
if (context.api === "orpc") {
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
if (await fs.pathExists(exampleWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebNuxtSrc,
webAppDir,
context,
false,
);
} else {
}
}
} else if (hasSvelteWeb && webAppDirExists) {
if (context.api === "orpc") {
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
if (await fs.pathExists(exampleWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSvelteSrc,
webAppDir,
context,
false,
);
} else {
} else if (hasSvelteWeb) {
if (context.api === "orpc") {
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
if (await fs.pathExists(exampleWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSvelteSrc,
webAppDir,
context,
false,
);
} else {
}
}
}
}