mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add mysql database
This commit is contained in:
5
.changeset/clever-dryers-search.md
Normal file
5
.changeset/clever-dryers-search.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add mysql database
|
||||||
@@ -33,6 +33,8 @@ export const dependencyVersionMap = {
|
|||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.14.0",
|
||||||
postgres: "^3.4.5",
|
postgres: "^3.4.5",
|
||||||
|
|
||||||
|
mysql2: "^3.14.0",
|
||||||
|
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
prisma: "^6.5.0",
|
prisma: "^6.5.0",
|
||||||
|
|
||||||
|
|||||||
@@ -30,58 +30,44 @@ export async function setupDatabase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (databaseType === "sqlite") {
|
if (orm === "prisma") {
|
||||||
if (orm === "drizzle") {
|
addPackageDependency({
|
||||||
|
dependencies: ["@prisma/client"],
|
||||||
|
devDependencies: ["prisma"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
} else if (orm === "drizzle") {
|
||||||
|
if (databaseType === "sqlite") {
|
||||||
addPackageDependency({
|
addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "@libsql/client"],
|
dependencies: ["drizzle-orm", "@libsql/client"],
|
||||||
devDependencies: ["drizzle-kit"],
|
devDependencies: ["drizzle-kit"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
} else if (orm === "prisma") {
|
} else if (databaseType === "postgres") {
|
||||||
addPackageDependency({
|
|
||||||
dependencies: ["@prisma/client"],
|
|
||||||
devDependencies: ["prisma"],
|
|
||||||
projectDir: serverDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setupTursoDb) {
|
|
||||||
await setupTurso(projectDir, true);
|
|
||||||
}
|
|
||||||
} else if (databaseType === "postgres") {
|
|
||||||
if (orm === "drizzle") {
|
|
||||||
addPackageDependency({
|
addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "postgres"],
|
dependencies: ["drizzle-orm", "postgres"],
|
||||||
devDependencies: ["drizzle-kit"],
|
devDependencies: ["drizzle-kit"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
} else if (orm === "prisma") {
|
} else if (databaseType === "mysql") {
|
||||||
addPackageDependency({
|
addPackageDependency({
|
||||||
dependencies: ["@prisma/client"],
|
dependencies: ["drizzle-orm", "mysql2"],
|
||||||
devDependencies: ["prisma"],
|
devDependencies: ["drizzle-kit"],
|
||||||
projectDir: serverDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
databaseType === "postgres" &&
|
|
||||||
orm === "prisma" &&
|
|
||||||
setupPrismaPostgresDb
|
|
||||||
) {
|
|
||||||
await setupPrismaPostgres(projectDir, packageManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (databaseType === "mongodb") {
|
|
||||||
if (orm === "prisma") {
|
|
||||||
addPackageDependency({
|
|
||||||
dependencies: ["@prisma/client"],
|
|
||||||
devDependencies: ["prisma"],
|
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (setupMongoDBAtlasDb) {
|
if (databaseType === "sqlite" && setupTursoDb) {
|
||||||
await setupMongoDBAtlas(projectDir);
|
await setupTurso(projectDir, orm === "drizzle");
|
||||||
}
|
} else if (
|
||||||
|
databaseType === "postgres" &&
|
||||||
|
orm === "prisma" &&
|
||||||
|
setupPrismaPostgresDb
|
||||||
|
) {
|
||||||
|
await setupPrismaPostgres(projectDir, packageManager);
|
||||||
|
} else if (databaseType === "mongodb" && setupMongoDBAtlasDb) {
|
||||||
|
await setupMongoDBAtlas(projectDir);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
s.stop(pc.red("Failed to set up database"));
|
s.stop(pc.red("Failed to set up database"));
|
||||||
|
|||||||
@@ -3,73 +3,50 @@ import fs from "fs-extra";
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
import { generateAuthSecret } from "./auth-setup";
|
import { generateAuthSecret } from "./auth-setup";
|
||||||
|
|
||||||
|
interface EnvVariable {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
condition: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addEnvVariablesToFile(
|
||||||
|
filePath: string,
|
||||||
|
variables: EnvVariable[],
|
||||||
|
): Promise<void> {
|
||||||
|
await fs.ensureDir(path.dirname(filePath));
|
||||||
|
|
||||||
|
let envContent = "";
|
||||||
|
if (await fs.pathExists(filePath)) {
|
||||||
|
envContent = await fs.readFile(filePath, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
for (const { key, value, condition } of variables) {
|
||||||
|
if (condition) {
|
||||||
|
const regex = new RegExp(`^${key}=.*$`, "m");
|
||||||
|
if (regex.test(envContent)) {
|
||||||
|
if (value) {
|
||||||
|
envContent = envContent.replace(regex, `${key}=${value}`);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
envContent += `\n${key}=${value}`;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
await fs.writeFile(filePath, envContent.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupEnvironmentVariables(
|
export async function setupEnvironmentVariables(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
options: ProjectConfig,
|
options: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
const envPath = path.join(serverDir, ".env");
|
const envPath = path.join(serverDir, ".env");
|
||||||
let envContent = "";
|
|
||||||
|
|
||||||
if (await fs.pathExists(envPath)) {
|
|
||||||
envContent = await fs.readFile(envPath, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!envContent.includes("CORS_ORIGIN")) {
|
|
||||||
const hasReactRouter = options.frontend.includes("react-router");
|
|
||||||
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
|
||||||
const hasTanStackStart = options.frontend.includes("tanstack-start");
|
|
||||||
|
|
||||||
let corsOrigin = "http://localhost:3000";
|
|
||||||
|
|
||||||
if (hasReactRouter) {
|
|
||||||
corsOrigin = "http://localhost:5173";
|
|
||||||
} else if (hasTanStackRouter || hasTanStackStart) {
|
|
||||||
corsOrigin = "http://localhost:3001";
|
|
||||||
}
|
|
||||||
|
|
||||||
envContent += `\nCORS_ORIGIN=${corsOrigin}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.auth) {
|
|
||||||
if (!envContent.includes("BETTER_AUTH_SECRET")) {
|
|
||||||
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!envContent.includes("BETTER_AUTH_URL")) {
|
|
||||||
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.database !== "none") {
|
|
||||||
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
|
|
||||||
let databaseUrlLine = "";
|
|
||||||
if (options.database === "sqlite") {
|
|
||||||
databaseUrlLine = "";
|
|
||||||
} else if (options.database === "postgres") {
|
|
||||||
databaseUrlLine = `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
|
|
||||||
} else if (options.database === "mongodb") {
|
|
||||||
databaseUrlLine = `\nDATABASE_URL="mongodb://localhost:27017/mydatabase"`;
|
|
||||||
}
|
|
||||||
envContent += databaseUrlLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.database === "sqlite" && options.dbSetup !== "turso") {
|
|
||||||
if (!envContent.includes("DATABASE_URL")) {
|
|
||||||
envContent += "\nDATABASE_URL=file:./local.db";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.examples?.includes("ai") &&
|
|
||||||
!envContent.includes("GOOGLE_GENERATIVE_AI_API_KEY")
|
|
||||||
) {
|
|
||||||
envContent += "\nGOOGLE_GENERATIVE_AI_API_KEY=";
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(envPath, envContent.trim());
|
|
||||||
|
|
||||||
const hasReactRouter = options.frontend.includes("react-router");
|
const hasReactRouter = options.frontend.includes("react-router");
|
||||||
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
||||||
@@ -77,39 +54,84 @@ export async function setupEnvironmentVariables(
|
|||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
hasReactRouter || hasTanStackRouter || hasTanStackStart;
|
hasReactRouter || hasTanStackRouter || hasTanStackStart;
|
||||||
|
|
||||||
|
let corsOrigin = "http://localhost:3000";
|
||||||
|
if (hasReactRouter) {
|
||||||
|
corsOrigin = "http://localhost:5173";
|
||||||
|
} else if (hasTanStackRouter || hasTanStackStart) {
|
||||||
|
corsOrigin = "http://localhost:3001";
|
||||||
|
}
|
||||||
|
|
||||||
|
let databaseUrl = "";
|
||||||
|
const specializedSetup =
|
||||||
|
options.dbSetup === "turso" ||
|
||||||
|
options.dbSetup === "prisma-postgres" ||
|
||||||
|
options.dbSetup === "mongodb-atlas";
|
||||||
|
|
||||||
|
if (!specializedSetup) {
|
||||||
|
if (options.database === "postgres") {
|
||||||
|
databaseUrl =
|
||||||
|
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public";
|
||||||
|
} else if (options.database === "mysql") {
|
||||||
|
databaseUrl = "mysql://root:password@localhost:3306/mydb";
|
||||||
|
} else if (options.database === "mongodb") {
|
||||||
|
databaseUrl = "mongodb://localhost:27017/mydatabase";
|
||||||
|
} else if (options.database === "sqlite") {
|
||||||
|
databaseUrl = "file:./local.db";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverVars: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "CORS_ORIGIN",
|
||||||
|
value: corsOrigin,
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "BETTER_AUTH_SECRET",
|
||||||
|
value: generateAuthSecret(),
|
||||||
|
condition: !!options.auth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "BETTER_AUTH_URL",
|
||||||
|
value: "http://localhost:3000",
|
||||||
|
condition: !!options.auth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DATABASE_URL",
|
||||||
|
value: databaseUrl,
|
||||||
|
condition:
|
||||||
|
options.database !== "none" && databaseUrl !== "" && !specializedSetup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: options.examples?.includes("ai") || false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await addEnvVariablesToFile(envPath, serverVars);
|
||||||
|
|
||||||
if (hasWebFrontend) {
|
if (hasWebFrontend) {
|
||||||
const clientDir = path.join(projectDir, "apps/web");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
await setupClientEnvFile(clientDir);
|
const clientVars: EnvVariable[] = [
|
||||||
|
{
|
||||||
|
key: "VITE_SERVER_URL",
|
||||||
|
value: "http://localhost:3000",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.frontend.includes("native")) {
|
if (options.frontend.includes("native")) {
|
||||||
const nativeDir = path.join(projectDir, "apps/native");
|
const nativeDir = path.join(projectDir, "apps/native");
|
||||||
const nativeEnvPath = path.join(nativeDir, ".env");
|
const nativeVars: EnvVariable[] = [
|
||||||
let nativeEnvContent = "";
|
{
|
||||||
|
key: "EXPO_PUBLIC_SERVER_URL",
|
||||||
if (await fs.pathExists(nativeEnvPath)) {
|
value: "http://localhost:3000",
|
||||||
nativeEnvContent = await fs.readFile(nativeEnvPath, "utf8");
|
condition: true,
|
||||||
}
|
},
|
||||||
|
];
|
||||||
if (!nativeEnvContent.includes("EXPO_PUBLIC_SERVER_URL")) {
|
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
||||||
nativeEnvContent += "EXPO_PUBLIC_SERVER_URL=http://localhost:3000\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(nativeEnvPath, nativeEnvContent.trim());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupClientEnvFile(clientDir: string) {
|
|
||||||
const clientEnvPath = path.join(clientDir, ".env");
|
|
||||||
let clientEnvContent = "";
|
|
||||||
|
|
||||||
if (await fs.pathExists(clientEnvPath)) {
|
|
||||||
clientEnvContent = await fs.readFile(clientEnvPath, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clientEnvContent.includes("VITE_SERVER_URL")) {
|
|
||||||
clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(clientEnvPath, clientEnvContent.trim());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -366,14 +366,15 @@ async function findGitignoreFiles(dir: string): Promise<string[]> {
|
|||||||
|
|
||||||
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
||||||
if (orm === "drizzle") {
|
if (orm === "drizzle") {
|
||||||
return database === "sqlite"
|
if (database === "sqlite") return "template/with-drizzle-sqlite";
|
||||||
? "template/with-drizzle-sqlite"
|
if (database === "postgres") return "template/with-drizzle-postgres";
|
||||||
: "template/with-drizzle-postgres";
|
if (database === "mysql") return "template/with-drizzle-mysql";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orm === "prisma") {
|
if (orm === "prisma") {
|
||||||
if (database === "sqlite") return "template/with-prisma-sqlite";
|
if (database === "sqlite") return "template/with-prisma-sqlite";
|
||||||
if (database === "postgres") return "template/with-prisma-postgres";
|
if (database === "postgres") return "template/with-prisma-postgres";
|
||||||
|
if (database === "mysql") return "template/with-prisma-mysql";
|
||||||
if (database === "mongodb") return "template/with-prisma-mongodb";
|
if (database === "mongodb") return "template/with-prisma-mongodb";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,14 +383,15 @@ function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
|||||||
|
|
||||||
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
||||||
if (orm === "drizzle") {
|
if (orm === "drizzle") {
|
||||||
return database === "sqlite"
|
if (database === "sqlite") return "with-drizzle-sqlite-lib";
|
||||||
? "with-drizzle-sqlite-lib"
|
if (database === "postgres") return "with-drizzle-postgres-lib";
|
||||||
: "with-drizzle-postgres-lib";
|
if (database === "mysql") return "with-drizzle-mysql-lib";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orm === "prisma") {
|
if (orm === "prisma") {
|
||||||
if (database === "sqlite") return "with-prisma-sqlite-lib";
|
if (database === "sqlite") return "with-prisma-sqlite-lib";
|
||||||
if (database === "postgres") return "with-prisma-postgres-lib";
|
if (database === "postgres") return "with-prisma-postgres-lib";
|
||||||
|
if (database === "mysql") return "with-prisma-mysql-lib";
|
||||||
if (database === "mongodb") return "with-prisma-mongodb-lib";
|
if (database === "mongodb") return "with-prisma-mongodb-lib";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async function main() {
|
|||||||
.option("-y, --yes", "Use default configuration")
|
.option("-y, --yes", "Use default configuration")
|
||||||
.option(
|
.option(
|
||||||
"--database <type>",
|
"--database <type>",
|
||||||
"Database type (none, sqlite, postgres, mongodb)",
|
"Database type (none, sqlite, postgres, mysql, mongodb)",
|
||||||
)
|
)
|
||||||
.option("--orm <type>", "ORM type (drizzle, prisma)")
|
.option("--orm <type>", "ORM type (drizzle, prisma)")
|
||||||
.option("--auth", "Include authentication")
|
.option("--auth", "Include authentication")
|
||||||
@@ -80,9 +80,7 @@ async function main() {
|
|||||||
const options = program.opts() as CLIOptions;
|
const options = program.opts() as CLIOptions;
|
||||||
const projectDirectory = program.args[0];
|
const projectDirectory = program.args[0];
|
||||||
|
|
||||||
validateOptions(options);
|
const flagConfig = processAndValidateFlags(options, projectDirectory);
|
||||||
|
|
||||||
const flagConfig = processFlags(options, projectDirectory);
|
|
||||||
|
|
||||||
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
||||||
log.info(pc.yellow("Using these pre-selected options:"));
|
log.info(pc.yellow("Using these pre-selected options:"));
|
||||||
@@ -129,41 +127,39 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOptions(options: CLIOptions): void {
|
function processAndValidateFlags(
|
||||||
if (
|
options: CLIOptions,
|
||||||
options.database &&
|
projectDirectory?: string,
|
||||||
!["none", "sqlite", "postgres", "mongodb"].includes(options.database)
|
): Partial<ProjectConfig> {
|
||||||
) {
|
const config: Partial<ProjectConfig> = {};
|
||||||
cancel(
|
|
||||||
pc.red(
|
if (options.database) {
|
||||||
`Invalid database type: ${options.database}. Must be none, sqlite, postgres, or mongodb.`,
|
if (
|
||||||
),
|
!["none", "sqlite", "postgres", "mysql", "mongodb"].includes(
|
||||||
);
|
options.database,
|
||||||
process.exit(1);
|
)
|
||||||
|
) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
`Invalid database type: ${options.database}. Must be none, sqlite, postgres, mysql, or mongodb.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.database = options.database as ProjectDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.orm && !["drizzle", "prisma"].includes(options.orm)) {
|
if (options.orm) {
|
||||||
cancel(
|
if (!["drizzle", "prisma"].includes(options.orm)) {
|
||||||
pc.red(`Invalid ORM type: ${options.orm}. Must be drizzle or prisma.`),
|
cancel(
|
||||||
);
|
pc.red(`Invalid ORM type: ${options.orm}. Must be drizzle or prisma.`),
|
||||||
process.exit(1);
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.orm = options.orm as ProjectOrm;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (config.database === "mongodb" && config.orm === "drizzle") {
|
||||||
options.dbSetup &&
|
|
||||||
!["turso", "prisma-postgres", "mongodb-atlas", "none"].includes(
|
|
||||||
options.dbSetup,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.database === "mongodb" && options.orm === "drizzle") {
|
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
pc.red(
|
||||||
"MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle",
|
"MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle",
|
||||||
@@ -172,7 +168,81 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.database === "none") {
|
if (options.dbSetup) {
|
||||||
|
if (
|
||||||
|
!["turso", "prisma-postgres", "mongodb-atlas", "none"].includes(
|
||||||
|
options.dbSetup,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.dbSetup !== "none") {
|
||||||
|
config.dbSetup = options.dbSetup as ProjectDBSetup;
|
||||||
|
|
||||||
|
if (options.dbSetup === "turso") {
|
||||||
|
if (options.database && options.database !== "sqlite") {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
`Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.database = "sqlite";
|
||||||
|
|
||||||
|
if (options.orm === "prisma") {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.orm = "drizzle";
|
||||||
|
} else if (options.dbSetup === "prisma-postgres") {
|
||||||
|
if (options.database && options.database !== "postgres") {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Prisma PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup prisma-postgres with a different database type.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.database = "postgres";
|
||||||
|
|
||||||
|
if (options.orm && options.orm !== "prisma") {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with a different ORM.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.orm = "prisma";
|
||||||
|
} else if (options.dbSetup === "mongodb-atlas") {
|
||||||
|
if (options.database && options.database !== "mongodb") {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"MongoDB Atlas setup requires MongoDB database. Cannot use --db-setup mongodb-atlas with a different database type.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.database = "mongodb";
|
||||||
|
config.orm = "prisma";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.dbSetup = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.database === "none") {
|
||||||
if (options.auth === true) {
|
if (options.auth === true) {
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
pc.red(
|
||||||
@@ -201,128 +271,30 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.dbSetup === "turso") {
|
if ("auth" in options) {
|
||||||
if (options.database && options.database !== "sqlite") {
|
config.auth = options.auth;
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.orm === "prisma") {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.dbSetup === "prisma-postgres") {
|
if (options.backend) {
|
||||||
if (options.database && options.database !== "postgres") {
|
if (!["hono", "elysia", "express"].includes(options.backend)) {
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
pc.red(
|
||||||
"Prisma PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup prisma-postgres with a different database type.",
|
`Invalid backend framework: ${options.backend}. Must be hono, elysia, or express.`,
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.orm && options.orm !== "prisma") {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
"Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with a different ORM.",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
config.backend = options.backend as ProjectBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.dbSetup === "mongodb-atlas") {
|
if (options.runtime) {
|
||||||
if (options.database && options.database !== "mongodb") {
|
if (!["bun", "node"].includes(options.runtime)) {
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`),
|
||||||
"MongoDB Atlas setup requires MongoDB database. Cannot use --db-setup mongodb-atlas with a different database type.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.packageManager &&
|
|
||||||
!["npm", "pnpm", "bun"].includes(options.packageManager)
|
|
||||||
) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.backend &&
|
|
||||||
!["hono", "elysia", "express"].includes(options.backend)
|
|
||||||
) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`Invalid backend framework: ${options.backend}. Must be hono, elysia, or express.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.runtime && !["bun", "node"].includes(options.runtime)) {
|
|
||||||
cancel(pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.examples &&
|
|
||||||
Array.isArray(options.examples) &&
|
|
||||||
options.examples.length > 0
|
|
||||||
) {
|
|
||||||
const validExamples = ["todo", "ai"];
|
|
||||||
const invalidExamples = options.examples.filter(
|
|
||||||
(example: string) => !validExamples.includes(example),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (invalidExamples.length > 0) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.examples.includes("ai") && options.backend === "elysia") {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
"AI example is only compatible with Hono backend. Cannot use --examples ai with --backend elysia",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.frontend &&
|
|
||||||
!options.frontend.some((f) =>
|
|
||||||
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
|
||||||
) &&
|
|
||||||
!options.frontend.includes("none")
|
|
||||||
) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
"Examples require a web frontend. Cannot use --examples with --frontend native only",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
config.runtime = options.runtime as ProjectRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.frontend && options.frontend.length > 0) {
|
if (options.frontend && options.frontend.length > 0) {
|
||||||
@@ -346,33 +318,42 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const webFrontends = options.frontend.filter(
|
if (options.frontend.includes("none")) {
|
||||||
(f) =>
|
if (options.frontend.length > 1) {
|
||||||
f === "tanstack-router" ||
|
cancel(pc.red(`Cannot combine 'none' with other frontend options.`));
|
||||||
f === "react-router" ||
|
process.exit(1);
|
||||||
f === "tanstack-start",
|
}
|
||||||
);
|
config.frontend = [];
|
||||||
|
} else {
|
||||||
if (webFrontends.length > 1) {
|
const validOptions = options.frontend.filter(
|
||||||
cancel(
|
(f): f is ProjectFrontend =>
|
||||||
pc.red(
|
f === "tanstack-router" ||
|
||||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router",
|
f === "react-router" ||
|
||||||
),
|
f === "tanstack-start" ||
|
||||||
|
f === "native",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.frontend.includes("none") && options.frontend.length > 1) {
|
const webFrontends = validOptions.filter(
|
||||||
cancel(pc.red(`Cannot combine 'none' with other frontend options.`));
|
(f) =>
|
||||||
process.exit(1);
|
f === "tanstack-router" ||
|
||||||
|
f === "react-router" ||
|
||||||
|
f === "tanstack-start",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (webFrontends.length > 1) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.frontend = validOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (options.addons && options.addons.length > 0) {
|
||||||
options.addons &&
|
|
||||||
Array.isArray(options.addons) &&
|
|
||||||
options.addons.length > 0
|
|
||||||
) {
|
|
||||||
const validAddons = ["pwa", "tauri", "biome", "husky", "none"];
|
const validAddons = ["pwa", "tauri", "biome", "husky", "none"];
|
||||||
const invalidAddons = options.addons.filter(
|
const invalidAddons = options.addons.filter(
|
||||||
(addon: string) => !validAddons.includes(addon),
|
(addon: string) => !validAddons.includes(addon),
|
||||||
@@ -387,108 +368,14 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.addons.includes("none") && options.addons.length > 1) {
|
|
||||||
cancel(pc.red(`Cannot combine 'none' with other addons.`));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const webSpecificAddons = ["pwa", "tauri"];
|
|
||||||
const hasWebSpecificAddons = options.addons.some((addon) =>
|
|
||||||
webSpecificAddons.includes(addon),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasWebSpecificAddons &&
|
|
||||||
options.frontend &&
|
|
||||||
!options.frontend.some((f) =>
|
|
||||||
["tanstack-router", "react-router"].includes(f),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
`PWA and Tauri addons require tanstack-router or react-router. Cannot use --addons ${options.addons
|
|
||||||
.filter((a) => webSpecificAddons.includes(a))
|
|
||||||
.join(", ")} with incompatible frontend options.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processFlags(
|
|
||||||
options: CLIOptions,
|
|
||||||
projectDirectory?: string,
|
|
||||||
): Partial<ProjectConfig> {
|
|
||||||
let frontend: ProjectFrontend[] | undefined = undefined;
|
|
||||||
|
|
||||||
if (options.frontend) {
|
|
||||||
if (options.frontend.includes("none")) {
|
|
||||||
frontend = [];
|
|
||||||
} else {
|
|
||||||
frontend = options.frontend.filter(
|
|
||||||
(f): f is ProjectFrontend =>
|
|
||||||
f === "tanstack-router" ||
|
|
||||||
f === "react-router" ||
|
|
||||||
f === "tanstack-start" ||
|
|
||||||
f === "native",
|
|
||||||
);
|
|
||||||
|
|
||||||
const webFrontends = frontend.filter(
|
|
||||||
(f) =>
|
|
||||||
f === "tanstack-router" ||
|
|
||||||
f === "react-router" ||
|
|
||||||
f === "tanstack-start",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (webFrontends.length > 1) {
|
|
||||||
const firstWebFrontend = webFrontends[0];
|
|
||||||
frontend = frontend.filter(
|
|
||||||
(f) => f === "native" || f === firstWebFrontend,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let examples: ProjectExamples[] | undefined;
|
|
||||||
if ("examples" in options) {
|
|
||||||
if (options.examples === false) {
|
|
||||||
examples = [];
|
|
||||||
} else if (Array.isArray(options.examples)) {
|
|
||||||
examples = options.examples.filter(
|
|
||||||
(ex): ex is ProjectExamples => ex === "todo" || ex === "ai",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
frontend &&
|
|
||||||
frontend.length > 0 &&
|
|
||||||
!frontend.some((f) =>
|
|
||||||
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
examples = [];
|
|
||||||
log.warn(
|
|
||||||
pc.yellow("Examples require web frontend - ignoring examples flag"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.includes("ai") && options.backend === "elysia") {
|
|
||||||
examples = examples.filter((ex) => ex !== "ai");
|
|
||||||
log.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"AI example is not compatible with Elysia - removing AI example",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let addons: ProjectAddons[] | undefined;
|
|
||||||
if (options.addons && Array.isArray(options.addons)) {
|
|
||||||
if (options.addons.includes("none")) {
|
if (options.addons.includes("none")) {
|
||||||
addons = [];
|
if (options.addons.length > 1) {
|
||||||
|
cancel(pc.red(`Cannot combine 'none' with other addons.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.addons = [];
|
||||||
} else {
|
} else {
|
||||||
addons = options.addons.filter(
|
const validOptions = options.addons.filter(
|
||||||
(addon): addon is ProjectAddons =>
|
(addon): addon is ProjectAddons =>
|
||||||
addon === "pwa" ||
|
addon === "pwa" ||
|
||||||
addon === "tauri" ||
|
addon === "tauri" ||
|
||||||
@@ -496,85 +383,111 @@ function processFlags(
|
|||||||
addon === "husky",
|
addon === "husky",
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCompatibleWebFrontend = frontend?.some(
|
const webSpecificAddons = ["pwa", "tauri"];
|
||||||
|
const hasWebSpecificAddons = validOptions.some((addon) =>
|
||||||
|
webSpecificAddons.includes(addon),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCompatibleWebFrontend = config.frontend?.some(
|
||||||
(f) => f === "tanstack-router" || f === "react-router",
|
(f) => f === "tanstack-router" || f === "react-router",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasCompatibleWebFrontend) {
|
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||||
const webSpecificAddons = ["pwa", "tauri"];
|
cancel(
|
||||||
const filteredAddons = addons.filter(
|
pc.red(
|
||||||
(addon) => !webSpecificAddons.includes(addon),
|
"PWA and Tauri addons require tanstack-router or react-router. Cannot use these addons with your frontend selection.",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
process.exit(1);
|
||||||
if (filteredAddons.length !== addons.length) {
|
|
||||||
log.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"PWA and Tauri addons require tanstack-router or react-router - removing these addons",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
addons = filteredAddons;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addons.includes("husky") && !addons.includes("biome")) {
|
if (validOptions.includes("husky") && !validOptions.includes("biome")) {
|
||||||
addons.push("biome");
|
validOptions.push("biome");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.addons = validOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let database = options.database as ProjectDatabase | undefined;
|
if ("examples" in options) {
|
||||||
let orm = options.orm as ProjectOrm | undefined;
|
if (options.examples === false) {
|
||||||
const auth = "auth" in options ? options.auth : undefined;
|
config.examples = [];
|
||||||
|
} else if (Array.isArray(options.examples)) {
|
||||||
|
const validExamples = ["todo", "ai"];
|
||||||
|
const invalidExamples = options.examples.filter(
|
||||||
|
(example: string) => !validExamples.includes(example),
|
||||||
|
);
|
||||||
|
|
||||||
const backend = options.backend as ProjectBackend | undefined;
|
if (invalidExamples.length > 0) {
|
||||||
const runtime = options.runtime as ProjectRuntime | undefined;
|
cancel(
|
||||||
const packageManager = options.packageManager as
|
pc.red(
|
||||||
| ProjectPackageManager
|
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
|
||||||
| undefined;
|
),
|
||||||
|
);
|
||||||
let dbSetup: ProjectDBSetup | undefined = undefined;
|
process.exit(1);
|
||||||
if (options.dbSetup) {
|
|
||||||
if (options.dbSetup === "none") {
|
|
||||||
dbSetup = "none";
|
|
||||||
} else {
|
|
||||||
dbSetup = options.dbSetup as ProjectDBSetup;
|
|
||||||
|
|
||||||
if (dbSetup === "turso") {
|
|
||||||
database = "sqlite";
|
|
||||||
|
|
||||||
if (orm === "prisma") {
|
|
||||||
log.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"Turso is not compatible with Prisma - switching to Drizzle",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
orm = "drizzle";
|
|
||||||
}
|
|
||||||
} else if (dbSetup === "prisma-postgres") {
|
|
||||||
database = "postgres";
|
|
||||||
orm = "prisma";
|
|
||||||
} else if (dbSetup === "mongodb-atlas") {
|
|
||||||
database = "mongodb";
|
|
||||||
orm = "prisma";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.examples.includes("ai") &&
|
||||||
|
(options.backend === "elysia" || config.backend === "elysia")
|
||||||
|
) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"AI example is only compatible with Hono backend. Cannot use --examples ai with --backend elysia",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasWebFrontend = config.frontend?.some((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.examples.length > 0 &&
|
||||||
|
!hasWebFrontend &&
|
||||||
|
(!options.frontend ||
|
||||||
|
!options.frontend.some((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Examples require a web frontend (tanstack-router, react-router, or tanstack-start). Cannot use --examples without a compatible frontend.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.examples = options.examples.filter(
|
||||||
|
(ex): ex is ProjectExamples => ex === "todo" || ex === "ai",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: Partial<ProjectConfig> = {};
|
if (options.packageManager) {
|
||||||
|
if (!["npm", "pnpm", "bun"].includes(options.packageManager)) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||||
|
}
|
||||||
|
|
||||||
if (projectDirectory) config.projectName = projectDirectory;
|
if ("git" in options) {
|
||||||
if (database !== undefined) config.database = database;
|
config.git = options.git;
|
||||||
if (orm !== undefined) config.orm = orm;
|
}
|
||||||
if (auth !== undefined) config.auth = auth;
|
|
||||||
if (packageManager) config.packageManager = packageManager;
|
if ("install" in options) {
|
||||||
if ("git" in options) config.git = options.git;
|
config.noInstall = !options.install;
|
||||||
if ("install" in options) config.noInstall = !options.install;
|
}
|
||||||
if (dbSetup !== undefined) config.dbSetup = dbSetup;
|
|
||||||
if (backend) config.backend = backend;
|
if (projectDirectory) {
|
||||||
if (runtime) config.runtime = runtime;
|
config.projectName = projectDirectory;
|
||||||
if (frontend !== undefined) config.frontend = frontend;
|
}
|
||||||
if (addons !== undefined) config.addons = addons;
|
|
||||||
if (examples !== undefined) config.examples = examples;
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,27 +45,6 @@ type PromptGroupResults = {
|
|||||||
export async function gatherConfig(
|
export async function gatherConfig(
|
||||||
flags: Partial<ProjectConfig>,
|
flags: Partial<ProjectConfig>,
|
||||||
): Promise<ProjectConfig> {
|
): Promise<ProjectConfig> {
|
||||||
if (flags.dbSetup) {
|
|
||||||
if (flags.dbSetup === "turso") {
|
|
||||||
flags.database = "sqlite";
|
|
||||||
|
|
||||||
if (flags.orm === "prisma") {
|
|
||||||
log.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"Turso is not compatible with Prisma - switching to Drizzle",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
flags.orm = "drizzle";
|
|
||||||
}
|
|
||||||
} else if (flags.dbSetup === "prisma-postgres") {
|
|
||||||
flags.database = "postgres";
|
|
||||||
flags.orm = "prisma";
|
|
||||||
} else if (flags.dbSetup === "mongodb-atlas") {
|
|
||||||
flags.database = "mongodb";
|
|
||||||
flags.orm = "prisma";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await group<PromptGroupResults>(
|
const result = await group<PromptGroupResults>(
|
||||||
{
|
{
|
||||||
projectName: async () => {
|
projectName: async () => {
|
||||||
@@ -84,7 +63,11 @@ export async function gatherConfig(
|
|||||||
results.frontend,
|
results.frontend,
|
||||||
),
|
),
|
||||||
dbSetup: ({ results }) =>
|
dbSetup: ({ results }) =>
|
||||||
getDBSetupChoice(results.database ?? "none", flags.dbSetup),
|
getDBSetupChoice(
|
||||||
|
results.database ?? "none",
|
||||||
|
flags.dbSetup,
|
||||||
|
results.orm,
|
||||||
|
),
|
||||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||||
examples: ({ results }) =>
|
examples: ({ results }) =>
|
||||||
getExamplesChoice(
|
getExamplesChoice(
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export async function getDatabaseChoice(
|
|||||||
label: "PostgreSQL",
|
label: "PostgreSQL",
|
||||||
hint: "powerful, open source object-relational database system",
|
hint: "powerful, open source object-relational database system",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "mysql",
|
||||||
|
label: "MySQL",
|
||||||
|
hint: "popular open-source relational database system",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "mongodb",
|
value: "mongodb",
|
||||||
label: "MongoDB",
|
label: "MongoDB",
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { cancel, isCancel, select } from "@clack/prompts";
|
import { cancel, isCancel, select } from "@clack/prompts";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectDBSetup } from "../types";
|
import type { ProjectDBSetup, ProjectOrm } from "../types";
|
||||||
|
|
||||||
export async function getDBSetupChoice(
|
export async function getDBSetupChoice(
|
||||||
databaseType: string,
|
databaseType: string,
|
||||||
dbSetup: ProjectDBSetup | undefined,
|
dbSetup: ProjectDBSetup | undefined,
|
||||||
|
orm?: ProjectOrm,
|
||||||
): Promise<ProjectDBSetup> {
|
): Promise<ProjectDBSetup> {
|
||||||
if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
|
if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
|
||||||
|
|
||||||
|
if (databaseType === "sqlite" && orm === "prisma") {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> =
|
let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> =
|
||||||
[];
|
[];
|
||||||
|
|
||||||
@@ -16,7 +21,7 @@ export async function getDBSetupChoice(
|
|||||||
{
|
{
|
||||||
value: "turso" as const,
|
value: "turso" as const,
|
||||||
label: "Turso",
|
label: "Turso",
|
||||||
hint: "SQLite for Production. Powered by libSQL.",
|
hint: "SQLite for Production. Powered by libSQL",
|
||||||
},
|
},
|
||||||
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export type ProjectDatabase = "sqlite" | "postgres" | "mongodb" | "none";
|
export type ProjectDatabase =
|
||||||
|
| "sqlite"
|
||||||
|
| "postgres"
|
||||||
|
| "mongodb"
|
||||||
|
| "mysql"
|
||||||
|
| "none";
|
||||||
export type ProjectOrm = "drizzle" | "prisma" | "none";
|
export type ProjectOrm = "drizzle" | "prisma" | "none";
|
||||||
export type ProjectPackageManager = "npm" | "pnpm" | "bun";
|
export type ProjectPackageManager = "npm" | "pnpm" | "bun";
|
||||||
export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";
|
export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";
|
||||||
|
|||||||
@@ -18,10 +18,12 @@
|
|||||||
"persistent": true
|
"persistent": true
|
||||||
},
|
},
|
||||||
"db:push": {
|
"db:push": {
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
},
|
},
|
||||||
"db:studio": {
|
"db:studio": {
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,9 @@ export const todoRouter = router({
|
|||||||
create: publicProcedure
|
create: publicProcedure
|
||||||
.input(z.object({ text: z.string().min(1) }))
|
.input(z.object({ text: z.string().min(1) }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return await db
|
return await db.insert(todo).values({
|
||||||
.insert(todo)
|
text: input.text,
|
||||||
.values({
|
});
|
||||||
text: input.text,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
toggle: publicProcedure
|
toggle: publicProcedure
|
||||||
@@ -26,16 +23,12 @@ export const todoRouter = router({
|
|||||||
return await db
|
return await db
|
||||||
.update(todo)
|
.update(todo)
|
||||||
.set({ completed: input.completed })
|
.set({ completed: input.completed })
|
||||||
.where(eq(todo.id, input.id))
|
.where(eq(todo.id, input.id));
|
||||||
.returning();
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: publicProcedure
|
delete: publicProcedure
|
||||||
.input(z.object({ id: z.number() }))
|
.input(z.object({ id: z.number() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return await db
|
return await db.delete(todo).where(eq(todo.id, input.id));
|
||||||
.delete(todo)
|
|
||||||
.where(eq(todo.id, input.id))
|
|
||||||
.returning();
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { db } from "../db";
|
||||||
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "mysql",
|
||||||
|
schema: schema,
|
||||||
|
}),
|
||||||
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -8,10 +8,4 @@ export const auth = betterAuth({
|
|||||||
}),
|
}),
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
emailAndPassword: { enabled: true },
|
emailAndPassword: { enabled: true },
|
||||||
advanced: {
|
|
||||||
defaultCookieAttributes: {
|
|
||||||
sameSite: "none",
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||||
|
import prisma from "../../prisma";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: prismaAdapter(prisma, {
|
||||||
|
provider: "mysql",
|
||||||
|
}),
|
||||||
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
|
emailAndPassword: { enabled: true },
|
||||||
|
});
|
||||||
@@ -8,10 +8,4 @@ export const auth = betterAuth({
|
|||||||
}),
|
}),
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
emailAndPassword: { enabled: true },
|
emailAndPassword: { enabled: true },
|
||||||
advanced: {
|
|
||||||
defaultCookieAttributes: {
|
|
||||||
sameSite: "none",
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,4 @@ export const auth = betterAuth({
|
|||||||
}),
|
}),
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
emailAndPassword: { enabled: true },
|
emailAndPassword: { enabled: true },
|
||||||
advanced: {
|
|
||||||
defaultCookieAttributes: {
|
|
||||||
sameSite: "none",
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./src/db/schema",
|
||||||
|
out: "./src/db/migrations",
|
||||||
|
dialect: "mysql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/mysql2";
|
||||||
|
|
||||||
|
export const db = drizzle({ connection: { uri: process.env.DATABASE_URL } });
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
mysqlTable,
|
||||||
|
varchar,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
} from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
|
export const user = mysqlTable("user", {
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: varchar("email", { length: 255 }).notNull().unique(),
|
||||||
|
emailVerified: boolean("email_verified").notNull(),
|
||||||
|
image: text("image"),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const session = mysqlTable("session", {
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
token: varchar("token", { length: 255 }).notNull().unique(),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
userId: varchar("user_id", { length: 36 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const account = mysqlTable("account", {
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
accountId: text("account_id").notNull(),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
userId: varchar("user_id", { length: 36 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
|
||||||
|
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||||
|
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||||
|
scope: text("scope"),
|
||||||
|
password: text("password"),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verification = mysqlTable("verification", {
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
createdAt: timestamp("created_at"),
|
||||||
|
updatedAt: timestamp("updated_at"),
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { mysqlTable, varchar, int, boolean } from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
|
export const todo = mysqlTable("todo", {
|
||||||
|
id: int("id").primaryKey().autoincrement(),
|
||||||
|
text: varchar("text", { length: 255 }).notNull(),
|
||||||
|
completed: boolean("completed").default(false).notNull(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
let prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
model User {
|
||||||
|
id String @id
|
||||||
|
name String @db.Text
|
||||||
|
email String
|
||||||
|
emailVerified Boolean
|
||||||
|
image String? @db.Text
|
||||||
|
createdAt DateTime
|
||||||
|
updatedAt DateTime
|
||||||
|
sessions Session[]
|
||||||
|
accounts Account[]
|
||||||
|
|
||||||
|
@@unique([email])
|
||||||
|
@@map("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id
|
||||||
|
expiresAt DateTime
|
||||||
|
token String
|
||||||
|
createdAt DateTime
|
||||||
|
updatedAt DateTime
|
||||||
|
ipAddress String? @db.Text
|
||||||
|
userAgent String? @db.Text
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([token])
|
||||||
|
@@map("session")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id
|
||||||
|
accountId String @db.Text
|
||||||
|
providerId String @db.Text
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
accessToken String? @db.Text
|
||||||
|
refreshToken String? @db.Text
|
||||||
|
idToken String? @db.Text
|
||||||
|
accessTokenExpiresAt DateTime?
|
||||||
|
refreshTokenExpiresAt DateTime?
|
||||||
|
scope String? @db.Text
|
||||||
|
password String? @db.Text
|
||||||
|
createdAt DateTime
|
||||||
|
updatedAt DateTime
|
||||||
|
|
||||||
|
@@map("account")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Verification {
|
||||||
|
id String @id
|
||||||
|
identifier String @db.Text
|
||||||
|
value String @db.Text
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime?
|
||||||
|
updatedAt DateTime?
|
||||||
|
|
||||||
|
@@map("verification")
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["prismaSchemaFolder"]
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "mysql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
model Todo {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
text String
|
||||||
|
completed Boolean @default(false)
|
||||||
|
|
||||||
|
@@map("todo")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user