Add backend framework selection between hono, elysiajs

This commit is contained in:
Aman Varshney
2025-03-26 11:41:41 +05:30
parent b6b113766e
commit 91fe9f861f
40 changed files with 451 additions and 345 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Add option to choose elysiajs as backend framework

View File

@@ -7,10 +7,7 @@
"bin": { "bin": {
"create-better-t-stack": "dist/index.js" "create-better-t-stack": "dist/index.js"
}, },
"files": [ "files": ["dist", "template"],
"dist",
"template"
],
"keywords": [], "keywords": [],
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,6 +1,7 @@
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import type { ProjectConfig } from "./types"; import type { ProjectConfig } from "./types";
import { getUserPkgManager } from "./utils/get-package-manager";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename); const distPath = path.dirname(__filename);
@@ -14,7 +15,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
addons: [], addons: [],
examples: [], examples: [],
git: true, git: true,
packageManager: "npm", packageManager: getUserPkgManager(),
noInstall: false, noInstall: false,
turso: false, turso: false,
backendFramework: "hono", backendFramework: "hono",
@@ -48,6 +49,15 @@ export const dependencyVersionMap = {
"@types/node": "^22.13.11", "@types/node": "^22.13.11",
"@types/bun": "^1.2.6", "@types/bun": "^1.2.6",
"@elysiajs/node": "^1.2.6",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/trpc": "^1.1.0",
elysia: "^1.2.25",
"@hono/trpc-server": "^0.3.4",
hono: "^4.7.5",
} as const; } as const;
export type AvailableDependencies = keyof typeof dependencyVersionMap; export type AvailableDependencies = keyof typeof dependencyVersionMap;

View File

@@ -0,0 +1,41 @@
import path from "node:path";
import type { AvailableDependencies } from "../constants";
import type { BackendFramework, Runtime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupBackendDependencies(
projectDir: string,
framework: BackendFramework,
runtime: Runtime,
): Promise<void> {
const serverDir = path.join(projectDir, "apps/server");
const dependencies: AvailableDependencies[] = [];
const devDependencies: AvailableDependencies[] = [];
if (framework === "hono") {
dependencies.push("hono", "@hono/trpc-server");
if (runtime === "node") {
dependencies.push("@hono/node-server");
devDependencies.push("tsx", "@types/node");
}
} else if (framework === "elysia") {
dependencies.push("elysia", "@elysiajs/cors", "@elysiajs/trpc");
if (runtime === "node") {
dependencies.push("@elysiajs/node");
devDependencies.push("tsx", "@types/node");
}
}
if (runtime === "bun") {
devDependencies.push("@types/bun");
}
addPackageDependency({
dependencies,
devDependencies,
projectDir: serverDir,
});
}

View File

@@ -5,6 +5,7 @@ import pc from "picocolors";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
import { setupAddons } from "./addons-setup"; import { setupAddons } from "./addons-setup";
import { setupAuth } from "./auth-setup"; import { setupAuth } from "./auth-setup";
import { setupBackendDependencies } from "./backend-framework-setup";
import { createReadme } from "./create-readme"; import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup"; import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup"; import { setupEnvironmentVariables } from "./env-setup";
@@ -16,6 +17,7 @@ import {
copyBaseTemplate, copyBaseTemplate,
fixGitignoreFiles, fixGitignoreFiles,
setupAuthTemplate, setupAuthTemplate,
setupBackendFramework,
setupOrmTemplate, setupOrmTemplate,
} from "./template-manager"; } from "./template-manager";
@@ -27,10 +29,14 @@ export async function createProject(options: ProjectConfig): Promise<string> {
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
await copyBaseTemplate(projectDir); await copyBaseTemplate(projectDir);
await fixGitignoreFiles(projectDir); await fixGitignoreFiles(projectDir);
await setupAuthTemplate(projectDir, options.auth); await setupBackendFramework(projectDir, options.backendFramework);
await setupBackendDependencies(
projectDir,
options.backendFramework,
options.runtime,
);
await setupOrmTemplate( await setupOrmTemplate(
projectDir, projectDir,
@@ -39,15 +45,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.auth, options.auth,
); );
await setupRuntime(projectDir, options.runtime);
await setupExamples(
projectDir,
options.examples,
options.orm,
options.auth,
);
await setupDatabase( await setupDatabase(
projectDir, projectDir,
options.database, options.database,
@@ -55,8 +52,24 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.turso ?? options.database === "sqlite", options.turso ?? options.database === "sqlite",
); );
await setupAuthTemplate(
projectDir,
options.auth,
options.backendFramework,
options.orm,
options.database,
);
await setupAuth(projectDir, options.auth); await setupAuth(projectDir, options.auth);
await setupRuntime(projectDir, options.runtime, options.backendFramework);
await setupExamples(
projectDir,
options.examples,
options.orm,
options.auth,
);
await setupEnvironmentVariables(projectDir, options); await setupEnvironmentVariables(projectDir, options);
await initializeGit(projectDir, options.git); await initializeGit(projectDir, options.git);
@@ -66,7 +79,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
} }
await updatePackageConfigurations(projectDir, options); await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options); await createReadme(projectDir, options);
displayPostInstallInstructions( displayPostInstallInstructions(

View File

@@ -33,9 +33,7 @@ export function displayPostInstallInstructions(
? getLintingInstructions(runCmd) ? getLintingInstructions(runCmd)
: ""; : "";
log.info(`${pc.cyan("Project created successfully!")} log.info(`${pc.bold("Next steps:")}
${pc.bold("Next steps:")}
${pc.cyan("1.")} ${cdCmd} ${pc.cyan("1.")} ${cdCmd}
${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev

View File

@@ -1,11 +1,12 @@
import path from "node:path"; import path from "node:path";
import fs from "fs-extra"; import fs from "fs-extra";
import type { Runtime } from "../types"; import type { BackendFramework, Runtime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime( export async function setupRuntime(
projectDir: string, projectDir: string,
runtime: Runtime, runtime: Runtime,
backendFramework: BackendFramework,
): Promise<void> { ): Promise<void> {
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
const serverIndexPath = path.join(serverDir, "src/index.ts"); const serverIndexPath = path.join(serverDir, "src/index.ts");
@@ -13,9 +14,19 @@ export async function setupRuntime(
const indexContent = await fs.readFile(serverIndexPath, "utf-8"); const indexContent = await fs.readFile(serverIndexPath, "utf-8");
if (runtime === "bun") { if (runtime === "bun") {
await setupBunRuntime(serverDir, serverIndexPath, indexContent); await setupBunRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
} else if (runtime === "node") { } else if (runtime === "node") {
await setupNodeRuntime(serverDir, serverIndexPath, indexContent); await setupNodeRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
} }
} }
@@ -23,6 +34,7 @@ async function setupBunRuntime(
serverDir: string, serverDir: string,
serverIndexPath: string, serverIndexPath: string,
indexContent: string, indexContent: string,
backendFramework: BackendFramework,
): Promise<void> { ): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json"); const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath); const packageJson = await fs.readJson(packageJsonPath);
@@ -40,7 +52,7 @@ async function setupBunRuntime(
projectDir: serverDir, projectDir: serverDir,
}); });
if (!indexContent.includes("export default app")) { if (backendFramework === "hono") {
const updatedContent = `${indexContent}\n\nexport default app;\n`; const updatedContent = `${indexContent}\n\nexport default app;\n`;
await fs.writeFile(serverIndexPath, updatedContent); await fs.writeFile(serverIndexPath, updatedContent);
} }
@@ -50,13 +62,8 @@ async function setupNodeRuntime(
serverDir: string, serverDir: string,
serverIndexPath: string, serverIndexPath: string,
indexContent: string, indexContent: string,
backendFramework: BackendFramework,
): Promise<void> { ): Promise<void> {
addPackageDependency({
dependencies: ["@hono/node-server"],
devDependencies: ["tsx", "@types/node"],
projectDir: serverDir,
});
const packageJsonPath = path.join(serverDir, "package.json"); const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath); const packageJson = await fs.readJson(packageJsonPath);
@@ -68,6 +75,17 @@ async function setupNodeRuntime(
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
addPackageDependency({
devDependencies: ["tsx", "@types/node"],
projectDir: serverDir,
});
if (backendFramework === "hono") {
addPackageDependency({
dependencies: ["@hono/node-server"],
projectDir: serverDir,
});
const importLine = 'import { serve } from "@hono/node-server";\n'; const importLine = 'import { serve } from "@hono/node-server";\n';
const serverCode = ` const serverCode = `
serve( serve(
@@ -85,10 +103,34 @@ serve(
const importSection = indexContent.substring(0, importEndIndex); const importSection = indexContent.substring(0, importEndIndex);
const restOfFile = indexContent.substring(importEndIndex); const restOfFile = indexContent.substring(importEndIndex);
const updatedContent = importSection + importLine + restOfFile + serverCode; const updatedContent =
importSection + importLine + restOfFile + serverCode;
await fs.writeFile(serverIndexPath, updatedContent); await fs.writeFile(serverIndexPath, updatedContent);
} else if (!indexContent.includes("serve(")) { }
const updatedContent = indexContent + serverCode; } else if (backendFramework === "elysia") {
addPackageDependency({
dependencies: ["@elysiajs/node"],
projectDir: serverDir,
});
if (!indexContent.includes("@elysiajs/node")) {
const nodeImport = 'import { node } from "@elysiajs/node";\n';
const firstImportEnd = indexContent.indexOf(
"\n",
indexContent.indexOf("import"),
);
const before = indexContent.substring(0, firstImportEnd + 1);
const after = indexContent.substring(firstImportEnd + 1);
let updatedContent = before + nodeImport + after;
updatedContent = updatedContent.replace(
/const app = new Elysia\([^)]*\)/,
"const app = new Elysia({ adapter: node() })",
);
await fs.writeFile(serverIndexPath, updatedContent); await fs.writeFile(serverIndexPath, updatedContent);
} }
} }
}

View File

@@ -1,7 +1,7 @@
import path from "node:path"; import path from "node:path";
import fs from "fs-extra"; import fs from "fs-extra";
import { PKG_ROOT } from "../constants"; import { PKG_ROOT } from "../constants";
import type { ProjectDatabase, ProjectOrm } from "../types"; import type { BackendFramework, ProjectDatabase, ProjectOrm } from "../types";
export async function copyBaseTemplate(projectDir: string): Promise<void> { export async function copyBaseTemplate(projectDir: string): Promise<void> {
const templateDir = path.join(PKG_ROOT, "template/base"); const templateDir = path.join(PKG_ROOT, "template/base");
@@ -11,15 +11,13 @@ export async function copyBaseTemplate(projectDir: string): Promise<void> {
await fs.copy(templateDir, projectDir); await fs.copy(templateDir, projectDir);
} }
export async function setupAuthTemplate( export async function setupBackendFramework(
projectDir: string, projectDir: string,
auth: boolean, framework: BackendFramework,
): Promise<void> { ): Promise<void> {
if (!auth) return; const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
if (await fs.pathExists(frameworkDir)) {
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth"); await fs.copy(frameworkDir, projectDir, { overwrite: true });
if (await fs.pathExists(authTemplateDir)) {
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
} }
} }
@@ -36,17 +34,83 @@ export async function setupOrmTemplate(
if (await fs.pathExists(ormTemplateDir)) { if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true }); await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const serverSrcPath = path.join(projectDir, "apps/server/src"); if (!auth) {
const libPath = path.join(serverSrcPath, "lib"); if (orm === "prisma") {
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib"); const authSchemaPath = path.join(
projectDir,
if (auth) { "apps/server/prisma/schema/auth.prisma",
if (await fs.pathExists(withAuthLibPath)) { );
await fs.remove(libPath); if (await fs.pathExists(authSchemaPath)) {
await fs.move(withAuthLibPath, libPath); await fs.remove(authSchemaPath);
}
} else if (orm === "drizzle") {
const authSchemaPath = path.join(
projectDir,
"apps/server/src/db/schema/auth.ts",
);
if (await fs.pathExists(authSchemaPath)) {
await fs.remove(authSchemaPath);
}
}
}
}
}
export async function setupAuthTemplate(
projectDir: string,
auth: boolean,
framework: BackendFramework,
orm: ProjectOrm,
database: ProjectDatabase,
): Promise<void> {
if (!auth) return;
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
const clientAuthDir = path.join(authTemplateDir, "apps/client");
const projectClientDir = path.join(projectDir, "apps/client");
await fs.copy(clientAuthDir, projectClientDir, { overwrite: true });
const serverAuthDir = path.join(authTemplateDir, "apps/server/src");
const projectServerDir = path.join(projectDir, "apps/server/src");
await fs.copy(
path.join(serverAuthDir, "lib/trpc.ts"),
path.join(projectServerDir, "lib/trpc.ts"),
{ overwrite: true },
);
await fs.copy(
path.join(serverAuthDir, "routers/index.ts"),
path.join(projectServerDir, "routers/index.ts"),
{ overwrite: true },
);
const contextFileName = `with-${framework}-context.ts`;
await fs.copy(
path.join(serverAuthDir, "lib", contextFileName),
path.join(projectServerDir, "lib/context.ts"),
{ overwrite: true },
);
const indexFileName = `with-${framework}-index.ts`;
await fs.copy(
path.join(serverAuthDir, indexFileName),
path.join(projectServerDir, "index.ts"),
{ overwrite: true },
);
const authLibFileName = getAuthLibDir(orm, database);
const authLibSourceDir = path.join(serverAuthDir, authLibFileName);
if (await fs.pathExists(authLibSourceDir)) {
const files = await fs.readdir(authLibSourceDir);
for (const file of files) {
await fs.copy(
path.join(authLibSourceDir, file),
path.join(projectServerDir, "lib", file),
{ overwrite: true },
);
} }
} else {
await fs.remove(withAuthLibPath);
} }
} }
} }
@@ -81,3 +145,19 @@ function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
return "template/base"; return "template/base";
} }
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
return database === "sqlite"
? "with-drizzle-sqlite-lib"
: "with-drizzle-postgres-lib";
}
if (orm === "prisma") {
return database === "sqlite"
? "with-prisma-sqlite-lib"
: "with-prisma-postgres-lib";
}
throw new Error("Invalid ORM or database configuration for auth setup");
}

View File

@@ -6,6 +6,7 @@ import { createProject } from "./helpers/create-project";
import { installDependencies } from "./helpers/install-dependencies"; import { installDependencies } from "./helpers/install-dependencies";
import { gatherConfig } from "./prompts/config-prompts"; import { gatherConfig } from "./prompts/config-prompts";
import type { import type {
BackendFramework,
ProjectAddons, ProjectAddons,
ProjectConfig, ProjectConfig,
ProjectExamples, ProjectExamples,
@@ -56,6 +57,7 @@ async function main() {
.option("--turso", "Set up Turso for SQLite database") .option("--turso", "Set up Turso for SQLite database")
.option("--no-turso", "Skip Turso setup for SQLite database") .option("--no-turso", "Skip Turso setup for SQLite database")
.option("--hono", "Use Hono backend framework") .option("--hono", "Use Hono backend framework")
.option("--elysia", "Use Elysia backend framework")
.option("--runtime <runtime>", "Specify runtime (bun or node)") .option("--runtime <runtime>", "Specify runtime (bun or node)")
.parse(); .parse();
@@ -68,6 +70,10 @@ async function main() {
const options = program.opts(); const options = program.opts();
const projectDirectory = program.args[0]; const projectDirectory = program.args[0];
let backendFramework: BackendFramework | undefined;
if (options.hono) backendFramework = "hono";
if (options.elysia) backendFramework = "elysia";
const flagConfig: Partial<ProjectConfig> = { const flagConfig: Partial<ProjectConfig> = {
...(projectDirectory && { projectName: projectDirectory }), ...(projectDirectory && { projectName: projectDirectory }),
...(options.database === false && { database: "none" }), ...(options.database === false && { database: "none" }),
@@ -82,7 +88,7 @@ async function main() {
...("git" in options && { git: options.git }), ...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }), ...("install" in options && { noInstall: !options.install }),
...("turso" in options && { turso: options.turso }), ...("turso" in options && { turso: options.turso }),
...(options.hono && { backendFramework: "hono" }), ...(backendFramework && { backendFramework }),
...(options.runtime && { runtime: options.runtime as Runtime }), ...(options.runtime && { runtime: options.runtime as Runtime }),
...((options.pwa || ...((options.pwa ||
options.tauri || options.tauri ||
@@ -124,7 +130,11 @@ async function main() {
database: database:
options.database === false options.database === false
? "none" ? "none"
: (options.database ?? DEFAULT_CONFIG.database), : options.sqlite
? "sqlite"
: options.postgres
? "postgres"
: DEFAULT_CONFIG.database,
orm: orm:
options.database === false options.database === false
? "none" ? "none"
@@ -133,12 +143,10 @@ async function main() {
: options.prisma : options.prisma
? "prisma" ? "prisma"
: DEFAULT_CONFIG.orm, : DEFAULT_CONFIG.orm,
auth: options.auth ?? DEFAULT_CONFIG.auth, auth: "auth" in options ? options.auth : DEFAULT_CONFIG.auth,
git: options.git ?? DEFAULT_CONFIG.git, git: "git" in options ? options.git : DEFAULT_CONFIG.git,
noInstall: noInstall:
"noInstall" in options "install" in options ? !options.install : DEFAULT_CONFIG.noInstall,
? options.noInstall
: DEFAULT_CONFIG.noInstall,
packageManager: packageManager:
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager, flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
addons: flagConfig.addons?.length addons: flagConfig.addons?.length
@@ -153,9 +161,7 @@ async function main() {
: flagConfig.database === "sqlite" : flagConfig.database === "sqlite"
? DEFAULT_CONFIG.turso ? DEFAULT_CONFIG.turso
: false, : false,
backendFramework: options.hono backendFramework: backendFramework ?? DEFAULT_CONFIG.backendFramework,
? "hono"
: DEFAULT_CONFIG.backendFramework,
runtime: options.runtime runtime: options.runtime
? (options.runtime as Runtime) ? (options.runtime as Runtime)
: DEFAULT_CONFIG.runtime, : DEFAULT_CONFIG.runtime,

View File

@@ -1,5 +1,6 @@
import { cancel, isCancel, multiselect } from "@clack/prompts"; import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { ProjectAddons } from "../types"; import type { ProjectAddons } from "../types";
export async function getAddonsChoice( export async function getAddonsChoice(
@@ -31,6 +32,7 @@ export async function getAddonsChoice(
hint: "Add Git hooks with Husky, lint-staged (requires Biome)", hint: "Add Git hooks with Husky, lint-staged (requires Biome)",
}, },
], ],
initialValues: DEFAULT_CONFIG.addons,
required: false, required: false,
}); });

View File

@@ -1,5 +1,6 @@
// import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, select } from "@clack/prompts";
// import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { BackendFramework } from "../types"; import type { BackendFramework } from "../types";
export async function getBackendFrameworkChoice( export async function getBackendFrameworkChoice(
@@ -7,24 +8,27 @@ export async function getBackendFrameworkChoice(
): Promise<BackendFramework> { ): Promise<BackendFramework> {
if (backendFramework !== undefined) return backendFramework; if (backendFramework !== undefined) return backendFramework;
return "hono"; const response = await select<BackendFramework>({
message: "Which backend framework would you like to use?",
options: [
{
value: "hono",
label: "Hono",
hint: "Lightweight, ultrafast web framework",
},
{
value: "elysia",
label: "Elysia",
hint: "TypeScript framework with end-to-end type safety)",
},
],
initialValue: DEFAULT_CONFIG.backendFramework,
});
// const response = await select<BackendFramework>({ if (isCancel(response)) {
// message: "Which backend framework would you like to use?", cancel(pc.red("Operation cancelled"));
// options: [ process.exit(0);
// { }
// value: "hono",
// label: "Hono", return response;
// hint: "Lightweight, ultrafast web framework",
// },
// ],
// initialValue: "hono",
// });
// if (isCancel(response)) {
// cancel(pc.red("Operation cancelled"));
// process.exit(0);
// }
// return response;
} }

View File

@@ -46,8 +46,8 @@ export async function gatherConfig(
projectName: async () => { projectName: async () => {
return getProjectName(flags.projectName); return getProjectName(flags.projectName);
}, },
runtime: () => getRuntimeChoice(flags.runtime),
backendFramework: () => getBackendFrameworkChoice(flags.backendFramework), backendFramework: () => getBackendFrameworkChoice(flags.backendFramework),
runtime: () => getRuntimeChoice(flags.runtime),
database: () => getDatabaseChoice(flags.database), database: () => getDatabaseChoice(flags.database),
orm: ({ results }) => orm: ({ results }) =>
getORMChoice(flags.orm, results.database !== "none"), getORMChoice(flags.orm, results.database !== "none"),

View File

@@ -1,5 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { ProjectDatabase } from "../types"; import type { ProjectDatabase } from "../types";
export async function getDatabaseChoice( export async function getDatabaseChoice(
@@ -26,7 +27,7 @@ export async function getDatabaseChoice(
hint: "Traditional relational database", hint: "Traditional relational database",
}, },
], ],
initialValue: "sqlite", initialValue: DEFAULT_CONFIG.database,
}); });
if (isCancel(response)) { if (isCancel(response)) {

View File

@@ -1,5 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { ProjectOrm } from "../types"; import type { ProjectOrm } from "../types";
export async function getORMChoice( export async function getORMChoice(
@@ -23,7 +24,7 @@ export async function getORMChoice(
hint: "Powerful, feature-rich ORM with schema migrations", hint: "Powerful, feature-rich ORM with schema migrations",
}, },
], ],
initialValue: "drizzle", initialValue: DEFAULT_CONFIG.orm,
}); });
if (isCancel(response)) { if (isCancel(response)) {

View File

@@ -1,5 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { Runtime } from "../types"; import type { Runtime } from "../types";
export async function getRuntimeChoice(runtime?: Runtime): Promise<Runtime> { export async function getRuntimeChoice(runtime?: Runtime): Promise<Runtime> {
@@ -19,7 +20,7 @@ export async function getRuntimeChoice(runtime?: Runtime): Promise<Runtime> {
hint: "Traditional Node.js runtime", hint: "Traditional Node.js runtime",
}, },
], ],
initialValue: "bun", initialValue: DEFAULT_CONFIG.runtime,
}); });
if (isCancel(response)) { if (isCancel(response)) {

View File

@@ -1,12 +1,13 @@
import { cancel, confirm, isCancel } from "@clack/prompts"; import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
export async function getTursoSetupChoice(turso?: boolean): Promise<boolean> { export async function getTursoSetupChoice(turso?: boolean): Promise<boolean> {
if (turso !== undefined) return turso; if (turso !== undefined) return turso;
const response = await confirm({ const response = await confirm({
message: "Set up a Turso database for this project?", message: "Set up a Turso database for this project?",
initialValue: true, initialValue: DEFAULT_CONFIG.turso,
}); });
if (isCancel(response)) { if (isCancel(response)) {

View File

@@ -1,13 +1,15 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none"; export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none"; export type ProjectOrm = "drizzle" | "prisma" | "none";
export type PackageManager = "npm" | "pnpm" | "bun"; export type PackageManager = "npm" | "pnpm" | "bun";
export type ProjectAddons = "pwa" | "tauri" | "biome" | "husky"; export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";
export type BackendFramework = "hono" | "elysia";
export type Runtime = "node" | "bun";
export type ProjectExamples = "todo"; export type ProjectExamples = "todo";
export type BackendFramework = "hono";
export type Runtime = "bun" | "node";
export interface ProjectConfig { export interface ProjectConfig {
projectName: string; projectName: string;
backendFramework: BackendFramework;
runtime: Runtime;
database: ProjectDatabase; database: ProjectDatabase;
orm: ProjectOrm; orm: ProjectOrm;
auth: boolean; auth: boolean;
@@ -17,6 +19,4 @@ export interface ProjectConfig {
packageManager: PackageManager; packageManager: PackageManager;
noInstall?: boolean; noInstall?: boolean;
turso?: boolean; turso?: boolean;
backendFramework: BackendFramework;
runtime: Runtime;
} }

View File

@@ -7,32 +7,55 @@ export function displayConfig(config: Partial<ProjectConfig>) {
if (config.projectName) { if (config.projectName) {
configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`); configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
} }
if (config.database) {
if (config.backendFramework !== undefined) {
configDisplay.push(
`${pc.blue("Backend Framework:")} ${config.backendFramework}`,
);
}
if (config.runtime !== undefined) {
configDisplay.push(`${pc.blue("Runtime:")} ${config.runtime}`);
}
if (config.database !== undefined) {
configDisplay.push(`${pc.blue("Database:")} ${config.database}`); configDisplay.push(`${pc.blue("Database:")} ${config.database}`);
} }
if (config.orm) {
if (config.orm !== undefined) {
configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`); configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`);
} }
if (config.auth !== undefined) { if (config.auth !== undefined) {
configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`); configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`);
} }
if (config.runtime) {
configDisplay.push(`${pc.blue("Runtime:")} ${config.runtime}`); if (config.addons !== undefined) {
const addonsText =
config.addons.length > 0 ? config.addons.join(", ") : "none";
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
} }
if (config.addons?.length) {
configDisplay.push(`${pc.blue("Addons:")} ${config.addons.join(", ")}`); if (config.examples !== undefined) {
const examplesText =
config.examples.length > 0 ? config.examples.join(", ") : "none";
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
} }
if (config.git !== undefined) { if (config.git !== undefined) {
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`); configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);
} }
if (config.packageManager) {
if (config.packageManager !== undefined) {
configDisplay.push( configDisplay.push(
`${pc.blue("Package Manager:")} ${config.packageManager}`, `${pc.blue("Package Manager:")} ${config.packageManager}`,
); );
} }
if (config.noInstall !== undefined) { if (config.noInstall !== undefined) {
configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`); configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`);
} }
if (config.turso !== undefined) { if (config.turso !== undefined) {
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`); configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`);
} }

View File

@@ -5,42 +5,36 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
if (config.database === "none") { if (config.database === "none") {
flags.push("--no-database"); flags.push("--no-database");
} else if (config.database === "sqlite") {
flags.push("--sqlite");
} else if (config.database === "postgres") {
flags.push("--postgres");
}
if (config.database !== "none") {
if (config.orm === "drizzle") {
flags.push("--drizzle");
} else if (config.orm === "prisma") {
flags.push("--prisma");
}
}
if (config.auth) {
flags.push("--auth");
} else { } else {
flags.push("--no-auth"); flags.push(`--${config.database}`);
if (config.orm) {
flags.push(`--${config.orm}`);
} }
if (config.git) { if (config.database === "sqlite") {
flags.push("--git"); flags.push(config.turso ? "--turso" : "--no-turso");
} else { }
flags.push("--no-git");
} }
if (config.noInstall) { flags.push(config.auth ? "--auth" : "--no-auth");
flags.push("--no-install");
} else { flags.push(config.git ? "--git" : "--no-git");
flags.push("--install");
} flags.push(config.noInstall ? "--no-install" : "--install");
if (config.packageManager) { if (config.packageManager) {
flags.push(`--${config.packageManager}`); flags.push(`--${config.packageManager}`);
} }
if (config.backendFramework) {
flags.push(`--${config.backendFramework}`);
}
if (config.runtime) {
flags.push(`--runtime ${config.runtime}`);
}
if (config.addons.length > 0) { if (config.addons.length > 0) {
for (const addon of config.addons) { for (const addon of config.addons) {
flags.push(`--${addon}`); flags.push(`--${addon}`);
@@ -55,21 +49,8 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
flags.push("--no-examples"); flags.push("--no-examples");
} }
if (config.database === "sqlite") {
if (config.turso) {
flags.push("--turso");
} else {
flags.push("--no-turso");
}
}
if (config.runtime) {
flags.push(`--runtime ${config.runtime}`);
}
const baseCommand = "npx create-better-t-stack"; const baseCommand = "npx create-better-t-stack";
const projectName = config.projectName ? ` ${config.projectName}` : ""; const projectName = config.projectName ? ` ${config.projectName}` : "";
const flagString = flags.length > 0 ? ` ${flags.join(" ")}` : "";
return `${baseCommand}${projectName}${flagString}`; return `${baseCommand}${projectName} ${flags.join(" ")}`;
} }

View File

@@ -8,10 +8,8 @@
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server" "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server"
}, },
"dependencies": { "dependencies": {
"@hono/trpc-server": "^0.3.4",
"@trpc/server": "^11.0.0", "@trpc/server": "^11.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"hono": "^4.7.5",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -8,7 +8,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": "./", "baseUrl": "./",
"outDir": "./dist", "outDir": "./dist",
"types": ["node"], "types": ["node", "bun"],
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
}, },

View File

@@ -1,13 +1,13 @@
import type { Context as HonoContext } from "hono"; import type { Context as ElysiaContext } from "elysia";
import { auth } from "./auth"; import { auth } from "./auth";
export type CreateContextOptions = { export type CreateContextOptions = {
hono: HonoContext; context: ElysiaContext;
}; };
export async function createContext({ hono }: CreateContextOptions) { export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: hono.req.raw.headers, headers: context.request.headers,
}); });
return { return {

View File

@@ -2,12 +2,12 @@ import type { Context as HonoContext } from "hono";
import { auth } from "./auth"; import { auth } from "./auth";
export type CreateContextOptions = { export type CreateContextOptions = {
hono: HonoContext; context: HonoContext;
}; };
export async function createContext({ hono }: CreateContextOptions) { export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: hono.req.raw.headers, headers: context.req.raw.headers,
}); });
return { return {

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import * as schema from "../db/schema/auth";
import { db } from "../db"; import { db } from "../db";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {

View File

@@ -0,0 +1,38 @@
import "dotenv/config";
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { auth } from "./lib/auth";
import { createContext } from "./lib/context";
import { appRouter } from "./routers/index";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const app = new Elysia()
.use(
cors({
origin: process.env.CORS_ORIGIN || "",
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
)
.all("/api/auth/*", async (context) => {
const { request } = context;
if (["POST", "GET"].includes(request.method)) {
return auth.handler(request);
} else {
context.error(405);
}
})
.all("/trpc/*", async (context) => {
const res = await fetchRequestHandler({
endpoint: "/trpc",
router: appRouter,
req: context.request,
createContext: () => createContext({ context }),
});
return res;
})
.get("/", () => "OK")
.listen(3000, () => {
console.log(`Server is running on http://localhost:3000`);
});

View File

@@ -1,3 +1,4 @@
import { trpcServer } from "@hono/trpc-server"; import { trpcServer } from "@hono/trpc-server";
import "dotenv/config"; import "dotenv/config";
import { Hono } from "hono"; import { Hono } from "hono";
@@ -27,8 +28,8 @@ app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({
router: appRouter, router: appRouter,
createContext: (_opts, hono) => { createContext: (_opts, context) => {
return createContext({ hono }); return createContext({ context });
}, },
}), }),
); );

View File

@@ -1,44 +0,0 @@
import { z } from "zod";
import { router, publicProcedure } from "../lib/trpc";
import { todo } from "../db/schema";
import { eq } from "drizzle-orm";
import { db } from "../db";
export const todoRouter = router({
getAll: publicProcedure.query(async () => {
return await db.select().from(todo).all();
}),
create: publicProcedure
.input(z.object({ text: z.string().min(1) }))
.mutation(async ({ input }) => {
return await db
.insert(todo)
.values({
text: input.text,
})
.returning()
.get();
}),
toggle: publicProcedure
.input(z.object({ id: z.number(), completed: z.boolean() }))
.mutation(async ({ input }) => {
return await db
.update(todo)
.set({ completed: input.completed })
.where(eq(todo.id, input.id))
.returning()
.get();
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db
.delete(todo)
.where(eq(todo.id, input.id))
.returning()
.get();
}),
});

View File

@@ -1,18 +0,0 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -1,24 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -1,24 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,27 @@
import "dotenv/config";
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { createContext } from "./lib/context";
import { appRouter } from "./routers/index";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const app = new Elysia()
.use(
cors({
origin: process.env.CORS_ORIGIN || "",
methods: ["GET", "POST", "OPTIONS"],
}),
)
.all("/trpc/*", async (context) => {
const res = await fetchRequestHandler({
endpoint: "/trpc",
router: appRouter,
req: context.request,
createContext: () => createContext({ context }),
});
return res;
})
.get("/", () => "OK")
.listen(3000, () => {
console.log(`Server is running on http://localhost:3000`);
});

View File

@@ -0,0 +1,13 @@
import type { Context as ElysiaContext } from "elysia";
export type CreateContextOptions = {
context: ElysiaContext;
};
export async function createContext({ context }: CreateContextOptions) {
return {
session: null,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -22,8 +22,8 @@ app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({
router: appRouter, router: appRouter,
createContext: (_opts, hono) => { createContext: (_opts, context) => {
return createContext({ hono }); return createContext({ context });
}, },
}), }),
); );

View File

@@ -1,10 +1,10 @@
import type { Context as HonoContext } from "hono"; import type { Context as HonoContext } from "hono";
export type CreateContextOptions = { export type CreateContextOptions = {
hono: HonoContext; context: HonoContext;
}; };
export async function createContext({ hono }: CreateContextOptions) { export async function createContext({ context }: CreateContextOptions) {
return { return {
session: null, session: null,
}; };

View File

@@ -1,18 +0,0 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -1,24 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -1,24 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});