@kevinmarrec/create-app 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +1 -1
  3. package/package.json +13 -15
  4. package/template/.docker/traefik/dynamic/tls.yml +4 -0
  5. package/template/.github/scripts/build-stats.md.ts +71 -0
  6. package/template/.github/workflows/ci.yml +16 -0
  7. package/template/.gitignore +2 -0
  8. package/template/.npmrc +0 -1
  9. package/template/.vscode/settings.json +1 -1
  10. package/template/api/.env.development +4 -0
  11. package/template/{server → api}/package.json +7 -7
  12. package/template/{server → api}/src/database/index.ts +1 -1
  13. package/template/api/src/main.ts +37 -0
  14. package/template/{server → api}/src/orpc/context.ts +4 -3
  15. package/template/api/src/orpc/index.ts +27 -0
  16. package/template/{server → api}/src/orpc/middlewares/auth.ts +2 -1
  17. package/template/api/src/orpc/plugins/error.ts +17 -0
  18. package/template/{server → api}/src/orpc/router/index.ts +13 -1
  19. package/template/app/.env +1 -0
  20. package/template/{client → app}/package.json +9 -10
  21. package/template/{client → app}/src/App.vue +2 -1
  22. package/template/{client → app}/src/composables/content.ts +2 -1
  23. package/template/{client → app}/src/lib/orpc.ts +2 -1
  24. package/template/{client → app}/src/main.ts +1 -1
  25. package/template/{client → app}/vite.config.ts +1 -1
  26. package/template/compose.yaml +65 -12
  27. package/template/docs/local-tls.md +152 -0
  28. package/template/knip.config.ts +5 -9
  29. package/template/package.json +11 -10
  30. package/template/tsconfig.json +2 -2
  31. package/template/.scripts/dev.ts +0 -8
  32. package/template/client/.env +0 -1
  33. package/template/server/.env.development +0 -4
  34. package/template/server/src/main.ts +0 -51
  35. package/template/server/src/orpc/handler.ts +0 -34
  36. package/template/server/src/orpc/index.ts +0 -18
  37. package/template/server/src/orpc/middlewares/index.ts +0 -1
  38. /package/template/{server → api}/src/auth/index.ts +0 -0
  39. /package/template/{server → api}/src/database/drizzle/config.ts +0 -0
  40. /package/template/{server → api}/src/database/migrations/0000_init.sql +0 -0
  41. /package/template/{server → api}/src/database/migrations/meta/0000_snapshot.json +0 -0
  42. /package/template/{server → api}/src/database/migrations/meta/_journal.json +0 -0
  43. /package/template/{server → api}/src/database/schema/accounts.ts +0 -0
  44. /package/template/{server → api}/src/database/schema/index.ts +0 -0
  45. /package/template/{server → api}/src/database/schema/sessions.ts +0 -0
  46. /package/template/{server → api}/src/database/schema/users.ts +0 -0
  47. /package/template/{server → api}/src/database/schema/verifications.ts +0 -0
  48. /package/template/{server → api}/src/env.d.ts +0 -0
  49. /package/template/{server → api}/src/utils/cors.ts +0 -0
  50. /package/template/{server → api}/src/utils/logger.ts +0 -0
  51. /package/template/{server → api}/tsconfig.json +0 -0
  52. /package/template/{client → app}/index.html +0 -0
  53. /package/template/{client → app}/public/favicon.svg +0 -0
  54. /package/template/{client → app}/public/robots.txt +0 -0
  55. /package/template/{client → app}/src/components/.gitkeep +0 -0
  56. /package/template/{client → app}/src/composables/auth.ts +0 -0
  57. /package/template/{client → app}/src/composables/index.ts +0 -0
  58. /package/template/{client → app}/src/env.d.ts +0 -0
  59. /package/template/{client → app}/src/locales/en.yml +0 -0
  60. /package/template/{client → app}/src/locales/fr.yml +0 -0
  61. /package/template/{client → app}/tsconfig.json +0 -0
  62. /package/template/{client → app}/uno.config.ts +0 -0
  63. /package/template/{client → app}/wrangler.json +0 -0
package/README.md CHANGED
@@ -16,4 +16,6 @@ CLI that scaffolds an opinionated [Bun](https://bun.sh) & [Vue](https://vuejs.or
16
16
 
17
17
  ```sh
18
18
  bun create @kevinmarrec/app
19
+ # OR
20
+ bunx @kevinmarrec/create-app
19
21
  ```
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { x } from "tinyexec";
8
8
  import fs from "node:fs/promises";
9
9
 
10
10
  //#region package.json
11
- var version = "0.8.0";
11
+ var version = "0.9.0";
12
12
 
13
13
  //#endregion
14
14
  //#region src/utils/fs.ts
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@kevinmarrec/create-app",
3
3
  "type": "module",
4
- "version": "0.8.0",
5
- "packageManager": "bun@1.3.1",
4
+ "version": "0.9.0",
5
+ "packageManager": "bun@1.3.2",
6
6
  "description": "CLI that scaffolds an opinionated Bun & Vue fullstack application.",
7
7
  "author": "Kevin Marrec <kevin@marrec.io>",
8
8
  "license": "MIT",
@@ -18,8 +18,8 @@
18
18
  ],
19
19
  "workspaces": [
20
20
  "template",
21
- "template/client",
22
- "template/server"
21
+ "template/api",
22
+ "template/app"
23
23
  ],
24
24
  "bin": {
25
25
  "create-app": "dist/index.js"
@@ -39,7 +39,7 @@
39
39
  "check:unused": "knip -n",
40
40
  "lint": "bun run check:eslint && bun run check:stylelint",
41
41
  "lint:inspect": "bunx @eslint/config-inspector",
42
- "playground": "bun --cwd template dev",
42
+ "playground": "bun run scripts/transform-compose.ts | docker compose -f -",
43
43
  "release": "bumpp",
44
44
  "update": "bunx taze -I -rwi",
45
45
  "test": "vitest",
@@ -53,20 +53,18 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@faker-js/faker": "^10.1.0",
56
- "@kevinmarrec/eslint-config": "^1.5.4",
57
- "@kevinmarrec/stylelint-config": "^1.5.4",
58
- "@kevinmarrec/tsconfig": "^1.5.4",
59
- "@types/bun": "^1.3.1",
60
- "@vitest/coverage-v8": "^4.0.7",
56
+ "@kevinmarrec/eslint-config": "^1.5.6",
57
+ "@kevinmarrec/stylelint-config": "^1.5.6",
58
+ "@kevinmarrec/tsconfig": "^1.5.6",
59
+ "@types/bun": "^1.3.2",
60
+ "@vitest/coverage-v8": "^4.0.9",
61
61
  "bumpp": "^10.3.1",
62
62
  "eslint": "^9.39.1",
63
- "filesize": "^11.0.13",
64
- "knip": "^5.67.1",
63
+ "knip": "^5.69.1",
65
64
  "stylelint": "^16.25.0",
66
- "tinyglobby": "^0.2.15",
67
- "tsdown": "^0.16.0",
65
+ "tsdown": "^0.16.4",
68
66
  "typescript": "^5.9.3",
69
- "vitest": "^4.0.7",
67
+ "vitest": "^4.0.9",
70
68
  "vue-tsc": "^3.1.3"
71
69
  }
72
70
  }
@@ -0,0 +1,4 @@
1
+ tls:
2
+ certificates:
3
+ - certFile: /certs/dev.localhost.crt
4
+ keyFile: /certs/dev.localhost.key
@@ -0,0 +1,71 @@
1
+ import * as fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { parseArgs } from 'node:util'
5
+
6
+ import { filesize } from 'filesize'
7
+ import { x } from 'tinyexec'
8
+ import { glob } from 'tinyglobby'
9
+
10
+ interface FileStats {
11
+ file: string
12
+ size: number
13
+ }
14
+
15
+ async function getFileStats(directory: string) {
16
+ const fileStats: FileStats[] = []
17
+
18
+ for (const file of await glob(['**/*'], { cwd: directory })) {
19
+ const size = fs.statSync(path.join(directory, file)).size
20
+ fileStats.push({
21
+ file,
22
+ size,
23
+ })
24
+ }
25
+
26
+ fileStats.sort((a, b) => {
27
+ // Sort so that files starting with 'assets/' come first
28
+ if (a.file.startsWith('assets/') && !b.file.startsWith('assets/')) return -1
29
+ if (!a.file.startsWith('assets/') && b.file.startsWith('assets/')) return 1
30
+
31
+ // Within that, files ending with '.js' come first
32
+ if (a.file.endsWith('.js') && !b.file.endsWith('.js')) return -1
33
+ if (!a.file.endsWith('.js') && b.file.endsWith('.js')) return 1
34
+
35
+ // Otherwise sort alphabetically
36
+ return a.file.localeCompare(b.file)
37
+ })
38
+
39
+ return fileStats
40
+ }
41
+
42
+ async function generateFileStatsMarkdown(directory: string) {
43
+ const fileStats = await getFileStats(directory)
44
+ return [
45
+ '| File | Size |',
46
+ '| :--- | ---: |',
47
+ ...fileStats.map(file => `| ${file.file} | ${filesize(file.size)} |`),
48
+ ].join('\n')
49
+ }
50
+
51
+ async function main() {
52
+ const { positionals: [directory] } = parseArgs({
53
+ args: process.argv.slice(2),
54
+ allowPositionals: true,
55
+ })
56
+
57
+ if (!directory) {
58
+ process.stdout.write('Usage: analyze <directory>\n')
59
+ process.exit(1)
60
+ }
61
+
62
+ await x('bun', ['run', 'build'], { nodeOptions: { cwd: directory } })
63
+
64
+ const markdownTable = await generateFileStatsMarkdown(path.join(directory, 'dist'))
65
+ process.stdout.write(`${markdownTable}\n`)
66
+ }
67
+
68
+ main().catch((error) => {
69
+ console.error(error)
70
+ process.exit(1)
71
+ })
@@ -24,3 +24,19 @@ jobs:
24
24
 
25
25
  - name: Type check
26
26
  run: bun run check:types
27
+ project-size:
28
+ name: Analyze project build size
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Setup Bun
32
+ uses: kevinmarrec/workflows/setup-bun@v1
33
+
34
+ - name: Check api build size
35
+ run: |
36
+ echo "## API" >> $GITHUB_STEP_SUMMARY
37
+ bun run .github/scripts/build-stats.md.ts api >> $GITHUB_STEP_SUMMARY
38
+
39
+ - name: Check app build size
40
+ run: |
41
+ echo "## App" >> $GITHUB_STEP_SUMMARY
42
+ bun run .github/scripts/build-stats.md.ts app >> $GITHUB_STEP_SUMMARY
@@ -1,4 +1,6 @@
1
+ *.crt
1
2
  *.db*
3
+ *.key
2
4
  *.local
3
5
  *.log
4
6
  *.sqlite
package/template/.npmrc CHANGED
@@ -1,4 +1,3 @@
1
1
  public-hoist-pattern[]=@types*
2
2
  public-hoist-pattern[]=*eslint*
3
3
  public-hoist-pattern[]=*stylelint*
4
- public-hoist-pattern[]=pino-pretty
@@ -67,6 +67,6 @@
67
67
  "i18n-ally.enabledFrameworks": ["vue"],
68
68
  "i18n-ally.keystyle": "nested",
69
69
  "i18n-ally.localesPaths": [
70
- "client/src/locales"
70
+ "app/src/locales"
71
71
  ]
72
72
  }
@@ -0,0 +1,4 @@
1
+ ALLOWED_ORIGINS=https://app.dev.localhost
2
+ AUTH_SECRET=foo
3
+ DATABASE_URL=.db
4
+ NODE_ENV=development
@@ -1,24 +1,24 @@
1
1
  {
2
- "name": "server",
2
+ "name": "api",
3
3
  "type": "module",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "bun --watch --no-clear-screen src/main.ts",
7
- "build": "bun build src/main.ts --compile --minify --sourcemap --outfile dist/server",
6
+ "dev": "bun --watch --no-clear-screen src/main.ts | pino-pretty",
7
+ "build": "bun build src/main.ts --compile --minify --sourcemap --outfile dist/api",
8
8
  "db:generate": "bun --bun run drizzle-kit generate --config src/database/drizzle/config.ts",
9
9
  "db:migrate": "bun --bun run drizzle-kit migrate --config src/database/drizzle/config.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@orpc/server": "^1.10.4",
12
+ "@orpc/server": "^1.11.2",
13
13
  "better-auth": "^1.3.34",
14
14
  "drizzle-orm": "^0.44.7",
15
15
  "pino": "^10.1.0",
16
16
  "valibot": "^1.1.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@electric-sql/pglite": "^0.3.13",
20
- "@types/bun": "^1.3.1",
21
- "drizzle-kit": "^0.31.6",
19
+ "@electric-sql/pglite": "^0.3.14",
20
+ "@types/bun": "^1.3.2",
21
+ "drizzle-kit": "^0.31.7",
22
22
  "pino-pretty": "^13.1.2"
23
23
  }
24
24
  }
@@ -1,7 +1,7 @@
1
1
  import { PGlite } from '@electric-sql/pglite'
2
- import { logger } from '@server/utils/logger'
3
2
  import { drizzle } from 'drizzle-orm/pglite'
4
3
 
4
+ import { logger } from '../utils/logger'
5
5
  import * as schema from './schema'
6
6
 
7
7
  export const db = drizzle({
@@ -0,0 +1,37 @@
1
+ import process from 'node:process'
2
+
3
+ import { createBetterAuth } from './auth'
4
+ import { db } from './database'
5
+ import { createRpc } from './orpc'
6
+ import { router } from './orpc/router'
7
+ import { cors } from './utils/cors'
8
+ import { logger } from './utils/logger'
9
+
10
+ const auth = createBetterAuth({ db, logger })
11
+ const rpc = createRpc({ auth, db, logger }, router)
12
+
13
+ const server = Bun.serve({
14
+ hostname: import.meta.env.HOST ?? '0.0.0.0',
15
+ port: import.meta.env.PORT ?? 4000,
16
+ routes: {
17
+ '/auth/*': cors(auth.handler),
18
+ '/rpc/*': cors(rpc.handler),
19
+ },
20
+ error(error) {
21
+ logger.error(error)
22
+ return new Response('Internal Server Error', { status: 500 })
23
+ },
24
+ })
25
+
26
+ logger.info(`Listening on ${server.url}`)
27
+
28
+ // Graceful Shutdown
29
+
30
+ async function gracefulShutdown() {
31
+ logger.info('Gracefully shutting down...')
32
+ await server.stop()
33
+ process.exit(0)
34
+ }
35
+
36
+ process.on('SIGINT', gracefulShutdown)
37
+ process.on('SIGTERM', gracefulShutdown)
@@ -1,7 +1,8 @@
1
1
  import type { RequestHeadersPluginContext, ResponseHeadersPluginContext } from '@orpc/server/plugins'
2
- import type { Auth } from '@server/auth'
3
- import type { Database } from '@server/database'
4
- import type { Logger } from '@server/utils/logger'
2
+
3
+ import type { Auth } from '../auth'
4
+ import type { Database } from '../database'
5
+ import type { Logger } from '../utils/logger'
5
6
 
6
7
  export interface Context extends RequestHeadersPluginContext, ResponseHeadersPluginContext {
7
8
  auth: Auth
@@ -0,0 +1,27 @@
1
+ import type { Router } from '@orpc/server'
2
+ import { RPCHandler } from '@orpc/server/fetch'
3
+ import { RequestHeadersPlugin, ResponseHeadersPlugin } from '@orpc/server/plugins'
4
+
5
+ import type { Context } from './context'
6
+ import { ErrorPlugin } from './plugins/error'
7
+
8
+ export function createRpc<T extends Context>(context: T, router: Router<any, T>) {
9
+ const handler = new RPCHandler<T>(router, {
10
+ plugins: [
11
+ new ErrorPlugin(),
12
+ new RequestHeadersPlugin(),
13
+ new ResponseHeadersPlugin(),
14
+ ],
15
+ })
16
+
17
+ return {
18
+ handler: async (req: Request) => {
19
+ const { matched, response } = await handler.handle(req, {
20
+ prefix: '/rpc',
21
+ context,
22
+ })
23
+
24
+ return matched ? response : new Response('Not found', { status: 404 })
25
+ },
26
+ }
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { ORPCError, os } from '@orpc/server'
2
- import type { Context } from '@server/orpc'
2
+
3
+ import type { Context } from '../context'
3
4
 
4
5
  export const authMiddleware = os
5
6
  .$context<Context>()
@@ -0,0 +1,17 @@
1
+ import { onError, ORPCError } from '@orpc/server'
2
+ import type { StandardHandlerOptions, StandardHandlerPlugin } from '@orpc/server/standard'
3
+
4
+ import type { Context } from '../context'
5
+
6
+ export class ErrorPlugin<T extends Context> implements StandardHandlerPlugin<T> {
7
+ init(options: StandardHandlerOptions<T>): void {
8
+ options.clientInterceptors ??= []
9
+ options.clientInterceptors.unshift(onError((error, { context }) => {
10
+ if (error instanceof ORPCError) {
11
+ throw error
12
+ }
13
+
14
+ context.logger.error(error)
15
+ }))
16
+ }
17
+ }
@@ -1,8 +1,20 @@
1
- import { authed, pub } from '@server/orpc'
1
+ import { os } from '@orpc/server'
2
2
  import * as v from 'valibot'
3
3
 
4
+ import type { Context } from '../context'
5
+ import { authMiddleware } from '../middlewares/auth'
6
+
4
7
  export type { RouterClient } from '@orpc/server'
5
8
 
9
+ const pub = os
10
+ .$context<Context>()
11
+ .errors({
12
+ UNAUTHORIZED: { status: 401 },
13
+ })
14
+
15
+ const authed = pub
16
+ .use(authMiddleware)
17
+
6
18
  export const router = {
7
19
  public: pub
8
20
  .input(v.any())
@@ -0,0 +1 @@
1
+ VITE_API_URL=https://api.dev.localhost
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "client",
2
+ "name": "app",
3
3
  "type": "module",
4
4
  "private": true,
5
5
  "scripts": {
@@ -10,26 +10,25 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@kevinmarrec/vue-i18n": "^1.1.3",
13
- "@orpc/client": "^1.10.4",
14
- "@orpc/tanstack-query": "1.10.3",
15
- "@tanstack/query-core": "^5.90.7",
16
- "@tanstack/vue-query": "^5.90.7",
13
+ "@orpc/client": "^1.11.2",
14
+ "@orpc/tanstack-query": "1.11.2",
15
+ "@tanstack/query-core": "^5.90.9",
16
+ "@tanstack/vue-query": "^5.91.1",
17
17
  "@unhead/vue": "^2.0.19",
18
18
  "@vueuse/core": "^14.0.0",
19
19
  "better-auth": "^1.3.34",
20
- "unocss": "^66.5.4",
21
- "vue": "^3.5.22"
20
+ "unocss": "^66.5.6",
21
+ "vue": "^3.5.24"
22
22
  },
23
23
  "devDependencies": {
24
- "@kevinmarrec/unocss-config": "^1.5.4",
24
+ "@kevinmarrec/unocss-config": "^1.5.6",
25
25
  "@kevinmarrec/vite-plugin-dark-mode": "^1.1.2",
26
26
  "@modyfi/vite-plugin-yaml": "^1.1.1",
27
27
  "@unhead/addons": "^2.0.19",
28
- "@unocss/vite": "^66.5.4",
29
28
  "@vitejs/plugin-vue": "^6.0.1",
30
29
  "beasties": "^0.3.5",
31
30
  "rollup-plugin-visualizer": "^6.0.5",
32
- "vite": "^7.2.0",
31
+ "vite": "^7.2.2",
33
32
  "vite-plugin-vue-devtools": "^8.0.3",
34
33
  "vite-ssg": "^28.2.2",
35
34
  "vite-tsconfig-paths": "^5.1.4"
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { useAuth, useContent, useHead, useI18n } from '@client/composables'
3
2
  import { ref } from 'vue'
4
3
 
4
+ import { useAuth, useContent, useHead, useI18n } from '~/app/composables'
5
+
5
6
  const { t } = useI18n()
6
7
  useHead({
7
8
  title: () => t('title'),
@@ -1,6 +1,7 @@
1
- import { orpc } from '@client/lib/orpc'
2
1
  import { useQuery } from '@tanstack/vue-query'
3
2
 
3
+ import { orpc } from '~/app/lib/orpc'
4
+
4
5
  export function useContent() {
5
6
  const publicContent = useQuery(orpc.public.queryOptions({})).data
6
7
  const privateContent = useQuery(orpc.private.queryOptions({})).data
@@ -1,7 +1,8 @@
1
1
  import { createORPCClient } from '@orpc/client'
2
2
  import { RPCLink } from '@orpc/client/fetch'
3
3
  import { createTanstackQueryUtils } from '@orpc/tanstack-query'
4
- import type { Router, RouterClient } from '@server/orpc/router'
4
+
5
+ import type { Router, RouterClient } from '~/api/orpc/router'
5
6
 
6
7
  const link = new RPCLink({
7
8
  url: `${import.meta.env.VITE_API_URL}/rpc`,
@@ -4,7 +4,7 @@ import { ViteSSG } from 'vite-ssg/single-page'
4
4
 
5
5
  import App from './App.vue'
6
6
 
7
- import 'uno.css'
7
+ import 'virtual:uno.css'
8
8
 
9
9
  export const createApp = ViteSSG(App, async ({ app }) => {
10
10
  const i18n = await createI18n({
@@ -3,9 +3,9 @@ import process from 'node:process'
3
3
  import darkMode from '@kevinmarrec/vite-plugin-dark-mode'
4
4
  import yaml from '@modyfi/vite-plugin-yaml'
5
5
  import unhead from '@unhead/addons/vite'
6
- import unocss from '@unocss/vite'
7
6
  import vue from '@vitejs/plugin-vue'
8
7
  import { visualizer } from 'rollup-plugin-visualizer'
8
+ import unocss from 'unocss/vite'
9
9
  import { defineConfig } from 'vite'
10
10
  import devtools from 'vite-plugin-vue-devtools'
11
11
  import tsconfigPaths from 'vite-tsconfig-paths'
@@ -1,22 +1,75 @@
1
1
  x-common: &common
2
- working_dir: /app
3
2
  volumes:
4
- - ./:/app
3
+ - ./:/code
4
+ working_dir: /code
5
5
  user: 1000:1000
6
6
 
7
7
  services:
8
- server:
8
+ traefik:
9
+ image: traefik:v3.6
10
+ container_name: traefik
11
+ restart: unless-stopped
12
+ security_opt:
13
+ - no-new-privileges:true
14
+ command:
15
+ # General configuration
16
+ - --log.level=INFO
17
+ - --api.dashboard=true
18
+ # Entrypoints for HTTP and HTTPS
19
+ - --entrypoints.websecure.address=:443
20
+ - --entrypoints.web.address=:80
21
+ # Redirect HTTP to HTTPS
22
+ - --entrypoints.web.http.redirections.entrypoint.to=websecure
23
+ - --entrypoints.web.http.redirections.entrypoint.scheme=https
24
+ # Providers (Docker & File for dynamic config)
25
+ - --providers.docker=true
26
+ - --providers.docker.exposedbydefault=false
27
+ - --providers.file.directory=/etc/traefik/dynamic
28
+ - --providers.file.watch=true
29
+ labels:
30
+ traefik.enable: 'true'
31
+ traefik.http.routers.traefik.rule: Host(`traefik.dev.localhost`)
32
+ traefik.http.routers.traefik.entrypoints: websecure
33
+ traefik.http.routers.traefik.tls: 'true'
34
+ traefik.http.routers.traefik.service: api@internal
35
+ ports:
36
+ - 80:80
37
+ - 443:443
38
+ volumes:
39
+ - /var/run/docker.sock:/var/run/docker.sock:ro
40
+ - ./.docker/traefik/dynamic:/etc/traefik/dynamic:ro # Mount the dynamic config directory
41
+ - ./.docker/traefik/certs:/certs:ro # Mount the certs directory
42
+
43
+ api:
9
44
  <<: *common
45
+ depends_on:
46
+ - traefik
47
+ container_name: api
10
48
  image: oven/bun:1-alpine
11
- command: [bun, --cwd, server, dev]
12
- ports:
13
- - 4000:4000
49
+ init: true
50
+ command: [bun, --cwd, api, dev]
51
+ environment:
52
+ - FORCE_COLOR=1
53
+ labels:
54
+ traefik.enable: 'true'
55
+ traefik.http.routers.api.rule: Host(`api.dev.localhost`)
56
+ traefik.http.routers.api.entrypoints: websecure
57
+ traefik.http.routers.api.tls: 'true'
58
+ traefik.http.services.api.loadbalancer.server.port: '4000'
14
59
 
15
- client:
60
+ app:
16
61
  <<: *common
17
62
  depends_on:
18
- - server
19
- image: imbios/bun-node:22-alpine
20
- command: [bun, --cwd, client, dev, --host, 0.0.0.0]
21
- ports:
22
- - 5173:5173
63
+ - api
64
+ container_name: app
65
+ image: imbios/bun-node:24-alpine
66
+ init: true
67
+ command: [bun, --cwd, app, dev, --host, 0.0.0.0]
68
+ environment:
69
+ - FORCE_COLOR=1
70
+ labels:
71
+ traefik.enable: 'true'
72
+ traefik.http.routers.app.rule: Host(`app.dev.localhost`)
73
+ traefik.http.routers.app.entrypoints: websecure
74
+ traefik.http.routers.app.tls: 'true'
75
+ traefik.http.services.app.loadbalancer.server.port: '5173'
@@ -0,0 +1,152 @@
1
+ # **Local TLS Setup Guide**
2
+
3
+ This guide provides the essential steps to install mkcert and generate trusted TLS certificates for your Traefik-secured local development environment.
4
+
5
+ Certificates are expected in `.docker/traefik/certs/`.
6
+
7
+ ## **Prerequisites**
8
+
9
+ - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed
10
+ - Ports 80 and 443 available on your system
11
+ - Administrator/sudo access for installing mkcert
12
+
13
+ ## **1. Install mkcert (Ubuntu/WSL)**
14
+
15
+ Download, install, and clean up the executable:
16
+
17
+ ```sh
18
+ curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
19
+ chmod +x mkcert-v*-linux-amd64
20
+ sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
21
+ rm mkcert-v*-linux-amd64
22
+ ```
23
+
24
+ Verify the installation:
25
+
26
+ ```sh
27
+ mkcert -version
28
+ ```
29
+
30
+ ## **2. Install the mkcert Root CA**
31
+
32
+ Install the local root CA to trust generated certificates:
33
+
34
+ ```sh
35
+ mkcert -install
36
+ ```
37
+
38
+ ### **Firefox and Zen Browser (Manual Import)**
39
+
40
+ Browsers like Firefox and Firefox-based Zen Browser require manual import into their internal trust stores.
41
+
42
+ 1. **Find Root CA File Path:**
43
+ Run the appropriate command below to get the full path to the rootCA.pem file:
44
+ - **Linux Native (or WSL for Linux Apps):**
45
+
46
+ ```sh
47
+ echo "$(mkcert -CAROOT)/rootCA.pem"
48
+ # /home/user/.local/share/mkcert/rootCA.pem
49
+ ```
50
+
51
+ - **WSL for Windows Host Applications:** Use wslpath to get the Windows-formatted path.
52
+ ```sh
53
+ wslpath -w "$(mkcert -CAROOT)/rootCA.pem"
54
+ # \\wsl.localhost\Ubuntu\home\user\.local\share\mkcert\rootCA.pem
55
+ ```
56
+
57
+ 2. **Import:** In the browser's settings:
58
+ - **Settings** -> **Privacy & Security**.
59
+ - **Certificates** -> **View Certificates...**
60
+ - **Authorities** tab -> **Import...**
61
+ - Select the rootCA.pem file (using the path found above)
62
+ - Check **"Trust this CA to identify websites"**
63
+ - Click **OK**
64
+
65
+ ## **3. Generate the `dev.localhost` Certificate**
66
+
67
+ Generate the wildcard certificate for `*.dev.localhost` and place the files where Traefik is configured to look (See [`tls.yml`](../.docker/traefik/dynamic/tls.yml)).
68
+
69
+ 1. **Create Directory:**
70
+
71
+ ```sh
72
+ mkdir -p .docker/traefik/certs
73
+ ```
74
+
75
+ 2. **Generate Certificate:**
76
+
77
+ ```sh
78
+ mkcert \
79
+ -cert-file .docker/traefik/certs/dev.localhost.crt \
80
+ -key-file .docker/traefik/certs/dev.localhost.key \
81
+ "dev.localhost" "*.dev.localhost"
82
+ ```
83
+
84
+ 3. **Verify Certificate:**
85
+
86
+ ```sh
87
+ openssl x509 -in .docker/traefik/certs/dev.localhost.crt -text -noout | grep -A 2 "Subject Alternative Name"
88
+ ```
89
+
90
+ ## **4. Validate Setup**
91
+
92
+ 1. **Start Services:**
93
+
94
+ ```sh
95
+ docker compose up -d
96
+ ```
97
+
98
+ 2. **Test in Browser:**
99
+
100
+ - Visit **https://traefik.dev.localhost** — you should see the Traefik dashboard
101
+ - Visit **https://app.dev.localhost** — you should see your app
102
+ - Both should load without SSL warnings
103
+
104
+ ## **5. Troubleshooting**
105
+
106
+ ### **Certificate Files Missing**
107
+
108
+ If Traefik fails to start or shows TLS errors, verify the certificate files exist:
109
+
110
+ ```sh
111
+ ls -la .docker/traefik/certs/
112
+ ```
113
+
114
+ ### **Browser Shows SSL Warning**
115
+
116
+ - Ensure mkcert root CA is installed (`mkcert -install`)
117
+ - For Firefox/Zen Browser, manually import the root CA (see section 2)
118
+ - Clear browser cache and restart the browser
119
+
120
+ ### **Certificate Expiration**
121
+
122
+ mkcert certificates are valid for a long time, but if you need to regenerate:
123
+
124
+ 1. **Remove old certificates:**
125
+
126
+ ```sh
127
+ rm .docker/traefik/certs/dev.localhost.*
128
+ ```
129
+
130
+ 2. **Regenerate (see section 3)**
131
+
132
+ ## **6. Clean Up**
133
+
134
+ 1. **Stop Services:**
135
+
136
+ ```sh
137
+ docker compose down
138
+ ```
139
+
140
+ 2. **Remove Certificates (Optional):**
141
+
142
+ ```sh
143
+ rm -rf .docker/traefik/certs
144
+ ```
145
+
146
+ 3. **Uninstall mkcert Root CA (Optional):**
147
+
148
+ ```sh
149
+ mkcert -uninstall
150
+ ```
151
+
152
+ > **Note:** Removing the root CA will cause SSL warnings in browsers until you reinstall it or regenerate certificates.
@@ -10,19 +10,15 @@ export default {
10
10
  '.': {
11
11
  entry: ['*.config.ts'],
12
12
  },
13
- 'client': {
14
- entry: ['src/main.ts'],
15
- ignoreDependencies: [
16
- '@vueuse/core',
17
- 'uno.css',
18
- ],
19
- },
20
- 'server': {
13
+ 'api': {
21
14
  drizzle: {
22
15
  config: 'src/database/drizzle/config.ts',
23
16
  },
17
+ },
18
+ 'app': {
19
+ entry: ['src/main.ts'],
24
20
  ignoreDependencies: [
25
- 'pino-pretty',
21
+ '@vueuse/core',
26
22
  ],
27
23
  },
28
24
  },
@@ -1,14 +1,14 @@
1
1
  {
2
- "name": "app",
2
+ "name": "project",
3
3
  "type": "module",
4
4
  "private": true,
5
- "packageManager": "bun@1.3.1",
5
+ "packageManager": "bun@1.3.2",
6
6
  "engines": {
7
7
  "node": "lts/*"
8
8
  },
9
9
  "workspaces": [
10
- "client",
11
- "server"
10
+ "api",
11
+ "app"
12
12
  ],
13
13
  "scripts": {
14
14
  "check": "bun run check:eslint && bun run check:stylelint && bun run check:unused && bun run check:types",
@@ -16,19 +16,20 @@
16
16
  "check:stylelint": "stylelint '**/*.{css,scss,vue}' --ignorePath .gitignore --cache",
17
17
  "check:types": "vue-tsc --noEmit",
18
18
  "check:unused": "knip -n",
19
- "dev": "bun .scripts/dev.ts",
20
19
  "lint": "bun run check:eslint && bun run check:stylelint",
21
20
  "lint:inspect": "bunx @eslint/config-inspector",
22
21
  "update": "bunx taze -I -rwi"
23
22
  },
24
23
  "devDependencies": {
25
- "@kevinmarrec/eslint-config": "^1.5.4",
26
- "@kevinmarrec/stylelint-config": "^1.5.4",
27
- "@kevinmarrec/tsconfig": "^1.5.4",
28
- "concurrently": "^9.2.1",
24
+ "@kevinmarrec/eslint-config": "^1.5.6",
25
+ "@kevinmarrec/stylelint-config": "^1.5.6",
26
+ "@kevinmarrec/tsconfig": "^1.5.6",
29
27
  "eslint": "^9.39.1",
30
- "knip": "^5.67.1",
28
+ "filesize": "^11.0.13",
29
+ "knip": "^5.69.1",
31
30
  "stylelint": "^16.25.0",
31
+ "tinyexec": "^1.0.2",
32
+ "tinyglobby": "^0.2.15",
32
33
  "typescript": "~5.9.3",
33
34
  "vue-tsc": "^3.1.3"
34
35
  }
@@ -2,8 +2,8 @@
2
2
  "extends": "@kevinmarrec/tsconfig",
3
3
  "compilerOptions": {
4
4
  "paths": {
5
- "@client/*": ["./client/src/*"],
6
- "@server/*": ["./server/src/*"]
5
+ "~/api/*": ["./api/src/*"],
6
+ "~/app/*": ["./app/src/*"]
7
7
  }
8
8
  },
9
9
  "exclude": ["**/dist/**"]
@@ -1,8 +0,0 @@
1
- import concurrently, { type ConcurrentlyCommandInput } from 'concurrently'
2
-
3
- const commandInputs: ConcurrentlyCommandInput[] = [
4
- { name: 'server', command: `bun --cwd server dev | pino-pretty`, prefixColor: 'blue' },
5
- { name: 'client', command: `bun --cwd client dev`, prefixColor: 'green' },
6
- ]
7
-
8
- concurrently(commandInputs)
@@ -1 +0,0 @@
1
- VITE_API_URL=http://localhost:4000
@@ -1,4 +0,0 @@
1
- ALLOWED_ORIGINS=http://localhost:5173,http://localhost:5174
2
- AUTH_SECRET=foo
3
- DATABASE_URL=.db
4
- NODE_ENV=development
@@ -1,51 +0,0 @@
1
- import process from 'node:process'
2
-
3
- import { createBetterAuth } from '@server/auth'
4
- import { db } from '@server/database'
5
- import { createRpcHandler } from '@server/orpc'
6
- import { router } from '@server/orpc/router'
7
- import { cors } from '@server/utils/cors'
8
- import { logger } from '@server/utils/logger'
9
-
10
- const auth = createBetterAuth({ db, logger })
11
- const rpcHandler = createRpcHandler(router)
12
-
13
- const routes = {
14
- '/auth/*': cors(async (req) => {
15
- return await auth.handler(req)
16
- }),
17
- '/rpc/*': cors(async (req) => {
18
- const { matched, response } = await rpcHandler.handle(req, {
19
- prefix: '/rpc',
20
- context: { auth, db, logger },
21
- })
22
-
23
- if (matched)
24
- return response
25
-
26
- return new Response('Not found', { status: 404 })
27
- }),
28
- }
29
-
30
- const server = Bun.serve({
31
- hostname: import.meta.env.HOST ?? '0.0.0.0',
32
- port: import.meta.env.PORT ?? 4000,
33
- routes,
34
- error(error) {
35
- logger.error(error)
36
- return new Response('Internal Server Error', { status: 500 })
37
- },
38
- })
39
-
40
- logger.info(`Listening on ${server.url}`)
41
-
42
- // Graceful Shutdown
43
-
44
- async function gracefulShutdown() {
45
- logger.info('Gracefully shutting down...')
46
- await server.stop()
47
- process.exit(0)
48
- }
49
-
50
- process.on('SIGINT', gracefulShutdown)
51
- process.on('SIGTERM', gracefulShutdown)
@@ -1,34 +0,0 @@
1
- import { onError, ORPCError, type Router } from '@orpc/server'
2
- import { RPCHandler } from '@orpc/server/fetch'
3
- import {
4
- RequestHeadersPlugin,
5
- ResponseHeadersPlugin,
6
- } from '@orpc/server/plugins'
7
- import { APIError } from 'better-auth/api'
8
-
9
- import type { Context } from './context'
10
-
11
- export function createRpcHandler<T extends Context>(router: Router<any, T>) {
12
- return new RPCHandler<T>(router, {
13
- plugins: [
14
- new RequestHeadersPlugin(),
15
- new ResponseHeadersPlugin(),
16
- ],
17
- clientInterceptors: [
18
- onError((error, { context }) => {
19
- if (error instanceof APIError) {
20
- throw new ORPCError(error.body?.code ?? 'INTERNAL_SERVER_ERROR', {
21
- status: error.statusCode,
22
- message: error.body?.message,
23
- })
24
- }
25
-
26
- if (error instanceof ORPCError) {
27
- throw error
28
- }
29
-
30
- context.logger.error(error)
31
- }),
32
- ],
33
- })
34
- }
@@ -1,18 +0,0 @@
1
- import { os } from '@orpc/server'
2
- import { authMiddleware } from '@server/orpc/middlewares'
3
-
4
- import type { Context } from './context'
5
-
6
- export { createRpcHandler } from './handler'
7
-
8
- export type { Context }
9
-
10
- export const pub = os
11
- .$context<Context>()
12
- .errors({
13
- UNAUTHORIZED: { status: 401 },
14
- })
15
-
16
- /** @beta */
17
- export const authed = pub
18
- .use(authMiddleware)
@@ -1 +0,0 @@
1
- export * from './auth'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes