mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add convex
This commit is contained in:
5
.changeset/tiny-plants-like.md
Normal file
5
.changeset/tiny-plants-like.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
add convex
|
||||
@@ -88,6 +88,12 @@ export const dependencyVersionMap = {
|
||||
"@trpc/tanstack-react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
|
||||
convex: "^1.23.0",
|
||||
"@convex-dev/react-query": "^0.0.0-alpha.8",
|
||||
|
||||
"@tanstack/react-query-devtools": "^5.69.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
} as const;
|
||||
|
||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,17 +123,17 @@ async function main() {
|
||||
.option("backend", {
|
||||
type: "string",
|
||||
describe: "Backend framework",
|
||||
choices: ["hono", "express", "next", "elysia"],
|
||||
choices: ["hono", "express", "next", "elysia", "convex"],
|
||||
})
|
||||
.option("runtime", {
|
||||
type: "string",
|
||||
describe: "Runtime",
|
||||
choices: ["bun", "node"],
|
||||
choices: ["bun", "node", "none"],
|
||||
})
|
||||
.option("api", {
|
||||
type: "string",
|
||||
describe: "API type",
|
||||
choices: ["trpc", "orpc"],
|
||||
choices: ["trpc", "orpc", "none"],
|
||||
})
|
||||
.completion()
|
||||
.recommendCommands()
|
||||
@@ -168,11 +168,17 @@ async function main() {
|
||||
...flagConfig,
|
||||
};
|
||||
|
||||
if (config.database === "none") {
|
||||
if (config.backend === "convex") {
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
} else if (config.database === "none") {
|
||||
config.orm = "none";
|
||||
config.auth = false;
|
||||
config.dbSetup = "none";
|
||||
config.examples = config.examples.filter((ex) => ex !== "todo");
|
||||
}
|
||||
|
||||
log.info(pc.yellow("Using these default/flag options:"));
|
||||
@@ -216,7 +222,9 @@ async function main() {
|
||||
cancel(pc.red(`Invalid arguments: ${error.message}`));
|
||||
} else {
|
||||
consola.error(`An unexpected error occurred: ${error.message}`);
|
||||
consola.error(error.stack);
|
||||
if (!error.message.includes("is only supported with")) {
|
||||
consola.error(error.stack);
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
@@ -232,6 +240,32 @@ function processAndValidateFlags(
|
||||
projectDirectory?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
const providedFlags: Set<string> = new Set(
|
||||
Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
|
||||
);
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as ProjectBackend;
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "convex"
|
||||
) {
|
||||
if (providedFlags.has("api") && options.api === "none") {
|
||||
consola.fatal(
|
||||
`'--api none' is only supported with '--backend convex'. Please choose 'trpc', 'orpc', or remove the --api flag.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
consola.fatal(
|
||||
`'--runtime none' is only supported with '--backend convex'. Please choose 'bun', 'node', or remove the --runtime flag.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as ProjectDatabase;
|
||||
@@ -248,12 +282,22 @@ function processAndValidateFlags(
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as ProjectBackend;
|
||||
}
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as ProjectRuntime;
|
||||
}
|
||||
if (options.api) {
|
||||
config.api = options.api as ProjectApi;
|
||||
}
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as ProjectDBSetup;
|
||||
}
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
config.projectName = projectDirectory;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
if (options.frontend.includes("none")) {
|
||||
if (options.frontend.length > 1) {
|
||||
@@ -283,9 +327,6 @@ function processAndValidateFlags(
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
}
|
||||
if (options.api) {
|
||||
config.api = options.api as ProjectApi;
|
||||
}
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
@@ -310,231 +351,283 @@ function processAndValidateFlags(
|
||||
config.examples = options.examples.filter(
|
||||
(ex): ex is ProjectExamples => ex !== "none",
|
||||
);
|
||||
if (config.backend !== "convex" && options.examples.includes("none")) {
|
||||
config.examples = [];
|
||||
} else {
|
||||
config.examples = ["todo"];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
config.projectName = projectDirectory;
|
||||
}
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as ProjectDBSetup;
|
||||
}
|
||||
|
||||
const effectiveDatabase =
|
||||
config.database ?? (options.yes ? DEFAULT_CONFIG.database : undefined);
|
||||
const effectiveOrm =
|
||||
config.orm ?? (options.yes ? DEFAULT_CONFIG.orm : undefined);
|
||||
const effectiveAuth =
|
||||
config.auth ?? (options.yes ? DEFAULT_CONFIG.auth : undefined);
|
||||
const effectiveDbSetup =
|
||||
config.dbSetup ?? (options.yes ? DEFAULT_CONFIG.dbSetup : undefined);
|
||||
const effectiveExamples =
|
||||
config.examples ?? (options.yes ? DEFAULT_CONFIG.examples : undefined);
|
||||
const effectiveFrontend =
|
||||
config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
||||
const effectiveApi =
|
||||
config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined);
|
||||
const effectiveBackend =
|
||||
config.backend ?? (options.yes ? DEFAULT_CONFIG.backend : undefined);
|
||||
if (config.backend === "convex") {
|
||||
const incompatibleFlags: string[] = [];
|
||||
|
||||
if (effectiveDatabase === "none") {
|
||||
if (effectiveOrm && effectiveOrm !== "none") {
|
||||
if (providedFlags.has("auth") && options.auth === true)
|
||||
incompatibleFlags.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
incompatibleFlags.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
incompatibleFlags.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
incompatibleFlags.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||
if (providedFlags.has("examples")) {
|
||||
incompatibleFlags.push("--examples");
|
||||
}
|
||||
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`Cannot use ORM '--orm ${effectiveOrm}' when database is 'none'.`,
|
||||
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them. The 'todo' example is included automatically with Convex.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.orm = "none";
|
||||
|
||||
if (effectiveAuth === true) {
|
||||
consola.fatal(
|
||||
"Authentication requires a database. Cannot use --auth when database is 'none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.auth = false;
|
||||
|
||||
if (effectiveDbSetup && effectiveDbSetup !== "none") {
|
||||
consola.fatal(
|
||||
`Database setup '--db-setup ${effectiveDbSetup}' requires a database. Cannot use when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = ["todo"];
|
||||
} else {
|
||||
const effectiveDatabase =
|
||||
config.database ?? (options.yes ? DEFAULT_CONFIG.database : undefined);
|
||||
const effectiveOrm =
|
||||
config.orm ?? (options.yes ? DEFAULT_CONFIG.orm : undefined);
|
||||
const effectiveAuth =
|
||||
config.auth ?? (options.yes ? DEFAULT_CONFIG.auth : undefined);
|
||||
const effectiveDbSetup =
|
||||
config.dbSetup ?? (options.yes ? DEFAULT_CONFIG.dbSetup : undefined);
|
||||
const effectiveExamples =
|
||||
config.examples ?? (options.yes ? DEFAULT_CONFIG.examples : undefined);
|
||||
const effectiveFrontend =
|
||||
config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
||||
const effectiveApi =
|
||||
config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined);
|
||||
const effectiveBackend =
|
||||
config.backend ?? (options.yes ? DEFAULT_CONFIG.backend : undefined);
|
||||
|
||||
if (effectiveExamples?.includes("todo")) {
|
||||
if (effectiveDatabase === "none") {
|
||||
if (providedFlags.has("orm") && options.orm !== "none") {
|
||||
consola.fatal(
|
||||
`Cannot use ORM '--orm ${options.orm}' when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.orm = "none";
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true) {
|
||||
consola.fatal(
|
||||
"Authentication requires a database. Cannot use --auth when database is 'none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.auth = false;
|
||||
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
|
||||
consola.fatal(
|
||||
`Database setup '--db-setup ${options.dbSetup}' requires a database. Cannot use when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.dbSetup = "none";
|
||||
}
|
||||
|
||||
if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") {
|
||||
consola.fatal(
|
||||
"The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.",
|
||||
"MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (config.examples) {
|
||||
config.examples = config.examples.filter((ex) => ex !== "todo");
|
||||
|
||||
if (config.dbSetup && config.dbSetup !== "none") {
|
||||
const dbSetup = config.dbSetup;
|
||||
if (dbSetup === "turso") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "sqlite") {
|
||||
consola.fatal(
|
||||
`Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (effectiveOrm === "prisma") {
|
||||
consola.fatal(
|
||||
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "sqlite";
|
||||
config.orm = "drizzle";
|
||||
} else if (dbSetup === "prisma-postgres") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
effectiveOrm &&
|
||||
effectiveOrm !== "prisma" &&
|
||||
effectiveOrm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "postgres";
|
||||
config.orm = "prisma";
|
||||
} else if (dbSetup === "mongodb-atlas") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "mongodb") {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
effectiveOrm &&
|
||||
effectiveOrm !== "prisma" &&
|
||||
effectiveOrm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires Prisma ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "mongodb";
|
||||
config.orm = "prisma";
|
||||
} else if (dbSetup === "neon") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "postgres";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") {
|
||||
consola.fatal(
|
||||
"MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const includesNuxt = effectiveFrontend?.includes("nuxt");
|
||||
const includesSvelte = effectiveFrontend?.includes("svelte");
|
||||
|
||||
if (config.dbSetup && config.dbSetup !== "none") {
|
||||
const dbSetup = config.dbSetup;
|
||||
if (dbSetup === "turso") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "sqlite") {
|
||||
if ((includesNuxt || includesSvelte) && effectiveApi === "trpc") {
|
||||
consola.fatal(
|
||||
`tRPC API is not supported with '${
|
||||
includesNuxt ? "nuxt" : "svelte"
|
||||
}' frontend. Please use --api orpc or remove '${
|
||||
includesNuxt ? "nuxt" : "svelte"
|
||||
}' from --frontend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
(includesNuxt || includesSvelte) &&
|
||||
effectiveApi !== "orpc" &&
|
||||
(!options.api || (options.yes && options.api !== "trpc"))
|
||||
) {
|
||||
if (config.api !== "none") {
|
||||
config.api = "orpc";
|
||||
}
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
const webSpecificAddons = ["pwa", "tauri"];
|
||||
const hasWebSpecificAddons = config.addons.some((addon) =>
|
||||
webSpecificAddons.includes(addon),
|
||||
);
|
||||
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
||||
(f) =>
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
(f === "nuxt" &&
|
||||
config.addons?.includes("tauri") &&
|
||||
!config.addons?.includes("pwa")) ||
|
||||
(f === "svelte" &&
|
||||
config.addons?.includes("tauri") &&
|
||||
!config.addons?.includes("pwa")),
|
||||
);
|
||||
|
||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||
let incompatibleAddon = "";
|
||||
if (config.addons.includes("pwa") && includesNuxt) {
|
||||
incompatibleAddon = "PWA addon is not compatible with Nuxt.";
|
||||
} else if (
|
||||
config.addons.includes("pwa") ||
|
||||
config.addons.includes("tauri")
|
||||
) {
|
||||
incompatibleAddon =
|
||||
"PWA and Tauri addons require tanstack-router, react-router, or Nuxt/Svelte (Tauri only).";
|
||||
}
|
||||
consola.fatal(
|
||||
`Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`,
|
||||
`${incompatibleAddon} Cannot use these addons with your frontend selection.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (effectiveOrm === "prisma") {
|
||||
consola.fatal(
|
||||
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
|
||||
|
||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
||||
consola.warn(
|
||||
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "sqlite";
|
||||
config.orm = "drizzle";
|
||||
} else if (dbSetup === "prisma-postgres") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
const onlyNativeFrontend =
|
||||
effectiveFrontend &&
|
||||
effectiveFrontend.length === 1 &&
|
||||
effectiveFrontend[0] === "native";
|
||||
|
||||
if (
|
||||
onlyNativeFrontend &&
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
consola.fatal(
|
||||
"Examples are not supported when only the 'native' frontend is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
if (
|
||||
effectiveOrm &&
|
||||
effectiveOrm !== "prisma" &&
|
||||
effectiveOrm !== "none"
|
||||
config.examples.includes("todo") &&
|
||||
effectiveBackend !== "convex" &&
|
||||
effectiveDatabase === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`,
|
||||
"The 'todo' example requires a database (unless using Convex). Cannot use --examples todo when database is 'none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "postgres";
|
||||
config.orm = "prisma";
|
||||
} else if (dbSetup === "mongodb-atlas") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "mongodb") {
|
||||
|
||||
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`,
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
effectiveOrm &&
|
||||
effectiveOrm !== "prisma" &&
|
||||
effectiveOrm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires Prisma ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "mongodb";
|
||||
config.orm = "prisma";
|
||||
} else if (dbSetup === "neon") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "postgres";
|
||||
}
|
||||
}
|
||||
|
||||
const includesNuxt = effectiveFrontend?.includes("nuxt");
|
||||
const includesSvelte = effectiveFrontend?.includes("svelte");
|
||||
|
||||
if ((includesNuxt || includesSvelte) && effectiveApi === "trpc") {
|
||||
consola.fatal(
|
||||
`tRPC API is not supported with '${
|
||||
includesNuxt ? "nuxt" : "svelte"
|
||||
}' frontend. Please use --api orpc or remove '${
|
||||
includesNuxt ? "nuxt" : "svelte"
|
||||
}' from --frontend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
(includesNuxt || includesSvelte) &&
|
||||
effectiveApi !== "orpc" &&
|
||||
(!options.api || (options.yes && options.api !== "trpc"))
|
||||
) {
|
||||
config.api = "orpc";
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
const webSpecificAddons = ["pwa", "tauri"];
|
||||
const hasWebSpecificAddons = config.addons.some((addon) =>
|
||||
webSpecificAddons.includes(addon),
|
||||
);
|
||||
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
||||
(f) =>
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
(f === "nuxt" &&
|
||||
config.addons?.includes("tauri") &&
|
||||
!config.addons?.includes("pwa")) ||
|
||||
(f === "svelte" &&
|
||||
config.addons?.includes("tauri") &&
|
||||
!config.addons?.includes("pwa")),
|
||||
);
|
||||
|
||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||
let incompatibleAddon = "";
|
||||
if (config.addons.includes("pwa") && includesNuxt) {
|
||||
incompatibleAddon = "PWA addon is not compatible with Nuxt.";
|
||||
} else if (
|
||||
config.addons.includes("pwa") ||
|
||||
config.addons.includes("tauri")
|
||||
) {
|
||||
incompatibleAddon =
|
||||
"PWA and Tauri addons require tanstack-router, react-router, or Nuxt/Svelte (Tauri only).";
|
||||
}
|
||||
consola.fatal(
|
||||
`${incompatibleAddon} Cannot use these addons with your frontend selection.`,
|
||||
const hasWebFrontendForExamples = effectiveFrontend?.some((f) =>
|
||||
[
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
].includes(f),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
||||
consola.warn(
|
||||
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
||||
);
|
||||
}
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
if (config.examples && config.examples.length > 0) {
|
||||
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const hasWebFrontendForExamples = effectiveFrontend?.some((f) =>
|
||||
[
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
].includes(f),
|
||||
);
|
||||
|
||||
if (config.examples.length > 0 && !hasWebFrontendForExamples) {
|
||||
consola.fatal(
|
||||
"Examples require a web frontend (tanstack-router, react-router, tanstack-start, next, nuxt, or svelte).",
|
||||
);
|
||||
process.exit(1);
|
||||
const noFrontendSelected =
|
||||
!effectiveFrontend || effectiveFrontend.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +637,14 @@ function processAndValidateFlags(
|
||||
main().catch((err) => {
|
||||
consola.error("Aborting installation due to unexpected error...");
|
||||
if (err instanceof Error) {
|
||||
consola.error(err.message);
|
||||
if (
|
||||
!err.message.includes("is only supported with") &&
|
||||
!err.message.includes("incompatible with")
|
||||
) {
|
||||
consola.error(err.message);
|
||||
consola.error(err.stack);
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectApi, ProjectFrontend } from "../types";
|
||||
import type { ProjectApi, ProjectBackend, ProjectFrontend } from "../types";
|
||||
|
||||
export async function getApiChoice(
|
||||
Api?: ProjectApi | undefined,
|
||||
frontend?: ProjectFrontend[],
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectApi> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (Api) return Api;
|
||||
|
||||
const includesNuxt = frontend?.includes("nuxt");
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import { cancel, confirm, isCancel, log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend } from "../types";
|
||||
|
||||
export async function getAuthChoice(
|
||||
auth: boolean | undefined,
|
||||
hasDatabase: boolean,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<boolean> {
|
||||
if (backend === "convex") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasDatabase) return false;
|
||||
|
||||
if (auth !== undefined) return auth;
|
||||
|
||||
@@ -31,6 +31,11 @@ export async function getBackendFrameworkChoice(
|
||||
label: "Elysia",
|
||||
hint: "Ergonomic web framework for building backend servers",
|
||||
},
|
||||
{
|
||||
value: "convex",
|
||||
label: "Convex",
|
||||
hint: "Reactive backend-as-a-service platform",
|
||||
},
|
||||
],
|
||||
initialValue: DEFAULT_CONFIG.backend,
|
||||
});
|
||||
|
||||
@@ -30,19 +30,19 @@ import { getRuntimeChoice } from "./runtime";
|
||||
|
||||
type PromptGroupResults = {
|
||||
projectName: string;
|
||||
frontend: ProjectFrontend[];
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
database: ProjectDatabase;
|
||||
orm: ProjectOrm;
|
||||
api: ProjectApi;
|
||||
auth: boolean;
|
||||
addons: ProjectAddons[];
|
||||
examples: ProjectExamples[];
|
||||
dbSetup: ProjectDBSetup;
|
||||
git: boolean;
|
||||
packageManager: ProjectPackageManager;
|
||||
install: boolean;
|
||||
dbSetup: ProjectDBSetup;
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
frontend: ProjectFrontend[];
|
||||
api: ProjectApi;
|
||||
};
|
||||
|
||||
export async function gatherConfig(
|
||||
@@ -57,12 +57,19 @@ export async function gatherConfig(
|
||||
backend: () => getBackendFrameworkChoice(flags.backend),
|
||||
runtime: ({ results }) =>
|
||||
getRuntimeChoice(flags.runtime, results.backend),
|
||||
database: () => getDatabaseChoice(flags.database),
|
||||
database: ({ results }) =>
|
||||
getDatabaseChoice(flags.database, results.backend),
|
||||
orm: ({ results }) =>
|
||||
getORMChoice(flags.orm, results.database !== "none", results.database),
|
||||
api: ({ results }) => getApiChoice(flags.api, results.frontend),
|
||||
getORMChoice(
|
||||
flags.orm,
|
||||
results.database !== "none",
|
||||
results.database,
|
||||
results.backend,
|
||||
),
|
||||
api: ({ results }) =>
|
||||
getApiChoice(flags.api, results.frontend, results.backend),
|
||||
auth: ({ results }) =>
|
||||
getAuthChoice(flags.auth, results.database !== "none"),
|
||||
getAuthChoice(flags.auth, results.database !== "none", results.backend),
|
||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||
examples: ({ results }) =>
|
||||
getExamplesChoice(
|
||||
@@ -76,6 +83,7 @@ export async function gatherConfig(
|
||||
results.database ?? "none",
|
||||
flags.dbSetup,
|
||||
results.orm,
|
||||
results.backend,
|
||||
),
|
||||
git: () => getGitChoice(flags.git),
|
||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||
@@ -89,9 +97,20 @@ export async function gatherConfig(
|
||||
},
|
||||
);
|
||||
|
||||
if (result.backend === "convex") {
|
||||
result.runtime = "none";
|
||||
result.database = "none";
|
||||
result.orm = "none";
|
||||
result.api = "none";
|
||||
result.auth = false;
|
||||
result.dbSetup = "none";
|
||||
}
|
||||
|
||||
return {
|
||||
projectName: result.projectName,
|
||||
frontend: result.frontend,
|
||||
backend: result.backend,
|
||||
runtime: result.runtime,
|
||||
database: result.database,
|
||||
orm: result.orm,
|
||||
auth: result.auth,
|
||||
@@ -101,8 +120,6 @@ export async function gatherConfig(
|
||||
packageManager: result.packageManager,
|
||||
install: result.install,
|
||||
dbSetup: result.dbSetup,
|
||||
backend: result.backend,
|
||||
runtime: result.runtime,
|
||||
api: result.api,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectDatabase } from "../types";
|
||||
import type { ProjectBackend, ProjectDatabase } from "../types";
|
||||
|
||||
export async function getDatabaseChoice(
|
||||
database?: ProjectDatabase,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectDatabase> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (database !== undefined) return database;
|
||||
|
||||
const response = await select<ProjectDatabase>({
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectDBSetup, ProjectOrm } from "../types";
|
||||
import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types";
|
||||
|
||||
export async function getDBSetupChoice(
|
||||
databaseType: string,
|
||||
dbSetup: ProjectDBSetup | undefined,
|
||||
orm?: ProjectOrm,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectDBSetup> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
|
||||
|
||||
if (databaseType === "none") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (databaseType === "sqlite" && orm === "prisma") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import { cancel, isCancel, log, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type {
|
||||
@@ -16,17 +16,32 @@ export async function getExamplesChoice(
|
||||
): Promise<ProjectExamples[]> {
|
||||
if (examples !== undefined) return examples;
|
||||
|
||||
if (backend === "convex") {
|
||||
return ["todo"];
|
||||
}
|
||||
|
||||
if (database === "none") return [];
|
||||
|
||||
const hasWebFrontend =
|
||||
frontends?.includes("react-router") ||
|
||||
frontends?.includes("tanstack-router") ||
|
||||
frontends?.includes("tanstack-start") ||
|
||||
frontends?.includes("next") ||
|
||||
frontends?.includes("nuxt") ||
|
||||
frontends?.includes("svelte");
|
||||
const onlyNative =
|
||||
frontends && frontends.length === 1 && frontends[0] === "native";
|
||||
if (onlyNative) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!hasWebFrontend) return [];
|
||||
const hasWebFrontend =
|
||||
frontends?.some((f) =>
|
||||
[
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
].includes(f),
|
||||
) ?? false;
|
||||
const noFrontendSelected = !frontends || frontends.length === 0;
|
||||
|
||||
if (!hasWebFrontend && !noFrontendSelected) return [];
|
||||
|
||||
let response: ProjectExamples[] | symbol = [];
|
||||
const options: { value: ProjectExamples; label: string; hint: string }[] = [
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectDatabase, ProjectOrm } from "../types";
|
||||
import type { ProjectBackend, ProjectDatabase, ProjectOrm } from "../types";
|
||||
|
||||
export async function getORMChoice(
|
||||
orm: ProjectOrm | undefined,
|
||||
hasDatabase: boolean,
|
||||
database?: ProjectDatabase,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectOrm> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!hasDatabase) return "none";
|
||||
if (orm !== undefined) return orm;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
@@ -7,6 +7,10 @@ export async function getRuntimeChoice(
|
||||
runtime?: ProjectRuntime,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectRuntime> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (runtime !== undefined) return runtime;
|
||||
|
||||
if (backend === "next") {
|
||||
|
||||
@@ -14,8 +14,8 @@ export type ProjectAddons =
|
||||
| "starlight"
|
||||
| "turborepo"
|
||||
| "none";
|
||||
export type ProjectBackend = "hono" | "elysia" | "express" | "next";
|
||||
export type ProjectRuntime = "node" | "bun";
|
||||
export type ProjectBackend = "hono" | "elysia" | "express" | "next" | "convex";
|
||||
export type ProjectRuntime = "node" | "bun" | "none";
|
||||
export type ProjectExamples = "todo" | "ai" | "none";
|
||||
export type ProjectFrontend =
|
||||
| "react-router"
|
||||
@@ -32,7 +32,7 @@ export type ProjectDBSetup =
|
||||
| "mongodb-atlas"
|
||||
| "neon"
|
||||
| "none";
|
||||
export type ProjectApi = "trpc" | "orpc";
|
||||
export type ProjectApi = "trpc" | "orpc" | "none";
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectName: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
||||
? config.frontend
|
||||
: [config.frontend];
|
||||
const frontendText =
|
||||
frontend.length > 0 && frontend[0] !== undefined && frontend[0] !== ""
|
||||
frontend.length > 0 && frontend[0] !== undefined
|
||||
? frontend.join(", ")
|
||||
: "none";
|
||||
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
}{{#if (eq backend "convex")}},
|
||||
"setup": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
{{else}}{{#unless (or (eq database "none") (eq orm "none"))}},
|
||||
"db:push": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
@@ -25,5 +30,6 @@
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
{{/unless}}{{/if}}
|
||||
}
|
||||
}
|
||||
@@ -92,8 +92,6 @@ export async function createContext(opts: any) {
|
||||
}
|
||||
|
||||
{{else}}
|
||||
// Default or fallback context if backend is not recognized or none
|
||||
// This might need adjustment based on your default behavior
|
||||
export async function createContext() {
|
||||
return {
|
||||
session: null,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
// import { authClient } from "~/lib/auth-client";
|
||||
const {$authClient} = useNuxtApp()
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
// import { authClient } from "~/lib/auth-client";
|
||||
const {$authClient} = useNuxtApp()
|
||||
|
||||
const emit = defineEmits(['switchToSignIn'])
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// import { authClient } from "~/lib/auth-client";
|
||||
const {$authClient} = useNuxtApp()
|
||||
const session = $authClient.useSession()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
.env.local
|
||||
@@ -0,0 +1,90 @@
|
||||
# Welcome to your Convex functions directory!
|
||||
|
||||
Write your Convex functions here.
|
||||
See https://docs.convex.dev/functions for more.
|
||||
|
||||
A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.number(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this query function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const data = useQuery(api.functions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
});
|
||||
```
|
||||
|
||||
A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.string(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this mutation function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const mutation = useMutation(api.functions.myMutationFunction);
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Use the Convex CLI to push your functions to a deployment. See everything
|
||||
the Convex CLI can do by running `npx convex -h` in your project root
|
||||
directory. To learn more, launch the docs with `npx convex docs`.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { query } from "./_generated/server";
|
||||
|
||||
export const get = query({
|
||||
handler: async () => {
|
||||
return "OK";
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export default defineSchema({
|
||||
todos: defineTable({
|
||||
text: v.string(),
|
||||
completed: v.boolean(),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const getAll = query({
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query("todos").collect();
|
||||
},
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
text: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const newTodoId = await ctx.db.insert("todos", {
|
||||
text: args.text,
|
||||
completed: false,
|
||||
});
|
||||
return await ctx.db.get(newTodoId);
|
||||
},
|
||||
});
|
||||
|
||||
export const toggle = mutation({
|
||||
args: {
|
||||
id: v.id("todos"),
|
||||
completed: v.boolean(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.id, { completed: args.completed });
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteTodo = mutation({
|
||||
args: {
|
||||
id: v.id("todos"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.id);
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
/* This TypeScript project config describes the environment that
|
||||
* Convex functions run in and is used to typecheck them.
|
||||
* You can modify it, but some settings required to use Convex.
|
||||
*/
|
||||
"compilerOptions": {
|
||||
/* These settings are not required by Convex and can be modified. */
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
/* These compiler options are required by Convex */
|
||||
"target": "ESNext",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"exclude": ["./_generated"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@{{projectName}}/backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./convex/*": "./convex/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "convex dev",
|
||||
"setup": "convex dev --until-success"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"convex": "^1.23.0"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
"name": "better-t-stack",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*"
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
|
||||
|
||||
@@ -8,61 +8,90 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel.d.ts";
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
export default function Todos() {
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
orpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
})
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
orpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
orpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
trpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
})
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
trpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
trpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const todos = useQuery(api.todos.getAll);
|
||||
const createTodo = useMutation(api.todos.create);
|
||||
const toggleTodo = useMutation(api.todos.toggle);
|
||||
const deleteTodo = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const handleAddTodo = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const text = newTodoText.trim();
|
||||
if (!text) return;
|
||||
await createTodo({ text });
|
||||
setNewTodoText("");
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||
toggleTodo({ id, completed: !currentCompleted });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||
deleteTodo({ id });
|
||||
};
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
orpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
})
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
orpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
orpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
trpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
})
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
trpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
trpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
})
|
||||
);
|
||||
{{/if}}
|
||||
|
||||
const handleAddTodo = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -78,6 +107,7 @@ export default function Todos() {
|
||||
const handleDeleteTodo = (id: number) => {
|
||||
deleteMutation.mutate({ id });
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto max-w-md py-10">
|
||||
@@ -95,62 +125,117 @@ export default function Todos() {
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
{{#if (eq backend "convex")}}
|
||||
{{!-- Convex mutations don't have an easy isPending state here, disable based on text --}}
|
||||
{{else}}
|
||||
disabled={createMutation.isPending}
|
||||
{{/if}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
{{#if (eq backend "convex")}}
|
||||
disabled={!newTodoText.trim()}
|
||||
{{else}}
|
||||
disabled={createMutation.isPending || !newTodoText.trim()}
|
||||
{{/if}}
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
{{#if (eq backend "convex")}}
|
||||
Add
|
||||
{{else}}
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
{{/if}}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todos.isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.data?.length === 0 ? (
|
||||
<p className="py-4 text-center">
|
||||
No todos yet. Add one above!
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.data?.map((todo) => (
|
||||
<li
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo.id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo.id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo.id}`}
|
||||
className={`${todo.completed ? "line-through" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo.id)}
|
||||
aria-label="Delete todo"
|
||||
{{#if (eq backend "convex")}}
|
||||
{todos === undefined ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
key={todo._id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo._id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{else}}
|
||||
{todos.isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.data?.length === 0 ? (
|
||||
<p className="py-4 text-center">
|
||||
No todos yet. Add one above!
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.data?.map((todo) => (
|
||||
<li
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo.id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo.id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo.id}`}
|
||||
className={`${todo.completed ? "line-through" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo.id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{/if}}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -8,17 +8,24 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel.d.ts";
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: TodosRoute,
|
||||
});
|
||||
@@ -26,48 +33,70 @@ export const Route = createFileRoute("/todos")({
|
||||
function TodosRoute() {
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
orpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
orpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
orpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
trpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
trpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
trpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const todos = useQuery(api.todos.getAll);
|
||||
const createTodo = useMutation(api.todos.create);
|
||||
const toggleTodo = useMutation(api.todos.toggle);
|
||||
const deleteTodo = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const handleAddTodo = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const text = newTodoText.trim();
|
||||
if (!text) return;
|
||||
await createTodo({ text });
|
||||
setNewTodoText("");
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||
toggleTodo({ id, completed: !currentCompleted });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||
deleteTodo({ id });
|
||||
};
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
orpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
orpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
orpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
trpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const toggleMutation = useMutation(
|
||||
trpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
const deleteMutation = useMutation(
|
||||
trpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
|
||||
const handleAddTodo = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -83,6 +112,7 @@ function TodosRoute() {
|
||||
const handleDeleteTodo = (id: number) => {
|
||||
deleteMutation.mutate({ id });
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md py-10">
|
||||
@@ -100,60 +130,116 @@ function TodosRoute() {
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
{{#if (eq backend "convex")}}
|
||||
{{else}}
|
||||
disabled={createMutation.isPending}
|
||||
{{/if}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
{{#if (eq backend "convex")}}
|
||||
disabled={!newTodoText.trim()}
|
||||
{{else}}
|
||||
disabled={createMutation.isPending || !newTodoText.trim()}
|
||||
{{/if}}
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
{{#if (eq backend "convex")}}
|
||||
Add
|
||||
{{else}}
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
{{/if}}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todos.isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.data?.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.data?.map((todo) => (
|
||||
<li
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo.id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo.id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo.id}`}
|
||||
className={`${todo.completed ? "line-through" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo.id)}
|
||||
aria-label="Delete todo"
|
||||
{{#if (eq backend "convex")}}
|
||||
{todos === undefined ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
key={todo._id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo._id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{else}}
|
||||
{todos.isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.data?.length === 0 ? (
|
||||
<p className="py-4 text-center">
|
||||
No todos yet. Add one above!
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.data?.map((todo) => (
|
||||
<li
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo.id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo.id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo.id}`}
|
||||
className={`${todo.completed ? "line-through" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo.id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{/if}}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -8,32 +8,79 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
{{#if (eq api "trpc")}}
|
||||
import { useTRPC } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { useORPC } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { convexQuery } from "@convex-dev/react-query";
|
||||
import { useMutation } from "convex/react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel.js";
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { useTRPC } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { useORPC } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: TodosRoute,
|
||||
});
|
||||
|
||||
function TodosRoute() {
|
||||
{{#if (eq api "trpc")}}
|
||||
const trpc = useTRPC();
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const orpc = useORPC();
|
||||
{{/if}}
|
||||
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const todosQuery = useSuspenseQuery(convexQuery(api.todos.getAll, {}));
|
||||
const todos = todosQuery.data;
|
||||
|
||||
const createTodo = useMutation(api.todos.create);
|
||||
const toggleTodo = useMutation(api.todos.toggle);
|
||||
const removeTodo = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const handleAddTodo = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const text = newTodoText.trim();
|
||||
if (text) {
|
||||
setNewTodoText("");
|
||||
try {
|
||||
await createTodo({ text });
|
||||
} catch (error) {
|
||||
console.error("Failed to add todo:", error);
|
||||
setNewTodoText(text);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTodo = async (id: Id<"todos">, completed: boolean) => {
|
||||
try {
|
||||
await toggleTodo({ id, completed: !completed });
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle todo:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTodo = async (id: Id<"todos">) => {
|
||||
try {
|
||||
await removeTodo({ id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete todo:", error);
|
||||
}
|
||||
};
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const trpc = useTRPC();
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const orpc = useORPC();
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
trpc.todo.create.mutationOptions({
|
||||
@@ -53,8 +100,8 @@ function TodosRoute() {
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||
const createMutation = useMutation(
|
||||
orpc.todo.create.mutationOptions({
|
||||
@@ -74,7 +121,7 @@ function TodosRoute() {
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
const handleAddTodo = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -90,12 +137,13 @@ function TodosRoute() {
|
||||
const handleDeleteTodo = (id: number) => {
|
||||
deleteMutation.mutate({ id });
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md py-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Todo List</CardTitle>
|
||||
<CardTitle>Todo List{{#if (eq backend "convex")}} (Convex){{/if}}</CardTitle>
|
||||
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -107,20 +155,72 @@ function TodosRoute() {
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
{{#unless (eq backend "convex")}}
|
||||
disabled={createMutation.isPending}
|
||||
{{/unless}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
{{#unless (eq backend "convex")}}
|
||||
disabled={createMutation.isPending || !newTodoText.trim()}
|
||||
{{else}}
|
||||
disabled={!newTodoText.trim()}
|
||||
{{/unless}}
|
||||
>
|
||||
{{#unless (eq backend "convex")}}
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
{{else}}
|
||||
Add
|
||||
{{/unless}}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
{todos?.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos?.map((todo) => (
|
||||
<li
|
||||
key={todo._id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo._id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
className={`${
|
||||
todo.completed
|
||||
? "text-muted-foreground line-through"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{else}}
|
||||
{todos.isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
@@ -161,6 +261,7 @@ function TodosRoute() {
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{{/if}}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { View, Text, ScrollView } from "react-native";
|
||||
import { Container } from "@/components/container";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api.js";
|
||||
{{/if}}
|
||||
|
||||
export default function Home() {
|
||||
{{#if (eq api "orpc")}}
|
||||
@@ -15,6 +20,9 @@ export default function Home() {
|
||||
{{#if (eq api "trpc")}}
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -28,15 +36,35 @@ export default function Home() {
|
||||
<View className="flex-row items-center gap-2">
|
||||
<View
|
||||
className={`h-2.5 w-2.5 rounded-full ${
|
||||
healthCheck.data ? "bg-green-500" : "bg-red-500"
|
||||
{{#if (or (eq api "orpc") (eq api "trpc"))}}
|
||||
healthCheck.data ? "bg-green-500" : "bg-red-500"
|
||||
{{else}}
|
||||
healthCheck ? "bg-green-500" : "bg-red-500"
|
||||
{{/if}}
|
||||
}`}
|
||||
/>
|
||||
<Text className="text-sm text-foreground">
|
||||
{healthCheck.isLoading
|
||||
? "Checking..."
|
||||
: healthCheck.data
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
{{#if (eq api "orpc")}}
|
||||
{healthCheck.isLoading
|
||||
? "Checking..."
|
||||
: healthCheck.data
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
{healthCheck.isLoading
|
||||
? "Checking..."
|
||||
: healthCheck.data
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
{healthCheck === undefined
|
||||
? "Checking..."
|
||||
: healthCheck === "OK"
|
||||
? "Connected"
|
||||
: "Error"}
|
||||
{{/if}}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{else}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
import { Stack } from "expo-router";
|
||||
import {
|
||||
DarkTheme,
|
||||
@@ -35,6 +39,12 @@ export const unstable_settings = {
|
||||
initialRouteName: "(drawer)",
|
||||
};
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
|
||||
unsavedChangesWarning: false,
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
export default function RootLayout() {
|
||||
const hasMounted = useRef(false);
|
||||
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
||||
@@ -58,6 +68,22 @@ export default function RootLayout() {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
{{#if (eq backend "convex")}}
|
||||
<ConvexProvider client={convex}>
|
||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
||||
<GestureHandlerRootView style=\{{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options=\{{ title: "Modal", presentation: "modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</ThemeProvider>
|
||||
</ConvexProvider>
|
||||
{{else}}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
||||
@@ -72,6 +98,7 @@ export default function RootLayout() {
|
||||
</GestureHandlerRootView>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
{{/if}}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"@radix-ui/react-label": "^2.1.3",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@tanstack/react-form": "^1.3.2",
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.487.0",
|
||||
@@ -29,7 +28,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tanstack/react-query-devtools": "^5.72.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"use client"
|
||||
{{#if (eq api "orpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
const TITLE_TEXT = `
|
||||
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
||||
@@ -24,10 +29,11 @@ const TITLE_TEXT = `
|
||||
`;
|
||||
|
||||
export default function Home() {
|
||||
{{#if (eq api "orpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{else if (eq api "orpc")}}
|
||||
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
{{else if (eq api "trpc")}}
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
|
||||
@@ -38,6 +44,18 @@ export default function Home() {
|
||||
<section className="rounded-lg border p-4">
|
||||
<h2 className="mb-2 font-medium">API Status</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{{#if (eq backend "convex")}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-green-500" : healthCheck === undefined ? "bg-orange-400" : "bg-red-500"}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{healthCheck === undefined
|
||||
? "Checking..."
|
||||
: healthCheck === "OK"
|
||||
? "Connected"
|
||||
: "Error"}
|
||||
</span>
|
||||
{{else}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
||||
/>
|
||||
@@ -48,46 +66,10 @@ export default function Home() {
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 font-medium">Core Features</h2>
|
||||
<ul className="grid grid-cols-2 gap-3">
|
||||
<FeatureItem
|
||||
title="Type-Safe API"
|
||||
description="End-to-end type safety with tRPC"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Modern React"
|
||||
description="TanStack Router + TanStack Query"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Fast Backend"
|
||||
description="Lightweight Hono server"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Beautiful UI"
|
||||
description="TailwindCSS + shadcn/ui components"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureItem({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="border-l-2 border-primary py-1 pl-3">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"use client"
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{else}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
{{#if (eq api "orpc")}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc, ORPCContext, queryClient } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { queryClient } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
import { ThemeProvider } from "./theme-provider";
|
||||
import { Toaster } from "./ui/sonner";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
||||
{{/if}}
|
||||
|
||||
export default function Providers({
|
||||
children,
|
||||
}: {
|
||||
@@ -21,6 +29,9 @@ export default function Providers({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{{#if (eq backend "convex")}}
|
||||
<ConvexProvider client={convex}>{children}</ConvexProvider>
|
||||
{{else}}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{{#if (eq api "orpc")}}
|
||||
<ORPCContext.Provider value={orpc}>
|
||||
@@ -31,6 +42,7 @@ export default function Providers({
|
||||
{children}
|
||||
{{/if}}
|
||||
</QueryClientProvider>
|
||||
{{/if}}
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"@react-router/node": "^7.4.1",
|
||||
"@react-router/serve": "^7.4.1",
|
||||
"@tanstack/react-form": "^1.2.3",
|
||||
"@tanstack/react-query": "^5.71.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"isbot": "^5.1.17",
|
||||
@@ -34,7 +33,6 @@
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.4.1",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tanstack/react-query-devtools": "^5.71.3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.1",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
@@ -14,11 +12,17 @@ import Header from "./components/header";
|
||||
import { ThemeProvider } from "./components/theme-provider";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc, ORPCContext, queryClient } from "./utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { queryClient } from "./utils/trpc";
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{else}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc, ORPCContext, queryClient } from "./utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { queryClient } from "./utils/trpc";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
@@ -52,7 +56,25 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
export default function App() {
|
||||
const convex = new ConvexReactClient(
|
||||
import.meta.env.VITE_CONVEX_URL as string,
|
||||
);
|
||||
|
||||
return (
|
||||
<ConvexProvider client={convex}>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</ConvexProvider>
|
||||
);
|
||||
}
|
||||
{{else if (eq api "orpc")}}
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -69,9 +91,7 @@ export default function App() {
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
{{else if (eq api "trpc")}}
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { Route } from "./+types/_index";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
{{else}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
const TITLE_TEXT = `
|
||||
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
||||
@@ -28,11 +33,11 @@ export function meta({}: Route.MetaArgs) {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{else if (eq api "orpc")}}
|
||||
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
{{else if (eq api "trpc")}}
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
|
||||
@@ -43,6 +48,18 @@ export default function Home() {
|
||||
<section className="rounded-lg border p-4">
|
||||
<h2 className="mb-2 font-medium">API Status</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{{#if (eq backend "convex")}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-green-500" : healthCheck === undefined ? "bg-orange-400" : "bg-red-500"}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{healthCheck === undefined
|
||||
? "Checking..."
|
||||
: healthCheck === "OK"
|
||||
? "Connected"
|
||||
: "Error"}
|
||||
</span>
|
||||
{{else}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${
|
||||
healthCheck.data ? "bg-green-500" : "bg-red-500"
|
||||
@@ -55,46 +72,10 @@ export default function Home() {
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 font-medium">Core Features</h2>
|
||||
<ul className="grid grid-cols-2 gap-3">
|
||||
<FeatureItem
|
||||
title="Type-Safe API"
|
||||
description="End-to-end type safety with tRPC"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Modern React"
|
||||
description="TanStack Router + TanStack Query"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Fast Backend"
|
||||
description="Lightweight Hono server"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Beautiful UI"
|
||||
description="TailwindCSS + shadcn/ui components"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureItem({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="border-l-2 border-primary py-1 pl-3">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-query-devtools": "^5.69.0",
|
||||
"@tanstack/react-router-devtools": "^1.114.27",
|
||||
"@tanstack/router-plugin": "^1.114.27",
|
||||
"@types/node": "^22.13.13",
|
||||
@@ -30,7 +29,6 @@
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-form": "^1.0.5",
|
||||
"@tailwindcss/vite": "^4.0.15",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/react-router": "^1.114.25",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import Loader from "./components/loader";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { orpc, queryClient } from "./utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { queryClient, trpc } from "./utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
|
||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq api "orpc")}}
|
||||
const router = createRouter({
|
||||
@@ -36,8 +42,18 @@ const router = createRouter({
|
||||
},
|
||||
});
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
defaultPendingComponent: () => <Loader />,
|
||||
context: {},
|
||||
Wrap: function WrapComponent({ children }) {
|
||||
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
|
||||
},
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
// Register things for typesafety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
|
||||
@@ -38,6 +38,9 @@ export interface RouterAppContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
export interface RouterAppContext {}
|
||||
{{/if}}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
component: RootComponent,
|
||||
@@ -106,4 +109,24 @@ function RootComponent() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
function RootComponent() {
|
||||
const isFetching = useRouterState({
|
||||
select: (s) => s.isLoading,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<HeadContent />
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
{isFetching ? <Loader /> : <Outlet />}
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
<TanStackRouterDevtools position="bottom-left" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc } from "@/utils/orpc";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api.js";
|
||||
{{/if}}
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomeComponent,
|
||||
@@ -34,6 +39,9 @@ function HomeComponent() {
|
||||
{{#if (eq api "trpc")}}
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-3xl px-4 py-2">
|
||||
@@ -42,6 +50,7 @@ function HomeComponent() {
|
||||
<section className="rounded-lg border p-4">
|
||||
<h2 className="mb-2 font-medium">API Status</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{{#if (or (eq api "orpc") (eq api "trpc"))}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
||||
/>
|
||||
@@ -52,46 +61,22 @@ function HomeComponent() {
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-green-500" : healthCheck === undefined ? "bg-orange-400" : "bg-red-500"}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{healthCheck === undefined
|
||||
? "Checking..."
|
||||
: healthCheck === "OK"
|
||||
? "Connected"
|
||||
: "Error"}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 font-medium">Core Features</h2>
|
||||
<ul className="grid grid-cols-2 gap-3">
|
||||
<FeatureItem
|
||||
title="Type-Safe API"
|
||||
description="End-to-end type safety with tRPC"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Modern React"
|
||||
description="TanStack Router + TanStack Query"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Fast Backend"
|
||||
description="Lightweight Hono server"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Beautiful UI"
|
||||
description="TailwindCSS + shadcn/ui components"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureItem({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="border-l-2 border-primary py-1 pl-3">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
{{#if (eq backend "convex")}}
|
||||
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
|
||||
import { ConvexQueryClient } from "@convex-dev/react-query";
|
||||
import { ConvexProvider } from "convex/react";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import Loader from "./components/loader";
|
||||
import "./index.css";
|
||||
{{else}}
|
||||
import {
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
@@ -7,18 +17,56 @@ import { createRouter as createTanstackRouter } from "@tanstack/react-router";
|
||||
import Loader from "./components/loader";
|
||||
import "./index.css";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
{{#if (eq api "trpc")}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||
import { toast } from "sonner";
|
||||
import type { AppRouter } from "../../server/src/routers";
|
||||
import { TRPCProvider } from "./utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { orpc, ORPCContext, queryClient } from "./utils/orpc";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
export function createRouter() {
|
||||
const CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL!;
|
||||
if (!CONVEX_URL) {
|
||||
console.error("missing envar VITE_CONVEX_URL");
|
||||
}
|
||||
const convexQueryClient = new ConvexQueryClient(CONVEX_URL);
|
||||
|
||||
const queryClient: QueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
queryKeyHashFn: convexQueryClient.hashFn(),
|
||||
queryFn: convexQueryClient.queryFn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
convexQueryClient.connect(queryClient);
|
||||
|
||||
const router = routerWithQueryClient(
|
||||
createTanStackRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
defaultPendingComponent: () => <Loader />,
|
||||
defaultNotFoundComponent: () => <div>Not Found</div>,
|
||||
context: { queryClient },
|
||||
Wrap: ({ children }) => (
|
||||
<ConvexProvider client={convexQueryClient.convexClient}>
|
||||
{children}
|
||||
</ConvexProvider>
|
||||
),
|
||||
}),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
export const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => {
|
||||
@@ -38,11 +86,7 @@ export const queryClient = new QueryClient({
|
||||
const trpcClient = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
{{#if (includes frontend 'next')}}
|
||||
url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`,
|
||||
{{else}}
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
@@ -59,8 +103,7 @@ const trpc = createTRPCOptionsProxy({
|
||||
client: trpcClient,
|
||||
queryClient: queryClient,
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
export const createRouter = () => {
|
||||
const router = createTanstackRouter({
|
||||
@@ -69,10 +112,10 @@ export const createRouter = () => {
|
||||
defaultPreloadStaleTime: 0,
|
||||
{{#if (eq api "trpc")}}
|
||||
context: { trpc, queryClient },
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
context: { orpc, queryClient },
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
defaultPendingComponent: () => <Loader />,
|
||||
defaultNotFoundComponent: () => <div>Not Found</div>,
|
||||
Wrap: ({ children }) => (
|
||||
@@ -93,6 +136,7 @@ export const createRouter = () => {
|
||||
|
||||
return router;
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
|
||||
@@ -11,26 +11,28 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import Header from "../components/header";
|
||||
import appCss from "../index.css?url";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
{{#if (eq api "trpc")}}
|
||||
import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||
import type { AppRouter } from "../../../server/src/routers";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import type { orpc } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
import Loader from "@/components/loader";
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
export interface RouterAppContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||
import type { AppRouter } from "../../../server/src/routers";
|
||||
export interface RouterAppContext {
|
||||
trpc: TRPCOptionsProxy<AppRouter>;
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import type { orpc } from "@/utils/orpc";
|
||||
export interface RouterAppContext {
|
||||
orpc: typeof orpc;
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
{{#if (eq api "trpc")}}
|
||||
import { useTRPC } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { useORPC } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
{{#if (eq backend "convex")}}
|
||||
import { convexQuery } from "@convex-dev/react-query";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
import { useTRPC } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
import { useORPC } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
{{/if}}
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomeComponent,
|
||||
@@ -28,13 +34,17 @@ const TITLE_TEXT = `
|
||||
`;
|
||||
|
||||
function HomeComponent() {
|
||||
{{#if (eq api "trpc")}}
|
||||
{{#if (eq backend "convex")}}
|
||||
const healthCheck = useSuspenseQuery(convexQuery(api.healthCheck.get, {}));
|
||||
{{else}}
|
||||
{{#if (eq api "trpc")}}
|
||||
const trpc = useTRPC();
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
{{/if}}
|
||||
{{#if (eq api "orpc")}}
|
||||
const orpc = useORPC();
|
||||
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
@@ -44,6 +54,18 @@ function HomeComponent() {
|
||||
<section className="rounded-lg border p-4">
|
||||
<h2 className="mb-2 font-medium">API Status</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{{#if (eq backend "convex")}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck.data === "OK" ? "bg-green-500" : healthCheck.isLoading ? "bg-orange-400" : "bg-red-500"}`}
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{healthCheck.isLoading
|
||||
? "Checking..."
|
||||
: healthCheck.data === "OK"
|
||||
? "Connected"
|
||||
: "Error"}
|
||||
</span>
|
||||
{{else}}
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
||||
/>
|
||||
@@ -54,46 +76,10 @@ function HomeComponent() {
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 font-medium">Core Features</h2>
|
||||
<ul className="grid grid-cols-2 gap-3">
|
||||
<FeatureItem
|
||||
title="Type-Safe API"
|
||||
description="End-to-end type safety with tRPC"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Modern React"
|
||||
description="TanStack Router + TanStack Query"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Fast Backend"
|
||||
description="Lightweight Hono server"
|
||||
/>
|
||||
<FeatureItem
|
||||
title="Beautiful UI"
|
||||
description="TailwindCSS + shadcn/ui components"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureItem({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="border-primary border-l-2 py-1 pl-3">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user