Add runtime selection feature between Bun and Node.js

This commit is contained in:
Aman Varshney
2025-03-26 01:40:39 +05:30
parent 45cd2fc113
commit 88afd53a4d
22 changed files with 10432 additions and 224 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Add runtime selection feature between Bun and Node.js

View File

@@ -19,6 +19,7 @@ Follow the prompts to configure your project.
- **Monorepo**: Turborepo for optimized build system and workspace management - **Monorepo**: Turborepo for optimized build system and workspace management
- **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components - **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components
- **Backend**: Hono, tRPC - **Backend**: Hono, tRPC
- **Runtime Options**: Choose between Bun or Node.js for your server
- **Database Options**: SQLite (via Turso), PostgreSQL, or no database - **Database Options**: SQLite (via Turso), PostgreSQL, or no database
- **ORM Selection**: Choose between Drizzle ORM or Prisma - **ORM Selection**: Choose between Drizzle ORM or Prisma
- **Authentication**: Optional auth setup with Better-Auth - **Authentication**: Optional auth setup with Better-Auth
@@ -60,6 +61,7 @@ Options:
--no-install Skip installing dependencies --no-install Skip installing dependencies
--turso Set up Turso for SQLite database (default with sqlite) --turso Set up Turso for SQLite database (default with sqlite)
--no-turso Skip Turso setup for SQLite database --no-turso Skip Turso setup for SQLite database
--runtime <runtime> Specify runtime (bun or node)
-h, --help Display help -h, --help Display help
``` ```
@@ -75,6 +77,11 @@ Create a project with specific options:
npx create-better-t-stack my-app --postgres --prisma --auth --pwa --biome npx create-better-t-stack my-app --postgres --prisma --auth --pwa --biome
``` ```
Create a project with Node.js runtime:
```bash
npx create-better-t-stack my-app --runtime node
```
## License ## License
MIT MIT

View File

@@ -12,10 +12,13 @@ export const DEFAULT_CONFIG: ProjectConfig = {
orm: "drizzle", orm: "drizzle",
auth: true, auth: true,
addons: [], addons: [],
examples: [],
git: true, git: true,
packageManager: "npm", packageManager: "npm",
noInstall: false, noInstall: false,
examples: ["todo"], turso: false,
backendFramework: "hono",
runtime: "bun",
}; };
export const dependencyVersionMap = { export const dependencyVersionMap = {
@@ -39,6 +42,12 @@ export const dependencyVersionMap = {
husky: "^9.1.7", husky: "^9.1.7",
"lint-staged": "^15.5.0", "lint-staged": "^15.5.0",
"@hono/node-server": "^1.14.0",
tsx: "^4.19.2",
"@types/node": "^22.13.11",
"@types/bun": "^1.2.6",
} as const; } as const;
export type AvailableDependencies = keyof typeof dependencyVersionMap; export type AvailableDependencies = keyof typeof dependencyVersionMap;

View File

@@ -11,6 +11,7 @@ import { setupEnvironmentVariables } from "./env-setup";
import { setupExamples } from "./examples-setup"; import { setupExamples } from "./examples-setup";
import { displayPostInstallInstructions } from "./post-installation"; import { displayPostInstallInstructions } from "./post-installation";
import { initializeGit, updatePackageConfigurations } from "./project-config"; import { initializeGit, updatePackageConfigurations } from "./project-config";
import { setupRuntime } from "./runtime-setup";
import { import {
copyBaseTemplate, copyBaseTemplate,
fixGitignoreFiles, fixGitignoreFiles,
@@ -38,6 +39,8 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.auth, options.auth,
); );
await setupRuntime(projectDir, options.runtime);
await setupExamples( await setupExamples(
projectDir, projectDir,
options.examples, options.examples,
@@ -73,6 +76,7 @@ export async function createProject(options: ProjectConfig): Promise<string> {
!options.noInstall, !options.noInstall,
options.orm, options.orm,
options.addons, options.addons,
options.runtime,
); );
return projectDir; return projectDir;

View File

@@ -1,6 +1,12 @@
import path from "node:path"; import path from "node:path";
import fs from "fs-extra"; import fs from "fs-extra";
import type { ProjectConfig, ProjectDatabase, ProjectOrm } from "../types"; import type {
ProjectAddons,
ProjectConfig,
ProjectDatabase,
ProjectOrm,
Runtime,
} from "../types";
export async function createReadme(projectDir: string, options: ProjectConfig) { export async function createReadme(projectDir: string, options: ProjectConfig) {
const readmePath = path.join(projectDir, "README.md"); const readmePath = path.join(projectDir, "README.md");
@@ -21,6 +27,7 @@ function generateReadmeContent(options: ProjectConfig): string {
auth, auth,
addons = [], addons = [],
orm = "drizzle", orm = "drizzle",
runtime = "bun",
} = options; } = options;
const packageManagerRunCmd = const packageManagerRunCmd =
@@ -32,7 +39,7 @@ This project was created with [Better-T-Stack](https://github.com/better-t-stack
## Features ## Features
${generateFeaturesList(database, auth, addons, orm)} ${generateFeaturesList(database, auth, addons, orm, runtime)}
## Getting Started ## Getting Started
@@ -71,38 +78,46 @@ ${generateScriptsList(packageManagerRunCmd, database, orm, auth)}
function generateFeaturesList( function generateFeaturesList(
database: ProjectDatabase, database: ProjectDatabase,
auth: boolean, auth: boolean,
features: string[], addons: ProjectAddons[],
orm: ProjectOrm, orm: ProjectOrm,
runtime: Runtime,
): string { ): string {
const featuresList = [ const addonsList = [
"- **TypeScript** - For type safety and improved developer experience", "- **TypeScript** - For type safety and improved developer experience",
"- **TanStack Router** - File-based routing with full type safety", "- **TanStack Router** - File-based routing with full type safety",
"- **TailwindCSS** - Utility-first CSS for rapid UI development", "- **TailwindCSS** - Utility-first CSS for rapid UI development",
"- **shadcn/ui** - Reusable UI components", "- **shadcn/ui** - Reusable UI components",
"- **Hono** - Lightweight, performant server framework", "- **Hono** - Lightweight, performant server framework",
"- **tRPC** - End-to-end type-safe APIs", "- **tRPC** - End-to-end type-safe APIs",
`- **${runtime === "bun" ? "Bun" : "Node.js"}** - Runtime environment`,
]; ];
if (database !== "none") { if (database !== "none") {
featuresList.push( addonsList.push(
`- **${orm === "drizzle" ? "Drizzle" : "Prisma"}** - TypeScript-first ORM`, `- **${orm === "drizzle" ? "Drizzle" : "Prisma"}** - TypeScript-first ORM`,
`- **${database === "sqlite" ? "SQLite/Turso" : "PostgreSQL"}** - Database engine`, `- **${database === "sqlite" ? "SQLite/Turso" : "PostgreSQL"}** - Database engine`,
); );
} }
if (auth) { if (auth) {
featuresList.push( addonsList.push(
"- **Authentication** - Email & password authentication with Better Auth", "- **Authentication** - Email & password authentication with Better Auth",
); );
} }
for (const feature of features) { for (const addon of addons) {
if (feature === "docker") { if (addon === "pwa") {
featuresList.push("- **Docker** - Containerized deployment"); addonsList.push("- **PWA** - Progressive Web App support");
} else if (addon === "tauri") {
addonsList.push("- **Tauri** - Build native desktop applications");
} else if (addon === "biome") {
addonsList.push("- **Biome** - Linting and formatting");
} else if (addon === "husky") {
addonsList.push("- **Husky** - Git hooks for code quality");
} }
} }
return featuresList.join("\n"); return addonsList.join("\n");
} }
function generateDatabaseSetup( function generateDatabaseSetup(

View File

@@ -5,6 +5,7 @@ import type {
ProjectAddons, ProjectAddons,
ProjectDatabase, ProjectDatabase,
ProjectOrm, ProjectOrm,
Runtime,
} from "../types"; } from "../types";
export function displayPostInstallInstructions( export function displayPostInstallInstructions(
@@ -14,6 +15,7 @@ export function displayPostInstallInstructions(
depsInstalled: boolean, depsInstalled: boolean,
orm?: ProjectOrm, orm?: ProjectOrm,
addons?: ProjectAddons[], addons?: ProjectAddons[],
runtime?: Runtime,
) { ) {
const runCmd = packageManager === "npm" ? "npm run" : packageManager; const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`; const cdCmd = `cd ${projectName}`;
@@ -21,7 +23,9 @@ export function displayPostInstallInstructions(
addons?.includes("husky") || addons?.includes("biome"); addons?.includes("husky") || addons?.includes("biome");
const databaseInstructions = const databaseInstructions =
database !== "none" ? getDatabaseInstructions(database, orm, runCmd) : ""; database !== "none"
? getDatabaseInstructions(database, orm, runCmd, runtime)
: "";
const tauriInstructions = addons?.includes("tauri") const tauriInstructions = addons?.includes("tauri")
? getTauriInstructions(runCmd) ? getTauriInstructions(runCmd)
: ""; : "";
@@ -49,6 +53,7 @@ function getDatabaseInstructions(
database: ProjectDatabase, database: ProjectDatabase,
orm?: ProjectOrm, orm?: ProjectOrm,
runCmd?: string, runCmd?: string,
runtime?: Runtime,
): string { ): string {
const instructions = []; const instructions = [];
@@ -59,6 +64,13 @@ function getDatabaseInstructions(
`${pc.dim("Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso")}`, `${pc.dim("Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso")}`,
); );
} }
if (runtime === "bun") {
instructions.push(
`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors, follow the guidance provided in the error messages`,
);
}
instructions.push( instructions.push(
`${pc.cyan("•")} Apply schema: ${pc.dim(`${runCmd} db:push`)}`, `${pc.cyan("•")} Apply schema: ${pc.dim(`${runCmd} db:push`)}`,
); );

View File

@@ -0,0 +1,94 @@
import path from "node:path";
import fs from "fs-extra";
import type { Runtime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime(
projectDir: string,
runtime: Runtime,
): Promise<void> {
const serverDir = path.join(projectDir, "apps/server");
const serverIndexPath = path.join(serverDir, "src/index.ts");
const indexContent = await fs.readFile(serverIndexPath, "utf-8");
if (runtime === "bun") {
await setupBunRuntime(serverDir, serverIndexPath, indexContent);
} else if (runtime === "node") {
await setupNodeRuntime(serverDir, serverIndexPath, indexContent);
}
}
async function setupBunRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
dev: "bun run --hot src/index.ts",
start: "bun run dist/src/index.js",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
addPackageDependency({
devDependencies: ["@types/bun"],
projectDir: serverDir,
});
if (!indexContent.includes("export default app")) {
const updatedContent = `${indexContent}\n\nexport default app;\n`;
await fs.writeFile(serverIndexPath, updatedContent);
}
}
async function setupNodeRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
): 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);
packageJson.scripts = {
...packageJson.scripts,
dev: "tsx watch src/index.ts",
start: "node dist/src/index.js",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
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}\`);
},
);\n`;
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);
}
}

View File

@@ -5,7 +5,12 @@ import { DEFAULT_CONFIG } from "./constants";
import { createProject } from "./helpers/create-project"; 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 { ProjectAddons, ProjectConfig, ProjectExamples } from "./types"; import type {
ProjectAddons,
ProjectConfig,
ProjectExamples,
Runtime,
} from "./types";
import { displayConfig } from "./utils/display-config"; import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command"; import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
@@ -50,6 +55,8 @@ async function main() {
.option("--no-install", "Skip installing dependencies") .option("--no-install", "Skip installing dependencies")
.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("--runtime <runtime>", "Specify runtime (bun or node)")
.parse(); .parse();
const s = spinner(); const s = spinner();
@@ -70,11 +77,13 @@ async function main() {
...(options.prisma && { orm: "prisma" }), ...(options.prisma && { orm: "prisma" }),
...("auth" in options && { auth: options.auth }), ...("auth" in options && { auth: options.auth }),
...(options.npm && { packageManager: "npm" }), ...(options.npm && { packageManager: "npm" }),
...(options.pnpm && { packageManager: " pnpm" }), ...(options.pnpm && { packageManager: "pnpm" }),
...(options.bun && { packageManager: "bun" }), ...(options.bun && { packageManager: "bun" }),
...("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" }),
...(options.runtime && { runtime: options.runtime as Runtime }),
...((options.pwa || ...((options.pwa ||
options.tauri || options.tauri ||
options.biome || options.biome ||
@@ -144,6 +153,12 @@ async function main() {
: flagConfig.database === "sqlite" : flagConfig.database === "sqlite"
? DEFAULT_CONFIG.turso ? DEFAULT_CONFIG.turso
: false, : false,
backendFramework: options.hono
? "hono"
: DEFAULT_CONFIG.backendFramework,
runtime: options.runtime
? (options.runtime as Runtime)
: DEFAULT_CONFIG.runtime,
} }
: await gatherConfig(flagConfig); : await gatherConfig(flagConfig);

View File

@@ -0,0 +1,30 @@
// import { cancel, isCancel, select } from "@clack/prompts";
// import pc from "picocolors";
import type { BackendFramework } from "../types";
export async function getBackendFrameworkChoice(
backendFramework?: BackendFramework,
): 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",
// },
// ],
// initialValue: "hono",
// });
// if (isCancel(response)) {
// cancel(pc.red("Operation cancelled"));
// process.exit(0);
// }
// return response;
}

View File

@@ -1,15 +1,18 @@
import { cancel, group } from "@clack/prompts"; import { cancel, group } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { import type {
BackendFramework,
PackageManager, PackageManager,
ProjectAddons, ProjectAddons,
ProjectConfig, ProjectConfig,
ProjectDatabase, ProjectDatabase,
ProjectExamples, ProjectExamples,
ProjectOrm, ProjectOrm,
Runtime,
} from "../types"; } from "../types";
import { getAddonsChoice } from "./addons"; import { getAddonsChoice } from "./addons";
import { getAuthChoice } from "./auth"; import { getAuthChoice } from "./auth";
import { getBackendFrameworkChoice } from "./backend-framework";
import { getDatabaseChoice } from "./database"; import { getDatabaseChoice } from "./database";
import { getExamplesChoice } from "./examples"; import { getExamplesChoice } from "./examples";
import { getGitChoice } from "./git"; import { getGitChoice } from "./git";
@@ -17,9 +20,10 @@ import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm"; import { getORMChoice } from "./orm";
import { getPackageManagerChoice } from "./package-manager"; import { getPackageManagerChoice } from "./package-manager";
import { getProjectName } from "./project-name"; import { getProjectName } from "./project-name";
import { getRuntimeChoice } from "./runtime";
import { getTursoSetupChoice } from "./turso"; import { getTursoSetupChoice } from "./turso";
interface PromptGroupResults { type PromptGroupResults = {
projectName: string; projectName: string;
database: ProjectDatabase; database: ProjectDatabase;
orm: ProjectOrm; orm: ProjectOrm;
@@ -30,7 +34,9 @@ interface PromptGroupResults {
packageManager: PackageManager; packageManager: PackageManager;
noInstall: boolean; noInstall: boolean;
turso: boolean; turso: boolean;
} backendFramework: BackendFramework;
runtime: Runtime;
};
export async function gatherConfig( export async function gatherConfig(
flags: Partial<ProjectConfig>, flags: Partial<ProjectConfig>,
@@ -40,6 +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),
database: () => getDatabaseChoice(flags.database), database: () => getDatabaseChoice(flags.database),
orm: ({ results }) => orm: ({ results }) =>
getORMChoice(flags.orm, results.database !== "none"), getORMChoice(flags.orm, results.database !== "none"),
@@ -75,5 +83,7 @@ export async function gatherConfig(
packageManager: result.packageManager, packageManager: result.packageManager,
noInstall: result.noInstall, noInstall: result.noInstall,
turso: result.turso, turso: result.turso,
backendFramework: result.backendFramework,
runtime: result.runtime,
}; };
} }

View File

@@ -1,6 +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 type { PackageManager } from "../types"; import type { PackageManager, Runtime } from "../types";
import { getUserPkgManager } from "../utils/get-package-manager"; import { getUserPkgManager } from "../utils/get-package-manager";
export async function getPackageManagerChoice( export async function getPackageManagerChoice(

View File

@@ -0,0 +1,31 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import type { Runtime } from "../types";
export async function getRuntimeChoice(runtime?: Runtime): Promise<Runtime> {
if (runtime !== undefined) return runtime;
const response = await select<Runtime>({
message: "Which runtime would you like to use?",
options: [
{
value: "bun",
label: "Bun",
hint: "Fast all-in-one JavaScript runtime",
},
{
value: "node",
label: "Node.js",
hint: "Traditional Node.js runtime",
},
],
initialValue: "bun",
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}

View File

@@ -3,6 +3,8 @@ 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" | "tauri" | "biome" | "husky";
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;
@@ -15,4 +17,6 @@ export interface ProjectConfig {
packageManager: PackageManager; packageManager: PackageManager;
noInstall?: boolean; noInstall?: boolean;
turso?: boolean; turso?: boolean;
backendFramework: BackendFramework;
runtime: Runtime;
} }

View File

@@ -16,6 +16,9 @@ export function displayConfig(config: Partial<ProjectConfig>) {
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?.length) { if (config.addons?.length) {
configDisplay.push(`${pc.blue("Addons:")} ${config.addons.join(", ")}`); configDisplay.push(`${pc.blue("Addons:")} ${config.addons.join(", ")}`);
} }

View File

@@ -63,6 +63,10 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
} }
} }
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(" ")}` : ""; const flagString = flags.length > 0 ? ` ${flags.join(" ")}` : "";

View File

@@ -3,15 +3,11 @@
"main": "src/index.ts", "main": "src/index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts",
"start": "node dist/src/index.js",
"build": "tsc && tsc-alias", "build": "tsc && tsc-alias",
"dev:bun": "bun run --hot src/index.ts",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"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/node-server": "^1.14.0",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@trpc/server": "^11.0.0", "@trpc/server": "^11.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
@@ -19,9 +15,7 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"tsc-alias": "^1.8.11", "tsc-alias": "^1.8.11",
"tsx": "^4.19.2",
"@types/node": "^22.13.11",
"typescript": "^5.8.2" "typescript": "^5.8.2"
} }
} }

View File

@@ -1,4 +1,3 @@
import { serve } from "@hono/node-server";
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";
@@ -12,30 +11,23 @@ const app = new Hono();
app.use(logger()); app.use(logger());
app.use( app.use(
"/*", "/*",
cors({ cors({
origin: process.env.CORS_ORIGIN || "", origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
}), }),
); );
app.use( app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({
router: appRouter, router: appRouter,
createContext: (_opts, hono) => { createContext: (_opts, hono) => {
return createContext({ hono }); return createContext({ hono });
}, },
}), }),
); );
app.get("/", (c) => { app.get("/", (c) => {
return c.text("OK"); return c.text("OK");
});
serve({
fetch: app.fetch,
port: 3000,
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
}); });

View File

@@ -31,10 +31,10 @@ export default function SignInForm({
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Sign in successful");
navigate({ navigate({
to: "/dashboard", to: "/dashboard",
}); });
toast.success("Sign in successful");
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message); toast.error(error.error.message);

View File

@@ -9,156 +9,156 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
export default function SignUpForm({ export default function SignUpForm({
onSwitchToSignIn, onSwitchToSignIn,
}: { }: {
onSwitchToSignIn: () => void; onSwitchToSignIn: () => void;
}) { }) {
const navigate = useNavigate({ const navigate = useNavigate({
from: "/", from: "/",
}); });
const { isPending } = authClient.useSession(); const { isPending } = authClient.useSession();
const form = useForm({ const form = useForm({
defaultValues: { defaultValues: {
email: "", email: "",
password: "", password: "",
name: "", name: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
await authClient.signUp.email( await authClient.signUp.email(
{ {
email: value.email, email: value.email,
password: value.password, password: value.password,
name: value.name, name: value.name,
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Sign up successful"); navigate({
navigate({ to: "/dashboard",
to: "/dashboard", });
}); toast.success("Sign up successful");
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message); toast.error(error.error.message);
}, },
}, },
); );
}, },
validators: { validators: {
onSubmit: z.object({ onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"), name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"), email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"), password: z.string().min(6, "Password must be at least 6 characters"),
}), }),
}, },
}); });
if (isPending) { if (isPending) {
return <Loader />; return <Loader />;
} }
return ( return (
<div className="mx-auto mt-10 max-w-md p-6"> <div className="mx-auto mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1> <h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
void form.handleSubmit(); void form.handleSubmit();
}} }}
className="space-y-4" className="space-y-4"
> >
<div> <div>
<form.Field name="name"> <form.Field name="name">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Name</Label> <Label htmlFor={field.name}>Name</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<div> <div>
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Email</Label> <Label htmlFor={field.name}>Email</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="email" type="email"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<div> <div>
<form.Field name="password"> <form.Field name="password">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Password</Label> <Label htmlFor={field.name}>Password</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="password" type="password"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
disabled={!state.canSubmit || state.isSubmitting} disabled={!state.canSubmit || state.isSubmitting}
> >
{state.isSubmitting ? "Submitting..." : "Sign Up"} {state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button> </Button>
)} )}
</form.Subscribe> </form.Subscribe>
</form> </form>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Button <Button
variant="link" variant="link"
onClick={onSwitchToSignIn} onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800" className="text-indigo-600 hover:text-indigo-800"
> >
Already have an account? Sign In Already have an account? Sign In
</Button> </Button>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,4 +1,3 @@
import { serve } from "@hono/node-server";
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";
@@ -13,34 +12,27 @@ const app = new Hono();
app.use(logger()); app.use(logger());
app.use( app.use(
"/*", "/*",
cors({ cors({
origin: process.env.CORS_ORIGIN || "", origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"], allowHeaders: ["Content-Type", "Authorization"],
credentials: true, credentials: true,
}), }),
); );
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw)); app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use( app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({
router: appRouter, router: appRouter,
createContext: (_opts, hono) => { createContext: (_opts, hono) => {
return createContext({ hono }); return createContext({ hono });
}, },
}), }),
); );
app.get("/", (c) => { app.get("/", (c) => {
return c.text("OK"); return c.text("OK");
});
serve({
fetch: app.fetch,
port: 3000,
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
}); });

View File

@@ -1,19 +1,17 @@
import { protectedProcedure, publicProcedure, router } from "../lib/trpc";
import { router, publicProcedure, protectedProcedure } from "../lib/trpc";
import { todoRouter } from "./todo"; import { todoRouter } from "./todo";
export const appRouter = router({ export const appRouter = router({
healthCheck: publicProcedure.query(() => { healthCheck: publicProcedure.query(() => {
return "OK"; return "OK";
}), }),
privateData: protectedProcedure.query(({ ctx }) => { privateData: protectedProcedure.query(({ ctx }) => {
return { return {
message: "This is private", message: "This is private",
user: ctx.session.user, user: ctx.session.user,
}; };
}), }),
todo: todoRouter, todo: todoRouter,
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

9989
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff