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": {
"create-better-t-stack": "dist/index.js"
},
"files": [
"dist",
"template"
],
"files": ["dist", "template"],
"keywords": [],
"repository": {
"type": "git",

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ProjectConfig } from "./types";
import { getUserPkgManager } from "./utils/get-package-manager";
const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename);
@@ -14,7 +15,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
addons: [],
examples: [],
git: true,
packageManager: "npm",
packageManager: getUserPkgManager(),
noInstall: false,
turso: false,
backendFramework: "hono",
@@ -48,6 +49,15 @@ export const dependencyVersionMap = {
"@types/node": "^22.13.11",
"@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;
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 { setupAddons } from "./addons-setup";
import { setupAuth } from "./auth-setup";
import { setupBackendDependencies } from "./backend-framework-setup";
import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup";
@@ -16,6 +17,7 @@ import {
copyBaseTemplate,
fixGitignoreFiles,
setupAuthTemplate,
setupBackendFramework,
setupOrmTemplate,
} from "./template-manager";
@@ -27,10 +29,14 @@ export async function createProject(options: ProjectConfig): Promise<string> {
await fs.ensureDir(projectDir);
await copyBaseTemplate(projectDir);
await fixGitignoreFiles(projectDir);
await setupAuthTemplate(projectDir, options.auth);
await setupBackendFramework(projectDir, options.backendFramework);
await setupBackendDependencies(
projectDir,
options.backendFramework,
options.runtime,
);
await setupOrmTemplate(
projectDir,
@@ -39,15 +45,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.auth,
);
await setupRuntime(projectDir, options.runtime);
await setupExamples(
projectDir,
options.examples,
options.orm,
options.auth,
);
await setupDatabase(
projectDir,
options.database,
@@ -55,8 +52,24 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.turso ?? options.database === "sqlite",
);
await setupAuthTemplate(
projectDir,
options.auth,
options.backendFramework,
options.orm,
options.database,
);
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 initializeGit(projectDir, options.git);
@@ -66,7 +79,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
}
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
displayPostInstallInstructions(

View File

@@ -33,9 +33,7 @@ export function displayPostInstallInstructions(
? getLintingInstructions(runCmd)
: "";
log.info(`${pc.cyan("Project created successfully!")}
${pc.bold("Next steps:")}
log.info(`${pc.bold("Next steps:")}
${pc.cyan("1.")} ${cdCmd}
${!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 fs from "fs-extra";
import type { Runtime } from "../types";
import type { BackendFramework, Runtime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime(
projectDir: string,
runtime: Runtime,
backendFramework: BackendFramework,
): Promise<void> {
const serverDir = path.join(projectDir, "apps/server");
const serverIndexPath = path.join(serverDir, "src/index.ts");
@@ -13,9 +14,19 @@ export async function setupRuntime(
const indexContent = await fs.readFile(serverIndexPath, "utf-8");
if (runtime === "bun") {
await setupBunRuntime(serverDir, serverIndexPath, indexContent);
await setupBunRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
} else if (runtime === "node") {
await setupNodeRuntime(serverDir, serverIndexPath, indexContent);
await setupNodeRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
}
}
@@ -23,6 +34,7 @@ async function setupBunRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
backendFramework: BackendFramework,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
@@ -40,7 +52,7 @@ async function setupBunRuntime(
projectDir: serverDir,
});
if (!indexContent.includes("export default app")) {
if (backendFramework === "hono") {
const updatedContent = `${indexContent}\n\nexport default app;\n`;
await fs.writeFile(serverIndexPath, updatedContent);
}
@@ -50,13 +62,8 @@ async function setupNodeRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
backendFramework: BackendFramework,
): Promise<void> {
addPackageDependency({
dependencies: ["@hono/node-server"],
devDependencies: ["tsx", "@types/node"],
projectDir: serverDir,
});
const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
@@ -68,27 +75,62 @@ async function setupNodeRuntime(
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
const importLine = 'import { serve } from "@hono/node-server";\n';
const serverCode = `
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 serverCode = `
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(\`Server is running on http://localhost:\${info.port}\`);
},
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(\`Server is running on http://localhost:\${info.port}\`);
},
);\n`;
if (!indexContent.includes("@hono/node-server")) {
const importEndIndex = indexContent.lastIndexOf("import");
const importSection = indexContent.substring(0, importEndIndex);
const restOfFile = indexContent.substring(importEndIndex);
if (!indexContent.includes("@hono/node-server")) {
const importEndIndex = indexContent.lastIndexOf("import");
const importSection = indexContent.substring(0, importEndIndex);
const restOfFile = indexContent.substring(importEndIndex);
const updatedContent = importSection + importLine + restOfFile + serverCode;
await fs.writeFile(serverIndexPath, updatedContent);
} else if (!indexContent.includes("serve(")) {
const updatedContent = indexContent + serverCode;
await fs.writeFile(serverIndexPath, updatedContent);
const updatedContent =
importSection + importLine + restOfFile + serverCode;
await fs.writeFile(serverIndexPath, updatedContent);
}
} 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);
}
}
}

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import fs from "fs-extra";
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> {
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);
}
export async function setupAuthTemplate(
export async function setupBackendFramework(
projectDir: string,
auth: boolean,
framework: BackendFramework,
): Promise<void> {
if (!auth) return;
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, projectDir, { overwrite: true });
}
}
@@ -36,17 +34,83 @@ export async function setupOrmTemplate(
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const serverSrcPath = path.join(projectDir, "apps/server/src");
const libPath = path.join(serverSrcPath, "lib");
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
if (auth) {
if (await fs.pathExists(withAuthLibPath)) {
await fs.remove(libPath);
await fs.move(withAuthLibPath, libPath);
if (!auth) {
if (orm === "prisma") {
const authSchemaPath = path.join(
projectDir,
"apps/server/prisma/schema/auth.prisma",
);
if (await fs.pathExists(authSchemaPath)) {
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";
}
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 { gatherConfig } from "./prompts/config-prompts";
import type {
BackendFramework,
ProjectAddons,
ProjectConfig,
ProjectExamples,
@@ -56,6 +57,7 @@ async function main() {
.option("--turso", "Set up Turso for SQLite database")
.option("--no-turso", "Skip Turso setup for SQLite database")
.option("--hono", "Use Hono backend framework")
.option("--elysia", "Use Elysia backend framework")
.option("--runtime <runtime>", "Specify runtime (bun or node)")
.parse();
@@ -68,6 +70,10 @@ async function main() {
const options = program.opts();
const projectDirectory = program.args[0];
let backendFramework: BackendFramework | undefined;
if (options.hono) backendFramework = "hono";
if (options.elysia) backendFramework = "elysia";
const flagConfig: Partial<ProjectConfig> = {
...(projectDirectory && { projectName: projectDirectory }),
...(options.database === false && { database: "none" }),
@@ -82,7 +88,7 @@ async function main() {
...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }),
...("turso" in options && { turso: options.turso }),
...(options.hono && { backendFramework: "hono" }),
...(backendFramework && { backendFramework }),
...(options.runtime && { runtime: options.runtime as Runtime }),
...((options.pwa ||
options.tauri ||
@@ -124,7 +130,11 @@ async function main() {
database:
options.database === false
? "none"
: (options.database ?? DEFAULT_CONFIG.database),
: options.sqlite
? "sqlite"
: options.postgres
? "postgres"
: DEFAULT_CONFIG.database,
orm:
options.database === false
? "none"
@@ -133,12 +143,10 @@ async function main() {
: options.prisma
? "prisma"
: DEFAULT_CONFIG.orm,
auth: options.auth ?? DEFAULT_CONFIG.auth,
git: options.git ?? DEFAULT_CONFIG.git,
auth: "auth" in options ? options.auth : DEFAULT_CONFIG.auth,
git: "git" in options ? options.git : DEFAULT_CONFIG.git,
noInstall:
"noInstall" in options
? options.noInstall
: DEFAULT_CONFIG.noInstall,
"install" in options ? !options.install : DEFAULT_CONFIG.noInstall,
packageManager:
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
addons: flagConfig.addons?.length
@@ -153,9 +161,7 @@ async function main() {
: flagConfig.database === "sqlite"
? DEFAULT_CONFIG.turso
: false,
backendFramework: options.hono
? "hono"
: DEFAULT_CONFIG.backendFramework,
backendFramework: backendFramework ?? DEFAULT_CONFIG.backendFramework,
runtime: options.runtime
? (options.runtime as Runtime)
: DEFAULT_CONFIG.runtime,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { Runtime } from "../types";
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",
},
],
initialValue: "bun",
initialValue: DEFAULT_CONFIG.runtime,
});
if (isCancel(response)) {

View File

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

View File

@@ -1,13 +1,15 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none";
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 BackendFramework = "hono";
export type Runtime = "bun" | "node";
export interface ProjectConfig {
projectName: string;
backendFramework: BackendFramework;
runtime: Runtime;
database: ProjectDatabase;
orm: ProjectOrm;
auth: boolean;
@@ -17,6 +19,4 @@ export interface ProjectConfig {
packageManager: PackageManager;
noInstall?: boolean;
turso?: boolean;
backendFramework: BackendFramework;
runtime: Runtime;
}

View File

@@ -7,32 +7,55 @@ export function displayConfig(config: Partial<ProjectConfig>) {
if (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}`);
}
if (config.orm) {
if (config.orm !== undefined) {
configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`);
}
if (config.auth !== undefined) {
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) {
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);
}
if (config.packageManager) {
if (config.packageManager !== undefined) {
configDisplay.push(
`${pc.blue("Package Manager:")} ${config.packageManager}`,
);
}
if (config.noInstall !== undefined) {
configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`);
}
if (config.turso !== undefined) {
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`);
}

View File

@@ -5,42 +5,36 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
if (config.database === "none") {
flags.push("--no-database");
} else if (config.database === "sqlite") {
flags.push("--sqlite");
} else if (config.database === "postgres") {
flags.push("--postgres");
}
} else {
flags.push(`--${config.database}`);
if (config.database !== "none") {
if (config.orm === "drizzle") {
flags.push("--drizzle");
} else if (config.orm === "prisma") {
flags.push("--prisma");
if (config.orm) {
flags.push(`--${config.orm}`);
}
if (config.database === "sqlite") {
flags.push(config.turso ? "--turso" : "--no-turso");
}
}
if (config.auth) {
flags.push("--auth");
} else {
flags.push("--no-auth");
}
flags.push(config.auth ? "--auth" : "--no-auth");
if (config.git) {
flags.push("--git");
} else {
flags.push("--no-git");
}
flags.push(config.git ? "--git" : "--no-git");
if (config.noInstall) {
flags.push("--no-install");
} else {
flags.push("--install");
}
flags.push(config.noInstall ? "--no-install" : "--install");
if (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) {
for (const addon of config.addons) {
flags.push(`--${addon}`);
@@ -55,21 +49,8 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
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 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"
},
"dependencies": {
"@hono/trpc-server": "^0.3.4",
"@trpc/server": "^11.0.0",
"dotenv": "^16.4.7",
"hono": "^4.7.5",
"zod": "^3.24.2"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@
"skipLibCheck": true,
"baseUrl": "./",
"outDir": "./dist",
"types": ["node"],
"types": ["node", "bun"],
"jsx": "react-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";
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({
headers: hono.req.raw.headers,
headers: context.request.headers,
});
return {

View File

@@ -2,12 +2,12 @@ import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
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({
headers: hono.req.raw.headers,
headers: context.req.raw.headers,
});
return {

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import * as schema from "../db/schema/auth";
import { db } from "../db";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
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 "dotenv/config";
import { Hono } from "hono";
@@ -27,8 +28,8 @@ app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: (_opts, hono) => {
return createContext({ hono });
createContext: (_opts, context) => {
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/*",
trpcServer({
router: appRouter,
createContext: (_opts, hono) => {
return createContext({ hono });
createContext: (_opts, context) => {
return createContext({ context });
},
}),
);

View File

@@ -1,10 +1,10 @@
import type { Context as HonoContext } from "hono";
export type CreateContextOptions = {
hono: HonoContext;
context: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
export async function createContext({ context }: CreateContextOptions) {
return {
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,
},
});
});