UNPKG

create-elysiajs

Version:

Scaffolding your Elysia project with the environment with easy!

1,641 lines (1,570 loc) 53.8 kB
#!/usr/bin/env node 'use strict'; var fs = require('node:fs/promises'); var path = require('node:path'); var enquirer = require('enquirer'); var minimist = require('minimist'); var task = require('tasuku'); var child_process = require('node:child_process'); var node_crypto = require('node:crypto'); var node_util = require('node:util'); function dedent(templ) { var values = []; for (var _i = 1; _i < arguments.length; _i++) { values[_i - 1] = arguments[_i]; } var strings = Array.from(typeof templ === "string" ? [templ] : templ); strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, ""); var indentLengths = strings.reduce(function(arr, str) { var matches = str.match(/\n([\t ]+|(?!\s).)/g); if (matches) { return arr.concat(matches.map(function(match) { var _a, _b; return (_b = (_a = match.match(/[\t ]/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; })); } return arr; }, []); if (indentLengths.length) { var pattern_1 = new RegExp("\n[ ]{" + Math.min.apply(Math, indentLengths) + "}", "g"); strings = strings.map(function(str) { return str.replace(pattern_1, "\n"); }); } strings[0] = strings[0].replace(/^\r?\n/, ""); var string = strings[0]; values.forEach(function(value, i) { var endentations = string.match(/(?:^|\n)( *)$/); var endentation = endentations ? endentations[1] : ""; var indentedValue = value; if (typeof value === "string" && value.includes("\n")) { indentedValue = String(value).split("\n").map(function(str, i2) { return i2 === 0 ? str : "" + endentation + str; }).join("\n"); } string += indentedValue + strings[i + 1]; }); return string; } const dependencies = { "elysia": "^1.2.25", typescript: "^5.8.2", "@types/bun": "^1.2.8", "@biomejs/biome": "^1.9.4", "eslint": "^9.23.0", "eslint-plugin-drizzle": "^0.2.3", "prisma": "^6.5.0", "@prisma/client": "^6.5.0", "drizzle-orm": "^0.41.0", "drizzle-kit": "^0.30.6", "pg": "^8.14.1", "@types/pg": "^8.11.11", postgres: "^3.4.5", "mysql2": "^3.14.0", husky: "^9.1.7", "@elysiajs/bearer": "^1.2.0", "@elysiajs/cors": "^1.2.0", "@elysiajs/html": "^1.2.0", "@kitajs/ts-html-plugin": "^4.1.1", "@elysiajs/jwt": "^1.2.0", "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "elysia-autoload": "^1.5.1", "@bogeychan/elysia-logger": "^0.1.8", "@antfu/eslint-config": "^4.11.0", "@gramio/init-data": "^0.0.3", "elysia-oauth2": "^2.0.0", "arctic": "^3.6.0", "env-var": "^7.5.0", "posthog-node": "^4.11.1", jobify: "^0.1.6", "ioredis": "^5.6.0", "@verrou/core": "^0.5.1", "@aws-sdk/client-s3": "^3.779.0", "@elysiajs/eden": "^1.2.0", "ioredis-mock": "^8.9.0", "@electric-sql/pglite": "^0.2.17", gramio: "^0.2.5" }; const nodeMajorVersion = process?.versions?.node?.split(".")[0]; if (nodeMajorVersion && Number(nodeMajorVersion) < 22) console.warn( `Node.js version ${process?.versions?.node} is not recommended for this template. Please upgrade to Node.js 22 or higher.` ); function detectPackageManager() { const userAgent = process.env.npm_config_user_agent; if (!userAgent) throw new Error( `Package manager was not detected. Please specify template with "--pm bun"` ); return userAgent.split(" ")[0].split("/")[0]; } async function createOrFindDir(path) { await fs.stat(path).catch(async () => fs.mkdir(path)); } class Preferences { projectName = ""; dir = ""; packageManager = "bun"; runtime = "Bun"; linter = "None"; orm = "None"; database = "PostgreSQL"; driver = "None"; git = true; others = []; plugins = []; // integration with create-gramio isMonorepo = false; docker = false; vscode = false; redis = false; locks = false; s3Client = "None"; meta = { databasePassword: node_crypto.randomBytes(12).toString("hex") }; noInstall = false; mockWithPGLite = false; telegramRelated = false; } const exec = node_util.promisify(child_process.exec); const pmExecuteMap = { npm: "npx", bun: "bun x", yarn: "yarn dlx", pnpm: "pnpm dlx" }; const pmRunMap = { npm: "npm run", bun: "bun", yarn: "yarn", pnpm: "pnpm" }; const pmLockFilesMap = { npm: "package.lock.json", bun: "bun.lock", yarn: "yarn.lock", pnpm: "pnpm-lock.yaml" }; const pmInstallFrozenLockfile = { npm: "npm ci", bun: "bun install --frozen-lockfile", yarn: "yarn install --frozen-lockfile", pnpm: "pnpm install --frozen-lockfile" }; const pmInstallFrozenLockfileProduction = { npm: "npm ci --production", bun: "bun install --frozen-lockfile --production", yarn: "yarn install --frozen-lockfile --production", pnpm: "pnpm install --frozen-lockfile --prod" }; function getPackageJson({ dir, projectName, linter, packageManager, orm, driver, others, plugins, isMonorepo, locks, redis, mockWithPGLite, telegramRelated, s3Client }) { const sample = { name: projectName, type: "module", scripts: { dev: packageManager === "bun" ? "bun --watch src/index.ts" : `${pmExecuteMap[packageManager]} tsx watch --env-file .env src/index.ts`, start: packageManager === "bun" ? "NODE_ENV=production bun run ./src/index.ts" : `NODE_ENV=production ${pmExecuteMap[packageManager]} tsx --env-file=.env --env-file=.env.production src/index.ts` }, dependencies: { elysia: dependencies.elysia, "env-var": dependencies["env-var"] }, devDependencies: { typescript: dependencies.typescript } }; sample.devDependencies["@types/bun"] = dependencies["@types/bun"]; if (linter === "Biome") { sample.scripts.lint = `${pmExecuteMap[packageManager]} @biomejs/biome check src`; sample.scripts["lint:fix"] = `${pmRunMap[packageManager]} lint --write`; sample.devDependencies["@biomejs/biome"] = dependencies["@biomejs/biome"]; } if (linter === "ESLint") { sample.scripts.lint = `${pmExecuteMap[packageManager]} eslint`; sample.scripts["lint:fix"] = `${pmExecuteMap[packageManager]} eslint --fix`; sample.devDependencies.eslint = dependencies.eslint; sample.devDependencies["@antfu/eslint-config"] = dependencies["@antfu/eslint-config"]; if (orm === "Drizzle") sample.devDependencies["eslint-plugin-drizzle"] = dependencies["eslint-plugin-drizzle"]; } if (orm === "Prisma") { sample.devDependencies.prisma = dependencies.prisma; sample.dependencies["@prisma/client"] = dependencies["@prisma/client"]; } if (orm === "Drizzle") { sample.dependencies["drizzle-orm"] = dependencies["drizzle-orm"]; sample.devDependencies["drizzle-kit"] = dependencies["drizzle-kit"]; if (driver === "node-postgres") { sample.dependencies.pg = dependencies.pg; sample.devDependencies["@types/pg"] = dependencies["@types/pg"]; } if (driver === "Postgres.JS") { sample.dependencies.postgres = dependencies.postgres; } if (driver === "MySQL 2") { sample.dependencies.mysql2 = dependencies.mysql2; } sample.scripts.generate = `${pmExecuteMap[packageManager]} drizzle-kit generate`; sample.scripts.push = `${pmExecuteMap[packageManager]} drizzle-kit push`; sample.scripts.migrate = `${pmExecuteMap[packageManager]} drizzle-kit migrate`; sample.scripts.studio = `${pmExecuteMap[packageManager]} drizzle-kit studio`; } if (others.includes("Husky")) { sample.devDependencies.husky = dependencies.husky; sample.scripts.prepare = "husky"; } if (plugins.includes("Bearer")) sample.dependencies["@elysiajs/bearer"] = dependencies["@elysiajs/bearer"]; if (plugins.includes("CORS")) sample.dependencies["@elysiajs/cors"] = dependencies["@elysiajs/cors"]; if (plugins.includes("HTML/JSX")) { sample.dependencies["@elysiajs/html"] = dependencies["@elysiajs/html"]; sample.dependencies["@kitajs/ts-html-plugin"] = dependencies["@kitajs/ts-html-plugin"]; } if (plugins.includes("JWT")) sample.dependencies["@elysiajs/jwt"] = dependencies["@elysiajs/jwt"]; if (plugins.includes("Server Timing")) sample.dependencies["@elysiajs/server-timing"] = dependencies["@elysiajs/server-timing"]; if (plugins.includes("Static")) sample.dependencies["@elysiajs/static"] = dependencies["@elysiajs/static"]; if (plugins.includes("Swagger")) sample.dependencies["@elysiajs/swagger"] = dependencies["@elysiajs/swagger"]; if (plugins.includes("Autoload")) sample.dependencies["elysia-autoload"] = dependencies["elysia-autoload"]; if (plugins.includes("Logger")) sample.dependencies["@bogeychan/elysia-logger"] = dependencies["@bogeychan/elysia-logger"]; if (plugins.includes("Oauth 2.0")) { sample.dependencies.arctic = dependencies.arctic; sample.dependencies["elysia-oauth2"] = dependencies["elysia-oauth2"]; } if (redis) { sample.dependencies.ioredis = dependencies.ioredis; if (mockWithPGLite) sample.devDependencies["ioredis-mock"] = dependencies["ioredis-mock"]; } if (others.includes("Jobify")) { sample.dependencies.jobify = dependencies.jobify; } if (others.includes("Posthog")) { sample.dependencies["posthog-node"] = dependencies["posthog-node"]; } if (locks) { sample.dependencies["@verrou/core"] = dependencies["@verrou/core"]; } if (isMonorepo) sample.dependencies["@gramio/init-data"] = dependencies["@gramio/init-data"]; if (others.includes("S3") && s3Client === "@aws-sdk/client-s3") { sample.dependencies["@aws-sdk/client-s3"] = dependencies["@aws-sdk/client-s3"]; } if (mockWithPGLite) { sample.devDependencies["@electric-sql/pglite"] = dependencies["@electric-sql/pglite"]; sample.devDependencies["@elysiajs/eden"] = dependencies["@elysiajs/eden"]; } if (telegramRelated && !isMonorepo) { sample.dependencies.gramio = dependencies.gramio; sample.dependencies["@gramio/init-data"] = dependencies["@gramio/init-data"]; } return JSON.stringify(sample, null, 2); } function getElysiaIndex({ orm, driver, plugins, telegramRelated, isMonorepo }) { const elysiaPlugins = []; const elysiaImports = [ `import { Elysia } from "elysia"`, `import { config } from "./config.ts"` ]; if (plugins.includes("Logger")) { elysiaImports.push(`import { logger } from "@bogeychan/elysia-logger"`); elysiaPlugins.push(".use(logger())"); } if (plugins.includes("Swagger")) { elysiaImports.push(`import { swagger } from "@elysiajs/swagger"`); elysiaPlugins.push(".use(swagger())"); } if (plugins.includes("Oauth 2.0")) { elysiaImports.push(`import { oauth2 } from "elysia-oauth2"`); elysiaPlugins.push(".use(oauth2({}))"); } if (plugins.includes("Bearer")) { elysiaImports.push(`import { bearer } from "@elysiajs/bearer"`); elysiaPlugins.push(".use(bearer())"); } if (plugins.includes("CORS")) { elysiaImports.push(`import { cors } from "@elysiajs/cors"`); elysiaPlugins.push(".use(cors())"); } if (plugins.includes("HTML/JSX")) { elysiaImports.push(`import { html } from "@elysiajs/html"`); elysiaPlugins.push(".use(html())"); } if (plugins.includes("JWT")) { elysiaImports.push(`import { jwt } from "@elysiajs/jwt"`); elysiaPlugins.push(".use(jwt({ secret: config.JWT_SECRET }))"); } if (plugins.includes("Server Timing")) { elysiaImports.push( `import { serverTiming } from "@elysiajs/server-timing"` ); elysiaPlugins.push(".use(serverTiming())"); } if (plugins.includes("Static")) { elysiaImports.push(`import { staticPlugin } from "@elysiajs/static"`); elysiaPlugins.push(".use(staticPlugin())"); } if (plugins.includes("Autoload")) { elysiaImports.push(`import { autoload } from "elysia-autoload"`); elysiaPlugins.push(".use(autoload())"); } elysiaPlugins.push(`.get("/", "Hello World")`); if (telegramRelated && !isMonorepo) { elysiaImports.push(`import { bot } from "./bot.ts"`); elysiaImports.push(`import { webhookHandler } from "./services/auth.ts"`); elysiaPlugins.push( `.post(\`/\${config.BOT_TOKEN}\`, webhookHandler(bot, "elysia"), { detail: { hide: true, }, })` ); } return [ ...elysiaImports, "", "export const app = new Elysia()", ...elysiaPlugins, plugins.includes("Autoload") ? "\nexport type ElysiaApp = typeof app" : "" ].join("\n"); } function getInstallCommands({ linter, orm, database, git, others }) { const commands = []; if (git) commands.push("git init"); commands.push("bun install"); if (others.includes("Husky") && linter !== "None") commands.push(`echo "bun lint:fix" > .husky/pre-commit`); if (orm === "Prisma") commands.push( `bunx prisma init --datasource-provider ${database.toLowerCase()}` ); if (linter === "Biome") commands.push("bunx @biomejs/biome init"); if (linter !== "None") commands.push("bun lint:fix"); return commands; } const driverNamesToDrizzle = { "node-postgres": "node-postgres", "Bun.sql": "bun-sql", "Postgres.JS": "postgres-js", "MySQL 2": "mysql2", "Bun SQLite": "bun-sqlite", None: "" }; const driverNames = { "node-postgres": "pg", "Bun.sql": "??", "Postgres.JS": "postgres", "MySQL 2": "mysql2", "Bun SQLite": "bun:sqlite", None: "" }; function getDBIndex({ orm, driver, packageManager }) { if (orm === "Prisma") return [ `import { PrismaClient } from "@prisma/client"`, "", "export const prisma = new PrismaClient()", "", `export * from "@prisma/client"` ].join("\n"); if (driver === "node-postgres") return [ `import { drizzle } from "drizzle-orm/node-postgres"`, `import { Client } from "pg"`, `import { config } from "../config.ts"`, "", "export const client = new Client({", " connectionString: config.DATABASE_URL,", "})", "", "export const db = drizzle({", " client,", ' casing: "snake_case",', "})" ].join("\n"); if (driver === "Postgres.JS") return [ `import { drizzle } from "drizzle-orm/postgres-js"`, `import postgres from "postgres"`, `import { config } from "../config.ts"`, "", "const client = postgres(config.DATABASE_URL)", "export const db = drizzle({", " client,", ' casing: "snake_case",', "})" ].join("\n"); if (driver === "Bun.sql") return [ `import { drizzle } from "drizzle-orm/bun-sql"`, `import { config } from "../config.ts"`, `import { SQL } from "bun"`, "", "export const sql = new SQL(config.DATABASE_URL)", "", "export const db = drizzle({", " client: sql,", ' casing: "snake_case",', "})" ].join("\n"); if (driver === "MySQL 2") return [ `import { drizzle } from "drizzle-orm/mysql2"`, `import mysql from "mysql2/promise"`, `import { config } from "../config.ts"`, "", "export const connection = await mysql.createConnection(config.DATABASE_URL)", `console.log("\u{1F5C4}\uFE0F Database was connected!")`, "", "export const db = drizzle({", " client: connection,", ' casing: "snake_case",', "})" ].join("\n"); if (driver === "Bun SQLite" && packageManager === "bun") return [ `import { drizzle } from "drizzle-orm/bun-sqlite"`, `import { Database } from "bun:sqlite";`, "", `export const sqlite = new Database("sqlite.db")`, "export const db = drizzle({", " client: sqlite,", ' casing: "snake_case",', "})" ].join("\n"); return [ `import { drizzle } from "drizzle-orm/better-sqlite3`, `import { Database } from "better-sqlite3";`, "", `export const sqlite = new Database("sqlite.db")`, "export const db = drizzle({", " client: sqlite,", ' casing: "snake_case",', "})" ].join("\n"); } function getDrizzleConfig({ database }) { return [ `import type { Config } from "drizzle-kit"`, `import env from "env-var"`, "", 'const DATABASE_URL = env.get("DATABASE_URL").required().asString()', "", "export default {", ` schema: "./src/db/schema.ts",`, ` out: "./drizzle",`, ` dialect: "${database.toLowerCase()}",`, ` casing: "snake_case",`, " dbCredentials: {", " url: DATABASE_URL", " }", "} satisfies Config" ].join("\n"); } function getTSConfig({ plugins }) { return JSON.stringify( { compilerOptions: { lib: ["ESNext"], module: "NodeNext", target: "ESNext", moduleResolution: "NodeNext", esModuleInterop: true, strict: true, skipLibCheck: true, allowSyntheticDefaultImports: true, noEmit: true, allowImportingTsExtensions: true, noUncheckedIndexedAccess: true, ...plugins.includes("HTML/JSX") ? { jsx: "react", jsxFactory: "Html.createElement", jsxFragmentFactory: "Html.Fragment", plugins: [{ name: "@kitajs/ts-html-plugin" }] } : {} }, include: ["src"] }, null, 2 ); } const connectionURLExamples = { PostgreSQL: "postgresql://root:mypassword@localhost:5432/mydb", MySQL: "mysql://root:mypassword@localhost:3306/mydb", SQLServer: "sqlserver://localhost:1433;database=mydb;user=root;password=mypassword;", CockroachDB: "postgresql://root:mypassword@localhost:26257/mydb?schema=public", MongoDB: "mongodb+srv://root:mypassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority", SQLite: "file:./sqlite.db" }; const composeServiceNames = { PostgreSQL: "postgres", MySQL: "localhost", SQLServer: "localhost", CockroachDB: "localhost", MongoDB: "localhost", SQLite: "file:./sqlite.db" }; function getEnvFile({ database, orm, plugins, projectName, redis, meta, telegramRelated }, isComposed = false) { const envs = []; if (orm !== "None") { let url = connectionURLExamples[database].replace("mydb", projectName).replace("root", projectName).replace("mypassword", meta.databasePassword); if (isComposed) url = url.replace("localhost", composeServiceNames[database]); envs.push(`DATABASE_URL="${url}"`); } if (telegramRelated) { envs.push(`BOT_TOKEN=""`); } if (isComposed && redis) envs.push("REDIS_HOST=redis"); if (plugins.includes("JWT")) envs.push(`JWT_SECRET="${node_crypto.randomBytes(12).toString("hex")}"`); envs.push("PORT=3000"); return envs.join("\n"); } function getConfigFile({ orm, redis, others, plugins, locks, telegramRelated }) { const envs = []; envs.push(`PORT: env.get("PORT").default(3000).asPortNumber()`); envs.push( `API_URL: env.get("API_URL").default(\`https://\${env.get("PUBLIC_DOMAIN").asString()}\`).asString()` ); if (telegramRelated) { envs.push(`BOT_TOKEN: env.get("BOT_TOKEN").required().asString()`); } if (orm !== "None") envs.push(`DATABASE_URL: env.get("DATABASE_URL").required().asString()`); if (redis) { envs.push( `REDIS_HOST: env.get("REDIS_HOST").default("localhost").asString()` ); } if (others.includes("Posthog")) { envs.push( `POSTHOG_API_KEY: env.get("POSTHOG_API_KEY").default("it's a secret").asString()` ); envs.push( `POSTHOG_HOST: env.get("POSTHOG_HOST").default("localhost").asString()` ); } if (others.includes("S3")) { envs.push( `S3_ENDPOINT: env.get("S3_ENDPOINT").default("localhost").asString()` ); envs.push( `S3_ACCESS_KEY_ID: env.get("S3_ACCESS_KEY_ID").default("minio").asString()` ); envs.push( `S3_SECRET_ACCESS_KEY: env.get("S3_SECRET_ACCESS_KEY").default("minio").asString()` ); } if (locks) { const stores = ["memory"]; if (redis) stores.push("redis"); envs.push( `LOCK_STORE: env.get("LOCK_STORE").default("memory").asEnum(${JSON.stringify(stores)})` ); } if (plugins.includes("JWT")) envs.push(`JWT_SECRET: env.get("JWT_SECRET").required().asString()`); return dedent` import env from "env-var"; export const config = { NODE_ENV: env .get("NODE_ENV") .default("development") .asEnum(["production", "test", "development"]), ${envs.join(",\n")} }`; } const links = { Bun: "[Bun](https://bun.sh/)", ElysiaJS: "[ElysiaJS](https://elysiajs.com/)", ESLint: "[ESLint](https://eslint.org/)", Biome: "[Biome](https://biomejs.dev/)", Prisma: "[Prisma](https://www.prisma.io/)", Drizzle: "[Drizzle](https://orm.drizzle.team/)", CORS: "[CORS](https://elysiajs.com/plugins/cors.html)", Swagger: "[Swagger](https://elysiajs.com/plugins/swagger.html)", JWT: "[JWT](https://elysiajs.com/plugins/jwt.html)", Autoload: "[Autoload](https://github.com/kravetsone/elysia-autoload)", "Oauth 2.0": "[Oauth 2.0](https://github.com/kravetsone/elysia-oauth2)", Logger: "[Logger](https://github.com/bogeychan/elysia-logger)", "HTML/JSX": "[HTML/JSX](https://elysiajs.com/plugins/html.html)", Static: "[Static](https://elysiajs.com/plugins/static.html)", Bearer: "[Bearer](https://elysiajs.com/plugins/bearer.html)", "Server Timing": "[Server Timing](https://elysiajs.com/plugins/server-timing.html)", Husky: "[Husky](https://typicode.github.io/husky/)", PostgreSQL: "[PostgreSQL](https://www.postgresql.org/)", MySQL: "[MySQL](https://www.mysql.com/)", MongoDB: "[MongoDB](https://www.mongodb.com/)", SQLite: "[SQLite](https://sqlite.org/)", SQLServer: "[SQLServer](https://www.microsoft.com/sql-server)", CockroachDB: "[CockroachDB](https://www.cockroachlabs.com/)", Jobify: "[Jobify](https://github.com/kravetsone/jobify)", Docker: "[Docker](https://www.docker.com/)", Posthog: "[Posthog](https://posthog.com/docs/libraries/node)", PGLite: "[PGLite](https://pglite.dev/)", S3: "[Minio](https://github.com/minio/minio)", Redis: "[Redis](https://redis.io/) + [ioredis](https://github.com/redis/ioredis)", IoRedisMock: "[ioredis-mock](https://www.npmjs.com/package/ioredis-mock)" }; const TESTS_REPO_LINK = "[tests](tree/main/tests)"; function getReadme({ dir, linter, orm, database, plugins, others, docker, mockWithPGLite, redis }) { const stack = []; stack.push(`- Web framework - ${links.ElysiaJS}`); if (linter !== "None") stack.push(`- Linter - ${links[linter]}`); if (orm !== "None") stack.push( `- ORM - ${links[orm]} (${links[database]})${mockWithPGLite ? ` (mocked with ${links.PGLite} in ${TESTS_REPO_LINK})` : ""}` ); if (plugins.length) stack.push(`- Elysia plugins - ${plugins.map((x) => links[x]).join(", ")}`); if (others.length) stack.push( `- Others tools - ${[ docker ? links.Docker : void 0, redis ? mockWithPGLite ? `${links.Redis} + ${links.IoRedisMock} in tests` : links.Redis : void 0, ...others.map((x) => links[x]) ].filter(Boolean).join(", ")}` ); const instruction = []; instruction.push("## Development\n"); if (docker) { instruction.push( "Start development services (DB, Redis etc):\n", "```bash", "docker compose -f docker-compose.dev.yml up", "```\n" ); } instruction.push("Start the project:\n", "```bash", "bun dev", "```\n"); if (orm === "Drizzle") { instruction.push( "## Migrations\n", "Push schema to Database:\n", "```bash", "bunx drizzle-kit push", "```", "Generate new migration:\n", "```bash", "bunx drizzle-kit generate", "```", "Apply migrations:\n", "```bash", "bunx drizzle-kit migrate", "```\n" ); } if (orm === "Prisma") { instruction.push( "## Migrations\n", "Generate new migration:\n", "```bash", "bunx prisma migrate dev", "```", "Apply migrations:\n", "```bash", "bunx prisma migrate deploy", "```\n" ); } if (mockWithPGLite) { instruction.push( "## Tests\n", `Tests are written with ${links.Bun}:test. `, "Mocks:\n", `- Postgres usage is mocked with ${links.PGLite}`, `- Redis usage is mocked with ${links.IoRedisMock}`, "\n", "```bash", "bun test", "```\n" ); } instruction.push("## Production\n"); if (docker) { instruction.push( "Run project in `production` mode:\n", "```bash", "docker compose up -d", "```" ); } else instruction.push( "Run project in `production` mode:\n", "```bash", "bun start", "```" ); return [ `# ${dir}`, "", "This template autogenerated by [create-elysiajs](https://github.com/kravetsone/create-elysiajs)", "", "### Stack", ...stack, "", // "### Instructions", ...instruction ].join("\n"); } function generateEslintConfig({ orm }) { return [ `import antfu from "@antfu/eslint-config"`, orm === "Drizzle" && `import drizzle from "eslint-plugin-drizzle";`, ` export default antfu( { stylistic: { indent: 2, quotes: "double", }, }, { files: ["**/*.js", "**/*.ts"], rules: { "node/prefer-global/process": "off", "no-console": "off", "antfu/no-top-level-await": "off", },`, orm === "Drizzle" && `plugins: { drizzle, },`, ` }, ); ` ].filter(Boolean).join("\n"); } const dbExportedMap = { Prisma: "prisma", Drizzle: "client" }; function getIndex({ others, orm, driver, telegramRelated, isMonorepo }) { const isShouldConnectToDB = orm !== "None" && driver !== "Postgres.JS" && driver !== "MySQL 2" && driver !== "Bun SQLite" && driver !== "Bun.sql"; const gracefulShutdownTasks = []; const imports = [ // `import { bot } from "./bot.ts"`, `import { config } from "./config.ts"` ]; const startUpTasks = []; imports.push(`import { app } from "./server.ts"`); gracefulShutdownTasks.push("await app.stop()"); if (others.includes("Posthog")) { imports.push(`import { posthog } from "./services/posthog.ts"`); gracefulShutdownTasks.push("await posthog.shutdown()"); } if (isShouldConnectToDB) { imports.push(`import { ${dbExportedMap[orm]} } from "./db/index.ts"`); startUpTasks.push(dedent` ${orm === "Prisma" ? "await prisma.$connect()" : "await client.connect()"} console.log("🗄️ Database was connected!")`); } startUpTasks.push( /*ts*/ ` app.listen(config.PORT, () => console.log(\`\u{1F98A} Server started at \${app.server?.url.origin}\`)) ` ); if (telegramRelated && !isMonorepo) { imports.push(`import { bot } from "./bot.ts"`); startUpTasks.push(dedent` if (config.NODE_ENV === "production") await bot.start({ webhook: { url: \`\${config.API_URL}/\${config.BOT_TOKEN}\`, }, }); else await bot.start();`); } return dedent` ${imports.join("\n")} const signals = ["SIGINT", "SIGTERM"]; for (const signal of signals) { process.on(signal, async () => { console.log(\`Received \${signal}. Initiating graceful shutdown...\`); ${gracefulShutdownTasks.join("\n")} process.exit(0); }) } process.on("uncaughtException", (error) => { console.error(error); }) process.on("unhandledRejection", (error) => { console.error(error); }) ${startUpTasks.join("\n")}`; } function getBotFile() { return dedent` import { Bot } from "gramio"; import { config } from "./config.ts"; export const bot = new Bot(config.BOT_TOKEN) .onStart(({ info }) => console.log(\`✨ Bot \${info.username} was started!\`))`; } const ormDockerCopy = { Prisma: "COPY --from=prerelease /usr/src/app/prisma ./prisma", Drizzle: dedent` COPY --from=prerelease /usr/src/app/drizzle ./drizzle COPY --from=prerelease /usr/src/app/drizzle.config.ts .` }; function getDockerfile({ packageManager, orm }) { if (packageManager === "bun") return dedent` # use the official Bun image # see all versions at https://hub.docker.com/r/oven/bun/tags FROM oven/bun:${process.versions.bun ?? "1.2.5"} AS base WORKDIR /usr/src/app # install dependencies into temp directory # this will cache them and speed up future builds FROM base AS install RUN mkdir -p /temp/dev COPY package.json bun.lock /temp/dev/ RUN cd /temp/dev && bun install --frozen-lockfile # install with --production (exclude devDependencies) RUN mkdir -p /temp/prod COPY package.json bun.lock /temp/prod/ RUN cd /temp/prod && bun install --frozen-lockfile --production # copy node_modules from temp directory # then copy all (non-ignored) project files into the image FROM base AS prerelease COPY --from=install /temp/dev/node_modules node_modules COPY . . ENV NODE_ENV=production RUN ${pmExecuteMap[packageManager]} tsc --noEmit # copy production dependencies and source code into final image FROM base AS release COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /usr/src/app/.env . COPY --from=prerelease /usr/src/app/.env.production . COPY --from=prerelease /usr/src/app/${pmLockFilesMap[packageManager]} . RUN mkdir -p /usr/src/app/src COPY --from=prerelease /usr/src/app/src ./src COPY --from=prerelease /usr/src/app/package.json . COPY --from=prerelease /usr/src/app/tsconfig.json . ${orm !== "None" ? ormDockerCopy[orm] : ""} ENTRYPOINT [ "bun", "start" ]`; return dedent` # Use the official Node.js 22 image. # See https://hub.docker.com/_/node for more information. FROM node:${process?.versions?.node ?? "22.12"} AS base # Create app directory WORKDIR /usr/src/app ${packageManager !== "npm" ? "npm install ${packageManager} -g" : ""} # Install dependencies into temp directory # This will cache them and speed up future builds FROM base AS install RUN mkdir -p /temp/dev COPY package.json ${pmLockFilesMap[packageManager]} /temp/dev/ RUN cd /temp/dev && ${pmInstallFrozenLockfile[packageManager]} # Install with --production (exclude devDependencies) RUN mkdir -p /temp/prod COPY package.json ${pmLockFilesMap[packageManager]} /temp/prod/ RUN cd /temp/prod && ${pmInstallFrozenLockfileProduction[packageManager]} # Copy node_modules from temp directory # Then copy all (non-ignored) project files into the image FROM base AS prerelease COPY --from=install /temp/dev/node_modules node_modules COPY . . ENV NODE_ENV=production RUN ${pmExecuteMap[packageManager]} tsc --noEmit # Copy production dependencies and source code into final image FROM base AS release COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /usr/src/app/.env . COPY --from=prerelease /usr/src/app/.env.production . RUN mkdir -p /usr/src/app/src COPY --from=prerelease /usr/src/app/src ./src COPY --from=prerelease /usr/src/app/${pmLockFilesMap[packageManager]} . COPY --from=prerelease /usr/src/app/package.json . COPY --from=prerelease /usr/src/app/tsconfig.json . ${orm !== "None" ? ormDockerCopy[orm] : ""} # TODO:// should be downloaded not at ENTRYPOINT ENTRYPOINT [ "${pmRunMap[packageManager]}", "start" ]`; } function getDockerCompose({ database, redis, projectName, meta, others }) { const volumes = []; if (database === "PostgreSQL") volumes.push("postgres_data:"); if (redis) volumes.push("redis_data:"); if (others.includes("S3")) volumes.push("minio_data:"); const services = [ /* yaml */ `bot: container_name: ${projectName}-bot restart: unless-stopped build: context: . dockerfile: Dockerfile environment: - NODE_ENV=production`, database === "PostgreSQL" ? ( /* yaml */ `postgres: container_name: ${projectName}-postgres image: postgres:latest restart: unless-stopped environment: - POSTGRES_USER=${projectName} - POSTGRES_PASSWORD=${meta.databasePassword} - POSTGRES_DB=${projectName} volumes: - postgres_data:/var/lib/postgresql/data` ) : "", redis ? ( /* yaml */ `redis: container_name: ${projectName}-redis image: redis:latest command: [ "redis-server", "--maxmemory-policy", "noeviction" ] restart: unless-stopped volumes: - redis_data:/data` ) : "", others.includes("S3") ? ( /* yaml */ `minio: container_name: ${projectName}-minio image: minio/minio:latest command: [ "minio", "server", "/data", "--console-address", ":9001" ] restart: unless-stopped environment: - MINIO_ACCESS_KEY=${projectName} - MINIO_SECRET_KEY=${meta.databasePassword} ports: - 9000:9000 - 9001:9001 volumes: - minio_data:/data healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 5` ) : "" ]; return dedent` services: ${services.filter(Boolean).join("\n")} volumes: ${volumes.join("\n")} networks: default: {} `; } function getDevelopmentDockerCompose({ database, redis, projectName, meta, others }) { const volumes = []; if (database === "PostgreSQL") volumes.push("postgres_data:"); if (redis) volumes.push("redis_data:"); if (others.includes("S3")) volumes.push("minio_data:"); const services = [ database === "PostgreSQL" ? ( /* yaml */ `postgres: container_name: ${projectName}-postgres image: postgres:latest restart: unless-stopped environment: - POSTGRES_USER=${projectName} - POSTGRES_PASSWORD=${meta.databasePassword} - POSTGRES_DB=${projectName} ports: - 5432:5432 volumes: - postgres_data:/var/lib/postgresql/data` ) : "", redis ? ( /* yaml */ `redis: container_name: ${projectName}-redis image: redis:latest command: [ "redis-server", "--maxmemory-policy", "noeviction" ] restart: unless-stopped ports: - 6379:6379 volumes: - redis_data:/data` ) : "", others.includes("S3") ? ( /* yaml */ `minio: container_name: ${projectName}-minio image: minio/minio:latest command: [ "minio", "server", "/data", "--console-address", ":9001" ] restart: unless-stopped environment: - MINIO_ACCESS_KEY=${projectName} - MINIO_SECRET_KEY=${meta.databasePassword} volumes: - minio_data:/data healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 5` ) : "" ]; return dedent` services: ${services.filter(Boolean).join("\n")} volumes: ${volumes.join("\n")} networks: default: {} `; } function getAuthPlugin() { return dedent` import { validateAndParseInitData, signInitData, getBotTokenSecretKey } from "@gramio/init-data"; import { Elysia, t } from "elysia"; import { config } from "../config.ts"; const secretKey = getBotTokenSecretKey(config.BOT_TOKEN); export const authElysia = new Elysia({ name: "auth", }) .guard({ headers: t.Object({ "x-init-data": t.String({ examples: [ signInitData( { user: { id: 1, first_name: "durov", username: "durov", }, }, secretKey ), ], }), }), response: { 401: t.Literal("UNAUTHORIZED"), }, }) .resolve(({ headers, error }) => { const result = validateAndParseInitData( headers["x-init-data"], secretKey ); if (!result || !result.user) return error("Unauthorized", "UNAUTHORIZED"); return { tgId: result.user.id, user: result.user, }; }) .as("plugin");`; } function getJobifyFile() { return dedent` import { initJobify } from "jobify" import { redis } from "./redis.ts" export const defineJob = initJobify(redis); `; } function getLocksFile({ redis }) { const imports = []; const stores = []; stores.push("memory: { driver: memoryStore() }"); imports.push(`import { memoryStore } from '@verrou/core/drivers/memory'`); if (redis) { stores.push("redis: { driver: redisStore({ connection: redis }) },"); imports.push(`import { redisStore } from '@verrou/core/drivers/redis'`); imports.push(`import { redis } from './redis.ts'`); } return dedent` import { Verrou } from "@verrou/core" import { config } from "../config.ts" ${imports.join("\n")} export const verrou = new Verrou({ default: config.LOCK_STORE, stores: { ${stores.join(",\n")} } }) `; } function getPosthogIndex() { return dedent` import { PostHog } from "posthog-node"; import { config } from "../config.ts"; export const posthog = new PostHog(config.POSTHOG_API_KEY, { host: config.POSTHOG_HOST, disabled: config.NODE_ENV !== "production", }); posthog.on("error", (err) => { console.error("PostHog had an error!", err) }) `; } function getRedisFile() { return dedent` import { Redis } from "ioredis"; import { config } from "../config.ts" export const redis = new Redis({ host: config.REDIS_HOST, // for bullmq maxRetriesPerRequest: null, }) `; } function getS3ServiceFile({ s3Client }) { if (s3Client === "Bun.S3Client") { return dedent` import { S3Client } from "bun"; import { config } from "../config.ts"; export const s3 = new S3Client({ endpoint: config.S3_ENDPOINT, accessKeyId: config.S3_ACCESS_KEY_ID, secretAccessKey: config.S3_SECRET_ACCESS_KEY, }); `; } if (s3Client === "@aws-sdk/client-s3") { return dedent` import { S3Client } from "@aws-sdk/client-s3"; import { config } from "../config.ts"; export const s3 = new S3Client({ endpoint: config.S3_ENDPOINT, region: "minio", credentials: { accessKeyId: config.S3_ACCESS_KEY_ID, secretAccessKey: config.S3_SECRET_ACCESS_KEY, }, }); `; } return ""; } function getPreloadFile({ redis, driver }) { const imports = []; const mocks = []; if (redis) { imports.push('import redis from "ioredis-mock"'); mocks.push( "mock.module('ioredis', () => ({ Redis: redis, default: redis }))" ); } return dedent`import { mock } from "bun:test"; import { join } from "node:path"; import { PGlite } from "@electric-sql/pglite"; import { drizzle } from "drizzle-orm/pglite"; import { migrate } from "drizzle-orm/pglite/migrator"; ${imports.join("\n")} console.time("PGLite init"); const pglite = new PGlite(); export const db = drizzle(pglite); mock.module("${driverNames[driver]}", () => ({ default: () => pglite })); mock.module("drizzle-orm/${driverNamesToDrizzle[driver]}", () => ({ drizzle })); ${mocks.join("\n")} await migrate(db, { migrationsFolder: join(import.meta.dir, "..", "drizzle"), }); console.timeEnd("PGLite init"); `; } function getTestsAPIFile({ redis, driver }) { return dedent`import { treaty } from "@elysiajs/eden"; import { app } from "../src/server.ts"; export const api = treaty(app);`; } function getTestsIndex({ redis, driver }) { return dedent`import { describe, it, expect } from "bun:test"; import { api } from "../api.ts"; describe("API - /", () => { it("/ - should return hello world", async () => { const response = await api.index.get(); expect(response.status).toBe(200); expect(response.data).toBe("Hello World"); }); }); `; } function getTestSharedFile() { return dedent`import { signInitData } from "@gramio/init-data"; export const BOT_TOKEN = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"; export const INIT_DATA = signInitData( { user: { id: 1, first_name: "durov", username: "durov", }, }, BOT_TOKEN, ); `; } const linterExtensionTag = { ESLint: "dbaeumer.vscode-eslint", Biome: "biomejs.biome" }; function getVSCodeExtensions({ linter, packageManager, docker, orm }) { const extensionsFile = { // just best general purpose extensions and i guess they useful recommendations: [ "usernamehw.errorlens", "YoavBls.pretty-ts-errors", "meganrogge.template-string-converter" ] }; if (packageManager === "bun") extensionsFile.recommendations.push("oven.bun-vscode"); if (linter !== "None") extensionsFile.recommendations.push(linterExtensionTag[linter]); if (docker) extensionsFile.recommendations.push("ms-azuretools.vscode-docker"); if (orm === "Drizzle") extensionsFile.recommendations.push("rphlmr.vscode-drizzle-orm"); if (orm === "Prisma") extensionsFile.recommendations.push("Prisma.prisma"); return JSON.stringify(extensionsFile, null, 2); } function getVSCodeSettings({ linter }) { let settingsFile = { "editor.formatOnSave": true }; if (linter !== "None") settingsFile = { ...settingsFile, "[javascript]": { "editor.defaultFormatter": linterExtensionTag[linter] }, "[typescript]": { "editor.defaultFormatter": linterExtensionTag[linter] } }; return JSON.stringify(settingsFile, null, 2); } const preferences = new Preferences(); const args = minimist(process.argv.slice(2)); const packageManager = args.pm || detectPackageManager(); if (packageManager !== "bun") throw new Error("Now supported only bun"); const dir = args._.at(0); if (!dir) throw new Error( "Specify the folder like this - bun create elysiajs dir-name" ); const projectDir = path.resolve(`${process.cwd()}/`, dir); process.on("unhandledRejection", async (error) => { const filesInTargetDirectory = await fs.readdir(projectDir); if (filesInTargetDirectory.length) { console.log(error); const { overwrite } = await enquirer.prompt({ type: "toggle", name: "overwrite", initial: "yes", message: `You exit the process. Do you want to delete the directory ${path.basename(projectDir)}?` }); if (!overwrite) { console.log("Cancelled..."); return process.exit(0); } } console.log("Template deleted..."); console.error(error); await fs.rm(projectDir, { recursive: true }); process.exit(0); }); createOrFindDir(projectDir).catch((e) => { console.error(e); process.exit(1); }).then(async () => { preferences.dir = dir; preferences.projectName = path.basename(projectDir); preferences.packageManager = packageManager; preferences.isMonorepo = !!args.monorepo; preferences.runtime = packageManager === "bun" ? "Bun" : "Node.js"; preferences.noInstall = !Boolean(args.install ?? true); const filesInTargetDirectory = await fs.readdir(projectDir); if (filesInTargetDirectory.length) { const { overwrite } = await enquirer.prompt({ type: "toggle", name: "overwrite", initial: "yes", message: ` ${filesInTargetDirectory.join( "\n" )} The directory ${preferences.projectName} is not empty. Do you want to delete the files?` }); if (!overwrite) { console.log("Cancelled..."); return process.exit(0); } await fs.rm(projectDir, { recursive: true }); await fs.mkdir(projectDir); } if (!args.monorepo) { const { telegramRelated } = await enquirer.prompt({ type: "toggle", name: "telegramRelated", initial: "no", message: "Is your project related to Telegram (Did you wants to validate init data and etc)?" }); preferences.telegramRelated = telegramRelated; const { linter } = await enquirer.prompt({ type: "select", name: "linter", message: "Select linters/formatters:", choices: ["None", "ESLint", "Biome"] }); preferences.linter = linter; const { orm } = await enquirer.prompt({ type: "select", name: "orm", message: "Select ORM/Query Builder:", choices: ["None", "Prisma", "Drizzle"] }); preferences.orm = orm; if (orm === "Prisma") { const { database } = await enquirer.prompt({ type: "select", name: "database", message: "Select DataBase for Prisma:", choices: [ "PostgreSQL", "MySQL", "MongoDB", "SQLite", "SQLServer", "CockroachDB" ] }); preferences.database = database; } if (orm === "Drizzle") { const { database } = await enquirer.prompt({ type: "select", name: "database", message: "Select DataBase for Drizzle:", choices: ["PostgreSQL", "MySQL", "SQLite"] }); const driversMap = { PostgreSQL: [ preferences.runtime === "Bun" ? "Bun.sql" : void 0, "node-postgres", "Postgres.JS" ].filter((x) => x !== void 0), MySQL: ["MySQL 2"], SQLite: ["Bun SQLite"] }; const { driver } = await enquirer.prompt({ type: "select", name: "driver", message: `Select driver for ${database}:`, choices: driversMap[database] }); preferences.database = database; preferences.driver = driver; if (database === "PostgreSQL") { const { mockWithPGLite } = await enquirer.prompt({ type: "toggle", name: "mockWithPGLite", initial: "yes", message: "Do you want to mock database in tests with PGLite (Postgres in WASM)?" }); preferences.mockWithPGLite = mockWithPGLite; } } } else { preferences.telegramRelated = true; } const { plugins } = await enquirer.prompt({ type: "multiselect", name: "plugins", message: "Select Elysia plugins: (Space to select, Enter to continue)", choices: [ "CORS", "Swagger", "JWT", "Autoload", "Oauth 2.0", "Logger", "HTML/JSX", "Static", "Bearer", "Server Timing" ] }); preferences.plugins = plugins; if (!args.monorepo) { const { others } = await enquirer.prompt({ type: "multiselect", name: "others", message: "Select others tools: (Space to select, Enter to continue)", choices: ["S3", "Posthog", "Jobify", "Husky"] }); preferences.others = others; if (others.includes("S3")) { const { s3Client } = await enquirer.prompt({ type: "select", name: "s3Client", message: "Select S3 client:", choices: ["Bun.S3Client", "@aws-sdk/client-s3"] }); preferences.s3Client = s3Client; } if (!others.includes("Husky")) { const { git } = await enquirer.prompt({ type: "toggle", name: "git", initial: "yes", message: "Create an empty Git repository?" }); preferences.git = git; } else preferences.git = true; const { locks } = await enquirer.prompt({ type: "toggle", name: "locks", initial: "yes", message: "Do you want to use Locks to prevent race conditions?" }); preferences.locks = locks; if (others.includes("Jobify")) { preferences.redis = true; } else { const { redis } = await enquirer.prompt({ type: "toggle", name: "redis", initial: "yes", message: "Do you want to use Redis?" }); preferences.redis = redis; } const { docker } = await enquirer.prompt({ type: "toggle", name: "docker", initial: "yes", message: "Create Dockerfile + docker.compose.yml?" }); preferences.docker = docker; const { vscode } = await enquirer.prompt({ type: "toggle", name: "vscode", initial: "yes", message: "Create .vscode folder with VSCode extensions recommendations and settings?" }); preferences.vscode = vscode; } await task("Generating a template...", async ({ setTitle }) => { if (plugins.includes("Static")) await fs.mkdir(projectDir + "/public"); if (preferences.linter === "ESLint") await fs.writeFile( `${projectDir}/eslint.config.mjs`, generateEslintConfig(preferences) ); await fs.writeFile( projectDir + "/package.json", getPackageJson(preferences) ); await fs.writeFile( projectDir + "/tsconfig.json", getTSConfig(preferences) ); await fs.writeFile(projectDir + "/.env", getEnvFile(preferences)); await fs.writeFile( projectDir + "/.env.production", getEnvFile(preferences, true) ); await fs.writeFile(projectDir + "/README.md", getReadme(preferences)); await fs.writeFile( projectDir + "/.gitignore", ["dist", "node_modules", ".env", ".env.production"].join("\n") ); await fs.mkdir(projectDir + "/src"); await fs.writeFile( projectDir + "/src/server.ts", getElysiaIndex(preferences) ); await fs.writeFile(projectDir + "/src/index.ts", getIndex(preferences)); await fs.writeFile( `${projectDir}/src/config.ts`, getConfigFile(preferences) ); await fs.mkdir(projectDir + "/