@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.
- package/README.md +2 -0
- package/dist/index.js +1 -1
- package/package.json +13 -15
- package/template/.docker/traefik/dynamic/tls.yml +4 -0
- package/template/.github/scripts/build-stats.md.ts +71 -0
- package/template/.github/workflows/ci.yml +16 -0
- package/template/.gitignore +2 -0
- package/template/.npmrc +0 -1
- package/template/.vscode/settings.json +1 -1
- package/template/api/.env.development +4 -0
- package/template/{server → api}/package.json +7 -7
- package/template/{server → api}/src/database/index.ts +1 -1
- package/template/api/src/main.ts +37 -0
- package/template/{server → api}/src/orpc/context.ts +4 -3
- package/template/api/src/orpc/index.ts +27 -0
- package/template/{server → api}/src/orpc/middlewares/auth.ts +2 -1
- package/template/api/src/orpc/plugins/error.ts +17 -0
- package/template/{server → api}/src/orpc/router/index.ts +13 -1
- package/template/app/.env +1 -0
- package/template/{client → app}/package.json +9 -10
- package/template/{client → app}/src/App.vue +2 -1
- package/template/{client → app}/src/composables/content.ts +2 -1
- package/template/{client → app}/src/lib/orpc.ts +2 -1
- package/template/{client → app}/src/main.ts +1 -1
- package/template/{client → app}/vite.config.ts +1 -1
- package/template/compose.yaml +65 -12
- package/template/docs/local-tls.md +152 -0
- package/template/knip.config.ts +5 -9
- package/template/package.json +11 -10
- package/template/tsconfig.json +2 -2
- package/template/.scripts/dev.ts +0 -8
- package/template/client/.env +0 -1
- package/template/server/.env.development +0 -4
- package/template/server/src/main.ts +0 -51
- package/template/server/src/orpc/handler.ts +0 -34
- package/template/server/src/orpc/index.ts +0 -18
- package/template/server/src/orpc/middlewares/index.ts +0 -1
- /package/template/{server → api}/src/auth/index.ts +0 -0
- /package/template/{server → api}/src/database/drizzle/config.ts +0 -0
- /package/template/{server → api}/src/database/migrations/0000_init.sql +0 -0
- /package/template/{server → api}/src/database/migrations/meta/0000_snapshot.json +0 -0
- /package/template/{server → api}/src/database/migrations/meta/_journal.json +0 -0
- /package/template/{server → api}/src/database/schema/accounts.ts +0 -0
- /package/template/{server → api}/src/database/schema/index.ts +0 -0
- /package/template/{server → api}/src/database/schema/sessions.ts +0 -0
- /package/template/{server → api}/src/database/schema/users.ts +0 -0
- /package/template/{server → api}/src/database/schema/verifications.ts +0 -0
- /package/template/{server → api}/src/env.d.ts +0 -0
- /package/template/{server → api}/src/utils/cors.ts +0 -0
- /package/template/{server → api}/src/utils/logger.ts +0 -0
- /package/template/{server → api}/tsconfig.json +0 -0
- /package/template/{client → app}/index.html +0 -0
- /package/template/{client → app}/public/favicon.svg +0 -0
- /package/template/{client → app}/public/robots.txt +0 -0
- /package/template/{client → app}/src/components/.gitkeep +0 -0
- /package/template/{client → app}/src/composables/auth.ts +0 -0
- /package/template/{client → app}/src/composables/index.ts +0 -0
- /package/template/{client → app}/src/env.d.ts +0 -0
- /package/template/{client → app}/src/locales/en.yml +0 -0
- /package/template/{client → app}/src/locales/fr.yml +0 -0
- /package/template/{client → app}/tsconfig.json +0 -0
- /package/template/{client → app}/uno.config.ts +0 -0
- /package/template/{client → app}/wrangler.json +0 -0
package/README.md
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevinmarrec/create-app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"packageManager": "bun@1.3.
|
|
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/
|
|
22
|
-
"template/
|
|
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
|
|
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.
|
|
57
|
-
"@kevinmarrec/stylelint-config": "^1.5.
|
|
58
|
-
"@kevinmarrec/tsconfig": "^1.5.
|
|
59
|
-
"@types/bun": "^1.3.
|
|
60
|
-
"@vitest/coverage-v8": "^4.0.
|
|
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
|
-
"
|
|
64
|
-
"knip": "^5.67.1",
|
|
63
|
+
"knip": "^5.69.1",
|
|
65
64
|
"stylelint": "^16.25.0",
|
|
66
|
-
"
|
|
67
|
-
"tsdown": "^0.16.0",
|
|
65
|
+
"tsdown": "^0.16.4",
|
|
68
66
|
"typescript": "^5.9.3",
|
|
69
|
-
"vitest": "^4.0.
|
|
67
|
+
"vitest": "^4.0.9",
|
|
70
68
|
"vue-tsc": "^3.1.3"
|
|
71
69
|
}
|
|
72
70
|
}
|
|
@@ -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
|
package/template/.gitignore
CHANGED
package/template/.npmrc
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
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/
|
|
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.
|
|
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.
|
|
20
|
-
"@types/bun": "^1.3.
|
|
21
|
-
"drizzle-kit": "^0.31.
|
|
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
|
}
|
|
@@ -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
|
-
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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": "
|
|
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.
|
|
14
|
-
"@orpc/tanstack-query": "1.
|
|
15
|
-
"@tanstack/query-core": "^5.90.
|
|
16
|
-
"@tanstack/vue-query": "^5.
|
|
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.
|
|
21
|
-
"vue": "^3.5.
|
|
20
|
+
"unocss": "^66.5.6",
|
|
21
|
+
"vue": "^3.5.24"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@kevinmarrec/unocss-config": "^1.5.
|
|
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.
|
|
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
|
-
|
|
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`,
|
|
@@ -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'
|
package/template/compose.yaml
CHANGED
|
@@ -1,22 +1,75 @@
|
|
|
1
1
|
x-common: &common
|
|
2
|
-
working_dir: /app
|
|
3
2
|
volumes:
|
|
4
|
-
- ./:/
|
|
3
|
+
- ./:/code
|
|
4
|
+
working_dir: /code
|
|
5
5
|
user: 1000:1000
|
|
6
6
|
|
|
7
7
|
services:
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
60
|
+
app:
|
|
16
61
|
<<: *common
|
|
17
62
|
depends_on:
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
package/template/knip.config.ts
CHANGED
|
@@ -10,19 +10,15 @@ export default {
|
|
|
10
10
|
'.': {
|
|
11
11
|
entry: ['*.config.ts'],
|
|
12
12
|
},
|
|
13
|
-
'
|
|
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
|
-
'
|
|
21
|
+
'@vueuse/core',
|
|
26
22
|
],
|
|
27
23
|
},
|
|
28
24
|
},
|
package/template/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "project",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"private": true,
|
|
5
|
-
"packageManager": "bun@1.3.
|
|
5
|
+
"packageManager": "bun@1.3.2",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "lts/*"
|
|
8
8
|
},
|
|
9
9
|
"workspaces": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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.
|
|
26
|
-
"@kevinmarrec/stylelint-config": "^1.5.
|
|
27
|
-
"@kevinmarrec/tsconfig": "^1.5.
|
|
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
|
-
"
|
|
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
|
}
|
package/template/tsconfig.json
CHANGED
package/template/.scripts/dev.ts
DELETED
|
@@ -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)
|
package/template/client/.env
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VITE_API_URL=http://localhost:4000
|
|
@@ -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
|
|
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
|