create-kuckit-app 0.2.0 → 0.2.1
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/package.json +1 -1
- package/templates/base/AGENTS.md +203 -0
- package/templates/base/apps/server/AGENTS.md +64 -8
- package/templates/base/apps/web/AGENTS.md +82 -8
- package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +82 -0
- package/templates/base/apps/web/src/modules.client.ts +4 -3
- package/templates/base/apps/web/src/routes/$.tsx +14 -0
- package/templates/base/apps/web/tsconfig.json +5 -1
- package/templates/base/apps/web/vite.config.ts +6 -0
- package/templates/base/packages/api/AGENTS.md +44 -5
- package/templates/base/packages/auth/AGENTS.md +17 -1
- package/templates/base/packages/db/AGENTS.md +16 -1
- package/templates/base/packages/items-module/AGENTS.md +99 -1
- package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +50 -68
package/package.json
CHANGED
package/templates/base/AGENTS.md
CHANGED
|
@@ -71,6 +71,209 @@ adapters → Implements ports, depends on domain
|
|
|
71
71
|
api → Wires everything together
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
## SDK Module System
|
|
75
|
+
|
|
76
|
+
> **SDK Documentation**: For comprehensive module patterns, see [@kuckit/sdk](https://github.com/draphonix/kuckit)
|
|
77
|
+
|
|
78
|
+
### Module Lifecycle
|
|
79
|
+
|
|
80
|
+
Modules are loaded in a specific sequence:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
1. createKuckitContainer()
|
|
84
|
+
└─► Core services registered (logger, db, cache, eventBus)
|
|
85
|
+
|
|
86
|
+
2. loadKuckitModules({ modules: [...] })
|
|
87
|
+
├─► Phase 1: register() - DI bindings for all modules
|
|
88
|
+
├─► Phase 2: registerApi() - API registrations collected
|
|
89
|
+
├─► Phase 3: onApiRegistrations callback (wire routers HERE)
|
|
90
|
+
├─► Phase 4: onBootstrap() - Startup logic
|
|
91
|
+
└─► Phase 5: onComplete callback
|
|
92
|
+
|
|
93
|
+
3. Application runs...
|
|
94
|
+
|
|
95
|
+
4. disposeContainer()
|
|
96
|
+
└─► For each module: onShutdown()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Core Services (CoreCradle)
|
|
100
|
+
|
|
101
|
+
After container creation, these services are available via DI:
|
|
102
|
+
|
|
103
|
+
| Token | Type | Lifetime | Description |
|
|
104
|
+
| ------------------ | ------------------ | --------- | --------------------------- |
|
|
105
|
+
| `config` | `CoreConfig` | Singleton | Application configuration |
|
|
106
|
+
| `db` | Drizzle | Singleton | Database query builder |
|
|
107
|
+
| `dbPool` | `Pool` | Singleton | PostgreSQL connection pool |
|
|
108
|
+
| `logger` | `Logger` | Singleton | Structured logging |
|
|
109
|
+
| `eventBus` | `EventBus` | Singleton | Pub/sub event system |
|
|
110
|
+
| `clock` | `Clock` | Singleton | Time abstraction (testable) |
|
|
111
|
+
| `cacheStore` | `CacheStore` | Singleton | Key-value cache |
|
|
112
|
+
| `rateLimiterStore` | `RateLimiterStore` | Singleton | Rate limiting |
|
|
113
|
+
| `auth` | Better-Auth | Singleton | Authentication utilities |
|
|
114
|
+
| `requestId` | `string` | Scoped | Per-request unique ID |
|
|
115
|
+
| `requestLogger` | `Logger` | Scoped | Logger with request context |
|
|
116
|
+
|
|
117
|
+
### Server Module Definition
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { defineKuckitModule, asClass, asFunction } from '@kuckit/sdk'
|
|
121
|
+
|
|
122
|
+
export const kuckitModule = defineKuckitModule({
|
|
123
|
+
id: 'myapp.billing',
|
|
124
|
+
displayName: 'Billing',
|
|
125
|
+
version: '1.0.0',
|
|
126
|
+
capabilities: ['nav.item', 'api.public'],
|
|
127
|
+
|
|
128
|
+
register(ctx) {
|
|
129
|
+
// Phase 1: Register DI bindings
|
|
130
|
+
ctx.container.register({
|
|
131
|
+
invoiceRepository: asClass(InvoiceRepository).scoped(),
|
|
132
|
+
createInvoice: asFunction(makeCreateInvoiceUseCase).scoped(),
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
registerApi(ctx) {
|
|
137
|
+
// Phase 2: Register API routes
|
|
138
|
+
ctx.addApiRegistration({
|
|
139
|
+
type: 'rpc-router',
|
|
140
|
+
name: 'invoices',
|
|
141
|
+
router: invoicesRouter,
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onBootstrap(ctx) {
|
|
146
|
+
// Phase 4: Startup logic (cache warming, logging, etc.)
|
|
147
|
+
ctx.container.resolve('logger').info('Billing module started')
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
onShutdown(ctx) {
|
|
151
|
+
// Cleanup on shutdown
|
|
152
|
+
ctx.container.resolve('logger').info('Billing module stopped')
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Client Module Definition
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { defineKuckitClientModule } from '@kuckit/sdk-react'
|
|
161
|
+
|
|
162
|
+
export const kuckitClientModule = defineKuckitClientModule({
|
|
163
|
+
id: 'myapp.billing',
|
|
164
|
+
displayName: 'Billing',
|
|
165
|
+
capabilities: ['nav.item', 'dashboard.widget'],
|
|
166
|
+
|
|
167
|
+
routes: [
|
|
168
|
+
{
|
|
169
|
+
id: 'billing-invoices',
|
|
170
|
+
path: '/billing/invoices',
|
|
171
|
+
component: InvoicesPage,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
|
|
175
|
+
navItems: [
|
|
176
|
+
{
|
|
177
|
+
id: 'billing-nav',
|
|
178
|
+
label: 'Billing',
|
|
179
|
+
href: '/billing/invoices',
|
|
180
|
+
icon: CreditCard,
|
|
181
|
+
order: 50,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
|
|
185
|
+
slots: {
|
|
186
|
+
'dashboard.widgets': {
|
|
187
|
+
component: BillingWidget,
|
|
188
|
+
order: 10,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Module Capabilities
|
|
195
|
+
|
|
196
|
+
Modules can declare capabilities for discovery:
|
|
197
|
+
|
|
198
|
+
- `nav.item` - Provides navigation items
|
|
199
|
+
- `settings.page` - Has settings page
|
|
200
|
+
- `dashboard.widget` - Provides dashboard widgets
|
|
201
|
+
- `api.webhook` - Exposes webhooks
|
|
202
|
+
- `api.public` - Has public API endpoints
|
|
203
|
+
- `slot.provider` - Provides slot components
|
|
204
|
+
|
|
205
|
+
### useRpc with TanStack Query
|
|
206
|
+
|
|
207
|
+
Module components use the `useRpc` hook to access the API:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
211
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
212
|
+
|
|
213
|
+
interface ItemsRpc {
|
|
214
|
+
items: {
|
|
215
|
+
list: (input: Record<string, never>) => Promise<Item[]>
|
|
216
|
+
create: (input: { name: string }) => Promise<Item>
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ItemsPage() {
|
|
221
|
+
const rpc = useRpc<ItemsRpc>()
|
|
222
|
+
const queryClient = useQueryClient()
|
|
223
|
+
|
|
224
|
+
const { data: items = [], isLoading } = useQuery({
|
|
225
|
+
queryKey: ['items'],
|
|
226
|
+
queryFn: () => rpc.items.list({}),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const createMutation = useMutation({
|
|
230
|
+
mutationFn: (data: { name: string }) => rpc.items.create(data),
|
|
231
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ... component render
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### oRPC Router Wiring (Important)
|
|
239
|
+
|
|
240
|
+
oRPC's `RPCHandler` captures the router object at construction time. Module routers must be wired **before** the handler is created:
|
|
241
|
+
|
|
242
|
+
1. Modules register routers via `registerApi()` hook
|
|
243
|
+
2. Server wires routers in `onApiRegistrations` into a **mutable router object**
|
|
244
|
+
3. `RPCHandler` is created **after** modules are loaded
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// apps/server/src/rpc-router-registry.ts
|
|
248
|
+
export const rootRpcRouter = { ...appRouter }
|
|
249
|
+
|
|
250
|
+
export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
|
|
251
|
+
for (const reg of registrations) {
|
|
252
|
+
if (reg.type === 'rpc-router') {
|
|
253
|
+
rootRpcRouter[reg.name] = reg.router
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Typing DI in Routers
|
|
260
|
+
|
|
261
|
+
Use a module-local interface to type `context.di.cradle`:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// In your module's router file
|
|
265
|
+
interface BillingCradle {
|
|
266
|
+
createInvoice: (input: CreateInvoiceInput) => Promise<Invoice>
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const invoicesRouter = {
|
|
270
|
+
create: protectedProcedure.input(createInvoiceSchema).handler(async ({ input, context }) => {
|
|
271
|
+
const { createInvoice } = context.di.cradle as BillingCradle
|
|
272
|
+
return createInvoice(input)
|
|
273
|
+
}),
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
74
277
|
## Common Tasks
|
|
75
278
|
|
|
76
279
|
### Add a new API endpoint
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - Server App
|
|
2
2
|
|
|
3
|
-
> See root [AGENTS.md](../../AGENTS.md) for
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -8,13 +8,69 @@ Express backend hosting oRPC API, Better-Auth authentication, and the Kuckit mod
|
|
|
8
8
|
|
|
9
9
|
## Key Files
|
|
10
10
|
|
|
11
|
-
| File
|
|
12
|
-
|
|
|
13
|
-
| `server.ts`
|
|
14
|
-
| `container.ts`
|
|
15
|
-
| `config/modules.ts`
|
|
16
|
-
| `rpc.ts`
|
|
17
|
-
| `
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
| ------------------------ | -------------------------- |
|
|
13
|
+
| `server.ts` | Entry point, bootstrap |
|
|
14
|
+
| `container.ts` | DI container setup |
|
|
15
|
+
| `config/modules.ts` | Module registration |
|
|
16
|
+
| `rpc.ts` | oRPC handler setup |
|
|
17
|
+
| `rpc-router-registry.ts` | Mutable router for modules |
|
|
18
|
+
| `auth.ts` | Better-Auth configuration |
|
|
19
|
+
|
|
20
|
+
## Module Loading Sequence
|
|
21
|
+
|
|
22
|
+
The server bootstraps in this order:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. createKuckitContainer() - Core services (db, logger, cache, etc.)
|
|
26
|
+
2. loadKuckitModules() with onApiRegistrations callback:
|
|
27
|
+
├─► register() hooks run (DI bindings)
|
|
28
|
+
├─► registerApi() hooks run (routers collected)
|
|
29
|
+
├─► onApiRegistrations() - Wire routers to rootRpcRouter
|
|
30
|
+
└─► onBootstrap() hooks run (startup logic)
|
|
31
|
+
3. setupRPC() - Create RPCHandler AFTER modules loaded
|
|
32
|
+
4. Express listens
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Router Registry Pattern
|
|
36
|
+
|
|
37
|
+
oRPC's `RPCHandler` captures the router at construction time. Modules wire their routers into a **mutable object** before the handler is created:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// rpc-router-registry.ts
|
|
41
|
+
export const rootRpcRouter = { ...appRouter }
|
|
42
|
+
|
|
43
|
+
export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
|
|
44
|
+
for (const reg of registrations) {
|
|
45
|
+
if (reg.type === 'rpc-router') {
|
|
46
|
+
rootRpcRouter[reg.name] = reg.router
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// server.ts
|
|
54
|
+
await loadKuckitModules({
|
|
55
|
+
container,
|
|
56
|
+
modules: getModuleSpecs(),
|
|
57
|
+
onApiRegistrations: (registrations) => {
|
|
58
|
+
wireModuleRpcRouters(registrations) // Wire BEFORE setupRPC()
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
setupRPC(app) // Now create handler with fully-wired router
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Per-Request Scoping
|
|
66
|
+
|
|
67
|
+
Each HTTP request gets a scoped container with:
|
|
68
|
+
|
|
69
|
+
- `requestId` - Unique request identifier
|
|
70
|
+
- `requestLogger` - Logger with request context
|
|
71
|
+
- `session` - Current user session (if authenticated)
|
|
72
|
+
|
|
73
|
+
Access scoped services in routers via `context.di.cradle`.
|
|
18
74
|
|
|
19
75
|
## Adding New Modules
|
|
20
76
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - Web App
|
|
2
2
|
|
|
3
|
-
> See root [AGENTS.md](../../AGENTS.md) for
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -8,13 +8,15 @@ React frontend using TanStack Router, TanStack Query, and Kuckit client modules.
|
|
|
8
8
|
|
|
9
9
|
## Key Files
|
|
10
10
|
|
|
11
|
-
| File
|
|
12
|
-
|
|
|
13
|
-
| `main.tsx`
|
|
14
|
-
| `modules.client.ts`
|
|
15
|
-
| `providers/KuckitProvider.tsx`
|
|
16
|
-
| `providers/ServicesProvider.tsx`
|
|
17
|
-
| `routes/`
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
| ---------------------------------- | --------------------------------- |
|
|
13
|
+
| `main.tsx` | App entry point |
|
|
14
|
+
| `modules.client.ts` | Client module registration |
|
|
15
|
+
| `providers/KuckitProvider.tsx` | Kuckit context setup |
|
|
16
|
+
| `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
|
|
17
|
+
| `routes/` | TanStack Router file-based routes |
|
|
18
|
+
| `routes/$.tsx` | Catch-all for module routes |
|
|
19
|
+
| `components/KuckitModuleRoute.tsx` | Renders module-registered routes |
|
|
18
20
|
|
|
19
21
|
## Adding Routes
|
|
20
22
|
|
|
@@ -24,6 +26,78 @@ Create files in `src/routes/`:
|
|
|
24
26
|
- `src/routes/users/$id.tsx` → `/users/:id`
|
|
25
27
|
- `src/routes/settings/index.tsx` → `/settings`
|
|
26
28
|
|
|
29
|
+
**File-based routes take precedence** over module-registered routes.
|
|
30
|
+
|
|
31
|
+
## Module Route Integration
|
|
32
|
+
|
|
33
|
+
Modules register routes via `defineKuckitClientModule`. These are rendered via a catch-all pattern:
|
|
34
|
+
|
|
35
|
+
1. Module registers route in `routes` array
|
|
36
|
+
2. `RouteRegistry` collects routes during `loadKuckitClientModules()`
|
|
37
|
+
3. Catch-all route (`$.tsx`) matches any unhandled path
|
|
38
|
+
4. `KuckitModuleRoute` looks up path in registry and renders the component
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// routes/$.tsx - Catches module routes
|
|
42
|
+
import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
|
|
43
|
+
|
|
44
|
+
export function Route() {
|
|
45
|
+
return <KuckitModuleRoute />
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Context Providers
|
|
50
|
+
|
|
51
|
+
The app wraps content in Kuckit providers:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<ServicesProvider>
|
|
55
|
+
{' '}
|
|
56
|
+
{/* RPC client, auth */}
|
|
57
|
+
<KuckitProvider>
|
|
58
|
+
{' '}
|
|
59
|
+
{/* Nav, slots, routes registries */}
|
|
60
|
+
<KuckitRpcProvider>
|
|
61
|
+
{' '}
|
|
62
|
+
{/* RPC context for modules */}
|
|
63
|
+
{children}
|
|
64
|
+
</KuckitRpcProvider>
|
|
65
|
+
</KuckitProvider>
|
|
66
|
+
</ServicesProvider>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Available hooks:**
|
|
70
|
+
|
|
71
|
+
- `useRpc<T>()` - Get typed RPC client for API calls
|
|
72
|
+
- `useNavItems()` - Flat navigation items list
|
|
73
|
+
- `useNavTree()` - Hierarchical navigation tree
|
|
74
|
+
- `useSlot(name)` - Get components for a slot
|
|
75
|
+
- `useHasSlot(name)` - Check if slot has content
|
|
76
|
+
|
|
77
|
+
## useRpc Hook
|
|
78
|
+
|
|
79
|
+
Module components must use `useRpc()` to access the API:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
83
|
+
import { useQuery } from '@tanstack/react-query'
|
|
84
|
+
|
|
85
|
+
interface MyRpc {
|
|
86
|
+
items: { list: (input: {}) => Promise<Item[]> }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function MyComponent() {
|
|
90
|
+
const rpc = useRpc<MyRpc>()
|
|
91
|
+
|
|
92
|
+
const { data } = useQuery({
|
|
93
|
+
queryKey: ['items'],
|
|
94
|
+
queryFn: () => rpc.items.list({}),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Important**: Never use `import.meta.env.VITE_SERVER_URL` directly in module components - it's unavailable in external packages. The `useRpc` hook provides the correctly-configured client.
|
|
100
|
+
|
|
27
101
|
## Using Module UI Components
|
|
28
102
|
|
|
29
103
|
```tsx
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useRouterState, Link, useNavigate } from '@tanstack/react-router'
|
|
2
|
+
import { useKuckit } from '@/providers/KuckitProvider'
|
|
3
|
+
import { useServices } from '@/providers/ServicesProvider'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dynamic route renderer for Kuckit module routes.
|
|
8
|
+
*
|
|
9
|
+
* This component looks up the current path in the RouteRegistry
|
|
10
|
+
* and renders the corresponding module component if found.
|
|
11
|
+
*
|
|
12
|
+
* For routes with meta.requiresAuth: true, redirects to /login if not authenticated.
|
|
13
|
+
*/
|
|
14
|
+
export function KuckitModuleRoute() {
|
|
15
|
+
const { routeRegistry } = useKuckit()
|
|
16
|
+
const { authClient } = useServices()
|
|
17
|
+
const { location } = useRouterState()
|
|
18
|
+
const navigate = useNavigate()
|
|
19
|
+
const pathname = location.pathname
|
|
20
|
+
|
|
21
|
+
const [authChecked, setAuthChecked] = useState(false)
|
|
22
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
23
|
+
|
|
24
|
+
// Find matching route in registry
|
|
25
|
+
const routeDef = routeRegistry.getAll().find((r) => r.path === pathname)
|
|
26
|
+
|
|
27
|
+
// Check auth for routes with requiresAuth
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
async function checkAuth() {
|
|
30
|
+
if (!routeDef) {
|
|
31
|
+
setAuthChecked(true)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if auth is required
|
|
36
|
+
if (routeDef.meta?.requiresAuth) {
|
|
37
|
+
const session = await authClient.getSession()
|
|
38
|
+
if (!session.data) {
|
|
39
|
+
// Redirect to login with return URL
|
|
40
|
+
navigate({
|
|
41
|
+
to: '/login',
|
|
42
|
+
search: { redirect: pathname },
|
|
43
|
+
})
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
setIsAuthenticated(true)
|
|
47
|
+
}
|
|
48
|
+
setAuthChecked(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
checkAuth()
|
|
52
|
+
}, [routeDef, pathname, navigate, authClient])
|
|
53
|
+
|
|
54
|
+
// Show loading while checking auth for protected routes
|
|
55
|
+
if (routeDef?.meta?.requiresAuth && !authChecked) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
58
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Protected route - waiting for redirect
|
|
64
|
+
if (routeDef?.meta?.requiresAuth && !isAuthenticated) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!routeDef) {
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
|
|
71
|
+
<h1 className="text-2xl font-bold">Page Not Found</h1>
|
|
72
|
+
<p className="text-muted-foreground">The page "{pathname}" could not be found.</p>
|
|
73
|
+
<Link to="/" className="text-primary hover:underline">
|
|
74
|
+
Go Home
|
|
75
|
+
</Link>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const Component = routeDef.component as React.ComponentType
|
|
81
|
+
return <Component />
|
|
82
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientModuleSpec } from '@kuckit/sdk-react'
|
|
2
|
+
import { kuckitClientModule as itemsClientModule } from '@__APP_NAME_KEBAB__/items-module/client'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Client modules configuration
|
|
@@ -7,9 +8,9 @@ import type { ClientModuleSpec } from '@kuckit/sdk-react'
|
|
|
7
8
|
*/
|
|
8
9
|
export const getClientModuleSpecs = (): ClientModuleSpec[] => {
|
|
9
10
|
const modules: ClientModuleSpec[] = [
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
//
|
|
11
|
+
// KUCKIT_CLIENT_MODULES_START
|
|
12
|
+
{ module: itemsClientModule },
|
|
13
|
+
// KUCKIT_CLIENT_MODULES_END
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
return modules
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Catch-all route for KuckitModule routes at root level.
|
|
6
|
+
*
|
|
7
|
+
* This route matches any path not handled by other file-based routes
|
|
8
|
+
* and uses the KuckitModuleRoute component to look up and render
|
|
9
|
+
* the appropriate module component from the RouteRegistry.
|
|
10
|
+
*/
|
|
11
|
+
// @ts-expect-error - Route types are generated when dev server runs
|
|
12
|
+
export const Route = createFileRoute('/$')({
|
|
13
|
+
component: KuckitModuleRoute,
|
|
14
|
+
})
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import react from '@vitejs/plugin-react'
|
|
3
3
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
|
4
|
+
import path from 'path'
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
6
7
|
plugins: [TanStackRouterVite(), react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@': path.resolve(__dirname, './src'),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
7
13
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - API Package
|
|
2
2
|
|
|
3
|
-
> See root [AGENTS.md](../../AGENTS.md) for
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -13,15 +13,54 @@ Shared API context, types, and procedure definitions for oRPC.
|
|
|
13
13
|
| `context.ts` | Request context type definitions |
|
|
14
14
|
| `index.ts` | Public exports |
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Procedure Types
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
|
-
import {
|
|
19
|
+
import { publicProcedure, protectedProcedure } from '@__APP_NAME_KEBAB__/api'
|
|
20
|
+
|
|
21
|
+
// Public - no authentication required
|
|
22
|
+
export const healthRouter = {
|
|
23
|
+
ping: publicProcedure.handler(() => 'pong'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Protected - requires authenticated user
|
|
27
|
+
export const itemsRouter = {
|
|
28
|
+
list: protectedProcedure.handler(async ({ context }) => {
|
|
29
|
+
const userId = context.session?.user?.id
|
|
30
|
+
// ...
|
|
31
|
+
}),
|
|
32
|
+
}
|
|
20
33
|
```
|
|
21
34
|
|
|
22
35
|
## Context Structure
|
|
23
36
|
|
|
24
37
|
The API context provides:
|
|
25
38
|
|
|
26
|
-
- `
|
|
27
|
-
- `
|
|
39
|
+
- `di` - Scoped DI container for the request
|
|
40
|
+
- `session` - Current user session (when using `protectedProcedure`)
|
|
41
|
+
|
|
42
|
+
## Typing DI Access in Routers
|
|
43
|
+
|
|
44
|
+
Use a module-local interface to type `context.di.cradle`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// In your module's router file
|
|
48
|
+
interface ItemsCradle {
|
|
49
|
+
itemRepository: ItemRepository
|
|
50
|
+
createItem: (input: CreateItemInput) => Promise<Item>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const itemsRouter = {
|
|
54
|
+
create: protectedProcedure.input(createItemSchema).handler(async ({ input, context }) => {
|
|
55
|
+
const { createItem } = context.di.cradle as ItemsCradle
|
|
56
|
+
return createItem(input)
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Why module-local interfaces:**
|
|
62
|
+
|
|
63
|
+
- Type assertion stays within the module
|
|
64
|
+
- Module remains self-contained
|
|
65
|
+
- Server app doesn't need to know about module internals
|
|
66
|
+
- Scales to any number of modules without server changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - Auth Package
|
|
2
2
|
|
|
3
|
-
> See root [AGENTS.md](../../AGENTS.md) for
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -36,6 +36,22 @@ await authClient.signOut()
|
|
|
36
36
|
const session = await authClient.getSession()
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Session in oRPC Context
|
|
40
|
+
|
|
41
|
+
The current user session is available in `protectedProcedure` handlers:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { protectedProcedure } from '@__APP_NAME_KEBAB__/api'
|
|
45
|
+
|
|
46
|
+
export const myRouter = {
|
|
47
|
+
getProfile: protectedProcedure.handler(async ({ context }) => {
|
|
48
|
+
const userId = context.session?.user?.id
|
|
49
|
+
const userEmail = context.session?.user?.email
|
|
50
|
+
// ... use session data
|
|
51
|
+
}),
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
39
55
|
## Configuration
|
|
40
56
|
|
|
41
57
|
Auth requires these environment variables:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - Database Package
|
|
2
2
|
|
|
3
|
-
> See root [AGENTS.md](../../AGENTS.md) for
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -50,6 +50,21 @@ bun run db:studio
|
|
|
50
50
|
bun run db:migrate
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## Module Schema Pattern
|
|
54
|
+
|
|
55
|
+
Modules can define their own schemas in `adapters/`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// packages/items-module/src/adapters/items.schema.ts
|
|
59
|
+
export const itemsTable = pgTable('items', {
|
|
60
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
61
|
+
userId: text('user_id').notNull(),
|
|
62
|
+
name: text('name').notNull(),
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The schema is imported by `drizzle.config.ts` for migration generation.
|
|
67
|
+
|
|
53
68
|
## Connection
|
|
54
69
|
|
|
55
70
|
```typescript
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# AGENTS.md - Items Module
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **SDK Documentation**: For detailed module patterns, see the root [AGENTS.md](../../AGENTS.md) SDK Module System section
|
|
4
|
+
>
|
|
5
|
+
> - Server modules: `defineKuckitModule()` from `@kuckit/sdk`
|
|
6
|
+
> - Client modules: `defineKuckitClientModule()` from `@kuckit/sdk-react`
|
|
4
7
|
|
|
5
8
|
## Purpose
|
|
6
9
|
|
|
@@ -110,3 +113,98 @@ export const kuckitModule = defineKuckitModule({
|
|
|
110
113
|
6. Implement use cases
|
|
111
114
|
7. Create API router
|
|
112
115
|
8. Register in server's `config/modules.ts`
|
|
116
|
+
|
|
117
|
+
## Registration in Apps
|
|
118
|
+
|
|
119
|
+
### Server Registration
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// apps/server/src/config/modules.ts
|
|
123
|
+
import { kuckitModule as itemsModule } from '@__APP_NAME_KEBAB__/items-module'
|
|
124
|
+
|
|
125
|
+
export const modules = [itemsModule]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Client Registration
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// apps/web/src/modules.client.ts
|
|
132
|
+
import { kuckitClientModule as itemsClient } from '@__APP_NAME_KEBAB__/items-module/client'
|
|
133
|
+
|
|
134
|
+
export const clientModules = [{ module: itemsClient }]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Client Module Pattern
|
|
138
|
+
|
|
139
|
+
The client module registers routes, navigation items, and slots:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// client-module.ts
|
|
143
|
+
import { defineKuckitClientModule } from '@kuckit/sdk-react'
|
|
144
|
+
import { ItemsPage } from './ui/ItemsPage'
|
|
145
|
+
|
|
146
|
+
export const kuckitClientModule = defineKuckitClientModule({
|
|
147
|
+
id: 'items',
|
|
148
|
+
displayName: 'Items',
|
|
149
|
+
|
|
150
|
+
routes: [
|
|
151
|
+
{
|
|
152
|
+
id: 'items-page',
|
|
153
|
+
path: '/items',
|
|
154
|
+
component: ItemsPage,
|
|
155
|
+
meta: { requiresAuth: true },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
|
|
159
|
+
navItems: [
|
|
160
|
+
{
|
|
161
|
+
id: 'items-nav',
|
|
162
|
+
label: 'Items',
|
|
163
|
+
href: '/items',
|
|
164
|
+
order: 20,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Using useRpc in Components
|
|
171
|
+
|
|
172
|
+
Module components access the API via the `useRpc` hook:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// ui/ItemsPage.tsx
|
|
176
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
177
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
178
|
+
|
|
179
|
+
interface ItemsRpc {
|
|
180
|
+
items: {
|
|
181
|
+
list: (input: Record<string, never>) => Promise<Item[]>
|
|
182
|
+
create: (input: { name: string; description?: string }) => Promise<Item>
|
|
183
|
+
delete: (input: { id: string }) => Promise<void>
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function ItemsPage() {
|
|
188
|
+
const rpc = useRpc<ItemsRpc>()
|
|
189
|
+
const queryClient = useQueryClient()
|
|
190
|
+
|
|
191
|
+
const { data: items = [], isLoading } = useQuery({
|
|
192
|
+
queryKey: ['items'],
|
|
193
|
+
queryFn: () => rpc.items.list({}),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const createMutation = useMutation({
|
|
197
|
+
mutationFn: (data: { name: string }) => rpc.items.create(data),
|
|
198
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const deleteMutation = useMutation({
|
|
202
|
+
mutationFn: (id: string) => rpc.items.delete({ id }),
|
|
203
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// ... render items list with create/delete handlers
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Important**: Never use `import.meta.env` directly in module components - use `useRpc()` instead. Module packages are bundled separately and don't have access to the host app's environment variables.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
2
4
|
|
|
3
5
|
interface Item {
|
|
4
6
|
id: string
|
|
@@ -8,85 +10,63 @@ interface Item {
|
|
|
8
10
|
updatedAt: Date
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
interface ItemsRpc {
|
|
14
|
+
items: {
|
|
15
|
+
list: (input: Record<string, never>) => Promise<Item[]>
|
|
16
|
+
create: (input: { name: string; description?: string }) => Promise<Item>
|
|
17
|
+
delete: (input: { id: string }) => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Items page component
|
|
13
|
-
* Demonstrates CRUD operations using the items module
|
|
23
|
+
* Demonstrates CRUD operations using the items module with useRpc and TanStack Query
|
|
14
24
|
*/
|
|
15
25
|
export function ItemsPage() {
|
|
16
|
-
const
|
|
26
|
+
const rpc = useRpc<ItemsRpc>()
|
|
27
|
+
const queryClient = useQueryClient()
|
|
17
28
|
const [newItemName, setNewItemName] = useState('')
|
|
18
29
|
const [newItemDescription, setNewItemDescription] = useState('')
|
|
19
|
-
const [loading, setLoading] = useState(true)
|
|
20
|
-
const [error, setError] = useState<string | null>(null)
|
|
21
30
|
|
|
22
|
-
const
|
|
31
|
+
const {
|
|
32
|
+
data: items = [],
|
|
33
|
+
isLoading,
|
|
34
|
+
error,
|
|
35
|
+
} = useQuery({
|
|
36
|
+
queryKey: ['items'],
|
|
37
|
+
queryFn: () => rpc.items.list({}),
|
|
38
|
+
})
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
40
|
+
const createMutation = useMutation({
|
|
41
|
+
mutationFn: (data: { name: string; description?: string }) => rpc.items.create(data),
|
|
42
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
43
|
+
})
|
|
28
44
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
credentials: 'include',
|
|
36
|
-
body: JSON.stringify({}),
|
|
37
|
-
})
|
|
38
|
-
if (response.ok) {
|
|
39
|
-
const data = await response.json()
|
|
40
|
-
setItems(data)
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
setError('Failed to load items')
|
|
44
|
-
} finally {
|
|
45
|
-
setLoading(false)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
45
|
+
const deleteMutation = useMutation({
|
|
46
|
+
mutationFn: (id: string) => rpc.items.delete({ id }),
|
|
47
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
48
|
+
})
|
|
48
49
|
|
|
49
50
|
const createItem = async (e: React.FormEvent) => {
|
|
50
51
|
e.preventDefault()
|
|
51
52
|
if (!newItemName.trim()) return
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
description: newItemDescription || undefined,
|
|
61
|
-
}),
|
|
62
|
-
})
|
|
63
|
-
if (response.ok) {
|
|
64
|
-
setNewItemName('')
|
|
65
|
-
setNewItemDescription('')
|
|
66
|
-
fetchItems()
|
|
54
|
+
createMutation.mutate(
|
|
55
|
+
{ name: newItemName, description: newItemDescription || undefined },
|
|
56
|
+
{
|
|
57
|
+
onSuccess: () => {
|
|
58
|
+
setNewItemName('')
|
|
59
|
+
setNewItemDescription('')
|
|
60
|
+
},
|
|
67
61
|
}
|
|
68
|
-
|
|
69
|
-
setError('Failed to create item')
|
|
70
|
-
}
|
|
62
|
+
)
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
const deleteItem =
|
|
74
|
-
|
|
75
|
-
const response = await fetch(`${apiUrl}/rpc/items.delete`, {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: { 'Content-Type': 'application/json' },
|
|
78
|
-
credentials: 'include',
|
|
79
|
-
body: JSON.stringify({ id }),
|
|
80
|
-
})
|
|
81
|
-
if (response.ok) {
|
|
82
|
-
fetchItems()
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
setError('Failed to delete item')
|
|
86
|
-
}
|
|
65
|
+
const deleteItem = (id: string) => {
|
|
66
|
+
deleteMutation.mutate(id)
|
|
87
67
|
}
|
|
88
68
|
|
|
89
|
-
if (
|
|
69
|
+
if (isLoading) {
|
|
90
70
|
return <div style={{ padding: '2rem' }}>Loading items...</div>
|
|
91
71
|
}
|
|
92
72
|
|
|
@@ -94,12 +74,9 @@ export function ItemsPage() {
|
|
|
94
74
|
<div style={{ padding: '2rem', fontFamily: 'system-ui', maxWidth: '600px', margin: '0 auto' }}>
|
|
95
75
|
<h1>Items</h1>
|
|
96
76
|
|
|
97
|
-
{error && (
|
|
77
|
+
{(error || createMutation.error || deleteMutation.error) && (
|
|
98
78
|
<div style={{ color: 'red', marginBottom: '1rem' }}>
|
|
99
|
-
{error}
|
|
100
|
-
<button onClick={() => setError(null)} style={{ marginLeft: '1rem' }}>
|
|
101
|
-
Dismiss
|
|
102
|
-
</button>
|
|
79
|
+
{error?.message || createMutation.error?.message || deleteMutation.error?.message}
|
|
103
80
|
</div>
|
|
104
81
|
)}
|
|
105
82
|
|
|
@@ -120,8 +97,12 @@ export function ItemsPage() {
|
|
|
120
97
|
onChange={(e) => setNewItemDescription(e.target.value)}
|
|
121
98
|
style={{ padding: '0.5rem', fontSize: '1rem' }}
|
|
122
99
|
/>
|
|
123
|
-
<button
|
|
124
|
-
|
|
100
|
+
<button
|
|
101
|
+
type="submit"
|
|
102
|
+
disabled={createMutation.isPending}
|
|
103
|
+
style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
104
|
+
>
|
|
105
|
+
{createMutation.isPending ? 'Adding...' : 'Add Item'}
|
|
125
106
|
</button>
|
|
126
107
|
</div>
|
|
127
108
|
</form>
|
|
@@ -148,6 +129,7 @@ export function ItemsPage() {
|
|
|
148
129
|
</div>
|
|
149
130
|
<button
|
|
150
131
|
onClick={() => deleteItem(item.id)}
|
|
132
|
+
disabled={deleteMutation.isPending}
|
|
151
133
|
style={{ cursor: 'pointer', color: 'red', background: 'none', border: 'none' }}
|
|
152
134
|
>
|
|
153
135
|
Delete
|