mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): prisma + workers, prisma + turso, planetscale (postgres/mysql) support (#567)
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
alwaysApply: true
|
alwaysApply: true
|
||||||
---
|
---
|
||||||
|
|
||||||
- use functional programming
|
- Always use functional programming; avoid object-oriented programming.
|
||||||
- use normal function syntax for functions do not use arrow functions
|
- Define functions using the standard function declaration syntax, not arrow functions.
|
||||||
- no emojis
|
- Do not include emojis.
|
||||||
- use type syntax, dont use interface syntax for types
|
- Use TypeScript type aliases instead of interface declarations.
|
||||||
|
- In Handlebars templates, avoid generic if/else blocks. Write explicit conditions, such as: use if (eq orm "prisma") for Prisma, and else if (eq orm "drizzle") for Drizzle.
|
||||||
|
|||||||
@@ -1,90 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "create-better-t-stack",
|
"name": "create-better-t-stack",
|
||||||
"version": "2.40.5",
|
"version": "2.40.4",
|
||||||
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Aman Varshney",
|
"author": "Aman Varshney",
|
||||||
"bin": {
|
"bin": {
|
||||||
"create-better-t-stack": "dist/cli.js"
|
"create-better-t-stack": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"templates",
|
"templates",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"better-t-stack",
|
"better-t-stack",
|
||||||
"typescript",
|
"typescript",
|
||||||
"boilerplate",
|
"boilerplate",
|
||||||
"starter",
|
"starter",
|
||||||
"cli",
|
"cli",
|
||||||
"turborepo",
|
"turborepo",
|
||||||
"trpc",
|
"trpc",
|
||||||
"better-auth",
|
"better-auth",
|
||||||
"monorepo",
|
"monorepo",
|
||||||
"fullstack",
|
"fullstack",
|
||||||
"type-safety",
|
"type-safety",
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
"expo",
|
"expo",
|
||||||
"hono",
|
"hono",
|
||||||
"elysia",
|
"elysia",
|
||||||
"drizzle",
|
"drizzle",
|
||||||
"prisma",
|
"prisma",
|
||||||
"tanstack",
|
"tanstack",
|
||||||
"tailwind",
|
"tailwind",
|
||||||
"shadcn",
|
"shadcn",
|
||||||
"pwa",
|
"pwa",
|
||||||
"tauri",
|
"tauri",
|
||||||
"biome"
|
"biome"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git",
|
"url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git",
|
||||||
"directory": "apps/cli"
|
"directory": "apps/cli"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"homepage": "https://better-t-stack.dev/",
|
"homepage": "https://better-t-stack.dev/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsdown",
|
||||||
"dev": "tsdown --watch",
|
"dev": "tsdown --watch",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"test": "bun run build && vitest run",
|
"test": "bun run build && vitest run",
|
||||||
"test:ui": "bun run build && vitest --ui",
|
"test:ui": "bun run build && vitest --ui",
|
||||||
"test:with-build": "bun run build && WITH_BUILD=1 vitest --ui",
|
"test:with-build": "bun run build && WITH_BUILD=1 vitest --ui",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@biomejs/js-api": "^3.0.0",
|
"@clack/prompts": "^1.0.0-alpha.4",
|
||||||
"@biomejs/wasm-nodejs": "^2.2.0",
|
"consola": "^3.4.2",
|
||||||
"@clack/prompts": "^1.0.0-alpha.4",
|
"execa": "^9.6.0",
|
||||||
"consola": "^3.4.2",
|
"fs-extra": "^11.3.1",
|
||||||
"execa": "^9.6.0",
|
"gradient-string": "^3.0.0",
|
||||||
"fs-extra": "^11.3.1",
|
"handlebars": "^4.7.8",
|
||||||
"gradient-string": "^3.0.0",
|
"jsonc-parser": "^3.3.1",
|
||||||
"handlebars": "^4.7.8",
|
"picocolors": "^1.1.1",
|
||||||
"jsonc-parser": "^3.3.1",
|
"tinyglobby": "^0.2.15",
|
||||||
"picocolors": "^1.1.1",
|
"trpc-cli": "^0.10.2",
|
||||||
"tinyglobby": "^0.2.14",
|
"ts-morph": "^27.0.0",
|
||||||
"trpc-cli": "^0.10.2",
|
"zod": "^4.1.5"
|
||||||
"ts-morph": "^26.0.0",
|
},
|
||||||
"zod": "^4.0.17"
|
"devDependencies": {
|
||||||
},
|
"@types/fs-extra": "^11.0.4",
|
||||||
"devDependencies": {
|
"@types/node": "^24.3.1",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"@types/node": "^24.3.0",
|
"tsdown": "^0.14.2",
|
||||||
"@vitest/ui": "^3.2.4",
|
"typescript": "^5.9.2",
|
||||||
"tsdown": "^0.14.1",
|
"vitest": "^3.2.4"
|
||||||
"typescript": "^5.9.2",
|
}
|
||||||
"vitest": "^3.2.4"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
"drizzle-kit": "^0.31.2",
|
"drizzle-kit": "^0.31.2",
|
||||||
|
"@planetscale/database": "^1.19.0",
|
||||||
|
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/client": "^0.15.9",
|
||||||
|
|
||||||
@@ -63,7 +64,11 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
"@prisma/client": "^6.15.0",
|
"@prisma/client": "^6.15.0",
|
||||||
prisma: "^6.15.0",
|
prisma: "^6.15.0",
|
||||||
|
"@prisma/adapter-d1": "^6.15.0",
|
||||||
"@prisma/extension-accelerate": "^2.0.2",
|
"@prisma/extension-accelerate": "^2.0.2",
|
||||||
|
"@prisma/adapter-libsql": "^6.15.0",
|
||||||
|
|
||||||
|
"@prisma/adapter-planetscale": "^6.15.0",
|
||||||
|
|
||||||
mongoose: "^8.14.0",
|
mongoose: "^8.14.0",
|
||||||
|
|
||||||
@@ -141,12 +146,12 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
wrangler: "^4.23.0",
|
wrangler: "^4.23.0",
|
||||||
"@cloudflare/vite-plugin": "^1.9.0",
|
"@cloudflare/vite-plugin": "^1.9.0",
|
||||||
"@opennextjs/cloudflare": "^1.3.0",
|
"@opennextjs/cloudflare": "^1.6.5",
|
||||||
"nitro-cloudflare-dev": "^0.2.2",
|
"nitro-cloudflare-dev": "^0.2.2",
|
||||||
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
||||||
"@cloudflare/workers-types": "^4.20250822.0",
|
"@cloudflare/workers-types": "^4.20250822.0",
|
||||||
|
|
||||||
alchemy: "^0.63.0",
|
alchemy: "^0.65.0",
|
||||||
// temporary workaround for alchemy + tanstack start
|
// temporary workaround for alchemy + tanstack start
|
||||||
nitropack: "^2.12.4",
|
nitropack: "^2.12.4",
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import fs from "fs-extra";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { writeBtsConfig } from "../../utils/bts-config";
|
import { writeBtsConfig } from "../../utils/bts-config";
|
||||||
import { exitWithError } from "../../utils/errors";
|
import { exitWithError } from "../../utils/errors";
|
||||||
import { formatProjectWithBiome } from "../../utils/format-with-biome";
|
|
||||||
import { setupAddons } from "../addons/addons-setup";
|
import { setupAddons } from "../addons/addons-setup";
|
||||||
import { setupExamples } from "../addons/examples-setup";
|
import { setupExamples } from "../addons/examples-setup";
|
||||||
import { setupApi } from "../core/api-setup";
|
import { setupApi } from "../core/api-setup";
|
||||||
@@ -86,8 +85,6 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
|
|
||||||
await writeBtsConfig(options);
|
await writeBtsConfig(options);
|
||||||
|
|
||||||
await formatProjectWithBiome(projectDir);
|
|
||||||
|
|
||||||
if (isConvex) {
|
if (isConvex) {
|
||||||
await runConvexCodegen(projectDir, options.packageManager);
|
await runConvexCodegen(projectDir, options.packageManager);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { setupCloudflareD1 } from "../database-providers/d1-setup";
|
|||||||
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
|
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
|
||||||
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
||||||
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
||||||
|
import { setupPlanetScale } from "../database-providers/planetscale-setup";
|
||||||
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
||||||
import { setupSupabase } from "../database-providers/supabase-setup";
|
import { setupSupabase } from "../database-providers/supabase-setup";
|
||||||
import { setupTurso } from "../database-providers/turso-setup";
|
import { setupTurso } from "../database-providers/turso-setup";
|
||||||
@@ -36,11 +37,29 @@ export async function setupDatabase(config: ProjectConfig) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (orm === "prisma") {
|
if (orm === "prisma") {
|
||||||
await addPackageDependency({
|
if (database === "mysql" && dbSetup === "planetscale") {
|
||||||
dependencies: ["@prisma/client"],
|
await addPackageDependency({
|
||||||
devDependencies: ["prisma"],
|
dependencies: [
|
||||||
projectDir: serverDir,
|
"@prisma/client",
|
||||||
});
|
"@prisma/adapter-planetscale",
|
||||||
|
"@planetscale/database",
|
||||||
|
],
|
||||||
|
devDependencies: ["prisma"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
} else if (database === "sqlite" && dbSetup === "turso") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@prisma/client", "@prisma/adapter-libsql"],
|
||||||
|
devDependencies: ["prisma"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@prisma/client"],
|
||||||
|
devDependencies: ["prisma"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (orm === "drizzle") {
|
} else if (orm === "drizzle") {
|
||||||
if (database === "sqlite") {
|
if (database === "sqlite") {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
@@ -55,6 +74,12 @@ export async function setupDatabase(config: ProjectConfig) {
|
|||||||
devDependencies: ["drizzle-kit", "@types/ws"],
|
devDependencies: ["drizzle-kit", "@types/ws"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
} else if (dbSetup === "planetscale") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["drizzle-orm", "pg"],
|
||||||
|
devDependencies: ["drizzle-kit", "@types/pg"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "pg"],
|
dependencies: ["drizzle-orm", "pg"],
|
||||||
@@ -63,11 +88,19 @@ export async function setupDatabase(config: ProjectConfig) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (database === "mysql") {
|
} else if (database === "mysql") {
|
||||||
await addPackageDependency({
|
if (dbSetup === "planetscale") {
|
||||||
dependencies: ["drizzle-orm", "mysql2"],
|
await addPackageDependency({
|
||||||
devDependencies: ["drizzle-kit"],
|
dependencies: ["drizzle-orm", "@planetscale/database"],
|
||||||
projectDir: serverDir,
|
devDependencies: ["drizzle-kit"],
|
||||||
});
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["drizzle-orm", "mysql2"],
|
||||||
|
devDependencies: ["drizzle-kit"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (orm === "mongoose") {
|
} else if (orm === "mongoose") {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
@@ -88,9 +121,15 @@ export async function setupDatabase(config: ProjectConfig) {
|
|||||||
await setupPrismaPostgres(config);
|
await setupPrismaPostgres(config);
|
||||||
} else if (dbSetup === "neon") {
|
} else if (dbSetup === "neon") {
|
||||||
await setupNeonPostgres(config);
|
await setupNeonPostgres(config);
|
||||||
|
} else if (dbSetup === "planetscale") {
|
||||||
|
await setupPlanetScale(config);
|
||||||
} else if (dbSetup === "supabase") {
|
} else if (dbSetup === "supabase") {
|
||||||
await setupSupabase(config);
|
await setupSupabase(config);
|
||||||
}
|
}
|
||||||
|
} else if (database === "mysql") {
|
||||||
|
if (dbSetup === "planetscale") {
|
||||||
|
await setupPlanetScale(config);
|
||||||
|
}
|
||||||
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
||||||
await setupMongoDBAtlas(config);
|
await setupMongoDBAtlas(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,16 +232,8 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let databaseUrl: string | null = null;
|
let databaseUrl: string | null = null;
|
||||||
const specializedSetup =
|
|
||||||
dbSetup === "turso" ||
|
|
||||||
dbSetup === "prisma-postgres" ||
|
|
||||||
dbSetup === "mongodb-atlas" ||
|
|
||||||
dbSetup === "neon" ||
|
|
||||||
dbSetup === "supabase" ||
|
|
||||||
dbSetup === "d1" ||
|
|
||||||
dbSetup === "docker";
|
|
||||||
|
|
||||||
if (database !== "none" && !specializedSetup) {
|
if (database !== "none" && dbSetup === "none") {
|
||||||
switch (database) {
|
switch (database) {
|
||||||
case "postgres":
|
case "postgres":
|
||||||
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
|
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
|
||||||
@@ -281,7 +273,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|||||||
{
|
{
|
||||||
key: "DATABASE_URL",
|
key: "DATABASE_URL",
|
||||||
value: databaseUrl,
|
value: databaseUrl,
|
||||||
condition: database !== "none" && !specializedSetup,
|
condition: database !== "none" && dbSetup === "none",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
|
|||||||
@@ -287,22 +287,40 @@ async function getDatabaseInstructions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dbSetup === "d1" && serverDeploy === "alchemy") {
|
if (dbSetup === "d1" && serverDeploy === "alchemy") {
|
||||||
instructions.push(
|
if (orm === "drizzle") {
|
||||||
`${pc.yellow(
|
|
||||||
"NOTE:",
|
|
||||||
)} D1 migrations are automatically handled by Alchemy`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orm === "prisma") {
|
|
||||||
if (dbSetup === "turso") {
|
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.yellow(
|
`${pc.yellow(
|
||||||
"NOTE:",
|
"NOTE:",
|
||||||
)} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`,
|
)} D1 migrations are automatically handled by Alchemy`,
|
||||||
|
);
|
||||||
|
} else if (orm === "prisma") {
|
||||||
|
instructions.push(
|
||||||
|
`${pc.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`,
|
||||||
|
);
|
||||||
|
instructions.push(
|
||||||
|
`${pc.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbSetup === "planetscale") {
|
||||||
|
if (database === "mysql" && orm === "drizzle") {
|
||||||
|
instructions.push(
|
||||||
|
`${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} Enable foreign key constraints in PlanetScale database settings`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (database === "mysql" && orm === "prisma") {
|
||||||
|
instructions.push(
|
||||||
|
`${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orm === "prisma") {
|
||||||
if (database === "mongodb" && dbSetup === "docker") {
|
if (database === "mongodb" && dbSetup === "docker") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.yellow(
|
`${pc.yellow(
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ async function updateRootPackageJson(
|
|||||||
}
|
}
|
||||||
if (options.orm === "prisma") {
|
if (options.orm === "prisma") {
|
||||||
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
||||||
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
} else if (options.orm === "drizzle") {
|
||||||
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
||||||
@@ -110,10 +108,8 @@ async function updateRootPackageJson(
|
|||||||
if (options.orm === "prisma") {
|
if (options.orm === "prisma") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`pnpm --filter ${backendPackageName} db:generate`;
|
`pnpm --filter ${backendPackageName} db:generate`;
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
scripts["db:migrate"] =
|
||||||
scripts["db:migrate"] =
|
`pnpm --filter ${backendPackageName} db:migrate`;
|
||||||
`pnpm --filter ${backendPackageName} db:migrate`;
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
} else if (options.orm === "drizzle") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`pnpm --filter ${backendPackageName} db:generate`;
|
`pnpm --filter ${backendPackageName} db:generate`;
|
||||||
@@ -149,10 +145,8 @@ async function updateRootPackageJson(
|
|||||||
if (options.orm === "prisma") {
|
if (options.orm === "prisma") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`npm run db:generate --workspace ${backendPackageName}`;
|
`npm run db:generate --workspace ${backendPackageName}`;
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
scripts["db:migrate"] =
|
||||||
scripts["db:migrate"] =
|
`npm run db:migrate --workspace ${backendPackageName}`;
|
||||||
`npm run db:migrate --workspace ${backendPackageName}`;
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
} else if (options.orm === "drizzle") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`npm run db:generate --workspace ${backendPackageName}`;
|
`npm run db:generate --workspace ${backendPackageName}`;
|
||||||
@@ -189,10 +183,8 @@ async function updateRootPackageJson(
|
|||||||
if (options.orm === "prisma") {
|
if (options.orm === "prisma") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`bun run --filter ${backendPackageName} db:generate`;
|
`bun run --filter ${backendPackageName} db:generate`;
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
scripts["db:migrate"] =
|
||||||
scripts["db:migrate"] =
|
`bun run --filter ${backendPackageName} db:migrate`;
|
||||||
`bun run --filter ${backendPackageName} db:migrate`;
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
} else if (options.orm === "drizzle") {
|
||||||
scripts["db:generate"] =
|
scripts["db:generate"] =
|
||||||
`bun run --filter ${backendPackageName} db:generate`;
|
`bun run --filter ${backendPackageName} db:generate`;
|
||||||
@@ -278,9 +270,7 @@ async function updateServerPackageJson(
|
|||||||
scripts["db:studio"] = "prisma studio";
|
scripts["db:studio"] = "prisma studio";
|
||||||
}
|
}
|
||||||
scripts["db:generate"] = "prisma generate";
|
scripts["db:generate"] = "prisma generate";
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
scripts["db:migrate"] = "prisma migrate dev";
|
||||||
scripts["db:migrate"] = "prisma migrate dev";
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
} else if (options.orm === "drizzle") {
|
||||||
scripts["db:push"] = "drizzle-kit push";
|
scripts["db:push"] = "drizzle-kit push";
|
||||||
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||||
|
|
||||||
export async function setupCloudflareD1(config: ProjectConfig) {
|
export async function setupCloudflareD1(config: ProjectConfig) {
|
||||||
const { projectDir, serverDeploy } = config;
|
const { projectDir, serverDeploy, orm } = config;
|
||||||
|
|
||||||
if (serverDeploy === "wrangler") {
|
if (serverDeploy === "wrangler") {
|
||||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||||
@@ -30,4 +31,28 @@ export async function setupCloudflareD1(config: ProjectConfig) {
|
|||||||
await addEnvVariablesToFile(envPath, variables);
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
} catch (_err) {}
|
} catch (_err) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(serverDeploy === "wrangler" || serverDeploy === "alchemy") &&
|
||||||
|
orm === "prisma"
|
||||||
|
) {
|
||||||
|
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||||
|
const variables: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value: "file:./local.db",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
|
} catch (_err) {}
|
||||||
|
|
||||||
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@prisma/adapter-d1"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cancel, isCancel, log, spinner, text } from "@clack/prompts";
|
import { cancel, isCancel, log, select, spinner, text } from "@clack/prompts";
|
||||||
import consola from "consola";
|
import consola from "consola";
|
||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { commandExists } from "../../utils/command-exists";
|
import { commandExists } from "../../utils/command-exists";
|
||||||
|
import { exitCancelled } from "../../utils/errors";
|
||||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||||
|
|
||||||
type MongoDBConfig = {
|
type MongoDBConfig = {
|
||||||
@@ -130,6 +131,32 @@ export async function setupMongoDBAtlas(config: ProjectConfig) {
|
|||||||
try {
|
try {
|
||||||
await fs.ensureDir(serverDir);
|
await fs.ensureDir(serverDir);
|
||||||
|
|
||||||
|
const mode = await select({
|
||||||
|
message: "MongoDB Atlas setup: choose mode",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Automatic",
|
||||||
|
value: "auto",
|
||||||
|
hint: "Automated setup with provider CLI, sets .env",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual",
|
||||||
|
value: "manual",
|
||||||
|
hint: "Manual setup, add env vars yourself",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(mode)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
if (mode === "manual") {
|
||||||
|
mainSpinner.stop("MongoDB Atlas manual setup selected");
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mainSpinner.stop("MongoDB Atlas setup ready");
|
mainSpinner.stop("MongoDB Atlas setup ready");
|
||||||
|
|
||||||
const config = await initMongoDBAtlas(serverDir);
|
const config = await initMongoDBAtlas(serverDir);
|
||||||
|
|||||||
@@ -158,6 +158,31 @@ export async function setupNeonPostgres(config: ProjectConfig) {
|
|||||||
const { packageManager, projectDir } = config;
|
const { packageManager, projectDir } = config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const mode = await select({
|
||||||
|
message: "Neon setup: choose mode",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Automatic",
|
||||||
|
value: "auto",
|
||||||
|
hint: "Automated setup with provider CLI, sets .env",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual",
|
||||||
|
value: "manual",
|
||||||
|
hint: "Manual setup, add env vars yourself",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(mode)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
if (mode === "manual") {
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const setupMethod = await select({
|
const setupMethod = await select({
|
||||||
message: "Choose your Neon setup method:",
|
message: "Choose your Neon setup method:",
|
||||||
options: [
|
options: [
|
||||||
|
|||||||
79
apps/cli/src/helpers/database-providers/planetscale-setup.ts
Normal file
79
apps/cli/src/helpers/database-providers/planetscale-setup.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import type { ProjectConfig } from "../../types";
|
||||||
|
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||||
|
|
||||||
|
export async function setupPlanetScale(config: ProjectConfig) {
|
||||||
|
const { projectDir, database, orm } = config;
|
||||||
|
|
||||||
|
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||||
|
|
||||||
|
if (database === "mysql" && orm === "drizzle") {
|
||||||
|
const variables: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value:
|
||||||
|
'mysql://username:password@host/database?ssl={"rejectUnauthorized":true}',
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DATABASE_HOST",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DATABASE_USERNAME",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DATABASE_PASSWORD",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(projectDir, "apps/server"));
|
||||||
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (database === "postgres" && orm === "prisma") {
|
||||||
|
const variables: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value: "postgresql://username:password@host/database?sslaccept=strict",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(projectDir, "apps/server"));
|
||||||
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (database === "postgres" && orm === "drizzle") {
|
||||||
|
const variables: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value:
|
||||||
|
"postgresql://username:password@host/database?sslmode=verify-full",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(projectDir, "apps/server"));
|
||||||
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (database === "mysql" && orm === "prisma") {
|
||||||
|
const variables: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value: "mysql://username:password@host/database?sslaccept=strict",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(projectDir, "apps/server"));
|
||||||
|
await addEnvVariablesToFile(envPath, variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -245,6 +245,31 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
|
|||||||
try {
|
try {
|
||||||
await fs.ensureDir(serverDir);
|
await fs.ensureDir(serverDir);
|
||||||
|
|
||||||
|
const mode = await select({
|
||||||
|
message: "Prisma Postgres setup: choose mode",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Automatic",
|
||||||
|
value: "auto",
|
||||||
|
hint: "Automated setup with provider CLI, sets .env",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual",
|
||||||
|
value: "manual",
|
||||||
|
hint: "Manual setup, add env vars yourself",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(mode)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
if (mode === "manual") {
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const setupOptions = [
|
const setupOptions = [
|
||||||
{
|
{
|
||||||
label: "Quick setup with create-db",
|
label: "Quick setup with create-db",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { log } from "@clack/prompts";
|
import { isCancel, log, select } from "@clack/prompts";
|
||||||
import { consola } from "consola";
|
import { consola } from "consola";
|
||||||
import { type ExecaError, execa } from "execa";
|
import { type ExecaError, execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { PackageManager, ProjectConfig } from "../../types";
|
import type { PackageManager, ProjectConfig } from "../../types";
|
||||||
|
import { exitCancelled } from "../../utils/errors";
|
||||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||||
|
|
||||||
@@ -159,6 +160,31 @@ export async function setupSupabase(config: ProjectConfig) {
|
|||||||
try {
|
try {
|
||||||
await fs.ensureDir(serverDir);
|
await fs.ensureDir(serverDir);
|
||||||
|
|
||||||
|
const mode = await select({
|
||||||
|
message: "Supabase setup: choose mode",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Automatic",
|
||||||
|
value: "auto",
|
||||||
|
hint: "Automated setup with provider CLI, sets .env",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual",
|
||||||
|
value: "manual",
|
||||||
|
hint: "Manual setup, add env vars yourself",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(mode)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
if (mode === "manual") {
|
||||||
|
displayManualSupabaseInstructions();
|
||||||
|
await writeSupabaseEnvFile(projectDir, "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const initialized = await initializeSupabase(serverDir, packageManager);
|
const initialized = await initializeSupabase(serverDir, packageManager);
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
displayManualSupabaseInstructions();
|
displayManualSupabaseInstructions();
|
||||||
|
|||||||
@@ -197,23 +197,49 @@ export async function setupTurso(config: ProjectConfig) {
|
|||||||
const { orm, projectDir } = config;
|
const { orm, projectDir } = config;
|
||||||
const _isDrizzle = orm === "drizzle";
|
const _isDrizzle = orm === "drizzle";
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
setupSpinner.start("Checking Turso CLI availability...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const mode = await select({
|
||||||
|
message: "Turso setup: choose mode",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Automatic",
|
||||||
|
value: "auto",
|
||||||
|
hint: "Automated setup with provider CLI, sets .env",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual",
|
||||||
|
value: "manual",
|
||||||
|
hint: "Manual setup, add env vars yourself",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(mode)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
|
if (mode === "manual") {
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSpinner.start("Checking Turso CLI availability...");
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
const isMac = platform === "darwin";
|
const isMac = platform === "darwin";
|
||||||
const _isLinux = platform === "linux";
|
const _isLinux = platform === "linux";
|
||||||
const isWindows = platform === "win32";
|
const isWindows = platform === "win32";
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
if (setupSpinner)
|
||||||
|
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
||||||
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
|
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
|
||||||
await writeEnvFile(projectDir);
|
await writeEnvFile(projectDir);
|
||||||
displayManualSetupInstructions();
|
displayManualSetupInstructions();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSpinner.stop("Turso CLI availability checked");
|
if (setupSpinner) setupSpinner.stop("Turso CLI availability checked");
|
||||||
|
|
||||||
const isCliInstalled = await isTursoInstalled();
|
const isCliInstalled = await isTursoInstalled();
|
||||||
|
|
||||||
@@ -273,7 +299,8 @@ export async function setupTurso(config: ProjectConfig) {
|
|||||||
|
|
||||||
log.success("Turso database setup completed successfully!");
|
log.success("Turso database setup completed successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setupSpinner.stop(pc.red("Turso CLI availability check failed"));
|
if (setupSpinner)
|
||||||
|
setupSpinner.stop(pc.red("Turso CLI availability check failed"));
|
||||||
consola.error(
|
consola.error(
|
||||||
pc.red(
|
pc.red(
|
||||||
`Error during Turso setup: ${
|
`Error during Turso setup: ${
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export async function setupNextAlchemyDeploy(
|
|||||||
if (!(await fs.pathExists(webAppDir))) return;
|
if (!(await fs.pathExists(webAppDir))) return;
|
||||||
|
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["alchemy", "dotenv"],
|
dependencies: ["@opennextjs/cloudflare"],
|
||||||
|
devDependencies: ["alchemy", "dotenv", "wrangler"],
|
||||||
projectDir: webAppDir,
|
projectDir: webAppDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,4 +30,22 @@ export async function setupNextAlchemyDeploy(
|
|||||||
}
|
}
|
||||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openNextConfigPath = path.join(webAppDir, "open-next.config.ts");
|
||||||
|
const openNextConfigContent = `import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({});
|
||||||
|
`;
|
||||||
|
|
||||||
|
await fs.writeFile(openNextConfigPath, openNextConfigContent);
|
||||||
|
|
||||||
|
const gitignorePath = path.join(webAppDir, ".gitignore");
|
||||||
|
if (await fs.pathExists(gitignorePath)) {
|
||||||
|
const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
|
||||||
|
if (!gitignoreContent.includes("wrangler.jsonc")) {
|
||||||
|
await fs.appendFile(gitignorePath, "\nwrangler.jsonc\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fs.writeFile(gitignorePath, "wrangler.jsonc\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "fs-extra";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { PackageManager, ProjectConfig } from "../../types";
|
import type { PackageManager, ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export async function setupServerDeploy(config: ProjectConfig) {
|
export async function setupServerDeploy(config: ProjectConfig) {
|
||||||
const { serverDeploy, webDeploy, projectDir } = config;
|
const { serverDeploy, webDeploy, projectDir } = config;
|
||||||
@@ -64,8 +65,11 @@ async function generateCloudflareWorkerTypes({
|
|||||||
const s = spinner();
|
const s = spinner();
|
||||||
try {
|
try {
|
||||||
s.start("Generating Cloudflare Workers types...");
|
s.start("Generating Cloudflare Workers types...");
|
||||||
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
const runCmd = getPackageExecutionCommand(
|
||||||
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
packageManager,
|
||||||
|
"wrangler types --env-interface CloudflareBindings",
|
||||||
|
);
|
||||||
|
await execa(runCmd, { cwd: serverDir, shell: true });
|
||||||
s.stop("Cloudflare Workers types generated successfully!");
|
s.stop("Cloudflare Workers types generated successfully!");
|
||||||
} catch {
|
} catch {
|
||||||
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { exitCancelled } from "../utils/errors";
|
|||||||
export async function getDBSetupChoice(
|
export async function getDBSetupChoice(
|
||||||
databaseType: string,
|
databaseType: string,
|
||||||
dbSetup: DatabaseSetup | undefined,
|
dbSetup: DatabaseSetup | undefined,
|
||||||
orm?: ORM,
|
_orm?: ORM,
|
||||||
backend?: Backend,
|
backend?: Backend,
|
||||||
runtime?: Runtime,
|
runtime?: Runtime,
|
||||||
): Promise<DatabaseSetup> {
|
): Promise<DatabaseSetup> {
|
||||||
@@ -19,10 +19,6 @@ export async function getDBSetupChoice(
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseType === "sqlite" && orm === "prisma") {
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
let options: Array<{ value: DatabaseSetup; label: string; hint: string }> =
|
let options: Array<{ value: DatabaseSetup; label: string; hint: string }> =
|
||||||
[];
|
[];
|
||||||
|
|
||||||
@@ -51,6 +47,11 @@ export async function getDBSetupChoice(
|
|||||||
label: "Neon Postgres",
|
label: "Neon Postgres",
|
||||||
hint: "Serverless Postgres with branching capability",
|
hint: "Serverless Postgres with branching capability",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "planetscale" as const,
|
||||||
|
label: "PlanetScale",
|
||||||
|
hint: "Serverless MySQL platform with branching (Postgres compatible)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "supabase" as const,
|
value: "supabase" as const,
|
||||||
label: "Supabase",
|
label: "Supabase",
|
||||||
@@ -70,6 +71,11 @@ export async function getDBSetupChoice(
|
|||||||
];
|
];
|
||||||
} else if (databaseType === "mysql") {
|
} else if (databaseType === "mysql") {
|
||||||
options = [
|
options = [
|
||||||
|
{
|
||||||
|
value: "planetscale" as const,
|
||||||
|
label: "PlanetScale",
|
||||||
|
hint: "Serverless MySQL platform with branching",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "docker" as const,
|
value: "docker" as const,
|
||||||
label: "Docker",
|
label: "Docker",
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ export async function getORMChoice(
|
|||||||
if (!hasDatabase) return "none";
|
if (!hasDatabase) return "none";
|
||||||
if (orm !== undefined) return orm;
|
if (orm !== undefined) return orm;
|
||||||
|
|
||||||
if (runtime === "workers") {
|
|
||||||
return "drizzle";
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
...(database === "mongodb"
|
...(database === "mongodb"
|
||||||
? [ormOptions.prisma, ormOptions.mongoose]
|
? [ormOptions.prisma, ormOptions.mongoose]
|
||||||
@@ -48,7 +44,12 @@ export async function getORMChoice(
|
|||||||
const response = await select<ORM>({
|
const response = await select<ORM>({
|
||||||
message: "Select ORM",
|
message: "Select ORM",
|
||||||
options,
|
options,
|
||||||
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
|
initialValue:
|
||||||
|
database === "mongodb"
|
||||||
|
? "prisma"
|
||||||
|
: runtime === "workers"
|
||||||
|
? "drizzle"
|
||||||
|
: DEFAULT_CONFIG.orm,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ export async function getDeploymentChoice(
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIncompatibleFrontend = frontend.some((f) => f === "next");
|
const availableDeployments = ["wrangler", "alchemy", "none"];
|
||||||
const availableDeployments = hasIncompatibleFrontend
|
|
||||||
? ["wrangler", "none"]
|
|
||||||
: ["wrangler", "alchemy", "none"];
|
|
||||||
|
|
||||||
const options: DeploymentOption[] = availableDeployments.map((deploy) => {
|
const options: DeploymentOption[] = availableDeployments.map((deploy) => {
|
||||||
const { label, hint } = getDeploymentDisplay(deploy as WebDeploy);
|
const { label, hint } = getDeploymentDisplay(deploy as WebDeploy);
|
||||||
@@ -64,9 +61,7 @@ export async function getDeploymentChoice(
|
|||||||
const response = await select<WebDeploy>({
|
const response = await select<WebDeploy>({
|
||||||
message: "Select web deployment",
|
message: "Select web deployment",
|
||||||
options,
|
options,
|
||||||
initialValue: hasIncompatibleFrontend
|
initialValue: DEFAULT_CONFIG.webDeploy,
|
||||||
? "wrangler"
|
|
||||||
: DEFAULT_CONFIG.webDeploy,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
@@ -82,8 +77,6 @@ export async function getDeploymentToAdd(
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIncompatibleFrontend = frontend.some((f) => f === "next");
|
|
||||||
|
|
||||||
const options: DeploymentOption[] = [];
|
const options: DeploymentOption[] = [];
|
||||||
|
|
||||||
if (existingDeployment !== "wrangler") {
|
if (existingDeployment !== "wrangler") {
|
||||||
@@ -95,7 +88,7 @@ export async function getDeploymentToAdd(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingDeployment !== "alchemy" && !hasIncompatibleFrontend) {
|
if (existingDeployment !== "alchemy") {
|
||||||
const { label, hint } = getDeploymentDisplay("alchemy");
|
const { label, hint } = getDeploymentDisplay("alchemy");
|
||||||
options.push({
|
options.push({
|
||||||
value: "alchemy",
|
value: "alchemy",
|
||||||
@@ -123,9 +116,7 @@ export async function getDeploymentToAdd(
|
|||||||
const response = await select<WebDeploy>({
|
const response = await select<WebDeploy>({
|
||||||
message: "Select web deployment",
|
message: "Select web deployment",
|
||||||
options,
|
options,
|
||||||
initialValue: hasIncompatibleFrontend
|
initialValue: DEFAULT_CONFIG.webDeploy,
|
||||||
? "wrangler"
|
|
||||||
: DEFAULT_CONFIG.webDeploy,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const DatabaseSetupSchema = z
|
|||||||
"turso",
|
"turso",
|
||||||
"neon",
|
"neon",
|
||||||
"prisma-postgres",
|
"prisma-postgres",
|
||||||
|
"planetscale",
|
||||||
"mongodb-atlas",
|
"mongodb-atlas",
|
||||||
"supabase",
|
"supabase",
|
||||||
"d1",
|
"d1",
|
||||||
|
|||||||
@@ -68,37 +68,13 @@ export function validateWorkersCompatibility(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
providedFlags.has("runtime") &&
|
|
||||||
options.runtime === "workers" &&
|
|
||||||
config.orm &&
|
|
||||||
config.orm !== "drizzle" &&
|
|
||||||
config.orm !== "none"
|
|
||||||
) {
|
|
||||||
exitWithError(
|
|
||||||
`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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
providedFlags.has("orm") &&
|
|
||||||
config.orm &&
|
|
||||||
config.orm !== "drizzle" &&
|
|
||||||
config.orm !== "none" &&
|
|
||||||
config.runtime === "workers"
|
|
||||||
) {
|
|
||||||
exitWithError(
|
|
||||||
`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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
providedFlags.has("runtime") &&
|
providedFlags.has("runtime") &&
|
||||||
options.runtime === "workers" &&
|
options.runtime === "workers" &&
|
||||||
config.database === "mongodb"
|
config.database === "mongodb"
|
||||||
) {
|
) {
|
||||||
exitWithError(
|
exitWithError(
|
||||||
"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.",
|
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +94,7 @@ export function validateWorkersCompatibility(
|
|||||||
config.runtime === "workers"
|
config.runtime === "workers"
|
||||||
) {
|
) {
|
||||||
exitWithError(
|
exitWithError(
|
||||||
"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.",
|
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,29 +237,3 @@ export function validateExamplesCompatibility(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAlchemyCompatibility(
|
|
||||||
webDeploy: WebDeploy | undefined,
|
|
||||||
serverDeploy: ServerDeploy | undefined,
|
|
||||||
frontends: Frontend[] = [],
|
|
||||||
) {
|
|
||||||
const isAlchemyWebDeploy = webDeploy === "alchemy";
|
|
||||||
const isAlchemyServerDeploy = serverDeploy === "alchemy";
|
|
||||||
|
|
||||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
|
||||||
const incompatibleFrontends = frontends.filter((f) => f === "next");
|
|
||||||
|
|
||||||
if (incompatibleFrontends.length > 0) {
|
|
||||||
const deployType =
|
|
||||||
isAlchemyWebDeploy && isAlchemyServerDeploy
|
|
||||||
? "web and server deployment"
|
|
||||||
: isAlchemyWebDeploy
|
|
||||||
? "web deployment"
|
|
||||||
: "server deployment";
|
|
||||||
|
|
||||||
exitWithError(
|
|
||||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
ensureSingleWebAndNative,
|
ensureSingleWebAndNative,
|
||||||
isWebFrontend,
|
isWebFrontend,
|
||||||
validateAddonsAgainstFrontends,
|
validateAddonsAgainstFrontends,
|
||||||
validateAlchemyCompatibility,
|
|
||||||
validateApiFrontendCompatibility,
|
validateApiFrontendCompatibility,
|
||||||
validateExamplesCompatibility,
|
validateExamplesCompatibility,
|
||||||
validateServerDeployRequiresBackend,
|
validateServerDeployRequiresBackend,
|
||||||
@@ -126,6 +125,10 @@ export function validateDatabaseSetup(
|
|||||||
errorMessage:
|
errorMessage:
|
||||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||||
},
|
},
|
||||||
|
planetscale: {
|
||||||
|
errorMessage:
|
||||||
|
"PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup.",
|
||||||
|
},
|
||||||
"mongodb-atlas": {
|
"mongodb-atlas": {
|
||||||
database: "mongodb",
|
database: "mongodb",
|
||||||
errorMessage:
|
errorMessage:
|
||||||
@@ -152,8 +155,15 @@ export function validateDatabaseSetup(
|
|||||||
if (dbSetup && dbSetup !== "none") {
|
if (dbSetup && dbSetup !== "none") {
|
||||||
const validation = setupValidations[dbSetup];
|
const validation = setupValidations[dbSetup];
|
||||||
|
|
||||||
if (validation.database && database !== validation.database) {
|
// Special handling for PlanetScale - supports both postgres and mysql
|
||||||
exitWithError(validation.errorMessage);
|
if (dbSetup === "planetscale") {
|
||||||
|
if (database !== "postgres" && database !== "mysql") {
|
||||||
|
exitWithError(validation.errorMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (validation.database && database !== validation.database) {
|
||||||
|
exitWithError(validation.errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validation.runtime && runtime !== validation.runtime) {
|
if (validation.runtime && runtime !== validation.runtime) {
|
||||||
@@ -416,12 +426,6 @@ export function validateFullConfig(
|
|||||||
config.database,
|
config.database,
|
||||||
config.frontend ?? [],
|
config.frontend ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
validateAlchemyCompatibility(
|
|
||||||
config.webDeploy,
|
|
||||||
config.serverDeploy,
|
|
||||||
config.frontend ?? [],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateConfigForProgrammaticUse(
|
export function validateConfigForProgrammaticUse(
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { Biome } from "@biomejs/js-api/nodejs";
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import { glob } from "tinyglobby";
|
|
||||||
|
|
||||||
export async function formatProjectWithBiome(projectDir: string) {
|
|
||||||
const biome = new Biome();
|
|
||||||
const { projectKey } = biome.openProject(projectDir);
|
|
||||||
|
|
||||||
biome.applyConfiguration(projectKey, {
|
|
||||||
formatter: {
|
|
||||||
enabled: true,
|
|
||||||
indentStyle: "tab",
|
|
||||||
},
|
|
||||||
javascript: {
|
|
||||||
formatter: {
|
|
||||||
quoteStyle: "double",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = await glob("**/*", {
|
|
||||||
cwd: projectDir,
|
|
||||||
dot: true,
|
|
||||||
absolute: true,
|
|
||||||
onlyFiles: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const filePath of files) {
|
|
||||||
try {
|
|
||||||
const ext = path.extname(filePath).toLowerCase();
|
|
||||||
const supported = new Set([
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".js",
|
|
||||||
".jsx",
|
|
||||||
".cjs",
|
|
||||||
".mjs",
|
|
||||||
".cts",
|
|
||||||
".mts",
|
|
||||||
".json",
|
|
||||||
".jsonc",
|
|
||||||
".md",
|
|
||||||
".mdx",
|
|
||||||
".css",
|
|
||||||
".scss",
|
|
||||||
".html",
|
|
||||||
]);
|
|
||||||
if (!supported.has(ext)) continue;
|
|
||||||
|
|
||||||
const original = await fs.readFile(filePath, "utf8");
|
|
||||||
const result = biome.formatContent(projectKey, original, { filePath });
|
|
||||||
const content = result?.content;
|
|
||||||
if (typeof content !== "string") continue;
|
|
||||||
if (content.length === 0 && original.length > 0) continue;
|
|
||||||
if (content !== original) {
|
|
||||||
await fs.writeFile(filePath, content);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
|
|||||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import prisma from "../../prisma";
|
import prisma from "../db";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: prismaAdapter(prisma, {
|
database: prismaAdapter(prisma, {
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
{{#if (or (eq runtime "bun") (eq runtime "node"))}}
|
{{#if (or (eq runtime "bun") (eq runtime "node"))}}
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
import { drizzle } from "drizzle-orm/planetscale-serverless";
|
||||||
|
|
||||||
|
export const db = drizzle({
|
||||||
|
connection: {
|
||||||
|
host: process.env.DATABASE_HOST,
|
||||||
|
username: process.env.DATABASE_USERNAME,
|
||||||
|
password: process.env.DATABASE_PASSWORD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
{{else}}
|
||||||
import { drizzle } from "drizzle-orm/mysql2";
|
import { drizzle } from "drizzle-orm/mysql2";
|
||||||
|
|
||||||
export const db = drizzle({
|
export const db = drizzle({
|
||||||
@@ -7,8 +18,21 @@ export const db = drizzle({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq runtime "workers")}}
|
{{#if (eq runtime "workers")}}
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
import { drizzle } from "drizzle-orm/planetscale-serverless";
|
||||||
|
import { env } from "cloudflare:workers";
|
||||||
|
|
||||||
|
export const db = drizzle({
|
||||||
|
connection: {
|
||||||
|
host: env.DATABASE_HOST,
|
||||||
|
username: env.DATABASE_USERNAME,
|
||||||
|
password: env.DATABASE_PASSWORD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
{{else}}
|
||||||
import { drizzle } from "drizzle-orm/mysql2";
|
import { drizzle } from "drizzle-orm/mysql2";
|
||||||
import { env } from "cloudflare:workers";
|
import { env } from "cloudflare:workers";
|
||||||
|
|
||||||
@@ -18,3 +42,4 @@ export const db = drizzle({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrismaClient } from "./generated/client";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
5
apps/cli/templates/db/prisma/mongodb/src/db/index.ts.hbs
Normal file
5
apps/cli/templates/db/prisma/mongodb/src/db/index.ts.hbs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrismaClient } from "./generated/client";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
@@ -11,9 +11,15 @@ generator client {
|
|||||||
{{#if (eq runtime "workers")}}
|
{{#if (eq runtime "workers")}}
|
||||||
runtime = "workerd"
|
runtime = "workerd"
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
previewFeatures = ["driverAdapters"]
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
relationMode = "prisma"
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
|
|||||||
12
apps/cli/templates/db/prisma/mysql/src/db/index.ts.hbs
Normal file
12
apps/cli/templates/db/prisma/mysql/src/db/index.ts.hbs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
import { PrismaPlanetScale } from '@prisma/adapter-planetscale'
|
||||||
|
|
||||||
|
const adapter = new PrismaPlanetScale({ url: process.env.DATABASE_URL })
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({adapter});
|
||||||
|
{{else}}
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrismaClient } from "./generated/client";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
@@ -19,4 +19,7 @@ datasource db {
|
|||||||
{{#if (eq dbSetup "supabase")}}
|
{{#if (eq dbSetup "supabase")}}
|
||||||
directUrl = env("DIRECT_URL")
|
directUrl = env("DIRECT_URL")
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq dbSetup "planetscale")}}
|
||||||
|
relationMode = "prisma"
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
@@ -1,10 +1,38 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { PrismaConfig } from "prisma";
|
import type { PrismaConfig } from "prisma";
|
||||||
|
{{#if (eq dbSetup "d1")}}
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq dbSetup "turso")}}
|
||||||
|
import { PrismaLibSQL } from "@prisma/adapter-libsql";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
{{#if (or (eq dbSetup "d1") (eq dbSetup "turso"))}}
|
||||||
|
experimental: {
|
||||||
|
adapter: true
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
schema: path.join("prisma", "schema"),
|
schema: path.join("prisma", "schema"),
|
||||||
migrations: {
|
migrations: {
|
||||||
path: path.join("prisma", "migrations"),
|
path: path.join("prisma", "migrations"),
|
||||||
}
|
},
|
||||||
|
{{#if (eq dbSetup "d1")}}
|
||||||
|
async adapter() {
|
||||||
|
return new PrismaD1({
|
||||||
|
CLOUDFLARE_D1_TOKEN: process.env.CLOUDFLARE_D1_TOKEN,
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
|
CLOUDFLARE_DATABASE_ID: process.env.CLOUDFLARE_DATABASE_ID,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq dbSetup "turso")}}
|
||||||
|
async adapter() {
|
||||||
|
return new PrismaLibSQL({
|
||||||
|
url: process.env.DATABASE_URL || "",
|
||||||
|
authToken: process.env.DATABASE_AUTH_TOKEN,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
} satisfies PrismaConfig;
|
} satisfies PrismaConfig;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrismaClient } from "./generated/client";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
@@ -10,10 +10,20 @@ generator client {
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (eq runtime "workers")}}
|
{{#if (eq runtime "workers")}}
|
||||||
runtime = "workerd"
|
runtime = "workerd"
|
||||||
|
{{#if (eq dbSetup "d1")}}
|
||||||
|
previewFeatures = ["driverAdapters"]
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq dbSetup "turso")}}
|
||||||
|
previewFeatures = ["driverAdapters"]
|
||||||
{{/if}}
|
{{/if}}
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
|
{{#if (eq dbSetup "turso")}}
|
||||||
|
url = "file:./local.db"
|
||||||
|
{{else}}
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
|
|||||||
28
apps/cli/templates/db/prisma/sqlite/src/db/index.ts.hbs
Normal file
28
apps/cli/templates/db/prisma/sqlite/src/db/index.ts.hbs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{{#if (eq dbSetup "d1")}}
|
||||||
|
import { env } from "cloudflare:workers";
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
|
||||||
|
const adapter = new PrismaD1(env.DB);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
{{else if (eq dbSetup "turso")}}
|
||||||
|
import { PrismaLibSQL } from "@prisma/adapter-libsql";
|
||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
|
||||||
|
const adapter = new PrismaLibSQL({
|
||||||
|
url: process.env.DATABASE_URL || "",
|
||||||
|
authToken: process.env.DATABASE_AUTH_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
{{else}}
|
||||||
|
import { PrismaClient } from "../../prisma/generated/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
{{/if}}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import alchemy from "alchemy";
|
import alchemy from "alchemy";
|
||||||
{{#if (eq webDeploy "alchemy")}}
|
{{#if (eq webDeploy "alchemy")}}
|
||||||
{{#if (includes frontend "next")}}
|
{{#if (includes frontend "next")}}
|
||||||
import { Next } from "alchemy/cloudflare";
|
import { Nextjs } from "alchemy/cloudflare";
|
||||||
{{else if (includes frontend "nuxt")}}
|
{{else if (includes frontend "nuxt")}}
|
||||||
import { Nuxt } from "alchemy/cloudflare";
|
import { Nuxt } from "alchemy/cloudflare";
|
||||||
{{else if (includes frontend "svelte")}}
|
{{else if (includes frontend "svelte")}}
|
||||||
@@ -44,13 +44,17 @@ await Exec("db-generate", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const db = await D1Database("database", {
|
const db = await D1Database("database", {
|
||||||
|
{{#if (eq orm "prisma")}}
|
||||||
|
migrationsDir: "apps/server/prisma/migrations",
|
||||||
|
{{else if (eq orm "drizzle")}}
|
||||||
migrationsDir: "apps/server/src/db/migrations",
|
migrationsDir: "apps/server/src/db/migrations",
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq webDeploy "alchemy")}}
|
{{#if (eq webDeploy "alchemy")}}
|
||||||
{{#if (includes frontend "next")}}
|
{{#if (includes frontend "next")}}
|
||||||
export const web = await Next("web", {
|
export const web = await Nextjs("web", {
|
||||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||||
bindings: {
|
bindings: {
|
||||||
{{#if (eq backend "convex")}}
|
{{#if (eq backend "convex")}}
|
||||||
|
|||||||
@@ -27,7 +27,12 @@
|
|||||||
"database_name": "YOUR_DB_NAME",
|
"database_name": "YOUR_DB_NAME",
|
||||||
"database_id": "YOUR_DB_ID",
|
"database_id": "YOUR_DB_ID",
|
||||||
"preview_database_id": "local-test-db",
|
"preview_database_id": "local-test-db",
|
||||||
|
{{#if (eq orm "drizzle")}}
|
||||||
"migrations_dir": "./src/db/migrations"
|
"migrations_dir": "./src/db/migrations"
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq orm "prisma")}}
|
||||||
|
"migrations_dir": "./prisma/migrations"
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import prisma from "../../prisma";
|
import prisma from "../db";
|
||||||
import { publicProcedure } from "../lib/orpc";
|
import { publicProcedure } from "../lib/orpc";
|
||||||
|
|
||||||
export const todoRouter = {
|
export const todoRouter = {
|
||||||
@@ -52,7 +52,7 @@ export const todoRouter = {
|
|||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import prisma from "../../prisma";
|
import prisma from "../db";
|
||||||
import { publicProcedure, router } from "../lib/trpc";
|
import { publicProcedure, router } from "../lib/trpc";
|
||||||
|
|
||||||
export const todoRouter = router({
|
export const todoRouter = router({
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
[install]
|
[install]
|
||||||
{{#if (or (or (includes frontend "nuxt") (includes frontend "native-nativewind")) (includes frontend
|
|
||||||
"native-unistyles"))}}
|
|
||||||
# linker = "isolated"
|
# linker = "isolated"
|
||||||
{{else}}
|
|
||||||
linker = "isolated"
|
|
||||||
{{/if}}
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
{{#if (or (eq webDeploy "alchemy") (eq webDeploy "wrangler"))}}
|
||||||
|
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
|
||||||
|
{{/if}}
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
@@ -5,3 +8,7 @@ const nextConfig: NextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
||||||
|
{{#if (or (eq webDeploy "alchemy") (eq webDeploy "wrangler"))}}
|
||||||
|
initOpenNextCloudflareForDev();
|
||||||
|
{{/if}}
|
||||||
@@ -2936,6 +2936,190 @@ describe("create-better-t-stack smoke", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scaffolds with MySQL + Drizzle + PlanetScale", async () => {
|
||||||
|
const projectName = "app-mysql-drizzle-planetscale";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"bun",
|
||||||
|
"--database",
|
||||||
|
"mysql",
|
||||||
|
"--orm",
|
||||||
|
"drizzle",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"none",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
assertScaffoldedProject(projectDir);
|
||||||
|
assertBtsConfig(projectDir, {
|
||||||
|
database: "mysql",
|
||||||
|
orm: "drizzle",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scaffolds with MySQL + Prisma + PlanetScale", async () => {
|
||||||
|
const projectName = "app-mysql-prisma-planetscale";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"bun",
|
||||||
|
"--database",
|
||||||
|
"mysql",
|
||||||
|
"--orm",
|
||||||
|
"prisma",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"none",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
assertScaffoldedProject(projectDir);
|
||||||
|
assertBtsConfig(projectDir, {
|
||||||
|
database: "mysql",
|
||||||
|
orm: "prisma",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scaffolds with PostgreSQL + Drizzle + PlanetScale", async () => {
|
||||||
|
const projectName = "app-postgres-drizzle-planetscale";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"bun",
|
||||||
|
"--database",
|
||||||
|
"postgres",
|
||||||
|
"--orm",
|
||||||
|
"drizzle",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"none",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
await assertScaffoldedProject(projectDir);
|
||||||
|
await assertBtsConfig(projectDir, {
|
||||||
|
database: "postgres",
|
||||||
|
orm: "drizzle",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scaffolds with PostgreSQL + Prisma + PlanetScale", async () => {
|
||||||
|
const projectName = "app-postgres-prisma-planetscale";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"bun",
|
||||||
|
"--database",
|
||||||
|
"postgres",
|
||||||
|
"--orm",
|
||||||
|
"prisma",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"none",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
assertScaffoldedProject(projectDir);
|
||||||
|
assertBtsConfig(projectDir, {
|
||||||
|
database: "postgres",
|
||||||
|
orm: "prisma",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("scaffolds oRPC with Next.js", async () => {
|
it("scaffolds oRPC with Next.js", async () => {
|
||||||
const projectName = "app-orpc-next";
|
const projectName = "app-orpc-next";
|
||||||
await runCli(
|
await runCli(
|
||||||
@@ -3210,6 +3394,100 @@ describe("create-better-t-stack smoke", () => {
|
|||||||
runtime: "node",
|
runtime: "node",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scaffolds with MySQL + Drizzle + PlanetScale + Node runtime", async () => {
|
||||||
|
const projectName = "app-mysql-drizzle-planetscale-node";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"node",
|
||||||
|
"--database",
|
||||||
|
"mysql",
|
||||||
|
"--orm",
|
||||||
|
"drizzle",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"none",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
await assertScaffoldedProject(projectDir);
|
||||||
|
await assertBtsConfig(projectDir, {
|
||||||
|
database: "mysql",
|
||||||
|
orm: "drizzle",
|
||||||
|
runtime: "node",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scaffolds with MySQL + Prisma + PlanetScale + Workers runtime", async () => {
|
||||||
|
const projectName = "app-mysql-prisma-planetscale-workers";
|
||||||
|
await runCli(
|
||||||
|
[
|
||||||
|
projectName,
|
||||||
|
|
||||||
|
"--frontend",
|
||||||
|
"tanstack-router",
|
||||||
|
"--backend",
|
||||||
|
"hono",
|
||||||
|
"--runtime",
|
||||||
|
"workers",
|
||||||
|
"--database",
|
||||||
|
"mysql",
|
||||||
|
"--orm",
|
||||||
|
"prisma",
|
||||||
|
"--api",
|
||||||
|
"trpc",
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
|
"--addons",
|
||||||
|
"none",
|
||||||
|
"--examples",
|
||||||
|
"none",
|
||||||
|
"--db-setup",
|
||||||
|
"planetscale",
|
||||||
|
"--web-deploy",
|
||||||
|
"none",
|
||||||
|
"--server-deploy",
|
||||||
|
"wrangler",
|
||||||
|
"--package-manager",
|
||||||
|
"bun",
|
||||||
|
"--no-install",
|
||||||
|
"--no-git",
|
||||||
|
],
|
||||||
|
workdir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDir = join(workdir, projectName);
|
||||||
|
assertScaffoldedProject(projectDir);
|
||||||
|
assertBtsConfig(projectDir, {
|
||||||
|
database: "mysql",
|
||||||
|
orm: "prisma",
|
||||||
|
runtime: "workers",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
(process.env.WITH_BUILD === "1" ? describe : describe.skip)(
|
(process.env.WITH_BUILD === "1" ? describe : describe.skip)(
|
||||||
@@ -3296,12 +3574,18 @@ describe("create-better-t-stack smoke", () => {
|
|||||||
"app-with-auth",
|
"app-with-auth",
|
||||||
"app-mysql-prisma",
|
"app-mysql-prisma",
|
||||||
"app-mysql-drizzle",
|
"app-mysql-drizzle",
|
||||||
|
"app-mysql-drizzle-planetscale",
|
||||||
|
"app-mysql-prisma-planetscale",
|
||||||
|
"app-postgres-drizzle-planetscale",
|
||||||
|
"app-postgres-prisma-planetscale",
|
||||||
"app-orpc-next",
|
"app-orpc-next",
|
||||||
"app-orpc-nuxt",
|
"app-orpc-nuxt",
|
||||||
"app-orpc-svelte",
|
"app-orpc-svelte",
|
||||||
"app-orpc-solid",
|
"app-orpc-solid",
|
||||||
"app-backend-next",
|
"app-backend-next",
|
||||||
"app-node-runtime",
|
"app-node-runtime",
|
||||||
|
"app-mysql-drizzle-planetscale-node",
|
||||||
|
"app-mysql-prisma-planetscale-workers",
|
||||||
].forEach((n) => {
|
].forEach((n) => {
|
||||||
projectNames.add(n);
|
projectNames.add(n);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Cloudflare Workers has specific compatibility requirements:
|
|||||||
| Component | Requirement | Reason |
|
| Component | Requirement | Reason |
|
||||||
|-----------|-------------|--------|
|
|-----------|-------------|--------|
|
||||||
| Backend | Must be `hono` | Only Hono supports Workers runtime |
|
| Backend | Must be `hono` | Only Hono supports Workers runtime |
|
||||||
| ORM | Must be `drizzle` or `none` | Workers doesn't support Prisma/Mongoose |
|
| ORM | Must be `drizzle` or `prisma` | Workers supports Drizzle and Prisma; Mongoose is not supported |
|
||||||
| Database | Cannot be `mongodb` | MongoDB requires Prisma/Mongoose |
|
| Database | Cannot be `mongodb` | MongoDB requires Prisma/Mongoose |
|
||||||
| Database Setup | Cannot be `docker` | Workers is serverless, no Docker support |
|
| Database Setup | Cannot be `docker` | Workers is serverless, no Docker support |
|
||||||
|
|
||||||
@@ -52,6 +52,9 @@ create-better-t-stack --runtime workers --backend express
|
|||||||
|
|
||||||
# ✅ Valid - Workers with Hono
|
# ✅ Valid - Workers with Hono
|
||||||
create-better-t-stack --runtime workers --backend hono --database sqlite --orm drizzle --db-setup d1
|
create-better-t-stack --runtime workers --backend hono --database sqlite --orm drizzle --db-setup d1
|
||||||
|
|
||||||
|
# ✅ Also valid - Workers with Prisma (D1)
|
||||||
|
create-better-t-stack --runtime workers --backend hono --database sqlite --orm prisma --db-setup d1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend Presets
|
### Backend Presets
|
||||||
@@ -123,8 +126,8 @@ create-better-t-stack --frontend next native-nativewind
|
|||||||
|
|
||||||
| Setup Provider | Required Database | Notes |
|
| Setup Provider | Required Database | Notes |
|
||||||
|---------------|------------------|-------|
|
|---------------|------------------|-------|
|
||||||
| `turso` | `sqlite` | Distributed SQLite |
|
| `turso` | `sqlite` | Distributed SQLite; works with Drizzle and Prisma |
|
||||||
| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime) |
|
| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime); works with Drizzle and Prisma |
|
||||||
| `neon` | `postgres` | Serverless PostgreSQL |
|
| `neon` | `postgres` | Serverless PostgreSQL |
|
||||||
| `supabase` | `postgres` | PostgreSQL with additional features |
|
| `supabase` | `postgres` | PostgreSQL with additional features |
|
||||||
| `prisma-postgres` | `postgres` | Managed PostgreSQL via Prisma |
|
| `prisma-postgres` | `postgres` | Managed PostgreSQL via Prisma |
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
title: Compatibility
|
|
||||||
description: Valid and invalid combinations across frontend, backend, runtime, database, and addons
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **Convex backend**: Sets database, ORM, and API to `none`; auth to `clerk` (if compatible frontends) or `none`
|
|
||||||
- **Backend `none`**: Forces API, ORM, database, authentication, and runtime to `none`; disables examples
|
|
||||||
- **Frontend `none`**: Backend-only project; PWA/Tauri/examples may be disabled
|
|
||||||
- **API `none`**: No tRPC/oRPC setup; use framework-native APIs
|
|
||||||
- **Database `none`**: Disables ORM and Better-Auth (but allows Clerk with Convex)
|
|
||||||
- **ORM `none`**: No ORM setup; manage DB manually
|
|
||||||
- **Runtime `none`**: Only with Convex backend or when backend is `none`
|
|
||||||
- **Auth `clerk`**: Only available with Convex backend and compatible frontends
|
|
||||||
|
|
||||||
## Cloudflare Workers
|
|
||||||
|
|
||||||
- Backend: `hono` only
|
|
||||||
- Database: `sqlite` with Cloudflare D1
|
|
||||||
- ORM: `drizzle` (or none)
|
|
||||||
- Not compatible with MongoDB
|
|
||||||
|
|
||||||
## Framework Notes
|
|
||||||
|
|
||||||
- SvelteKit, Nuxt, and SolidJS frontends are only compatible with `orpc` API layer
|
|
||||||
- PWA addon requires a web frontend: TanStack Router, React Router, Next.js, or SolidJS
|
|
||||||
- Tauri addon requires React (TanStack Router/React Router), Nuxt, SvelteKit, SolidJS, or Next.js
|
|
||||||
- AI example is not compatible with Elysia backend or SolidJS frontend
|
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
"bts-config",
|
"bts-config",
|
||||||
"analytics",
|
"analytics",
|
||||||
"contributing",
|
"contributing",
|
||||||
"compatibility",
|
|
||||||
"faq"
|
"faq"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"postinstall": "fumadocs-mdx",
|
"postinstall": "fumadocs-mdx",
|
||||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||||
"generate-analytics": "bun scripts/generate-analytics.ts",
|
"generate-analytics": "bun scripts/generate-analytics.ts",
|
||||||
"generate-schema": "bun scripts/generate-schema.ts"
|
"generate-schema": "bun scripts/generate-schema.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-t-stack/backend": "workspace:*",
|
"@better-t-stack/backend": "workspace:*",
|
||||||
"@erquhart/convex-oss-stats": "^0.8.1",
|
"@erquhart/convex-oss-stats": "^0.8.1",
|
||||||
"@number-flow/react": "^0.5.10",
|
"@number-flow/react": "^0.5.10",
|
||||||
"@opennextjs/cloudflare": "^1.6.3",
|
"@opennextjs/cloudflare": "^1.6.3",
|
||||||
"@orama/orama": "^3.1.11",
|
"@orama/orama": "^3.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"clsx": "^2.1.1",
|
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"convex": "^1.25.4",
|
"class-variance-authority": "^0.7.1",
|
||||||
"convex-helpers": "^0.1.104",
|
"clsx": "^2.1.1",
|
||||||
"culori": "^4.0.2",
|
"convex": "^1.25.4",
|
||||||
"date-fns": "^4.1.0",
|
"convex-helpers": "^0.1.104",
|
||||||
"fumadocs-core": "15.6.7",
|
"culori": "^4.0.2",
|
||||||
"fumadocs-mdx": "11.7.3",
|
"date-fns": "^4.1.0",
|
||||||
"fumadocs-ui": "15.6.7",
|
"fumadocs-core": "15.6.7",
|
||||||
"lucide-react": "^0.536.0",
|
"fumadocs-mdx": "11.7.3",
|
||||||
"motion": "^12.23.12",
|
"fumadocs-ui": "15.6.7",
|
||||||
"next": "15.3.5",
|
"lucide-react": "^0.536.0",
|
||||||
"next-themes": "^0.4.6",
|
"motion": "^12.23.12",
|
||||||
"nuqs": "^2.5.2",
|
"next": "15.3.5",
|
||||||
"papaparse": "^5.5.3",
|
"next-themes": "^0.4.6",
|
||||||
"posthog-js": "^1.258.5",
|
"nuqs": "^2.5.2",
|
||||||
"qrcode": "^1.5.4",
|
"papaparse": "^5.5.3",
|
||||||
"radix-ui": "^1.4.2",
|
"posthog-js": "^1.258.5",
|
||||||
"react": "^19.1.1",
|
"qrcode": "^1.5.4",
|
||||||
"react-dom": "^19.1.1",
|
"radix-ui": "^1.4.2",
|
||||||
"react-tweet": "^3.2.2",
|
"react": "^19.1.1",
|
||||||
"recharts": "2.15.4",
|
"react-dom": "^19.1.1",
|
||||||
"remark": "^15.0.1",
|
"react-tweet": "^3.2.2",
|
||||||
"remark-gfm": "^4.0.1",
|
"recharts": "2.15.4",
|
||||||
"remark-mdx": "^3.1.0",
|
"remark": "^15.0.1",
|
||||||
"shiki": "^3.9.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.6",
|
"remark-mdx": "^3.1.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"shiki": "^3.9.1",
|
||||||
},
|
"sonner": "^2.0.6",
|
||||||
"devDependencies": {
|
"tailwind-merge": "^3.3.1"
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
},
|
||||||
"@types/culori": "^4.0.0",
|
"devDependencies": {
|
||||||
"@types/mdx": "^2.0.13",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "24.1.0",
|
"@types/culori": "^4.0.0",
|
||||||
"@types/papaparse": "^5.3.16",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/node": "24.1.0",
|
||||||
"@types/react": "^19.1.9",
|
"@types/papaparse": "^5.3.16",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/qrcode": "^1.5.5",
|
||||||
"eslint": "^9.32.0",
|
"@types/react": "^19.1.9",
|
||||||
"eslint-config-next": "15.4.5",
|
"@types/react-dom": "^19.1.7",
|
||||||
"postcss": "^8.5.6",
|
"eslint": "^9.32.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"eslint-config-next": "15.4.5",
|
||||||
"tw-animate-css": "^1.3.6",
|
"postcss": "^8.5.6",
|
||||||
"typescript": "^5.9.2",
|
"tailwindcss": "^4.1.11",
|
||||||
"wrangler": "^4.27.0"
|
"tw-animate-css": "^1.3.6",
|
||||||
}
|
"typescript": "^5.9.2",
|
||||||
|
"wrangler": "^4.27.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
apps/web/src/app/(home)/new/_components/action-buttons.tsx
Normal file
62
apps/web/src/app/(home)/new/_components/action-buttons.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { RefreshCw, Settings, Shuffle, Star } from "lucide-react";
|
||||||
|
|
||||||
|
interface ActionButtonsProps {
|
||||||
|
onReset: () => void;
|
||||||
|
onRandom: () => void;
|
||||||
|
onSave: () => void;
|
||||||
|
onLoad: () => void;
|
||||||
|
hasSavedStack: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionButtons({
|
||||||
|
onReset,
|
||||||
|
onRandom,
|
||||||
|
onSave,
|
||||||
|
onLoad,
|
||||||
|
hasSavedStack,
|
||||||
|
}: ActionButtonsProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onReset}
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
title="Reset to defaults"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3 w-3" />
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRandom}
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
title="Generate a random stack"
|
||||||
|
>
|
||||||
|
<Shuffle className="h-3 w-3" />
|
||||||
|
Random
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSave}
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
title="Save current preferences"
|
||||||
|
>
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{hasSavedStack && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onLoad}
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
title="Load saved preferences"
|
||||||
|
>
|
||||||
|
<Settings className="h-3 w-3" />
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
apps/web/src/app/(home)/new/_components/preset-dropdown.tsx
Normal file
43
apps/web/src/app/(home)/new/_components/preset-dropdown.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronDown, Zap } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { PRESET_TEMPLATES } from "@/lib/constant";
|
||||||
|
|
||||||
|
interface PresetDropdownProps {
|
||||||
|
onApplyPreset: (presetId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PresetDropdown({ onApplyPreset }: PresetDropdownProps) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Zap className="h-3 w-3" />
|
||||||
|
Presets
|
||||||
|
<ChevronDown className="ml-auto h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-64 bg-fd-background">
|
||||||
|
{PRESET_TEMPLATES.map((preset) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={preset.id}
|
||||||
|
onClick={() => onApplyPreset(preset.id)}
|
||||||
|
className="flex flex-col items-start gap-1 p-3"
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm">{preset.name}</div>
|
||||||
|
<div className="text-xs">{preset.description}</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
apps/web/src/app/(home)/new/_components/share-button.tsx
Normal file
25
apps/web/src/app/(home)/new/_components/share-button.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Share2 } from "lucide-react";
|
||||||
|
import { ShareDialog } from "@/components/ui/share-dialog";
|
||||||
|
import type { StackState } from "@/lib/constant";
|
||||||
|
|
||||||
|
interface ShareButtonProps {
|
||||||
|
stackUrl: string;
|
||||||
|
stackState: StackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareButton({ stackUrl, stackState }: ShareButtonProps) {
|
||||||
|
return (
|
||||||
|
<ShareDialog stackUrl={stackUrl} stackState={stackState}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
|
title="Share your stack"
|
||||||
|
>
|
||||||
|
<Share2 className="h-3 w-3" />
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
</ShareDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,13 +5,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
RefreshCw,
|
|
||||||
Settings,
|
Settings,
|
||||||
Share2,
|
|
||||||
Shuffle,
|
|
||||||
Star,
|
|
||||||
Terminal,
|
Terminal,
|
||||||
Zap,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -27,11 +22,9 @@ import { toast } from "sonner";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ShareDialog } from "@/components/ui/share-dialog";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -51,7 +44,10 @@ import {
|
|||||||
generateStackSharingUrl,
|
generateStackSharingUrl,
|
||||||
} from "@/lib/stack-utils";
|
} from "@/lib/stack-utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ActionButtons } from "./action-buttons";
|
||||||
import { getBadgeColors } from "./get-badge-color";
|
import { getBadgeColors } from "./get-badge-color";
|
||||||
|
import { PresetDropdown } from "./preset-dropdown";
|
||||||
|
import { ShareButton } from "./share-button";
|
||||||
import { TechIcon } from "./tech-icon";
|
import { TechIcon } from "./tech-icon";
|
||||||
import {
|
import {
|
||||||
analyzeStackCompatibility,
|
analyzeStackCompatibility,
|
||||||
@@ -60,6 +56,7 @@ import {
|
|||||||
isOptionCompatible,
|
isOptionCompatible,
|
||||||
validateProjectName,
|
validateProjectName,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { YoloToggle } from "./yolo-toggle";
|
||||||
|
|
||||||
const StackBuilder = () => {
|
const StackBuilder = () => {
|
||||||
const [stack, setStack] = useStackState();
|
const [stack, setStack] = useStackState();
|
||||||
@@ -407,7 +404,12 @@ const StackBuilder = () => {
|
|||||||
|
|
||||||
const applyPreset = (presetId: string) => {
|
const applyPreset = (presetId: string) => {
|
||||||
const preset = PRESET_TEMPLATES.find(
|
const preset = PRESET_TEMPLATES.find(
|
||||||
(template) => template.id === presetId,
|
(template: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
stack: StackState;
|
||||||
|
}) => template.id === presetId,
|
||||||
);
|
);
|
||||||
if (preset) {
|
if (preset) {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
@@ -505,92 +507,41 @@ const StackBuilder = () => {
|
|||||||
|
|
||||||
<div className="mt-auto border-border border-t pt-4">
|
<div className="mt-auto border-border border-t pt-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<ActionButtons
|
||||||
<button
|
onReset={resetStack}
|
||||||
type="button"
|
onRandom={getRandomStack}
|
||||||
onClick={resetStack}
|
onSave={saveCurrentStack}
|
||||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
onLoad={loadSavedStack}
|
||||||
title="Reset to defaults"
|
hasSavedStack={!!lastSavedStack}
|
||||||
>
|
/>
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={getRandomStack}
|
|
||||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
|
||||||
title="Generate a random stack"
|
|
||||||
>
|
|
||||||
<Shuffle className="h-3.5 w-3.5" />
|
|
||||||
Random
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="flex gap-1">
|
||||||
<button
|
<ShareButton stackUrl={getStackUrl()} stackState={stack} />
|
||||||
type="button"
|
|
||||||
onClick={saveCurrentStack}
|
|
||||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
|
||||||
title="Save current preferences"
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5" />
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{lastSavedStack ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={loadSavedStack}
|
|
||||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
|
||||||
title="Load saved preferences"
|
|
||||||
>
|
|
||||||
<Settings className="h-3.5 w-3.5" />
|
|
||||||
Load
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="h-9" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShareDialog stackUrl={getStackUrl()} stackState={stack}>
|
<PresetDropdown onApplyPreset={applyPreset} />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
|
||||||
title="Share your stack"
|
|
||||||
>
|
|
||||||
<Share2 className="h-3.5 w-3.5" />
|
|
||||||
Share Stack
|
|
||||||
</button>
|
|
||||||
</ShareDialog>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||||
>
|
|
||||||
<Zap className="h-3.5 w-3.5" />
|
|
||||||
Quick Preset
|
|
||||||
<ChevronDown className="ml-auto h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
className="w-64 bg-fd-background"
|
|
||||||
>
|
|
||||||
{PRESET_TEMPLATES.map((preset) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={preset.id}
|
|
||||||
onClick={() => applyPreset(preset.id)}
|
|
||||||
className="flex flex-col items-start gap-1 p-3"
|
|
||||||
>
|
>
|
||||||
<div className="font-medium text-sm">
|
<Settings className="h-3 w-3" />
|
||||||
{preset.name}
|
Settings
|
||||||
</div>
|
<ChevronDown className="ml-auto h-3 w-3" />
|
||||||
<div className="text-xs">{preset.description}</div>
|
</button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
))}
|
<DropdownMenuContent
|
||||||
</DropdownMenuContent>
|
align="end"
|
||||||
</DropdownMenu>
|
className="w-64 bg-fd-background"
|
||||||
|
>
|
||||||
|
<YoloToggle
|
||||||
|
stack={stack}
|
||||||
|
onToggle={(yolo) => setStack({ yolo })}
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ interface CompatibilityResult {
|
|||||||
export const analyzeStackCompatibility = (
|
export const analyzeStackCompatibility = (
|
||||||
stack: StackState,
|
stack: StackState,
|
||||||
): CompatibilityResult => {
|
): CompatibilityResult => {
|
||||||
|
// Skip all validation if YOLO mode is enabled
|
||||||
|
if (stack.yolo === "true") {
|
||||||
|
return {
|
||||||
|
adjustedStack: null,
|
||||||
|
notes: {},
|
||||||
|
changes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const nextStack = { ...stack };
|
const nextStack = { ...stack };
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const notes: CompatibilityResult["notes"] = {};
|
const notes: CompatibilityResult["notes"] = {};
|
||||||
@@ -352,12 +361,12 @@ export const analyzeStackCompatibility = (
|
|||||||
"Database set to 'SQLite' (Turso hosting requires SQLite database)",
|
"Database set to 'SQLite' (Turso hosting requires SQLite database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.orm !== "drizzle") {
|
if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") {
|
||||||
notes.dbSetup.notes.push(
|
notes.dbSetup.notes.push(
|
||||||
"Turso requires Drizzle ORM. It will be selected.",
|
"Turso requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||||
);
|
);
|
||||||
notes.orm.notes.push(
|
notes.orm.notes.push(
|
||||||
"Turso DB setup requires Drizzle ORM. It will be selected.",
|
"Turso DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||||
);
|
);
|
||||||
notes.dbSetup.hasIssue = true;
|
notes.dbSetup.hasIssue = true;
|
||||||
notes.orm.hasIssue = true;
|
notes.orm.hasIssue = true;
|
||||||
@@ -366,7 +375,7 @@ export const analyzeStackCompatibility = (
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message:
|
message:
|
||||||
"ORM set to 'Drizzle' (Turso hosting requires Drizzle ORM)",
|
"ORM set to 'Drizzle' (Turso hosting requires Drizzle or Prisma ORM)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.dbSetup === "prisma-postgres") {
|
} else if (nextStack.dbSetup === "prisma-postgres") {
|
||||||
@@ -454,6 +463,27 @@ export const analyzeStackCompatibility = (
|
|||||||
"Database set to 'PostgreSQL' (Supabase hosting requires PostgreSQL database)",
|
"Database set to 'PostgreSQL' (Supabase hosting requires PostgreSQL database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (nextStack.dbSetup === "planetscale") {
|
||||||
|
if (
|
||||||
|
nextStack.database !== "postgres" &&
|
||||||
|
nextStack.database !== "mysql"
|
||||||
|
) {
|
||||||
|
notes.dbSetup.notes.push(
|
||||||
|
"PlanetScale requires PostgreSQL or MySQL. PostgreSQL will be selected.",
|
||||||
|
);
|
||||||
|
notes.database.notes.push(
|
||||||
|
"PlanetScale DB setup requires PostgreSQL or MySQL. PostgreSQL will be selected.",
|
||||||
|
);
|
||||||
|
notes.dbSetup.hasIssue = true;
|
||||||
|
notes.database.hasIssue = true;
|
||||||
|
nextStack.database = "postgres";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "dbSetup",
|
||||||
|
message:
|
||||||
|
"Database set to 'PostgreSQL' (PlanetScale supports PostgreSQL and MySQL)",
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (nextStack.dbSetup === "d1") {
|
} else if (nextStack.dbSetup === "d1") {
|
||||||
if (nextStack.database !== "sqlite") {
|
if (nextStack.database !== "sqlite") {
|
||||||
notes.dbSetup.notes.push(
|
notes.dbSetup.notes.push(
|
||||||
@@ -471,6 +501,23 @@ export const analyzeStackCompatibility = (
|
|||||||
message: "Database set to 'SQLite' (required by Cloudflare D1)",
|
message: "Database set to 'SQLite' (required by Cloudflare D1)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") {
|
||||||
|
notes.dbSetup.notes.push(
|
||||||
|
"Cloudflare D1 requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||||
|
);
|
||||||
|
notes.orm.notes.push(
|
||||||
|
"Cloudflare D1 DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||||
|
);
|
||||||
|
notes.dbSetup.hasIssue = true;
|
||||||
|
notes.orm.hasIssue = true;
|
||||||
|
nextStack.orm = "drizzle";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "dbSetup",
|
||||||
|
message:
|
||||||
|
"ORM set to 'Drizzle' (Cloudflare D1 requires Drizzle or Prisma ORM)",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (nextStack.runtime !== "workers") {
|
if (nextStack.runtime !== "workers") {
|
||||||
notes.dbSetup.notes.push(
|
notes.dbSetup.notes.push(
|
||||||
"Cloudflare D1 requires Cloudflare Workers runtime. It will be selected.",
|
"Cloudflare D1 requires Cloudflare Workers runtime. It will be selected.",
|
||||||
@@ -487,22 +534,6 @@ export const analyzeStackCompatibility = (
|
|||||||
message: "Runtime set to 'Cloudflare Workers' (required by D1)",
|
message: "Runtime set to 'Cloudflare Workers' (required by D1)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.orm !== "drizzle") {
|
|
||||||
notes.dbSetup.notes.push(
|
|
||||||
"Cloudflare D1 requires Drizzle ORM. It will be selected.",
|
|
||||||
);
|
|
||||||
notes.orm.notes.push(
|
|
||||||
"Cloudflare D1 DB setup requires Drizzle ORM. It will be selected.",
|
|
||||||
);
|
|
||||||
notes.dbSetup.hasIssue = true;
|
|
||||||
notes.orm.hasIssue = true;
|
|
||||||
nextStack.orm = "drizzle";
|
|
||||||
changed = true;
|
|
||||||
changes.push({
|
|
||||||
category: "dbSetup",
|
|
||||||
message: "ORM set to 'Drizzle' (required by Cloudflare D1)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (nextStack.backend !== "hono") {
|
if (nextStack.backend !== "hono") {
|
||||||
notes.dbSetup.notes.push(
|
notes.dbSetup.notes.push(
|
||||||
"Cloudflare D1 requires Hono backend. It will be selected.",
|
"Cloudflare D1 requires Hono backend. It will be selected.",
|
||||||
@@ -581,6 +612,9 @@ export const analyzeStackCompatibility = (
|
|||||||
} else if (nextStack.dbSetup === "mongodb-atlas") {
|
} else if (nextStack.dbSetup === "mongodb-atlas") {
|
||||||
selectedDatabase = "mongodb";
|
selectedDatabase = "mongodb";
|
||||||
databaseName = "MongoDB";
|
databaseName = "MongoDB";
|
||||||
|
} else if (nextStack.dbSetup === "planetscale") {
|
||||||
|
selectedDatabase = "postgres";
|
||||||
|
databaseName = "PostgreSQL";
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.dbSetup.notes.push(
|
notes.dbSetup.notes.push(
|
||||||
@@ -618,24 +652,6 @@ export const analyzeStackCompatibility = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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' (Cloudflare Workers runtime only supports Drizzle or no ORM)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStack.database === "mongodb") {
|
if (nextStack.database === "mongodb") {
|
||||||
notes.runtime.notes.push(
|
notes.runtime.notes.push(
|
||||||
"Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.",
|
"Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.",
|
||||||
@@ -1050,49 +1066,6 @@ export const analyzeStackCompatibility = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
|
||||||
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
|
|
||||||
|
|
||||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
|
||||||
const incompatibleFrontends = nextStack.webFrontend.filter(
|
|
||||||
(f) => f === "next",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (incompatibleFrontends.length > 0) {
|
|
||||||
const deployType =
|
|
||||||
isAlchemyWebDeploy && isAlchemyServerDeploy
|
|
||||||
? "web and server deployment"
|
|
||||||
: isAlchemyWebDeploy
|
|
||||||
? "web deployment"
|
|
||||||
: "server deployment";
|
|
||||||
|
|
||||||
notes.webFrontend.notes.push(
|
|
||||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}. These frontends will be removed.`,
|
|
||||||
);
|
|
||||||
notes.webDeploy.notes.push(
|
|
||||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
|
|
||||||
);
|
|
||||||
notes.serverDeploy.notes.push(
|
|
||||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
|
|
||||||
);
|
|
||||||
notes.webFrontend.hasIssue = true;
|
|
||||||
notes.webDeploy.hasIssue = true;
|
|
||||||
notes.serverDeploy.hasIssue = true;
|
|
||||||
|
|
||||||
nextStack.webFrontend = nextStack.webFrontend.filter((f) => f !== "next");
|
|
||||||
|
|
||||||
if (nextStack.webFrontend.length === 0) {
|
|
||||||
nextStack.webFrontend = ["tanstack-router"];
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = true;
|
|
||||||
changes.push({
|
|
||||||
category: "alchemy",
|
|
||||||
message: `Removed ${incompatibleFrontends.join(" and ")} frontend (temporarily not compatible with Alchemy ${deployType} - support coming soon)`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nextStack.serverDeploy === "alchemy" &&
|
nextStack.serverDeploy === "alchemy" &&
|
||||||
(nextStack.runtime !== "workers" || nextStack.backend !== "hono")
|
(nextStack.runtime !== "workers" || nextStack.backend !== "hono")
|
||||||
@@ -1223,15 +1196,6 @@ export const getDisabledReason = (
|
|||||||
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
||||||
const finalStack = adjustedStack ?? simulatedStack;
|
const finalStack = adjustedStack ?? simulatedStack;
|
||||||
|
|
||||||
if (category === "webFrontend" && optionId === "next") {
|
|
||||||
const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy";
|
|
||||||
const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy";
|
|
||||||
|
|
||||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
|
||||||
return "Next.js is temporarily not compatible with Alchemy deployment. Support coming soon!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category === "webFrontend" && optionId === "solid") {
|
if (category === "webFrontend" && optionId === "solid") {
|
||||||
if (finalStack.backend === "convex") {
|
if (finalStack.backend === "convex") {
|
||||||
return "Solid is not compatible with Convex backend. Try TanStack Router, React Router, or Next.js instead.";
|
return "Solid is not compatible with Convex backend. Try TanStack Router, React Router, or Next.js instead.";
|
||||||
@@ -1383,11 +1347,17 @@ export const getDisabledReason = (
|
|||||||
if (finalStack.database === "none") {
|
if (finalStack.database === "none") {
|
||||||
return "Prisma ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB).";
|
return "Prisma ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB).";
|
||||||
}
|
}
|
||||||
|
if (finalStack.dbSetup === "turso" && finalStack.database !== "sqlite") {
|
||||||
|
return "Turso setup requires SQLite database. Select SQLite first.";
|
||||||
|
}
|
||||||
|
if (finalStack.dbSetup === "d1" && finalStack.database !== "sqlite") {
|
||||||
|
return "Cloudflare D1 setup requires SQLite database. Select SQLite first.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category === "dbSetup" && optionId === "turso") {
|
if (category === "dbSetup" && optionId === "turso") {
|
||||||
if (finalStack.orm !== "drizzle") {
|
if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") {
|
||||||
return "Turso requires Drizzle ORM. Select Drizzle first.";
|
return "Turso requires Drizzle or Prisma ORM. Select Drizzle or Prisma first.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,8 +1368,8 @@ export const getDisabledReason = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (category === "dbSetup" && optionId === "d1") {
|
if (category === "dbSetup" && optionId === "d1") {
|
||||||
if (finalStack.orm !== "drizzle") {
|
if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") {
|
||||||
return "Cloudflare D1 requires Drizzle ORM. Select Drizzle first.";
|
return "Cloudflare D1 requires Drizzle or Prisma ORM. Select Drizzle or Prisma first.";
|
||||||
}
|
}
|
||||||
if (finalStack.runtime !== "workers") {
|
if (finalStack.runtime !== "workers") {
|
||||||
return "Cloudflare D1 requires Cloudflare Workers runtime. Select Workers runtime first.";
|
return "Cloudflare D1 requires Cloudflare Workers runtime. Select Workers runtime first.";
|
||||||
@@ -1461,15 +1431,20 @@ export const getDisabledReason = (
|
|||||||
finalStack.dbSetup !== "docker" &&
|
finalStack.dbSetup !== "docker" &&
|
||||||
finalStack.dbSetup !== "prisma-postgres" &&
|
finalStack.dbSetup !== "prisma-postgres" &&
|
||||||
finalStack.dbSetup !== "neon" &&
|
finalStack.dbSetup !== "neon" &&
|
||||||
finalStack.dbSetup !== "supabase"
|
finalStack.dbSetup !== "supabase" &&
|
||||||
|
finalStack.dbSetup !== "planetscale"
|
||||||
) {
|
) {
|
||||||
return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, or Basic Setup. Select one of these options or change database.";
|
return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, PlanetScale, or Basic Setup. Select one of these options or change database.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category === "database" && optionId === "mysql") {
|
if (category === "database" && optionId === "mysql") {
|
||||||
if (finalStack.dbSetup !== "none" && finalStack.dbSetup !== "docker") {
|
if (
|
||||||
return "MySQL database only works with Docker or Basic Setup. Select one of these options or change database.";
|
finalStack.dbSetup !== "none" &&
|
||||||
|
finalStack.dbSetup !== "docker" &&
|
||||||
|
finalStack.dbSetup !== "planetscale"
|
||||||
|
) {
|
||||||
|
return "MySQL database only works with Docker, PlanetScale, or Basic Setup. Select one of these options or change database.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,12 +1559,27 @@ export const getDisabledReason = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (category === "dbSetup" && optionId === "planetscale") {
|
||||||
|
if (finalStack.database !== "postgres" && finalStack.database !== "mysql") {
|
||||||
|
return "PlanetScale requires PostgreSQL or MySQL database. Select PostgreSQL or MySQL first.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (category === "dbSetup" && optionId === "supabase") {
|
if (category === "dbSetup" && optionId === "supabase") {
|
||||||
if ((finalStack.database as string) !== "postgres") {
|
if ((finalStack.database as string) !== "postgres") {
|
||||||
return "Supabase requires PostgreSQL database. Select PostgreSQL first.";
|
return "Supabase requires PostgreSQL database. Select PostgreSQL first.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
category === "database" &&
|
||||||
|
(finalStack.dbSetup as string) === "planetscale"
|
||||||
|
) {
|
||||||
|
if (optionId !== "postgres" && optionId !== "mysql") {
|
||||||
|
return "Selected DB Setup 'PlanetScale' requires PostgreSQL or MySQL. Select PostgreSQL or MySQL, or change DB Setup.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
category === "database" &&
|
category === "database" &&
|
||||||
(finalStack.dbSetup as string) === "supabase"
|
(finalStack.dbSetup as string) === "supabase"
|
||||||
@@ -1607,5 +1597,8 @@ export const isOptionCompatible = (
|
|||||||
category: keyof typeof TECH_OPTIONS,
|
category: keyof typeof TECH_OPTIONS,
|
||||||
optionId: string,
|
optionId: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
if (currentStack.yolo === "true") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return getDisabledReason(currentStack, category, optionId) === null;
|
return getDisabledReason(currentStack, category, optionId) === null;
|
||||||
};
|
};
|
||||||
|
|||||||
49
apps/web/src/app/(home)/new/_components/yolo-toggle.tsx
Normal file
49
apps/web/src/app/(home)/new/_components/yolo-toggle.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import type { StackState } from "@/lib/constant";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface YoloToggleProps {
|
||||||
|
stack: StackState;
|
||||||
|
onToggle: (yolo: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function YoloToggle({ stack, onToggle }: YoloToggleProps) {
|
||||||
|
const isYoloEnabled = stack.yolo === "true";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip delayDuration={100}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex w-full items-center gap-3 p-3">
|
||||||
|
<AlertTriangle className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<div className="flex flex-1 flex-col items-start">
|
||||||
|
<div className="font-medium text-sm">YOLO Mode</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{isYoloEnabled ? "Enabled" : "Disabled"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isYoloEnabled}
|
||||||
|
onCheckedChange={(checked) => onToggle(checked ? "true" : "false")}
|
||||||
|
className={cn(
|
||||||
|
isYoloEnabled && "data-[state=checked]:bg-destructive",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" align="start" className="max-w-xs">
|
||||||
|
<p className="text-xs">
|
||||||
|
Disables all validation and adds --yolo flag to the command. Use at
|
||||||
|
your own risk!
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -114,7 +114,6 @@ export function ShareDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate QR code using local qrcode library
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generateQRCode = async () => {
|
const generateQRCode = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -264,25 +263,6 @@ export function ShareDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded border border-border">
|
|
||||||
<div className="border-border border-b px-3 py-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-primary text-xs">▶</span>
|
|
||||||
<span className="font-mono font-semibold text-foreground text-xs">
|
|
||||||
OUTPUT.URL
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span className="text-primary">$</span>
|
|
||||||
<code className="flex-1 truncate text-muted-foreground">
|
|
||||||
{stackUrl}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Switch as SwitchPrimitive } from "radix-ui";
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|||||||
47
apps/web/src/components/ui/toggle.tsx
Normal file
47
apps/web/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 min-w-9 px-2",
|
||||||
|
sm: "h-8 min-w-8 px-1.5",
|
||||||
|
lg: "h-10 min-w-10 px-2.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants };
|
||||||
@@ -328,6 +328,13 @@ export const TECH_OPTIONS: Record<
|
|||||||
icon: `${ICON_BASE_URL}/supabase.svg`,
|
icon: `${ICON_BASE_URL}/supabase.svg`,
|
||||||
color: "from-emerald-400 to-emerald-600",
|
color: "from-emerald-400 to-emerald-600",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "planetscale",
|
||||||
|
name: "PlanetScale",
|
||||||
|
description: "Serverless MySQL platform with branching",
|
||||||
|
icon: `${ICON_BASE_URL}/planetscale.svg`,
|
||||||
|
color: "from-orange-400 to-orange-600",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "docker",
|
id: "docker",
|
||||||
name: "Docker",
|
name: "Docker",
|
||||||
@@ -604,6 +611,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
api: "trpc",
|
api: "trpc",
|
||||||
webDeploy: "none",
|
webDeploy: "none",
|
||||||
serverDeploy: "none",
|
serverDeploy: "none",
|
||||||
|
yolo: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -628,6 +636,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
api: "none",
|
api: "none",
|
||||||
webDeploy: "none",
|
webDeploy: "none",
|
||||||
serverDeploy: "none",
|
serverDeploy: "none",
|
||||||
|
yolo: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -652,6 +661,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
api: "trpc",
|
api: "trpc",
|
||||||
webDeploy: "none",
|
webDeploy: "none",
|
||||||
serverDeploy: "none",
|
serverDeploy: "none",
|
||||||
|
yolo: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -676,6 +686,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
api: "trpc",
|
api: "trpc",
|
||||||
webDeploy: "none",
|
webDeploy: "none",
|
||||||
serverDeploy: "none",
|
serverDeploy: "none",
|
||||||
|
yolo: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -700,6 +711,7 @@ export const PRESET_TEMPLATES = [
|
|||||||
api: "trpc",
|
api: "trpc",
|
||||||
webDeploy: "alchemy",
|
webDeploy: "alchemy",
|
||||||
serverDeploy: "alchemy",
|
serverDeploy: "alchemy",
|
||||||
|
yolo: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -722,6 +734,7 @@ export type StackState = {
|
|||||||
api: string;
|
api: string;
|
||||||
webDeploy: string;
|
webDeploy: string;
|
||||||
serverDeploy: string;
|
serverDeploy: string;
|
||||||
|
yolo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_STACK: StackState = {
|
export const DEFAULT_STACK: StackState = {
|
||||||
@@ -742,6 +755,7 @@ export const DEFAULT_STACK: StackState = {
|
|||||||
api: "trpc",
|
api: "trpc",
|
||||||
webDeploy: "none",
|
webDeploy: "none",
|
||||||
serverDeploy: "none",
|
serverDeploy: "none",
|
||||||
|
yolo: "false",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isStackDefault = <K extends keyof StackState>(
|
export const isStackDefault = <K extends keyof StackState>(
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ export const stackUrlKeys: UrlKeys<Record<keyof StackState, unknown>> = {
|
|||||||
install: "i",
|
install: "i",
|
||||||
webDeploy: "wd",
|
webDeploy: "wd",
|
||||||
serverDeploy: "sd",
|
serverDeploy: "sd",
|
||||||
|
yolo: "yolo",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export const stackParsers = {
|
|||||||
serverDeploy: parseAsStringEnum<StackState["serverDeploy"]>(
|
serverDeploy: parseAsStringEnum<StackState["serverDeploy"]>(
|
||||||
getValidIds("serverDeploy"),
|
getValidIds("serverDeploy"),
|
||||||
).withDefault(DEFAULT_STACK.serverDeploy),
|
).withDefault(DEFAULT_STACK.serverDeploy),
|
||||||
|
yolo: parseAsStringEnum<StackState["yolo"]>(["true", "false"]).withDefault(
|
||||||
|
DEFAULT_STACK.yolo,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stackQueryStatesOptions = {
|
export const stackQueryStatesOptions = {
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ const serverStackParsers = {
|
|||||||
serverDeploy: parseAsStringEnumServer<StackState["serverDeploy"]>(
|
serverDeploy: parseAsStringEnumServer<StackState["serverDeploy"]>(
|
||||||
getValidIds("serverDeploy"),
|
getValidIds("serverDeploy"),
|
||||||
).withDefault(DEFAULT_STACK.serverDeploy),
|
).withDefault(DEFAULT_STACK.serverDeploy),
|
||||||
|
yolo: parseAsStringEnumServer<StackState["yolo"]>([
|
||||||
|
"true",
|
||||||
|
"false",
|
||||||
|
]).withDefault(DEFAULT_STACK.yolo),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadStackParams = createLoader(serverStackParsers, {
|
export const loadStackParams = createLoader(serverStackParsers, {
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ export function generateStackCommand(stack: StackState): string {
|
|||||||
`--examples ${stack.examples.join(" ") || "none"}`,
|
`--examples ${stack.examples.join(" ") || "none"}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (stack.yolo === "true") {
|
||||||
|
flags.push("--yolo");
|
||||||
|
}
|
||||||
|
|
||||||
return `${base} ${projectName} ${flags.join(" ")}`;
|
return `${base} ${projectName} ${flags.join(" ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"!**/out",
|
"!**/out",
|
||||||
"!**/templates",
|
"!**/templates",
|
||||||
"!**/.turbo",
|
"!**/.turbo",
|
||||||
"!**/package.json",
|
|
||||||
"!**/analytics-minimal.json",
|
"!**/analytics-minimal.json",
|
||||||
"!**/schema.json",
|
"!**/schema.json",
|
||||||
"!**/_generated",
|
"!**/_generated",
|
||||||
|
|||||||
62
bun.lock
62
bun.lock
@@ -20,8 +20,6 @@
|
|||||||
"create-better-t-stack": "dist/cli.js",
|
"create-better-t-stack": "dist/cli.js",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@biomejs/js-api": "^3.0.0",
|
|
||||||
"@biomejs/wasm-nodejs": "^2.2.0",
|
|
||||||
"@clack/prompts": "^1.0.0-alpha.4",
|
"@clack/prompts": "^1.0.0-alpha.4",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
@@ -30,16 +28,16 @@
|
|||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"tinyglobby": "^0.2.14",
|
"tinyglobby": "^0.2.15",
|
||||||
"trpc-cli": "^0.10.2",
|
"trpc-cli": "^0.10.2",
|
||||||
"ts-morph": "^26.0.0",
|
"ts-morph": "^27.0.0",
|
||||||
"zod": "^4.0.17",
|
"zod": "^4.1.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.1",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"tsdown": "^0.14.1",
|
"tsdown": "^0.14.2",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
},
|
},
|
||||||
@@ -55,6 +53,8 @@
|
|||||||
"@orama/orama": "^3.1.11",
|
"@orama/orama": "^3.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -271,10 +271,6 @@
|
|||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
|
||||||
|
|
||||||
"@biomejs/js-api": ["@biomejs/js-api@3.0.0", "", { "peerDependencies": { "@biomejs/wasm-bundler": "^2.2.0", "@biomejs/wasm-nodejs": "^2.2.0", "@biomejs/wasm-web": "^2.2.0" }, "optionalPeers": ["@biomejs/wasm-bundler", "@biomejs/wasm-nodejs", "@biomejs/wasm-web"] }, "sha512-5QcGJFj9IO+yXl76ICjvkdE38uxRcTDsBzcCZHEZ+ma+Te/nbvJg4A3KtAds9HCrEF0JKLWiyjMhAbqazuJvYA=="],
|
|
||||||
|
|
||||||
"@biomejs/wasm-nodejs": ["@biomejs/wasm-nodejs@2.2.2", "", {}, "sha512-GxI0ejyXaCjIq6SxBr9e4jyf3zmI5Eyyq5fF0dlb4nECVXwAgUAnT6hQ5502ZACvBxoWmreXw08im/l3APpvIw=="],
|
|
||||||
|
|
||||||
"@clack/core": ["@clack/core@1.0.0-alpha.4", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-VCtU+vjyKPMSakVrB9q1bOnXN7QW/w4+YQDQCOF59GrzydW+169i0fVx/qzRRXJgt8KGj/pZZ/JxXroFZIDByg=="],
|
"@clack/core": ["@clack/core@1.0.0-alpha.4", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-VCtU+vjyKPMSakVrB9q1bOnXN7QW/w4+YQDQCOF59GrzydW+169i0fVx/qzRRXJgt8KGj/pZZ/JxXroFZIDByg=="],
|
||||||
|
|
||||||
"@clack/prompts": ["@clack/prompts@1.0.0-alpha.4", "", { "dependencies": { "@clack/core": "1.0.0-alpha.4", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-KnmtDF2xQGoI5AlBme9akHtvCRV0RKAARUXHBQO2tMwnY8B08/4zPWigT7uLK25UPrMCEqnyQPkKRjNdhPbf8g=="],
|
"@clack/prompts": ["@clack/prompts@1.0.0-alpha.4", "", { "dependencies": { "@clack/core": "1.0.0-alpha.4", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-KnmtDF2xQGoI5AlBme9akHtvCRV0RKAARUXHBQO2tMwnY8B08/4zPWigT7uLK25UPrMCEqnyQPkKRjNdhPbf8g=="],
|
||||||
@@ -939,7 +935,7 @@
|
|||||||
|
|
||||||
"@trpc/server": ["@trpc/server@11.5.0", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-0IBtkmUCeO2ycn4K45/cqsujnlCQrSvkCo7lFDpg3kGMIPiLyLRciID5IiS7prEjRjeITa+od2aaHTIwONApVw=="],
|
"@trpc/server": ["@trpc/server@11.5.0", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-0IBtkmUCeO2ycn4K45/cqsujnlCQrSvkCo7lFDpg3kGMIPiLyLRciID5IiS7prEjRjeITa+od2aaHTIwONApVw=="],
|
||||||
|
|
||||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
"@ts-morph/common": ["@ts-morph/common@0.28.0", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-4w6X/oFmvXcwux6y6ExfM/xSqMHw20cYwFJH+BlYrtGa6nwY9qGq8GXnUs1sVYeF2o/KT3S8hAH6sKBI3VOkBg=="],
|
||||||
|
|
||||||
"@tsconfig/node18": ["@tsconfig/node18@1.0.3", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="],
|
"@tsconfig/node18": ["@tsconfig/node18@1.0.3", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="],
|
||||||
|
|
||||||
@@ -995,7 +991,7 @@
|
|||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
|
||||||
|
|
||||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||||
|
|
||||||
@@ -1545,7 +1541,7 @@
|
|||||||
|
|
||||||
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
|
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
@@ -2481,7 +2477,7 @@
|
|||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
||||||
|
|
||||||
@@ -2513,7 +2509,7 @@
|
|||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||||
|
|
||||||
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
|
"ts-morph": ["ts-morph@27.0.0", "", { "dependencies": { "@ts-morph/common": "~0.28.0", "code-block-writer": "^13.0.3" } }, "sha512-xcqelpTR5PCuZMs54qp9DE3t7tPgA2v/P1/qdW4ke5b3Y5liTGTYj6a/twT35EQW/H5okRqp1UOqwNlgg0K0eQ=="],
|
||||||
|
|
||||||
"ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="],
|
"ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="],
|
||||||
|
|
||||||
@@ -3213,8 +3209,6 @@
|
|||||||
|
|
||||||
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
|
||||||
|
|
||||||
"@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
"@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||||
|
|
||||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||||
@@ -3301,22 +3295,40 @@
|
|||||||
|
|
||||||
"@ts-morph/common/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
"@ts-morph/common/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||||
|
|
||||||
|
"@types/fs-extra/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
|
"@types/jsonfile/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
|
"@types/node-fetch/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
|
"@types/papaparse/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
|
"@types/qrcode/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
|
"@vitest/ui/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
"body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||||
|
|
||||||
"changelogen/c12": ["c12@1.11.2", "", { "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^1.21.6", "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.4" }, "optionalPeers": ["magicast"] }, "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew=="],
|
"changelogen/c12": ["c12@1.11.2", "", { "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^1.21.6", "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.4" }, "optionalPeers": ["magicast"] }, "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew=="],
|
||||||
|
|
||||||
"changelogen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"changelogen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
|
|
||||||
"changelogen/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"changelogen/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
|
"changelogithub/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
"cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"cloudflare/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="],
|
"cloudflare/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="],
|
||||||
@@ -3325,6 +3337,8 @@
|
|||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
@@ -3349,6 +3363,8 @@
|
|||||||
|
|
||||||
"fumadocs-mdx/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
"fumadocs-mdx/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||||
|
|
||||||
|
"fumadocs-mdx/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
"glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||||
|
|
||||||
"htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
"htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
@@ -3413,10 +3429,16 @@
|
|||||||
|
|
||||||
"trpc-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"trpc-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"tsdown/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"vite/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
"vite/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||||
|
|
||||||
|
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
|
|
||||||
|
"vitest/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"web/@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
|
"web/@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
@@ -3843,8 +3865,6 @@
|
|||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
||||||
|
|
||||||
"@node-minify/core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
"@node-minify/core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
||||||
|
|
||||||
"@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
"@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||||
@@ -3875,6 +3895,8 @@
|
|||||||
|
|
||||||
"@smithy/util-endpoints/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.5", "", { "dependencies": { "@smithy/types": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ=="],
|
"@smithy/util-endpoints/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.5", "", { "dependencies": { "@smithy/types": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
"changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|||||||
90
package.json
90
package.json
@@ -1,47 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "better-t-stack",
|
"name": "better-t-stack",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
"dev": "turbo dev",
|
"dev": "turbo dev",
|
||||||
"dev:cli": "turbo run dev --filter=create-better-t-stack",
|
"dev:cli": "turbo run dev --filter=create-better-t-stack",
|
||||||
"cli": "cd apps/cli && node dist/cli.js",
|
"cli": "cd apps/cli && node dist/cli.js",
|
||||||
"dev:web": "turbo run dev --filter=web",
|
"dev:web": "turbo run dev --filter=web",
|
||||||
"build:web": "turbo run build --filter=web",
|
"build:web": "turbo run build --filter=web",
|
||||||
"build:cli": "turbo run build --filter=create-better-t-stack",
|
"build:cli": "turbo run build --filter=create-better-t-stack",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"check": "turbo check",
|
"check": "turbo check",
|
||||||
"format": "biome check --write .",
|
"format": "biome check --write .",
|
||||||
"release": "bun run scripts/release.ts",
|
"release": "bun run scripts/release.ts",
|
||||||
"bump": "bun run scripts/bump-version.ts",
|
"bump": "bun run scripts/bump-version.ts",
|
||||||
"canary": "bun run scripts/canary-release.ts",
|
"canary": "bun run scripts/canary-release.ts",
|
||||||
"deploy:convex": "turbo run --filter=@better-t-stack/backend deploy",
|
"deploy:convex": "turbo run --filter=@better-t-stack/backend deploy",
|
||||||
"deploy:web": "bun run --filter=web deploy",
|
"deploy:web": "bun run --filter=web deploy",
|
||||||
"generate": "bun run --filter=web generate-analytics && bun run --filter=web generate-schema",
|
"generate": "bun run --filter=web generate-analytics && bun run --filter=web generate-schema",
|
||||||
"deploy": "bun run deploy:web"
|
"deploy": "bun run deploy:web"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.0",
|
"@biomejs/biome": "2.2.0",
|
||||||
"changelogithub": "^13.16.0",
|
"changelogithub": "^13.16.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"turbo": "^2.5.6",
|
"turbo": "^2.5.6",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": [
|
"*": [
|
||||||
"bun biome check --write ."
|
"bun biome check --write ."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.20",
|
"packageManager": "bun@1.2.20",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@better-t-stack/backend",
|
"name": "@better-t-stack/backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "convex dev",
|
"dev": "convex dev",
|
||||||
"dev:setup": "convex dev --configure --until-success",
|
"dev:setup": "convex dev --configure --until-success",
|
||||||
"deploy": "convex deploy"
|
"deploy": "convex deploy"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@erquhart/convex-oss-stats": "^0.8.1",
|
"@erquhart/convex-oss-stats": "^0.8.1",
|
||||||
"convex": "^1.25.4",
|
"convex": "^1.25.4",
|
||||||
"convex-helpers": "^0.1.104"
|
"convex-helpers": "^0.1.104"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user