diff --git a/.github/workflows/deploy.yml b/.github/workflows/docs.yml similarity index 100% rename from .github/workflows/deploy.yml rename to .github/workflows/docs.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..66db2f9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Lint, format, and test + +on: + pull_request: + branches: [main] + +jobs: + lint: + name: Lint and format + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Lint and format the code + run: bunx biome ci --linter-enabled=true --formatter-enabled=true . + + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Test the code + run: bun test diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..1d67559 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +bun lint-staged diff --git a/.vscode/settings.json b/.vscode/settings.json index ff30c44..78664b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.tabSize": 2 -} \ No newline at end of file + "editor.tabSize": 2 +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fecf225..78f5441 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,24 +12,12 @@ Hey there! We're thrilled that you'd like to contribute to this project. Your he > [!NOTE] > **Windows users**: see [special note](#special-note-for-windows-users) below. -This project uses [Bun](https://bun.sh) as a runtime as well as a package manager. You'll need to install it and use as your package manager for this project. it We recommend using [antfu/ni](https://github.com/antfu/ni) to avoid worrying about package managers when switching projects. +This project uses [Bun](https://bun.sh) as a runtime as well as a package manager. It's a modern, fast, and lightweight alternative to [Node.js](https://nodejs.org/en/) and [npm](https://www.npmjs.com/). To install Bun, run the following command: -We will use `ni`'s commands in the following code snippets. If you are not using it, you can run `bun i` instead of `ni`, and `bun run` instead of `nr`. - -* **Install the latest version of [Bun](https://bun.sh)** ```sh curl -fsSL https://bun.sh/install | bash ``` -* **Install [@antfu/ni](https://github.com/antfu/ni)** - ```sh - bun i -g @antfu/ni - ``` - -* **Install dependencies** - ```sh - ni - ``` ### Special note for Windows users @@ -37,33 +25,27 @@ This guide assumes you are using a Unix-like environment, since [Bun is working ## 💡 Commands -### `nr dev` - -Start the development environment. - -If it's a Node.js package, it will start the build process in watch mode, or [stub the passive watcher when using `unbuild`](https://antfu.me/posts/publish-esm-and-cjs#stubbing). +### `bun dev` -If it's a frontend project, it usually starts the dev server. You can then develop and see the changes in real time. +Start the development environment in watch mode. -### `nr play` +### `bun build` -If it's a Node.js package, it starts a dev server for the playground. The code is usually under `playground/`. +Build the project for production. The result is under `dist/`. -### `nr build` +### `bun lint` -Build the project for production. The result is usually under `dist/`. +We use [Biome](https://biomejs.dev/) for **both linting and formatting**. It is an ultra-fast, Rust based linter and formatter. +It also lints JSON. -### `nr lint` - -We use [ESLint](https://eslint.org/) for **both linting and formatting**. It also lints for JSON, YAML and Markdown files if exists. - -You can run `nr lint --fix` to let ESLint formats and lints the code. - -Learn more about the [ESLint Setup](#eslint). +You can run `bun lint --apply` to apply any safe fixes automatically. [**We don't use Prettier**](#no-prettier). -### `nr test` +### `bun test` + +> [!NOTE] +> This is just a placeholder for now. We will add more details later once tests are formally added. Run the tests. We mostly using [Vitest](https://vitest.dev/) - a replacement of [Jest](https://jestjs.io/). @@ -75,13 +57,13 @@ Vitest runs in [watch mode by default](https://vitest.dev/guide/features.html#wa For some projects, we might have multiple types of tests set up. For example `nr test:unit` for unit tests, `nr test:e2e` for end-to-end tests. `nr test` commonly run them together, you can run them separately as needed. -### `nr docs` +### `bun docs` -If the project contains documentation, you can run `nr docs` to start the documentation dev server. Use `nr docs:build` to build the docs for production. +Start the documentation dev server. Use `bun docs:build` to build the docs for production. -### `nr` +### `bun run` -For more, you can run bare `nr`, which will prompt a list of all available scripts. +Print a full list of available scripts. ## 🙌 The Road to a Great Pull Request @@ -143,7 +125,7 @@ When ready to publish a new release, we run `nr release`. It prompts a list for There are two kinds of publishing setups, both performed by `nr release`. -
+
#### Build Locally @@ -161,7 +143,7 @@ In `package.json`, we usually have: So whenever you run `npm publish`, it will make sure you have the latest change in the distribution. - + #### Build on CI @@ -177,38 +159,41 @@ Changelogs are always generated by GitHub Actions. ## 📖 References -### ESLint +### Lint -We use [ESLint](https://eslint.org/) for both linting and formatting with [`antfu/eslint-config`](https://github.com/antfu/eslint-config). +We use [Biome](https://biomejs.dev/) for both linting and formatting with [a few custom rules](./biome.json). It is an ultra-fast, Rust based linter and formatter. -
+
#### IDE Setup -We recommend using [VS Code](https://code.visualstudio.com/) along with the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +We recommend using [VS Code](https://code.visualstudio.com/) along with the [Biome extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome). With the settings on the right, you can have auto fix and formatting when you save the code you are editing. -
+

VS Code's `settings.json` ```json { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll": false, - "source.fixAll.eslint": true + "quickfix.biome": true } } ```
-### ⚠️ No Prettier - -Since ESLint is already configured to format the code, there is no need to duplicate the functionality with Prettier ([*Why I don't Use Prettier*](https://antfu.me/posts/why-not-prettier)). To format the code, you can run `nr lint --fix` or referring the [ESLint section](#eslint) for IDE Setup. +### No Prettier -If you have Prettier installed in your editor, we recommend you disable it when working on the project to avoid conflict. +> [!WARNING] +> Since ESLint is already configured to format the code, there is no need to duplicate the functionality with Prettier ([*Why I don't Use Prettier*](https://antfu.me/posts/why-not-prettier)). To format the code, you can run `bun lint --apply` or refer to the [Lint section](#lint) for IDE Setup. +> +> If you have Prettier installed in your editor, we recommend you disable it when working on the project to avoid conflict. +> Instead, you may use the [Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome). ## 🗒 Additional Info @@ -226,17 +211,3 @@ In case you are interested in, here are some interesting tools, many of which in - [esno](https://github.com/antfu/esno) - TypeScript runner - [taze](https://github.com/antfu/taze) - dependency updater - [bumpp](https://github.com/antfu/bumpp) - version bumpper - -In addition of `ni`, here is a few shell aliases to be even lazier: - -```bash -alias d="nr dev" -alias b="nr build" -alias t="nr test" -alias tu="nr test -u" -alias p="nr play" -alias c="nr typecheck" -alias lint="nr lint" -alias lintf="nr lint --fix" -alias release="nr release" -``` \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index cde1f81..5154753 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/db/config.ts b/db/config.ts index 83dda5b..1ed4651 100644 --- a/db/config.ts +++ b/db/config.ts @@ -1,18 +1,18 @@ -import type { Config } from "drizzle-kit"; +import type { Config } from 'drizzle-kit'; export const dbCredentials = { - host: process.env.POSTGRES_HOST || "0.0.0.0", - port: parseInt(process.env.POSTGRES_PORT || '5432'), - user: process.env.POSTGRES_USER || "postgres", - password: process.env.POSTGRES_PASSWORD || "postgres", - database: process.env.POSTGRES_DB || "medium" -} + host: process.env.POSTGRES_HOST || '0.0.0.0', + port: parseInt(process.env.POSTGRES_PORT || '5432'), + user: process.env.POSTGRES_USER || 'postgres', + password: process.env.POSTGRES_PASSWORD || 'postgres', + database: process.env.POSTGRES_DB || 'medium', +}; export const dbCredentialsString = `postgres://${dbCredentials.user}:${dbCredentials.password}@${dbCredentials.host}:${dbCredentials.port}/${dbCredentials.database}`; export default { - out: "./src/db/migrations", - schema: "**/*.schema.ts", - breakpoints: false, - driver: "pg", - dbCredentials + out: './src/db/migrations', + schema: '**/*.schema.ts', + breakpoints: false, + driver: 'pg', + dbCredentials, } satisfies Config; diff --git a/db/migrations/meta/0000_snapshot.json b/db/migrations/meta/0000_snapshot.json index 7c09e13..354ebd1 100644 --- a/db/migrations/meta/0000_snapshot.json +++ b/db/migrations/meta/0000_snapshot.json @@ -72,4 +72,4 @@ "tables": {}, "columns": {} } -} \ No newline at end of file +} diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 31eae40..51409b4 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -10,4 +10,4 @@ "breakpoints": false } ] -} \ No newline at end of file +} diff --git a/db/migrations/migrate.ts b/db/migrations/migrate.ts index 49ffe7b..8448dd3 100644 --- a/db/migrations/migrate.ts +++ b/db/migrations/migrate.ts @@ -1,5 +1,7 @@ -import {drizzle} from "drizzle-orm/postgres-js"; -import {migrate} from "drizzle-orm/postgres-js/migrator"; -import {migrationClient} from "@/database.providers"; +import { drizzle } from 'drizzle-orm/postgres-js'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; +import { migrationClient } from '@/database.providers'; -await migrate(drizzle(migrationClient), {migrationsFolder: `${import.meta.dir}`}); +await migrate(drizzle(migrationClient), { + migrationsFolder: `${import.meta.dir}`, +}); diff --git a/db/seed.ts b/db/seed.ts index 01c482c..5d17cbc 100644 --- a/db/seed.ts +++ b/db/seed.ts @@ -1,20 +1,20 @@ import { exit } from 'process'; import { db } from '@/database.providers'; -import {users} from "@/users/users.schema"; +import { users } from '@/users/users.schema'; const data = { - id: users.id.default, - email: 'test@email.com', - username: 'test', - password: 'test', - bio: 'test', - image: 'test', -} -console.log("Inserting user: ", data) -await db.insert(users).values(data) -console.log("User inserted") + id: users.id.default, + email: 'test@email.com', + username: 'test', + password: 'test', + bio: 'test', + image: 'test', +}; +console.log('Inserting user: ', data); +await db.insert(users).values(data); +console.log('User inserted'); const userResult = await db.select().from(users); -console.log("User result: ", userResult); +console.log('User result: ', userResult); exit(0); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 7bbc6bc..045ab89 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,14 +1,14 @@ -import { defineConfig } from 'vitepress' +import { defineConfig } from 'vitepress'; // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "RealWorld Elysia Docs", - description: "A Vite Press docs of Real World", + title: 'RealWorld Elysia Docs', + description: 'A Vite Press docs of Real World', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' } + { text: 'Examples', link: '/markdown-examples' }, ], sidebar: [ @@ -16,13 +16,13 @@ export default defineConfig({ text: 'Examples', items: [ { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' } - ] - } + { text: 'Runtime API Examples', link: '/api-examples' }, + ], + }, ], socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } - ] - } -}) + { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, + ], + }, +}); diff --git a/package.json b/package.json index 67e9b96..76c847c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "elysia-realworld-example-app", + "title": "Elysia Realworld Example App", "version": "1.0.50", + "description": "A RealWorld example for an app in the backend created with Bun and Elysia.", "scripts": { "start": "bun run src/main.ts", "dev": "bun run --watch src/main.ts", "test": "echo \"Error: no test specified\" && exit 1", - "docs:dev": "vitepress dev docs", + "format": "biome format --write .", + "lint": "biome lint .", + "docs": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "db:up": "./scripts/create-start-container-with-env.sh", @@ -13,7 +17,8 @@ "db:migrate": "bun run db/migrations/migrate.ts", "db:push": "bun drizzle-kit push:pg --config=db/config.ts", "db:seed": "bun run db/seed.ts", - "db:studio": "bun drizzle-kit studio --config=db/config.ts" + "db:studio": "bun drizzle-kit studio --config=db/config.ts", + "prepare": "husky install" }, "dependencies": { "@elysiajs/swagger": "^0.7.3", @@ -23,8 +28,11 @@ "postgres": "^3.3.5" }, "devDependencies": { + "@biomejs/biome": "1.2.2", "bun-types": "latest", "drizzle-kit": "^0.19.13", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", "pg": "^8.11.3", "vitepress": "^1.0.0-rc.15" }, @@ -32,5 +40,9 @@ "typescript": "^5.0.0" }, "module": "src/index.ts", - "type": "module" + "type": "module", + "lint-staged": { + "*": "biome format --write", + "*.ts": "biome lint --apply" + } } diff --git a/renovate.json b/renovate.json index a6f8372..a325100 100644 --- a/renovate.json +++ b/renovate.json @@ -1,13 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base", - ":disableDependencyDashboard" - ], + "extends": ["config:base", ":disableDependencyDashboard"], "vulnerabilityAlerts": { "automerge": true, - "labels": [ - "security" - ] + "labels": ["security"] } -} \ No newline at end of file +} diff --git a/src/app.module.ts b/src/app.module.ts index bd6c3c7..a12f597 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,7 @@ -// the file name is in the spirit of NestJS, where app module is the device in charge of putting together all the pieces of the app -// see: https://docs.nestjs.com/modules - -import { Elysia } from "elysia"; -import { swagger } from "@elysiajs/swagger"; -import { usersPlugin } from "@/users/users.plugin"; +import { Elysia } from 'elysia'; +import { swagger } from '@elysiajs/swagger'; +import { usersPlugin } from '@users/users.plugin'; +import { title, version, description } from '../package.json'; /** * Add all plugins to the app @@ -13,9 +11,9 @@ export const setupApp = () => { .use( swagger({ documentation: { - info: { title: "RealWorld Medium backend", version: "v1" }, + info: { title, version, description }, }, - }) + }), ) - .group("/api", (app) => app.use(usersPlugin)); + .group('/api', (app) => app.use(usersPlugin)); }; diff --git a/src/database.providers.ts b/src/database.providers.ts index 6d94c51..bab3f20 100644 --- a/src/database.providers.ts +++ b/src/database.providers.ts @@ -1,6 +1,6 @@ -import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; -import { dbCredentialsString } from "@db/config"; +import { dbCredentialsString } from '@db/config'; +import { PostgresJsDatabase, drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; // for migrations export const migrationClient = postgres(dbCredentialsString, { max: 1 }); diff --git a/src/main.ts b/src/main.ts index 24426e9..45a1101 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ -import { Elysia } from "elysia"; -import { setupApp } from "@/app.module"; +import { setupApp } from '@/app.module'; +import { Elysia } from 'elysia'; -const app = new Elysia().use(setupApp).listen(3000); +const app = new Elysia({ prefix: '/api' }).use(setupApp).listen(3000); console.log( - `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}` + `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, ); diff --git a/src/users/users.module.ts b/src/users/users.module.ts index fc33177..2a592d9 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,7 +1,7 @@ -import { Elysia } from "elysia"; -import { UsersService } from "@/users/users.service"; -import { UsersRepository } from "@/users/users.repository"; -import { db } from "@/database.providers"; +import { db } from '@/database.providers'; +import { UsersRepository } from '@/users/users.repository'; +import { UsersService } from '@/users/users.service'; +import { Elysia } from 'elysia'; export const setupUsers = () => { const usersRepository = new UsersRepository(db); diff --git a/src/users/users.plugin.ts b/src/users/users.plugin.ts index 5d691df..a8001fb 100644 --- a/src/users/users.plugin.ts +++ b/src/users/users.plugin.ts @@ -1,10 +1,18 @@ -import { Elysia } from "elysia"; -import { setupUsers } from "@/users/users.module"; +import { Elysia } from 'elysia'; +import { setupUsers } from '@/users/users.module'; export const usersPlugin = new Elysia() .use(setupUsers) - .group("/users", (app) => - app - .post("", ({ store }) => store.usersService.findAll()) - .post("/login", ({ store }) => store.usersService.findAll()) + .model({}) + .group( + '/users', + { + detail: { + tags: ['Users'], + }, + }, + (app) => + app + .post('', ({ store }) => store.usersService.findAll()) + .post('/login', ({ store }) => store.usersService.findAll()), ); diff --git a/src/users/users.repository.ts b/src/users/users.repository.ts index 5404b02..16048f5 100644 --- a/src/users/users.repository.ts +++ b/src/users/users.repository.ts @@ -1,8 +1,8 @@ // users.repository.ts // in charge of database interactions -import { users } from "./users.schema"; -import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { users } from './users.schema'; export class UsersRepository { // the type here is diff --git a/src/users/users.schema.ts b/src/users/users.schema.ts index bade696..aea35d3 100644 --- a/src/users/users.schema.ts +++ b/src/users/users.schema.ts @@ -1,25 +1,25 @@ -import { sql } from "drizzle-orm"; -import { pgTable, text, date, serial } from "drizzle-orm/pg-core"; -import { createInsertSchema, createSelectSchema } from "drizzle-typebox"; -import { Type } from "@sinclair/typebox"; +import { Type } from '@sinclair/typebox'; +import { sql } from 'drizzle-orm'; +import { date, pgTable, serial, text } from 'drizzle-orm/pg-core'; +import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'; -export const users = pgTable("users", { - id: serial("id").primaryKey(), - email: text("email").notNull(), - bio: text("bio").notNull(), - image: text("image").notNull(), - password: text("password").notNull(), - username: text("username").notNull(), - created_at: date("created_at").default(sql`CURRENT_DATE`), - updated_at: date("updated_at").default(sql`CURRENT_DATE`), +export const users = pgTable('users', { + id: serial('id').primaryKey(), + email: text('email').notNull(), + bio: text('bio').notNull(), + image: text('image').notNull(), + password: text('password').notNull(), + username: text('username').notNull(), + created_at: date('created_at').default(sql`CURRENT_DATE`), + updated_at: date('updated_at').default(sql`CURRENT_DATE`), }); // Schema for inserting a user - can be used to validate API requests const insertUserSchemaRaw = createInsertSchema(users); export const insertUserSchema = Type.Omit(insertUserSchemaRaw, [ - "id", - "created_at", - "updated_at", + 'id', + 'created_at', + 'updated_at', ]); // Schema for selecting a user - can be used to validate API responses diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 038b11b..2e9bede 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,7 +1,7 @@ // users.service.ts // in charge of business logic - generate slug, fetch data from other services, cache something, etc. -import { UsersRepository } from "@/users/users.repository"; +import { UsersRepository } from '@/users/users.repository'; export class UsersService { constructor(private readonly repository: UsersRepository) {} diff --git a/tsconfig.json b/tsconfig.json index f20c171..d58b6a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,13 +32,14 @@ "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ "@db/*": ["./db/*"], "@/*": ["./src/*"], + "@users/*": ["./src/users/*"], }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */