diff --git a/.changeset/proud-seals-admire.md b/.changeset/proud-seals-admire.md new file mode 100644 index 0000000..dcb82ce --- /dev/null +++ b/.changeset/proud-seals-admire.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add cloudflare workers support for hono diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 613adfa..5112458 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -105,6 +105,8 @@ export const dependencyVersionMap = { "@tanstack/solid-query": "^5.75.0", "@tanstack/solid-query-devtools": "^5.75.0", + + wrangler: "^4.20.0", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/apps/cli/src/helpers/project-generation/env-setup.ts b/apps/cli/src/helpers/project-generation/env-setup.ts index fbaa684..ee14151 100644 --- a/apps/cli/src/helpers/project-generation/env-setup.ts +++ b/apps/cli/src/helpers/project-generation/env-setup.ts @@ -200,7 +200,11 @@ export async function setupEnvironmentVariables( databaseUrl = "mongodb://localhost:27017/mydatabase"; break; case "sqlite": - databaseUrl = "file:./local.db"; + if (config.runtime === "workers") { + databaseUrl = "http://127.0.0.1:8080"; + } else { + databaseUrl = "file:./local.db"; + } break; } } @@ -234,4 +238,11 @@ export async function setupEnvironmentVariables( ]; await addEnvVariablesToFile(envPath, serverVars); + + if (config.runtime === "workers") { + const devVarsPath = path.join(serverDir, ".dev.vars"); + try { + await fs.copy(envPath, devVarsPath); + } catch (_err) {} + } } diff --git a/apps/cli/src/helpers/project-generation/post-installation.ts b/apps/cli/src/helpers/project-generation/post-installation.ts index 4534286..76d673b 100644 --- a/apps/cli/src/helpers/project-generation/post-installation.ts +++ b/apps/cli/src/helpers/project-generation/post-installation.ts @@ -93,7 +93,18 @@ export function displayPostInstallInstructions( )}\n`; output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`; } else { - output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`; + if (runtime !== "workers") { + output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`; + } + + if (runtime === "workers") { + output += `${pc.cyan(`${stepCounter++}.`)} bun dev\n`; + output += `${pc.cyan( + `${stepCounter++}.`, + )} cd apps/server && bun run cf-typegen\n\n`; + } else { + output += "\n"; + } } output += `${pc.bold("Your project will be available at:")}\n`; diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 6edaf6b..7780d31 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -818,4 +818,17 @@ export async function handleExtras( await processTemplate(npmrcTemplateSrc, npmrcDest, context); } } + + if (context.runtime === "workers") { + const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers"); + if (await fs.pathExists(runtimeWorkersDir)) { + await processAndCopyFiles( + "**/*", + runtimeWorkersDir, + projectDir, + context, + false, + ); + } + } } diff --git a/apps/cli/src/helpers/setup/runtime-setup.ts b/apps/cli/src/helpers/setup/runtime-setup.ts index c7f6158..7d2238f 100644 --- a/apps/cli/src/helpers/setup/runtime-setup.ts +++ b/apps/cli/src/helpers/setup/runtime-setup.ts @@ -20,6 +20,8 @@ export async function setupRuntime(config: ProjectConfig): Promise { await setupBunRuntime(serverDir, backend); } else if (runtime === "node") { await setupNodeRuntime(serverDir, backend); + } else if (runtime === "workers") { + await setupWorkersRuntime(serverDir); } } @@ -80,3 +82,26 @@ async function setupNodeRuntime( }); } } + +async function setupWorkersRuntime(serverDir: string): Promise { + const packageJsonPath = path.join(serverDir, "package.json"); + if (!(await fs.pathExists(packageJsonPath))) return; + + const packageJson = await fs.readJson(packageJsonPath); + + packageJson.scripts = { + ...packageJson.scripts, + dev: "wrangler dev --port=3000", + start: "wrangler dev", + deploy: "wrangler deploy", + build: "wrangler deploy --dry-run", + "cf-typegen": "wrangler types --env-interface CloudflareBindings", + }; + + await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); + + await addPackageDependency({ + devDependencies: ["wrangler"], + projectDir: serverDir, + }); +} diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 4db4aad..adccc41 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -57,13 +57,14 @@ export async function gatherConfig( runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend), database: ({ results }) => - getDatabaseChoice(flags.database, results.backend), + getDatabaseChoice(flags.database, results.backend, results.runtime), orm: ({ results }) => getORMChoice( flags.orm, results.database !== "none", results.database, results.backend, + results.runtime, ), api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend), diff --git a/apps/cli/src/prompts/database.ts b/apps/cli/src/prompts/database.ts index 2ba1046..3e1eae2 100644 --- a/apps/cli/src/prompts/database.ts +++ b/apps/cli/src/prompts/database.ts @@ -1,11 +1,12 @@ import { cancel, isCancel, select } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { Backend, Database } from "../types"; +import type { Backend, Database, Runtime } from "../types"; export async function getDatabaseChoice( database?: Database, backend?: Backend, + runtime?: Runtime, ): Promise { if (backend === "convex" || backend === "none") { return "none"; @@ -13,35 +14,44 @@ export async function getDatabaseChoice( if (database !== undefined) return database; + const databaseOptions: Array<{ + value: Database; + label: string; + hint: string; + }> = [ + { + value: "none", + label: "None", + hint: "No database setup", + }, + { + value: "sqlite", + label: "SQLite", + hint: "lightweight, server-less, embedded relational database", + }, + { + value: "postgres", + label: "PostgreSQL", + hint: "powerful, open source object-relational database system", + }, + { + value: "mysql", + label: "MySQL", + hint: "popular open-source relational database system", + }, + ]; + + if (runtime !== "workers") { + databaseOptions.push({ + value: "mongodb", + label: "MongoDB", + hint: "open-source NoSQL database that stores data in JSON-like documents called BSON", + }); + } + const response = await select({ message: "Select database", - options: [ - { - value: "none", - label: "None", - hint: "No database setup", - }, - { - value: "sqlite", - label: "SQLite", - hint: "lightweight, server-less, embedded relational database", - }, - { - value: "postgres", - label: "PostgreSQL", - hint: "powerful, open source object-relational database system", - }, - { - value: "mysql", - label: "MySQL", - hint: "popular open-source relational database system", - }, - { - value: "mongodb", - label: "MongoDB", - hint: "open-source NoSQL database that stores data in JSON-like documents called BSON", - }, - ], + options: databaseOptions, initialValue: DEFAULT_CONFIG.database, }); diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index b5614f1..736f2a2 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -1,7 +1,7 @@ import { cancel, isCancel, select } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { Backend, Database, ORM } from "../types"; +import type { Backend, Database, ORM, Runtime } from "../types"; const ormOptions = { prisma: { @@ -26,6 +26,7 @@ export async function getORMChoice( hasDatabase: boolean, database?: Database, backend?: Backend, + runtime?: Runtime, ): Promise { if (backend === "convex") { return "none"; @@ -34,6 +35,10 @@ export async function getORMChoice( if (!hasDatabase) return "none"; if (orm !== undefined) return orm; + if (runtime === "workers") { + return "drizzle"; + } + const options = [ ...(database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] diff --git a/apps/cli/src/prompts/runtime.ts b/apps/cli/src/prompts/runtime.ts index 4c691a2..03f2470 100644 --- a/apps/cli/src/prompts/runtime.ts +++ b/apps/cli/src/prompts/runtime.ts @@ -17,20 +17,34 @@ export async function getRuntimeChoice( return "node"; } + const runtimeOptions: Array<{ + value: Runtime; + label: string; + hint: string; + }> = [ + { + value: "bun", + label: "Bun", + hint: "Fast all-in-one JavaScript runtime", + }, + { + value: "node", + label: "Node.js", + hint: "Traditional Node.js runtime", + }, + ]; + + if (backend === "hono") { + runtimeOptions.push({ + value: "workers", + label: "Cloudflare Workers (beta)", + hint: "Edge runtime on Cloudflare's global network", + }); + } + const response = await select({ message: "Select runtime", - options: [ - { - value: "bun", - label: "Bun", - hint: "Fast all-in-one JavaScript runtime", - }, - { - value: "node", - label: "Node.js", - hint: "Traditional Node.js runtime", - }, - ], + options: runtimeOptions, initialValue: DEFAULT_CONFIG.runtime, }); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 72d90bf..1120de7 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -16,8 +16,10 @@ export const BackendSchema = z export type Backend = z.infer; export const RuntimeSchema = z - .enum(["bun", "node", "none"]) - .describe("Runtime environment"); + .enum(["bun", "node", "workers", "none"]) + .describe( + "Runtime environment (workers only available with hono backend and drizzle orm)", + ); export type Runtime = z.infer; export const FrontendSchema = z diff --git a/apps/cli/src/utils/template-processor.ts b/apps/cli/src/utils/template-processor.ts index aaf7009..7d1d2ce 100644 --- a/apps/cli/src/utils/template-processor.ts +++ b/apps/cli/src/utils/template-processor.ts @@ -28,9 +28,9 @@ export async function processTemplate( } } -handlebars.registerHelper("or", (a, b) => a || b); - handlebars.registerHelper("eq", (a, b) => a === b); +handlebars.registerHelper("and", (a, b) => a && b); +handlebars.registerHelper("or", (a, b) => a || b); handlebars.registerHelper( "includes", diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 094a690..2d114dc 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -358,6 +358,78 @@ export function processAndValidateFlags( process.exit(1); } + if ( + providedFlags.has("runtime") && + options.runtime === "workers" && + config.backend && + config.backend !== "hono" + ) { + consola.fatal( + `Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`, + ); + process.exit(1); + } + + if ( + providedFlags.has("backend") && + config.backend && + config.backend !== "hono" && + config.runtime === "workers" + ) { + consola.fatal( + `Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`, + ); + process.exit(1); + } + + if ( + providedFlags.has("runtime") && + options.runtime === "workers" && + config.orm && + config.orm !== "drizzle" && + config.orm !== "none" + ) { + consola.fatal( + `Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`, + ); + process.exit(1); + } + + if ( + providedFlags.has("orm") && + config.orm && + config.orm !== "drizzle" && + config.orm !== "none" && + config.runtime === "workers" + ) { + consola.fatal( + `ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`, + ); + process.exit(1); + } + + if ( + providedFlags.has("runtime") && + options.runtime === "workers" && + config.database === "mongodb" + ) { + consola.fatal( + "Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.", + ); + process.exit(1); + } + + if ( + providedFlags.has("database") && + config.database === "mongodb" && + config.runtime === "workers" + ) { + consola.fatal( + "MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.", + ); + process.exit(1); + } + return config; } @@ -368,6 +440,33 @@ export function validateConfigCompatibility( const effectiveBackend = config.backend; const effectiveFrontend = config.frontend; const effectiveApi = config.api; + const effectiveRuntime = config.runtime; + + if (effectiveRuntime === "workers" && effectiveBackend !== "hono") { + consola.fatal( + `Cloudflare Workers runtime is only supported with Hono backend. Current backend: ${effectiveBackend}. Please use a different runtime or change to Hono backend.`, + ); + process.exit(1); + } + + const effectiveOrm = config.orm; + if ( + effectiveRuntime === "workers" && + effectiveOrm !== "drizzle" && + effectiveOrm !== "none" + ) { + consola.fatal( + `Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Current ORM: ${effectiveOrm}. Please use a different runtime or change to Drizzle ORM or no ORM.`, + ); + process.exit(1); + } + + if (effectiveRuntime === "workers" && effectiveDatabase === "mongodb") { + consola.fatal( + "Cloudflare Workers runtime is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.", + ); + process.exit(1); + } const includesNuxt = effectiveFrontend?.includes("nuxt"); const includesSvelte = effectiveFrontend?.includes("svelte"); diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs index de189fe..26ceb84 100644 --- a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -14,21 +14,22 @@ export const auth = betterAuth({ {{#if (eq database "mongodb")}}provider: "mongodb"{{/if}} }), trustedOrigins: [ - process.env.CORS_ORIGIN || "",{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - "my-better-t-app://",{{/if}} + process.env.CORS_ORIGIN || "", + {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + "my-better-t-app://", + {{/if}} ], emailAndPassword: { enabled: true, } - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - , - plugins: [expo()] + , plugins: [expo()] {{/if}} }); {{/if}} {{#if (eq orm "drizzle")}} +{{#if (or (eq runtime "bun") (eq runtime "node"))}} import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} @@ -45,20 +46,52 @@ export const auth = betterAuth({ schema: schema, }), trustedOrigins: [ - process.env.CORS_ORIGIN || "",{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - "my-better-t-app://",{{/if}} + process.env.CORS_ORIGIN || "", + {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + "my-better-t-app://", + {{/if}} ], emailAndPassword: { enabled: true, - } - + }, + secret: process.env.BETTER_AUTH_SECRET, + baseURL: process.env.BETTER_AUTH_URL, {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - , - plugins: [expo()] + plugins: [expo()], {{/if}} }); {{/if}} +{{#if (eq runtime "workers")}} +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} +import { expo } from "@better-auth/expo"; +{{/if}} +import { db } from "@/db"; +import * as schema from "../db/schema/auth"; +import { env } from "cloudflare:workers"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + {{#if (eq database "postgres")}}provider: "pg",{{/if}} + {{#if (eq database "sqlite")}}provider: "sqlite",{{/if}} + {{#if (eq database "mysql")}}provider: "mysql",{{/if}} + schema: schema, + }), + trustedOrigins: [env.CORS_ORIGIN], + emailAndPassword: { + enabled: true, + }, + secret: env.BETTER_AUTH_SECRET, + baseURL: env.BETTER_AUTH_URL, + {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + plugins: [expo()], + {{/if}} +}); +{{/if}} +{{/if}} + {{#if (eq orm "mongoose")}} import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; @@ -70,16 +103,16 @@ import { client } from "../db"; export const auth = betterAuth({ database: mongodbAdapter(client), trustedOrigins: [ - process.env.CORS_ORIGIN || "",{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - "my-better-t-app://",{{/if}} + process.env.CORS_ORIGIN || "", + {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + "my-better-t-app://", + {{/if}} ], emailAndPassword: { enabled: true, } - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - , - plugins: [expo()] + , plugins: [expo()] {{/if}} }); {{/if}} @@ -93,16 +126,16 @@ import { expo } from "@better-auth/expo"; export const auth = betterAuth({ database: "", // Invalid configuration trustedOrigins: [ - process.env.CORS_ORIGIN || "",{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - "my-better-t-app://",{{/if}} + process.env.CORS_ORIGIN || "", + {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + "my-better-t-app://", + {{/if}} ], emailAndPassword: { enabled: true, } - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - , - plugins: [expo()] + , plugins: [expo()] {{/if}} }); {{/if}} diff --git a/apps/cli/templates/backend/server/hono/src/index.ts.hbs b/apps/cli/templates/backend/server/hono/src/index.ts.hbs index 1385500..0fc09ae 100644 --- a/apps/cli/templates/backend/server/hono/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/hono/src/index.ts.hbs @@ -1,4 +1,9 @@ +{{#if (or (eq runtime "bun") (eq runtime "node"))}} import "dotenv/config"; +{{/if}} +{{#if (eq runtime "workers")}} +import { env } from "cloudflare:workers"; +{{/if}} {{#if (eq api "orpc")}} import { RPCHandler } from "@orpc/server/fetch"; import { createContext } from "./lib/context"; @@ -15,26 +20,33 @@ import { auth } from "./lib/auth"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { logger } from "hono/logger"; -{{#if (includes examples "ai")}} +{{#if (and (includes examples "ai") (or (eq runtime "bun") (eq runtime "node")))}} import { streamText } from "ai"; import { google } from "@ai-sdk/google"; import { stream } from "hono/streaming"; {{/if}} +{{#if (and (includes examples "ai") (eq runtime "workers"))}} +import { streamText } from "ai"; +import { stream } from "hono/streaming"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +{{/if}} const app = new Hono(); app.use(logger()); -app.use( - "/*", - cors({ - origin: process.env.CORS_ORIGIN || "", - allowMethods: ["GET", "POST", "OPTIONS"], - {{#if auth}} - allowHeaders: ["Content-Type", "Authorization"], - credentials: true, - {{/if}} - }) -); +app.use("/*", cors({ + {{#if (or (eq runtime "bun") (eq runtime "node"))}} + origin: process.env.CORS_ORIGIN || "", + {{/if}} + {{#if (eq runtime "workers")}} + origin: env.CORS_ORIGIN || "", + {{/if}} + allowMethods: ["GET", "POST", "OPTIONS"], + {{#if auth}} + allowHeaders: ["Content-Type", "Authorization"], + credentials: true, + {{/if}} +})); {{#if auth}} app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw)); @@ -48,6 +60,7 @@ app.use("/rpc/*", async (c, next) => { prefix: "/rpc", context: context, }); + if (matched) { return c.newResponse(response.body, response); } @@ -64,11 +77,10 @@ app.use("/trpc/*", trpcServer({ })); {{/if}} -{{#if (includes examples "ai")}} +{{#if (and (includes examples "ai") (or (eq runtime "bun") (eq runtime "node")))}} app.post("/ai", async (c) => { const body = await c.req.json(); const messages = body.messages || []; - const result = streamText({ model: google("gemini-1.5-flash"), messages, @@ -76,7 +88,24 @@ app.post("/ai", async (c) => { c.header("X-Vercel-AI-Data-Stream", "v1"); c.header("Content-Type", "text/plain; charset=utf-8"); + return stream(c, (stream) => stream.pipe(result.toDataStream())); +}); +{{/if}} +{{#if (and (includes examples "ai") (eq runtime "workers"))}} +app.post("/ai", async (c) => { + const body = await c.req.json(); + const messages = body.messages || []; + const google = createGoogleGenerativeAI({ + apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY, + }); + const result = streamText({ + model: google("gemini-1.5-flash"), + messages, + }); + + c.header("X-Vercel-AI-Data-Stream", "v1"); + c.header("Content-Type", "text/plain; charset=utf-8"); return stream(c, (stream) => stream.pipe(result.toDataStream())); }); {{/if}} @@ -95,5 +124,10 @@ serve({ console.log(`Server is running on http://localhost:${info.port}`); }); {{else}} + {{#if (eq runtime "bun")}} export default app; + {{/if}} + {{#if (eq runtime "workers")}} +export default app; + {{/if}} {{/if}} diff --git a/apps/cli/templates/backend/server/server-base/tsconfig.json.hbs b/apps/cli/templates/backend/server/server-base/tsconfig.json.hbs index ac15790..169fe4f 100644 --- a/apps/cli/templates/backend/server/server-base/tsconfig.json.hbs +++ b/apps/cli/templates/backend/server/server-base/tsconfig.json.hbs @@ -1,35 +1,39 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "verbatimModuleSyntax": true, - "strict": true, - "skipLibCheck": true, - "baseUrl": "./", - "paths": { - "@/*": ["./src/*"] - {{#if (eq orm 'prisma')}}, - "prisma": ["node_modules/prisma"] - {{/if}} - }, - "outDir": "./dist", - "types": [ - {{#if (eq runtime 'node')}} - "node" - {{else if (eq runtime 'bun')}} - "bun" + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "strict": true, + "skipLibCheck": true, + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + {{#if (eq orm "prisma")}}, + "prisma": ["node_modules/prisma"] + {{/if}} + }, + "outDir": "./dist", + "types": [ + {{#if (eq runtime "node")}} + "node" + {{else if (eq runtime "bun")}} + "bun" + {{else if (eq runtime "workers")}} + "./worker-configuration", + "node" {{else}} - "node", "bun" + "node", + "bun" {{/if}} ], - {{#unless (or (eq backend "convex") (eq backend "none"))}} - "composite": true, - {{/unless}} - "jsx": "react-jsx"{{#if (eq backend 'hono')}}, - "jsxImportSource": "hono/jsx"{{/if}} - }, - "tsc-alias": { - "resolveFullPaths": true - } + {{#unless (or (eq backend "convex") (eq backend "none"))}} + "composite": true, + {{/unless}} + "jsx": "react-jsx"{{#if (eq backend "hono")}}, + "jsxImportSource": "hono/jsx"{{/if}} + }, + "tsc-alias": { + "resolveFullPaths": true + } } diff --git a/apps/cli/templates/db/drizzle/mysql/drizzle.config.ts b/apps/cli/templates/db/drizzle/mysql/drizzle.config.ts.hbs similarity index 100% rename from apps/cli/templates/db/drizzle/mysql/drizzle.config.ts rename to apps/cli/templates/db/drizzle/mysql/drizzle.config.ts.hbs diff --git a/apps/cli/templates/db/drizzle/mysql/src/db/index.ts b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts deleted file mode 100644 index 4cd1cea..0000000 --- a/apps/cli/templates/db/drizzle/mysql/src/db/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { drizzle } from "drizzle-orm/mysql2"; - -export const db = drizzle({ connection: { uri: process.env.DATABASE_URL } }); diff --git a/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs new file mode 100644 index 0000000..b51809a --- /dev/null +++ b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs @@ -0,0 +1,20 @@ +{{#if (or (eq runtime "bun") (eq runtime "node"))}} +import { drizzle } from "drizzle-orm/mysql2"; + +export const db = drizzle({ + connection: { + uri: process.env.DATABASE_URL, + }, +}); +{{/if}} + +{{#if (eq runtime "workers")}} +import { drizzle } from "drizzle-orm/mysql2"; +import { env } from "cloudflare:workers"; + +export const db = drizzle({ + connection: { + uri: env.DATABASE_URL, + }, +}); +{{/if}} diff --git a/apps/cli/templates/db/drizzle/postgres/drizzle.config.ts b/apps/cli/templates/db/drizzle/postgres/drizzle.config.ts.hbs similarity index 100% rename from apps/cli/templates/db/drizzle/postgres/drizzle.config.ts rename to apps/cli/templates/db/drizzle/postgres/drizzle.config.ts.hbs diff --git a/apps/cli/templates/db/drizzle/postgres/src/db/index.ts b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts deleted file mode 100644 index 745361c..0000000 --- a/apps/cli/templates/db/drizzle/postgres/src/db/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { drizzle } from "drizzle-orm/node-postgres"; - -export const db = drizzle(process.env.DATABASE_URL || ""); diff --git a/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs new file mode 100644 index 0000000..3873607 --- /dev/null +++ b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs @@ -0,0 +1,12 @@ +{{#if (or (eq runtime "bun") (eq runtime "node"))}} +import { drizzle } from "drizzle-orm/node-postgres"; + +export const db = drizzle(process.env.DATABASE_URL || ""); +{{/if}} + +{{#if (eq runtime "workers")}} +import { drizzle } from "drizzle-orm/node-postgres"; +import { env } from "cloudflare:workers"; + +export const db = drizzle(env.DATABASE_URL || ""); +{{/if}} diff --git a/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts b/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs similarity index 85% rename from apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts rename to apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs index b2190e7..48cfec8 100644 --- a/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts +++ b/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs @@ -6,6 +6,8 @@ export default defineConfig({ dialect: "turso", dbCredentials: { url: process.env.DATABASE_URL || "", + {{#if (eq dbSetup "turso")}} authToken: process.env.DATABASE_AUTH_TOKEN, + {{/if}} }, }); diff --git a/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts deleted file mode 100644 index 6797ec0..0000000 --- a/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { drizzle } from "drizzle-orm/libsql"; -import { createClient } from "@libsql/client"; - -const client = createClient({ - url: process.env.DATABASE_URL || "", - authToken: process.env.DATABASE_AUTH_TOKEN , -}); - -export const db = drizzle({ client }); diff --git a/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs new file mode 100644 index 0000000..18dd260 --- /dev/null +++ b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs @@ -0,0 +1,28 @@ +{{#if (or (eq runtime "bun") (eq runtime "node"))}} +import { drizzle } from "drizzle-orm/libsql"; +import { createClient } from "@libsql/client"; + +const client = createClient({ + url: process.env.DATABASE_URL || "", + {{#if (eq dbSetup "turso")}} + authToken: process.env.DATABASE_AUTH_TOKEN, + {{/if}} +}); + +export const db = drizzle({ client }); +{{/if}} + +{{#if (eq runtime "workers")}} +import { drizzle } from "drizzle-orm/libsql"; +import { env } from "cloudflare:workers"; +import { createClient } from "@libsql/client"; + +const client = createClient({ + url: env.DATABASE_URL || "", + {{#if (eq dbSetup "turso")}} + authToken: env.DATABASE_AUTH_TOKEN, + {{/if}} +}); + +export const db = drizzle({ client }); +{{/if}} diff --git a/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts b/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts.hbs similarity index 100% rename from apps/cli/templates/db/mongoose/mongodb/src/db/index.ts rename to apps/cli/templates/db/mongoose/mongodb/src/db/index.ts.hbs diff --git a/apps/cli/templates/db/prisma/mongodb/prisma.config.ts b/apps/cli/templates/db/prisma/mongodb/prisma.config.ts.hbs similarity index 100% rename from apps/cli/templates/db/prisma/mongodb/prisma.config.ts rename to apps/cli/templates/db/prisma/mongodb/prisma.config.ts.hbs diff --git a/apps/cli/templates/db/prisma/mongodb/prisma/index.ts b/apps/cli/templates/db/prisma/mongodb/prisma/index.ts.hbs similarity index 100% rename from apps/cli/templates/db/prisma/mongodb/prisma/index.ts rename to apps/cli/templates/db/prisma/mongodb/prisma/index.ts.hbs diff --git a/apps/cli/templates/db/prisma/postgres/prisma.config.ts b/apps/cli/templates/db/prisma/postgres/prisma.config.ts.hbs similarity index 74% rename from apps/cli/templates/db/prisma/postgres/prisma.config.ts rename to apps/cli/templates/db/prisma/postgres/prisma.config.ts.hbs index 9e92886..40bb927 100644 --- a/apps/cli/templates/db/prisma/postgres/prisma.config.ts +++ b/apps/cli/templates/db/prisma/postgres/prisma.config.ts.hbs @@ -1,4 +1,8 @@ +{{#if (eq dbSetup "prisma-postgres")}} // import "dotenv/config"; uncomment this to load .env +{{else}} +import "dotenv/config"; +{{/if}} import path from "node:path"; import type { PrismaConfig } from "prisma"; diff --git a/apps/cli/templates/runtime/workers/apps/server/wrangler.toml.hbs b/apps/cli/templates/runtime/workers/apps/server/wrangler.toml.hbs new file mode 100644 index 0000000..186ae0f --- /dev/null +++ b/apps/cli/templates/runtime/workers/apps/server/wrangler.toml.hbs @@ -0,0 +1,18 @@ +name = "{{projectName}}-server" +main = "src/index.ts" +compatibility_date = "2025-06-15" +compatibility_flags = ["nodejs_compat"] + +[vars] +NODE_ENV = "production" + +# Non-sensitive environment variables (visible in dashboard) +# CORS_ORIGIN = "https://your-frontend-domain.com" +# BETTER_AUTH_URL = "https://your-worker-domain.workers.dev" + +# ⚠️ SENSITIVE DATA: Use `wrangler secret put` instead of adding here +# Don't put these in [vars] - they'll be visible in the dashboard! +# - DATABASE_URL +# - DATABASE_AUTH_TOKEN +# - GOOGLE_GENERATIVE_AI_API_KEY +# - BETTER_AUTH_SECRET diff --git a/apps/web/public/icon/workers.svg b/apps/web/public/icon/workers.svg new file mode 100644 index 0000000..0eb6672 --- /dev/null +++ b/apps/web/public/icon/workers.svg @@ -0,0 +1,2 @@ + + diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 66c4b1e..322be6f 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -81,23 +81,6 @@ const CATEGORY_ORDER: Array = [ "install", ]; -const hasWebFrontend = (webFrontend: string[]) => - webFrontend.some((f) => - [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - ].includes(f), - ); - -const checkHasNativeFrontend = (nativeFrontend: string[]) => - nativeFrontend.includes("native-nativewind") || - nativeFrontend.includes("native-unistyles"); - const hasPWACompatibleFrontend = (webFrontend: string[]) => webFrontend.some((f) => ["tanstack-router", "react-router", "solid", "next"].includes(f), @@ -552,6 +535,60 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } } + if (nextStack.runtime === "workers") { + if (nextStack.backend !== "hono") { + notes.runtime.notes.push( + "Cloudflare Workers runtime requires Hono backend. Hono will be selected.", + ); + notes.backend.notes.push( + "Cloudflare Workers runtime requires Hono backend. It will be selected.", + ); + notes.runtime.hasIssue = true; + notes.backend.hasIssue = true; + nextStack.backend = "hono"; + changed = true; + changes.push({ + category: "runtime", + message: "Backend set to 'Hono' (required by Cloudflare Workers)", + }); + } + + if (nextStack.orm !== "drizzle" && nextStack.orm !== "none") { + notes.runtime.notes.push( + "Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.", + ); + notes.orm.notes.push( + "Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.", + ); + notes.runtime.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + changes.push({ + category: "runtime", + message: "ORM set to 'Drizzle' (required by Cloudflare Workers)", + }); + } + + if (nextStack.database === "mongodb") { + notes.runtime.notes.push( + "Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.", + ); + notes.database.notes.push( + "MongoDB is not compatible with Cloudflare Workers runtime. SQLite will be selected.", + ); + notes.runtime.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "sqlite"; + changed = true; + changes.push({ + category: "runtime", + message: + "Database set to 'SQLite' (MongoDB not compatible with Workers)", + }); + } + } + const isNuxt = nextStack.webFrontend.includes("nuxt"); const isSvelte = nextStack.webFrontend.includes("svelte"); const isSolid = nextStack.webFrontend.includes("solid"); @@ -627,7 +664,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const incompatibleExamples: string[] = []; - // Note: Examples are now supported with Native-only frontends if ( nextStack.database === "none" && nextStack.examples.includes("todo") @@ -709,29 +745,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { }; }; -const getCompatibilityRules = (stack: StackState) => { - const isConvex = stack.backend === "convex"; - const isBackendNone = stack.backend === "none"; - const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend); - const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend); - const hasSolid = stack.webFrontend.includes("solid"); - const hasNuxt = stack.webFrontend.includes("nuxt"); - const hasSvelte = stack.webFrontend.includes("svelte"); - - return { - isConvex, - isBackendNone, - hasWebFrontend: hasWebFrontendSelected, - hasNativeFrontend, - hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend), - hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend), - hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid, - hasSolid, - hasNuxt, - hasSvelte, - }; -}; - const generateCommand = (stackState: StackState): string => { let base: string; switch (stackState.packageManager) { @@ -863,8 +876,6 @@ const StackBuilder = () => { [stack], ); - const rules = useMemo(() => getCompatibilityRules(stack), [stack]); - const getRandomStack = () => { const randomStack: Partial = {}; @@ -973,342 +984,6 @@ const StackBuilder = () => { } }; - const disabledReasons = useMemo(() => { - const reasons = new Map(); - const addRule = (category: string, techId: string, reason: string) => { - reasons.set(`${category}-${techId}`, reason); - }; - - for (const category of CATEGORY_ORDER) { - const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS] || []; - const catKey = category as keyof StackState; - - for (const tech of options) { - const techId = tech.id; - - if (rules.isConvex) { - const convexDefaults: Record = { - runtime: "none", - database: "none", - orm: "none", - api: "none", - auth: "false", - dbSetup: "none", - examples: ["todo"], - }; - - if ( - ["runtime", "database", "orm", "api", "auth", "dbSetup"].includes( - catKey, - ) - ) { - const requiredValue = convexDefaults[catKey]; - if (catKey === "auth") { - if (techId === "true" && requiredValue === "false") { - addRule( - category, - techId, - "Disabled: Convex backend requires Authentication to be disabled.", - ); - } - } else if (String(techId) !== String(requiredValue)) { - addRule( - category, - techId, - `Disabled: Convex backend requires ${getCategoryDisplayName( - catKey, - )} to be '${requiredValue}'.`, - ); - } - } else if (catKey === "examples") { - const requiredExamples = convexDefaults.examples as string[]; - if ( - !requiredExamples.includes(techId) && - techId !== "none" && - options.find((o) => o.id === techId) - ) { - addRule( - category, - techId, - "Disabled: Convex backend only supports the 'Todo' example.", - ); - } - } else if ( - catKey === "webFrontend" && - (techId === "nuxt" || techId === "solid") - ) { - addRule( - category, - techId, - `Disabled: Convex backend is not compatible with ${tech.name}.`, - ); - } - continue; - } - - if (rules.isBackendNone) { - if (catKey === "auth" && techId === "true") { - addRule( - category, - techId, - "Disabled: Authentication requires a backend.", - ); - } else if ( - ["database", "orm", "api", "runtime", "dbSetup"].includes(catKey) && - techId !== "none" - ) { - addRule( - category, - techId, - `Disabled: ${getCategoryDisplayName( - catKey, - )} cannot be selected when 'No Backend' is chosen (will be 'None').`, - ); - } else if (catKey === "examples" && techId !== "none") { - addRule( - category, - techId, - "Disabled: Examples cannot be selected when 'No Backend' is chosen.", - ); - } - } - - if (catKey === "runtime" && techId === "none" && !rules.isConvex) { - addRule( - category, - techId, - "Disabled: Runtime 'None' is only available with Convex backend.", - ); - } - - if (catKey === "api") { - if (techId !== "none" && (rules.isConvex || rules.isBackendNone)) { - addRule( - category, - techId, - rules.isConvex - ? "Disabled: Convex backend requires API to be 'None'." - : "Disabled: No backend requires API to be 'None'.", - ); - } - if (techId === "trpc" && rules.hasNuxtOrSvelteOrSolid) { - const frontendName = rules.hasNuxt - ? "Nuxt" - : rules.hasSvelte - ? "Svelte" - : "Solid"; - addRule( - category, - techId, - `Disabled: tRPC is not supported with ${frontendName}. oRPC will be automatically selected.`, - ); - } - } - - if (catKey === "orm") { - if ( - stack.database === "none" && - techId !== "none" && - !rules.isConvex - ) { - addRule( - category, - techId, - "Disabled: ORM requires a database. Select a database or 'No ORM'.", - ); - } else if (stack.database === "mongodb") { - if ( - techId !== "prisma" && - techId !== "mongoose" && - techId !== "none" - ) { - addRule( - category, - techId, - "Disabled: With MongoDB, use Prisma, Mongoose, or No ORM.", - ); - } - } else if (["sqlite", "postgres", "mysql"].includes(stack.database)) { - if (techId === "mongoose") { - addRule( - category, - techId, - "Disabled: Mongoose ORM is for MongoDB. Choose a different ORM for relational databases.", - ); - } - } - - if (stack.dbSetup === "turso" && techId !== "drizzle") { - addRule( - category, - techId, - "Disabled: Turso DB setup requires Drizzle ORM.", - ); - } else if ( - stack.dbSetup === "prisma-postgres" && - techId !== "prisma" - ) { - addRule( - category, - techId, - "Disabled: Prisma PostgreSQL setup requires Prisma ORM.", - ); - } else if ( - stack.dbSetup === "mongodb-atlas" && - techId !== "prisma" && - techId !== "mongoose" - ) { - addRule( - category, - techId, - "Disabled: MongoDB Atlas setup requires Prisma or Mongoose ORM.", - ); - } - } - - if (catKey === "dbSetup" && techId !== "none") { - if (stack.database === "none" && !rules.isBackendNone) { - addRule( - category, - techId, - "Disabled: A database must be selected to use this DB setup. Select 'Basic Setup' or a database first.", - ); - } - - if (techId === "turso") { - if (stack.database !== "sqlite" && stack.database !== "none") { - addRule( - category, - techId, - "Disabled: Turso requires SQLite. (Will auto-select if chosen)", - ); - } - if (stack.orm !== "drizzle" && stack.orm !== "none") { - addRule( - category, - techId, - "Disabled: Turso requires Drizzle ORM. (Will auto-select if chosen)", - ); - } - } else if (techId === "prisma-postgres") { - if (stack.database !== "postgres" && stack.database !== "none") { - addRule( - category, - techId, - "Disabled: Requires PostgreSQL. (Will auto-select if chosen)", - ); - } - if (stack.orm !== "prisma" && stack.orm !== "none") { - addRule( - category, - techId, - "Disabled: Requires Prisma ORM. (Will auto-select if chosen)", - ); - } - } else if (techId === "mongodb-atlas") { - if (stack.database !== "mongodb" && stack.database !== "none") { - addRule( - category, - techId, - "Disabled: Requires MongoDB. (Will auto-select if chosen)", - ); - } - if ( - stack.orm !== "prisma" && - stack.orm !== "mongoose" && - stack.orm !== "none" - ) { - addRule( - category, - techId, - "Disabled: Requires Prisma or Mongoose ORM. (Will auto-select Prisma if chosen)", - ); - } - } else if (techId === "neon") { - if (stack.database !== "postgres" && stack.database !== "none") { - addRule( - category, - techId, - "Disabled: Neon requires PostgreSQL. (Will auto-select if chosen)", - ); - } - } else if (techId === "supabase") { - if (stack.database !== "postgres" && stack.database !== "none") { - addRule( - category, - techId, - "Disabled: Supabase (local) requires PostgreSQL. (Will auto-select if chosen)", - ); - } - } - } - - if (catKey === "auth" && techId === "true") { - if (stack.database === "none" && !rules.isBackendNone) { - addRule( - category, - techId, - "Disabled: Authentication requires a database.", - ); - } - } - - if (catKey === "addons") { - if (techId === "pwa" && !rules.hasPWACompatible) { - addRule( - category, - techId, - "Disabled: PWA addon requires a compatible frontend (e.g., TanStack Router, Solid).", - ); - } - if (techId === "tauri" && !rules.hasTauriCompatible) { - addRule( - category, - techId, - "Disabled: Tauri addon requires a compatible frontend (e.g., TanStack Router, Nuxt, Svelte, Solid, Next.js).", - ); - } - } - - if (catKey === "examples" && techId !== "none") { - if (stack.api === "none" && !rules.isConvex && !rules.isBackendNone) { - addRule( - category, - techId, - "Disabled: Examples require an API. Cannot be selected when API is 'None'.", - ); - } - if ( - stack.database === "none" && - techId === "todo" && - !rules.isConvex - ) { - addRule( - category, - techId, - "Disabled: The 'Todo' example requires a database.", - ); - } - if (stack.backend === "elysia" && techId === "ai") { - addRule( - category, - techId, - "Disabled: The 'AI' example is not compatible with an Elysia backend.", - ); - } - if (rules.hasSolid && techId === "ai") { - addRule( - category, - techId, - "Disabled: The 'AI' example is not compatible with a Solid frontend.", - ); - } - } - } - } - return reasons; - }, [stack, rules]); - const selectedBadges = (() => { const badges: React.ReactNode[] = []; for (const category of CATEGORY_ORDER) { @@ -1402,6 +1077,20 @@ const StackBuilder = () => { useEffect(() => { if (compatibilityAnalysis.adjustedStack) { if (compatibilityAnalysis.changes.length > 0) { + if (compatibilityAnalysis.changes.length === 1) { + toast.info(compatibilityAnalysis.changes[0].message, { + duration: 4000, + }); + } else if (compatibilityAnalysis.changes.length > 1) { + const message = `${ + compatibilityAnalysis.changes.length + } compatibility adjustments made:\n${compatibilityAnalysis.changes + .map((c) => `• ${c.message}`) + .join("\n")}`; + toast.info(message, { + duration: 5000, + }); + } } setLastChanges(compatibilityAnalysis.changes); setStack(compatibilityAnalysis.adjustedStack); @@ -1804,36 +1493,19 @@ const StackBuilder = () => { isSelected = currentValue === tech.id; } - const disabledReason = disabledReasons.get( - `${categoryKey}-${tech.id}`, - ); - const isDisabled = !!disabledReason; - return ( - !isDisabled && handleTechSelect( categoryKey as keyof typeof TECH_OPTIONS, tech.id, @@ -1862,27 +1534,19 @@ const StackBuilder = () => { {tech.name} - {isDisabled && !isSelected && ( - - )}

{tech.description}

- {tech.default && !isSelected && !isDisabled && ( + {tech.default && !isSelected && ( Default )}
- {isDisabled && disabledReason && ( - -

{disabledReason}

-
- )}
); })} diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 9167c18..5550bf7 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -132,6 +132,20 @@ export const TECH_OPTIONS = { icon: "/icon/node.svg", color: "from-green-400 to-green-600", }, + { + id: "workers", + name: "Cloudflare Workers (beta)", + description: "Serverless runtime for the edge", + icon: "/icon/workers.svg", + color: "from-orange-400 to-orange-600", + }, + { + id: "none", + name: "No Runtime", + description: "No specific runtime", + icon: "", + color: "from-gray-400 to-gray-600", + }, ], backend: [ { diff --git a/bun.lock b/bun.lock index bdbc98f..e60c2cd 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.18.3", + "version": "2.18.5", "bin": { "create-better-t-stack": "dist/index.js", },