create-edhor-stack 0.1.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/LICENSE +21 -0
- package/README.md +75 -0
- package/STACK.md +1086 -0
- package/dist/index.js +3181 -0
- package/package.json +44 -0
- package/templates/apps/api-elysia/package.json +21 -0
- package/templates/apps/api-elysia/src/index.ts +59 -0
- package/templates/apps/api-elysia/src/lib/eden.ts +25 -0
- package/templates/apps/api-elysia/src/lib/env.ts +18 -0
- package/templates/apps/api-elysia/src/routes/health.ts +13 -0
- package/templates/apps/api-elysia/src/routes/users.ts +117 -0
- package/templates/apps/api-elysia/tsconfig.json +15 -0
- package/templates/apps/api-hono/package.json +20 -0
- package/templates/apps/api-hono/src/index.ts +66 -0
- package/templates/apps/api-hono/src/lib/env.ts +18 -0
- package/templates/apps/api-hono/src/routes/health.ts +20 -0
- package/templates/apps/api-hono/src/routes/users.ts +110 -0
- package/templates/apps/api-hono/tsconfig.json +15 -0
- package/templates/apps/mobile/.env.example +9 -0
- package/templates/apps/mobile/app/_layout.tsx +16 -0
- package/templates/apps/mobile/app/index.tsx +39 -0
- package/templates/apps/mobile/app.json +37 -0
- package/templates/apps/mobile/assets/adaptive-icon.png +0 -0
- package/templates/apps/mobile/assets/favicon.png +0 -0
- package/templates/apps/mobile/assets/icon.png +0 -0
- package/templates/apps/mobile/assets/splash-icon.png +0 -0
- package/templates/apps/mobile/package.json +39 -0
- package/templates/apps/mobile/src/api/client.ts +51 -0
- package/templates/apps/mobile/src/api/index.ts +3 -0
- package/templates/apps/mobile/src/api/queries.ts +24 -0
- package/templates/apps/mobile/src/api/schemas.ts +32 -0
- package/templates/apps/mobile/src/lib/env.ts +40 -0
- package/templates/apps/mobile/src/lib/query-client.ts +28 -0
- package/templates/apps/mobile/src/lib/result.ts +45 -0
- package/templates/apps/mobile/src/lib/store.ts +63 -0
- package/templates/apps/mobile/tsconfig.json +10 -0
- package/templates/apps/web/.env.example +11 -0
- package/templates/apps/web/package.json +29 -0
- package/templates/apps/web/src/lib/env.ts +52 -0
- package/templates/apps/web/src/lib/queries.ts +27 -0
- package/templates/apps/web/src/lib/query-client.ts +11 -0
- package/templates/apps/web/src/router.tsx +17 -0
- package/templates/apps/web/src/routes/__root.tsx +32 -0
- package/templates/apps/web/src/routes/index.tsx +16 -0
- package/templates/apps/web/src/styles.css +26 -0
- package/templates/apps/web/tsconfig.json +10 -0
- package/templates/apps/web/vite.config.ts +21 -0
- package/templates/base/.claude/settings.json +33 -0
- package/templates/base/.claude/skills/add-api-endpoint.md +137 -0
- package/templates/base/.claude/skills/add-component.md +79 -0
- package/templates/base/.claude/skills/add-route.md +134 -0
- package/templates/base/.claude/skills/add-store.md +158 -0
- package/templates/base/.husky/pre-commit +1 -0
- package/templates/base/.lintstagedrc +4 -0
- package/templates/base/.node-version +1 -0
- package/templates/base/AGENTS.md +135 -0
- package/templates/base/CLAUDE.md.hbs +139 -0
- package/templates/base/Dockerfile +32 -0
- package/templates/base/biome.json +52 -0
- package/templates/base/fly.toml.hbs +20 -0
- package/templates/base/gitignore +36 -0
- package/templates/base/package.json.hbs +22 -0
- package/templates/base/tsconfig.json +14 -0
- package/templates/base/turbo.json +22 -0
- package/templates/packages/shared/package.json +17 -0
- package/templates/packages/shared/src/index.ts +4 -0
- package/templates/packages/shared/src/schemas.ts +50 -0
- package/templates/packages/shared/src/types.ts +47 -0
- package/templates/packages/shared/src/utils.ts +87 -0
- package/templates/packages/shared/tsconfig.json +14 -0
- package/templates/packages/stripe/package.json +18 -0
- package/templates/packages/stripe/src/client.ts +110 -0
- package/templates/packages/stripe/src/index.ts +3 -0
- package/templates/packages/stripe/src/schemas.ts +65 -0
- package/templates/packages/stripe/src/webhooks.ts +91 -0
- package/templates/packages/stripe/tsconfig.json +14 -0
- package/templates/packages/ui/components.json +19 -0
- package/templates/packages/ui/package.json +29 -0
- package/templates/packages/ui/src/components/button.tsx +58 -0
- package/templates/packages/ui/src/index.ts +5 -0
- package/templates/packages/ui/src/lib/utils.ts +6 -0
- package/templates/packages/ui/src/styles.css +120 -0
- package/templates/packages/ui/tsconfig.json +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-edhor-stack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold opinionated Bun + Turborepo projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-edhor-stack": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"STACK.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun run src/index.ts",
|
|
16
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
17
|
+
"prepublishOnly": "bun run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"scaffold",
|
|
22
|
+
"turborepo",
|
|
23
|
+
"bun",
|
|
24
|
+
"tanstack",
|
|
25
|
+
"template"
|
|
26
|
+
],
|
|
27
|
+
"author": "Jonas Rohde",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/edhor1608/create-edhor-stack"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^0.10.0",
|
|
35
|
+
"execa": "^9.5.0",
|
|
36
|
+
"fs-extra": "^11.3.0",
|
|
37
|
+
"picocolors": "^1.1.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/fs-extra": "^11.0.4",
|
|
41
|
+
"@types/node": "^22.15.21",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun run --hot src/index.ts",
|
|
7
|
+
"start": "bun run src/index.ts",
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"elysia": "^1.2.0",
|
|
12
|
+
"@elysiajs/cors": "^1.2.0",
|
|
13
|
+
"@elysiajs/swagger": "^1.2.0",
|
|
14
|
+
"@t3-oss/env-core": "^0.12.0",
|
|
15
|
+
"zod": "^3.24.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/bun": "latest",
|
|
19
|
+
"typescript": "^5.8.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { cors } from '@elysiajs/cors';
|
|
3
|
+
import { swagger } from '@elysiajs/swagger';
|
|
4
|
+
|
|
5
|
+
import { healthRoutes } from './routes/health';
|
|
6
|
+
import { usersRoutes } from './routes/users';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// APP SETUP
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const app = new Elysia()
|
|
13
|
+
.use(
|
|
14
|
+
cors({
|
|
15
|
+
origin: ['http://localhost:3000', 'http://localhost:8081'],
|
|
16
|
+
credentials: true,
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
.use(
|
|
20
|
+
swagger({
|
|
21
|
+
documentation: {
|
|
22
|
+
info: {
|
|
23
|
+
title: '{{name}} API',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// ROUTES
|
|
31
|
+
// ============================================================================
|
|
32
|
+
.get('/', () => ({
|
|
33
|
+
name: '{{name}}-api',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
docs: '/swagger',
|
|
36
|
+
}))
|
|
37
|
+
.use(healthRoutes)
|
|
38
|
+
.use(usersRoutes)
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// ERROR HANDLING
|
|
41
|
+
// ============================================================================
|
|
42
|
+
.onError(({ code, error, set }) => {
|
|
43
|
+
if (code === 'NOT_FOUND') {
|
|
44
|
+
set.status = 404;
|
|
45
|
+
return { error: 'Not Found' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error('Error:', error);
|
|
49
|
+
set.status = 500;
|
|
50
|
+
return { error: 'Internal Server Error' };
|
|
51
|
+
})
|
|
52
|
+
.listen(process.env.PORT || 4000);
|
|
53
|
+
|
|
54
|
+
console.log(
|
|
55
|
+
`🦊 Elysia server running at http://${app.server?.hostname}:${app.server?.port}`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Export type for Eden client (end-to-end type safety)
|
|
59
|
+
export type App = typeof app;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eden Client Setup
|
|
3
|
+
*
|
|
4
|
+
* Elysia provides end-to-end type safety via Eden Treaty.
|
|
5
|
+
* This file shows how to create a typed client for your API.
|
|
6
|
+
*
|
|
7
|
+
* Install in your web/mobile app:
|
|
8
|
+
* bun add @elysiajs/eden
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { treaty } from '@elysiajs/eden';
|
|
12
|
+
* import type { App } from '@{{name}}/api';
|
|
13
|
+
*
|
|
14
|
+
* const api = treaty<App>('http://localhost:4000');
|
|
15
|
+
*
|
|
16
|
+
* // Fully typed API calls
|
|
17
|
+
* const { data, error } = await api.api.users.get();
|
|
18
|
+
* const { data: user } = await api.api.users({ id: 'uuid' }).get();
|
|
19
|
+
* const { data: newUser } = await api.api.users.post({
|
|
20
|
+
* email: 'test@example.com',
|
|
21
|
+
* name: 'Test User',
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
server: {
|
|
6
|
+
PORT: z.string().default('4000'),
|
|
7
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
8
|
+
DATABASE_URL: z.string().url().optional(),
|
|
9
|
+
API_SECRET: z.string().min(32).optional(),
|
|
10
|
+
},
|
|
11
|
+
runtimeEnv: {
|
|
12
|
+
PORT: process.env.PORT,
|
|
13
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
14
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
15
|
+
API_SECRET: process.env.API_SECRET,
|
|
16
|
+
},
|
|
17
|
+
emptyStringAsUndefined: true,
|
|
18
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
|
|
3
|
+
export const healthRoutes = new Elysia({ prefix: '/health' })
|
|
4
|
+
.get('/', () => ({
|
|
5
|
+
status: 'ok',
|
|
6
|
+
timestamp: new Date().toISOString(),
|
|
7
|
+
uptime: process.uptime(),
|
|
8
|
+
}))
|
|
9
|
+
.get('/ready', () => {
|
|
10
|
+
// Add database/external service checks here
|
|
11
|
+
return { ready: true };
|
|
12
|
+
})
|
|
13
|
+
.get('/live', () => ({ live: true }));
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Elysia, t } from 'elysia';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// MOCK DATA (replace with database)
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
interface User {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const users: User[] = [
|
|
15
|
+
{
|
|
16
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
17
|
+
email: 'john@example.com',
|
|
18
|
+
name: 'John Doe',
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// ROUTES
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export const usersRoutes = new Elysia({ prefix: '/api/users' })
|
|
28
|
+
// List users
|
|
29
|
+
.get('/', () => ({ users, total: users.length }))
|
|
30
|
+
|
|
31
|
+
// Get user by ID
|
|
32
|
+
.get(
|
|
33
|
+
'/:id',
|
|
34
|
+
({ params: { id }, set }) => {
|
|
35
|
+
const user = users.find((u) => u.id === id);
|
|
36
|
+
|
|
37
|
+
if (!user) {
|
|
38
|
+
set.status = 404;
|
|
39
|
+
return { error: 'User not found' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return user;
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
params: t.Object({
|
|
46
|
+
id: t.String({ format: 'uuid' }),
|
|
47
|
+
}),
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Create user
|
|
52
|
+
.post(
|
|
53
|
+
'/',
|
|
54
|
+
({ body, set }) => {
|
|
55
|
+
const newUser: User = {
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
...body,
|
|
58
|
+
createdAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
users.push(newUser);
|
|
62
|
+
set.status = 201;
|
|
63
|
+
return newUser;
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
body: t.Object({
|
|
67
|
+
email: t.String({ format: 'email' }),
|
|
68
|
+
name: t.String({ minLength: 1 }),
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Update user
|
|
74
|
+
.patch(
|
|
75
|
+
'/:id',
|
|
76
|
+
({ params: { id }, body, set }) => {
|
|
77
|
+
const userIndex = users.findIndex((u) => u.id === id);
|
|
78
|
+
|
|
79
|
+
if (userIndex === -1) {
|
|
80
|
+
set.status = 404;
|
|
81
|
+
return { error: 'User not found' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
users[userIndex] = { ...users[userIndex], ...body };
|
|
85
|
+
return users[userIndex];
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
params: t.Object({
|
|
89
|
+
id: t.String({ format: 'uuid' }),
|
|
90
|
+
}),
|
|
91
|
+
body: t.Object({
|
|
92
|
+
email: t.Optional(t.String({ format: 'email' })),
|
|
93
|
+
name: t.Optional(t.String({ minLength: 1 })),
|
|
94
|
+
}),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Delete user
|
|
99
|
+
.delete(
|
|
100
|
+
'/:id',
|
|
101
|
+
({ params: { id }, set }) => {
|
|
102
|
+
const userIndex = users.findIndex((u) => u.id === id);
|
|
103
|
+
|
|
104
|
+
if (userIndex === -1) {
|
|
105
|
+
set.status = 404;
|
|
106
|
+
return { error: 'User not found' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
users.splice(userIndex, 1);
|
|
110
|
+
return { success: true };
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
params: t.Object({
|
|
114
|
+
id: t.String({ format: 'uuid' }),
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["bun-types"],
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["./src/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{name}}/api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun run --hot src/index.ts",
|
|
7
|
+
"start": "bun run src/index.ts",
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"hono": "^4.7.0",
|
|
12
|
+
"@hono/zod-validator": "^0.4.0",
|
|
13
|
+
"@t3-oss/env-core": "^0.12.0",
|
|
14
|
+
"zod": "^3.24.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/bun": "latest",
|
|
18
|
+
"typescript": "^5.8.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { logger } from 'hono/logger';
|
|
4
|
+
import { prettyJSON } from 'hono/pretty-json';
|
|
5
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
6
|
+
|
|
7
|
+
import { healthRoutes } from './routes/health';
|
|
8
|
+
import { usersRoutes } from './routes/users';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// APP SETUP
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
const app = new Hono();
|
|
15
|
+
|
|
16
|
+
// Global middleware
|
|
17
|
+
app.use('*', logger());
|
|
18
|
+
app.use('*', secureHeaders());
|
|
19
|
+
app.use('*', prettyJSON());
|
|
20
|
+
app.use(
|
|
21
|
+
'*',
|
|
22
|
+
cors({
|
|
23
|
+
origin: ['http://localhost:3000', 'http://localhost:8081'],
|
|
24
|
+
credentials: true,
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// ROUTES
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
app.route('/health', healthRoutes);
|
|
33
|
+
app.route('/api/users', usersRoutes);
|
|
34
|
+
|
|
35
|
+
// Root route
|
|
36
|
+
app.get('/', (c) => {
|
|
37
|
+
return c.json({
|
|
38
|
+
name: '{{name}}-api',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
docs: '/health',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 404 handler
|
|
45
|
+
app.notFound((c) => {
|
|
46
|
+
return c.json({ error: 'Not Found', path: c.req.path }, 404);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Error handler
|
|
50
|
+
app.onError((err, c) => {
|
|
51
|
+
console.error('Error:', err);
|
|
52
|
+
return c.json({ error: 'Internal Server Error' }, 500);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// SERVER
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const port = process.env.PORT || 4000;
|
|
60
|
+
|
|
61
|
+
console.log(`🔥 Hono server running at http://localhost:${port}`);
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
port,
|
|
65
|
+
fetch: app.fetch,
|
|
66
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
server: {
|
|
6
|
+
PORT: z.string().default('4000'),
|
|
7
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
8
|
+
DATABASE_URL: z.string().url().optional(),
|
|
9
|
+
API_SECRET: z.string().min(32).optional(),
|
|
10
|
+
},
|
|
11
|
+
runtimeEnv: {
|
|
12
|
+
PORT: process.env.PORT,
|
|
13
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
14
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
15
|
+
API_SECRET: process.env.API_SECRET,
|
|
16
|
+
},
|
|
17
|
+
emptyStringAsUndefined: true,
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
|
|
3
|
+
export const healthRoutes = new Hono();
|
|
4
|
+
|
|
5
|
+
healthRoutes.get('/', (c) => {
|
|
6
|
+
return c.json({
|
|
7
|
+
status: 'ok',
|
|
8
|
+
timestamp: new Date().toISOString(),
|
|
9
|
+
uptime: process.uptime(),
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
healthRoutes.get('/ready', (c) => {
|
|
14
|
+
// Add database/external service checks here
|
|
15
|
+
return c.json({ ready: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
healthRoutes.get('/live', (c) => {
|
|
19
|
+
return c.json({ live: true });
|
|
20
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { zValidator } from '@hono/zod-validator';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// SCHEMAS
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
const CreateUserSchema = z.object({
|
|
10
|
+
email: z.string().email(),
|
|
11
|
+
name: z.string().min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const UpdateUserSchema = z.object({
|
|
15
|
+
email: z.string().email().optional(),
|
|
16
|
+
name: z.string().min(1).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const UserIdSchema = z.object({
|
|
20
|
+
id: z.string().uuid(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// MOCK DATA (replace with database)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
interface User {
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
name: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const users: User[] = [
|
|
35
|
+
{
|
|
36
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
37
|
+
email: 'john@example.com',
|
|
38
|
+
name: 'John Doe',
|
|
39
|
+
createdAt: new Date().toISOString(),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// ROUTES
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export const usersRoutes = new Hono();
|
|
48
|
+
|
|
49
|
+
// List users
|
|
50
|
+
usersRoutes.get('/', (c) => {
|
|
51
|
+
return c.json({ users, total: users.length });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Get user by ID
|
|
55
|
+
usersRoutes.get('/:id', zValidator('param', UserIdSchema), (c) => {
|
|
56
|
+
const { id } = c.req.valid('param');
|
|
57
|
+
const user = users.find((u) => u.id === id);
|
|
58
|
+
|
|
59
|
+
if (!user) {
|
|
60
|
+
return c.json({ error: 'User not found' }, 404);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return c.json(user);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Create user
|
|
67
|
+
usersRoutes.post('/', zValidator('json', CreateUserSchema), (c) => {
|
|
68
|
+
const data = c.req.valid('json');
|
|
69
|
+
|
|
70
|
+
const newUser: User = {
|
|
71
|
+
id: crypto.randomUUID(),
|
|
72
|
+
...data,
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
users.push(newUser);
|
|
77
|
+
return c.json(newUser, 201);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Update user
|
|
81
|
+
usersRoutes.patch(
|
|
82
|
+
'/:id',
|
|
83
|
+
zValidator('param', UserIdSchema),
|
|
84
|
+
zValidator('json', UpdateUserSchema),
|
|
85
|
+
(c) => {
|
|
86
|
+
const { id } = c.req.valid('param');
|
|
87
|
+
const data = c.req.valid('json');
|
|
88
|
+
|
|
89
|
+
const userIndex = users.findIndex((u) => u.id === id);
|
|
90
|
+
if (userIndex === -1) {
|
|
91
|
+
return c.json({ error: 'User not found' }, 404);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
users[userIndex] = { ...users[userIndex], ...data };
|
|
95
|
+
return c.json(users[userIndex]);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Delete user
|
|
100
|
+
usersRoutes.delete('/:id', zValidator('param', UserIdSchema), (c) => {
|
|
101
|
+
const { id } = c.req.valid('param');
|
|
102
|
+
|
|
103
|
+
const userIndex = users.findIndex((u) => u.id === id);
|
|
104
|
+
if (userIndex === -1) {
|
|
105
|
+
return c.json({ error: 'User not found' }, 404);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
users.splice(userIndex, 1);
|
|
109
|
+
return c.json({ success: true });
|
|
110
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["bun-types"],
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["./src/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Environment Variables
|
|
2
|
+
# Copy this file to .env.local and fill in the values
|
|
3
|
+
|
|
4
|
+
# Client-side (prefixed with EXPO_PUBLIC_)
|
|
5
|
+
EXPO_PUBLIC_API_URL=https://api.example.com
|
|
6
|
+
# EXPO_PUBLIC_SENTRY_DSN=https://...@sentry.io/...
|
|
7
|
+
|
|
8
|
+
# Server-side (for API routes if using)
|
|
9
|
+
# API_SECRET=your-secret-key
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
2
|
+
import { Stack } from 'expo-router';
|
|
3
|
+
import { StatusBar } from 'expo-status-bar';
|
|
4
|
+
|
|
5
|
+
import { persister, queryClient } from '../src/lib/query-client';
|
|
6
|
+
|
|
7
|
+
export default function RootLayout() {
|
|
8
|
+
return (
|
|
9
|
+
<PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
|
|
10
|
+
<Stack>
|
|
11
|
+
<Stack.Screen name="index" options={{ title: '{{name}}' }} />
|
|
12
|
+
</Stack>
|
|
13
|
+
<StatusBar style="auto" />
|
|
14
|
+
</PersistQueryClientProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useAppStore } from '@/lib/store';
|
|
4
|
+
|
|
5
|
+
export default function HomeScreen() {
|
|
6
|
+
// Example: using Zustand store with selector (prevents unnecessary re-renders)
|
|
7
|
+
const theme = useAppStore((state) => state.theme);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<View style={styles.container}>
|
|
11
|
+
<Text style={styles.title}>Welcome to {{name}}</Text>
|
|
12
|
+
<Text style={styles.subtitle}>Edit app/index.tsx to get started</Text>
|
|
13
|
+
<Text style={styles.hint}>Current theme: {theme}</Text>
|
|
14
|
+
</View>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const styles = StyleSheet.create({
|
|
19
|
+
container: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
padding: 24,
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
fontSize: 24,
|
|
27
|
+
fontWeight: 'bold',
|
|
28
|
+
marginBottom: 8,
|
|
29
|
+
},
|
|
30
|
+
subtitle: {
|
|
31
|
+
fontSize: 16,
|
|
32
|
+
color: '#666',
|
|
33
|
+
marginBottom: 16,
|
|
34
|
+
},
|
|
35
|
+
hint: {
|
|
36
|
+
fontSize: 14,
|
|
37
|
+
color: '#999',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "{{name}}",
|
|
4
|
+
"slug": "{{name}}",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"scheme": "{{name}}",
|
|
9
|
+
"userInterfaceStyle": "automatic",
|
|
10
|
+
"newArchEnabled": true,
|
|
11
|
+
"splash": {
|
|
12
|
+
"image": "./assets/splash-icon.png",
|
|
13
|
+
"resizeMode": "contain",
|
|
14
|
+
"backgroundColor": "#ffffff"
|
|
15
|
+
},
|
|
16
|
+
"ios": {
|
|
17
|
+
"supportsTablet": true,
|
|
18
|
+
"bundleIdentifier": "com.{{name}}.app"
|
|
19
|
+
},
|
|
20
|
+
"android": {
|
|
21
|
+
"adaptiveIcon": {
|
|
22
|
+
"foregroundImage": "./assets/adaptive-icon.png",
|
|
23
|
+
"backgroundColor": "#ffffff"
|
|
24
|
+
},
|
|
25
|
+
"package": "com.{{name}}.app"
|
|
26
|
+
},
|
|
27
|
+
"web": {
|
|
28
|
+
"bundler": "metro",
|
|
29
|
+
"output": "static",
|
|
30
|
+
"favicon": "./assets/favicon.png"
|
|
31
|
+
},
|
|
32
|
+
"plugins": ["expo-router"],
|
|
33
|
+
"experiments": {
|
|
34
|
+
"typedRoutes": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|