@donkeylabs/server 0.1.1 → 0.1.2
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/cli/donkeylabs +6 -0
- package/context.d.ts +17 -0
- package/docs/api-client.md +520 -0
- package/docs/cache.md +437 -0
- package/docs/cli.md +353 -0
- package/docs/core-services.md +338 -0
- package/docs/cron.md +465 -0
- package/docs/errors.md +303 -0
- package/docs/events.md +460 -0
- package/docs/handlers.md +549 -0
- package/docs/jobs.md +556 -0
- package/docs/logger.md +316 -0
- package/docs/middleware.md +682 -0
- package/docs/plugins.md +524 -0
- package/docs/project-structure.md +493 -0
- package/docs/rate-limiter.md +525 -0
- package/docs/router.md +566 -0
- package/docs/sse.md +542 -0
- package/docs/svelte-frontend.md +324 -0
- package/package.json +12 -9
- package/registry.d.ts +11 -0
- package/src/index.ts +1 -1
- package/src/server.ts +1 -0
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
Plugins are the core building blocks of this framework. Each plugin encapsulates database schema, business logic, custom handlers, and middleware into a self-contained module.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Creating a Plugin](#creating-a-plugin)
|
|
8
|
+
- [Plugin with Database Schema](#plugin-with-database-schema)
|
|
9
|
+
- [Plugin with Configuration](#plugin-with-configuration)
|
|
10
|
+
- [Plugin Dependencies](#plugin-dependencies)
|
|
11
|
+
- [Custom Handlers](#custom-handlers)
|
|
12
|
+
- [Custom Middleware](#custom-middleware)
|
|
13
|
+
- [Plugin Context](#plugin-context)
|
|
14
|
+
- [Type Inference](#type-inference)
|
|
15
|
+
- [Plugin Lifecycle](#plugin-lifecycle)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Creating a Plugin
|
|
20
|
+
|
|
21
|
+
The simplest plugin exports a service that becomes available to all route handlers:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// plugins/greeter/index.ts
|
|
25
|
+
import { createPlugin } from "../../core";
|
|
26
|
+
|
|
27
|
+
export const greeterPlugin = createPlugin.define({
|
|
28
|
+
name: "greeter",
|
|
29
|
+
service: async (ctx) => {
|
|
30
|
+
// Return the service object
|
|
31
|
+
return {
|
|
32
|
+
sayHello: (name: string) => `Hello, ${name}!`,
|
|
33
|
+
sayGoodbye: (name: string) => `Goodbye, ${name}!`,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Usage in routes:**
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
router.route("greet").typed({
|
|
43
|
+
input: z.object({ name: z.string() }),
|
|
44
|
+
handle: async (input, ctx) => {
|
|
45
|
+
// Access via ctx.plugins.<pluginName>
|
|
46
|
+
return { message: ctx.plugins.greeter.sayHello(input.name) };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Plugin with Database Schema
|
|
54
|
+
|
|
55
|
+
Plugins can define their own database tables. The framework automatically merges all plugin schemas for full type safety.
|
|
56
|
+
|
|
57
|
+
### Step 1: Create Migration
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// plugins/users/migrations/001_create_users.ts
|
|
61
|
+
import type { Kysely } from "kysely";
|
|
62
|
+
|
|
63
|
+
export async function up(db: Kysely<any>) {
|
|
64
|
+
await db.schema
|
|
65
|
+
.createTable("users")
|
|
66
|
+
.ifNotExists()
|
|
67
|
+
.addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
|
|
68
|
+
.addColumn("email", "text", (c) => c.notNull().unique())
|
|
69
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
70
|
+
.addColumn("created_at", "text", (c) => c.defaultTo("CURRENT_TIMESTAMP"))
|
|
71
|
+
.execute();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function down(db: Kysely<any>) {
|
|
75
|
+
await db.schema.dropTable("users").execute();
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Step 2: Generate Schema Types
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
bun scripts/generate-types.ts users
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This creates `plugins/users/schema.ts` with Kysely types.
|
|
86
|
+
|
|
87
|
+
### Step 3: Define Plugin with Schema
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// plugins/users/index.ts
|
|
91
|
+
import { createPlugin } from "../../core";
|
|
92
|
+
import type { DB as UsersSchema } from "./schema";
|
|
93
|
+
|
|
94
|
+
export const usersPlugin = createPlugin
|
|
95
|
+
.withSchema<UsersSchema>() // Enable typed database access
|
|
96
|
+
.define({
|
|
97
|
+
name: "users",
|
|
98
|
+
service: async (ctx) => {
|
|
99
|
+
return {
|
|
100
|
+
async create(email: string, name: string) {
|
|
101
|
+
// ctx.db is fully typed with UsersSchema!
|
|
102
|
+
const result = await ctx.db
|
|
103
|
+
.insertInto("users")
|
|
104
|
+
.values({ email, name })
|
|
105
|
+
.returning(["id", "email", "name"])
|
|
106
|
+
.executeTakeFirstOrThrow();
|
|
107
|
+
return result;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async findByEmail(email: string) {
|
|
111
|
+
return ctx.db
|
|
112
|
+
.selectFrom("users")
|
|
113
|
+
.selectAll()
|
|
114
|
+
.where("email", "=", email)
|
|
115
|
+
.executeTakeFirst();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async list() {
|
|
119
|
+
return ctx.db.selectFrom("users").selectAll().execute();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Plugin with Configuration
|
|
129
|
+
|
|
130
|
+
Plugins can accept configuration at registration time using the factory pattern:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// plugins/email/index.ts
|
|
134
|
+
import { createPlugin } from "../../core";
|
|
135
|
+
|
|
136
|
+
export interface EmailConfig {
|
|
137
|
+
apiKey: string;
|
|
138
|
+
fromAddress: string;
|
|
139
|
+
sandbox?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const emailPlugin = createPlugin
|
|
143
|
+
.withConfig<EmailConfig>() // Declare config type
|
|
144
|
+
.define({
|
|
145
|
+
name: "email",
|
|
146
|
+
service: async (ctx) => {
|
|
147
|
+
// Access config via ctx.config
|
|
148
|
+
const { apiKey, fromAddress, sandbox } = ctx.config;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
async send(to: string, subject: string, body: string) {
|
|
152
|
+
if (sandbox) {
|
|
153
|
+
console.log(`[Sandbox] Would send to ${to}: ${subject}`);
|
|
154
|
+
return { id: "sandbox-" + Date.now() };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Real implementation using apiKey
|
|
158
|
+
const response = await fetch("https://api.email.com/send", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
161
|
+
body: JSON.stringify({ from: fromAddress, to, subject, body }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return response.json();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
getConfig() {
|
|
168
|
+
return { fromAddress, sandbox };
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Registering with config:**
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// The plugin becomes a factory function when using withConfig
|
|
179
|
+
server.registerPlugin(
|
|
180
|
+
emailPlugin({
|
|
181
|
+
apiKey: process.env.EMAIL_API_KEY!,
|
|
182
|
+
fromAddress: "noreply@example.com",
|
|
183
|
+
sandbox: process.env.NODE_ENV !== "production",
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Plugin Dependencies
|
|
191
|
+
|
|
192
|
+
Plugins can depend on other plugins. Dependencies are resolved automatically in topological order.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// plugins/notifications/index.ts
|
|
196
|
+
import { createPlugin } from "../../core";
|
|
197
|
+
|
|
198
|
+
export const notificationsPlugin = createPlugin.define({
|
|
199
|
+
name: "notifications",
|
|
200
|
+
dependencies: ["users", "email"] as const, // Declare dependencies
|
|
201
|
+
service: async (ctx) => {
|
|
202
|
+
// Access dependency services via ctx.deps
|
|
203
|
+
const { users, email } = ctx.deps;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
async notifyUser(userId: number, message: string) {
|
|
207
|
+
// Use the users plugin service
|
|
208
|
+
const user = await users.findById(userId);
|
|
209
|
+
if (!user) throw new Error("User not found");
|
|
210
|
+
|
|
211
|
+
// Use the email plugin service
|
|
212
|
+
await email.send(user.email, "Notification", message);
|
|
213
|
+
|
|
214
|
+
// Log using core services
|
|
215
|
+
ctx.core.logger.info("Notification sent", { userId, message });
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Dependency Rules
|
|
223
|
+
|
|
224
|
+
1. **Dependencies must be registered first** - The framework validates this at startup
|
|
225
|
+
2. **No circular dependencies** - TypeScript will show an error at compile time
|
|
226
|
+
3. **No self-dependencies** - Compile-time error if plugin depends on itself
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// This will cause a TypeScript error:
|
|
230
|
+
createPlugin.define({
|
|
231
|
+
name: "foo",
|
|
232
|
+
dependencies: ["foo"] as const, // Error: Plugin 'foo' cannot depend on itself
|
|
233
|
+
// ...
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Custom Handlers
|
|
240
|
+
|
|
241
|
+
Plugins can define custom request handlers for specialized processing:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// plugins/api/index.ts
|
|
245
|
+
import { createPlugin } from "../../core";
|
|
246
|
+
import { createHandler } from "../../handlers";
|
|
247
|
+
import type { ServerContext } from "../../router";
|
|
248
|
+
|
|
249
|
+
// Define handler signature
|
|
250
|
+
type XMLHandler = (xmlBody: string, ctx: ServerContext) => Promise<string>;
|
|
251
|
+
|
|
252
|
+
// Create handler implementation
|
|
253
|
+
const XMLRequestHandler = createHandler<XMLHandler>(
|
|
254
|
+
async (req, def, handle, ctx) => {
|
|
255
|
+
const body = await req.text();
|
|
256
|
+
|
|
257
|
+
// Validate XML content type
|
|
258
|
+
if (!req.headers.get("content-type")?.includes("xml")) {
|
|
259
|
+
return new Response("Content-Type must be application/xml", { status: 400 });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = await handle(body, ctx);
|
|
263
|
+
|
|
264
|
+
return new Response(result, {
|
|
265
|
+
headers: { "Content-Type": "application/xml" },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
export const apiPlugin = createPlugin.define({
|
|
271
|
+
name: "api",
|
|
272
|
+
handlers: {
|
|
273
|
+
xml: XMLRequestHandler, // Register handler
|
|
274
|
+
},
|
|
275
|
+
service: async () => ({}),
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**After adding handlers, regenerate registry:**
|
|
280
|
+
|
|
281
|
+
```sh
|
|
282
|
+
bun run gen:registry
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Using custom handler in routes:**
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
router.route("process").xml({
|
|
289
|
+
handle: async (xmlBody, ctx) => {
|
|
290
|
+
// Process XML and return XML response
|
|
291
|
+
return `<response><status>ok</status></response>`;
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Custom Middleware
|
|
299
|
+
|
|
300
|
+
Plugins can provide middleware that can be applied to routes:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// plugins/auth/index.ts
|
|
304
|
+
import { createPlugin } from "../../core";
|
|
305
|
+
import { createMiddleware } from "../../middleware";
|
|
306
|
+
|
|
307
|
+
export interface AuthRequiredConfig {
|
|
308
|
+
roles?: string[];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const AuthRequiredMiddleware = createMiddleware<AuthRequiredConfig>(
|
|
312
|
+
async (req, ctx, next, config) => {
|
|
313
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
314
|
+
|
|
315
|
+
if (!token) {
|
|
316
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
317
|
+
status: 401,
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Verify token and set user
|
|
323
|
+
const user = await verifyToken(token);
|
|
324
|
+
ctx.user = user;
|
|
325
|
+
|
|
326
|
+
// Check roles if specified
|
|
327
|
+
if (config?.roles && !config.roles.some((r) => user.roles.includes(r))) {
|
|
328
|
+
return new Response(JSON.stringify({ error: "Forbidden" }), {
|
|
329
|
+
status: 403,
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return next();
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
export const authPlugin = createPlugin.define({
|
|
339
|
+
name: "auth",
|
|
340
|
+
middleware: {
|
|
341
|
+
authRequired: AuthRequiredMiddleware,
|
|
342
|
+
},
|
|
343
|
+
service: async (ctx) => ({
|
|
344
|
+
// Auth service methods...
|
|
345
|
+
}),
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Using middleware in routes:**
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// After running bun run gen:registry, middleware methods appear on router.middleware
|
|
353
|
+
|
|
354
|
+
router.middleware
|
|
355
|
+
.authRequired({ roles: ["admin"] })
|
|
356
|
+
.route("admin-only")
|
|
357
|
+
.typed({
|
|
358
|
+
handle: async (input, ctx) => {
|
|
359
|
+
// ctx.user is guaranteed to be set and have admin role
|
|
360
|
+
return { user: ctx.user };
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Plugin Context
|
|
368
|
+
|
|
369
|
+
The `PluginContext` passed to the service function provides:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
interface PluginContext<Deps, Schema, Config> {
|
|
373
|
+
// Core services (always available)
|
|
374
|
+
core: {
|
|
375
|
+
db: Kysely<any>;
|
|
376
|
+
config: Record<string, any>;
|
|
377
|
+
logger: Logger;
|
|
378
|
+
cache: Cache;
|
|
379
|
+
events: Events;
|
|
380
|
+
cron: Cron;
|
|
381
|
+
jobs: Jobs;
|
|
382
|
+
sse: SSE;
|
|
383
|
+
rateLimiter: RateLimiter;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Typed database (if using withSchema)
|
|
387
|
+
db: Kysely<Schema>;
|
|
388
|
+
|
|
389
|
+
// Dependency services (based on dependencies array)
|
|
390
|
+
deps: Deps;
|
|
391
|
+
|
|
392
|
+
// Plugin configuration (if using withConfig)
|
|
393
|
+
config: Config;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Example using all context features:**
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
export const analyticsPlugin = createPlugin
|
|
401
|
+
.withSchema<AnalyticsSchema>()
|
|
402
|
+
.withConfig<{ trackingId: string }>()
|
|
403
|
+
.define({
|
|
404
|
+
name: "analytics",
|
|
405
|
+
dependencies: ["users"] as const,
|
|
406
|
+
service: async (ctx) => {
|
|
407
|
+
// Schedule daily report
|
|
408
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
409
|
+
ctx.core.logger.info("Running daily analytics");
|
|
410
|
+
// ...
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Listen for events
|
|
414
|
+
ctx.core.events.on("user.created", async (data) => {
|
|
415
|
+
await ctx.db.insertInto("events").values({
|
|
416
|
+
type: "user_created",
|
|
417
|
+
data: JSON.stringify(data),
|
|
418
|
+
}).execute();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
track(event: string, data: any) {
|
|
423
|
+
ctx.core.events.emit(`analytics.${event}`, {
|
|
424
|
+
trackingId: ctx.config.trackingId,
|
|
425
|
+
...data,
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Type Inference
|
|
436
|
+
|
|
437
|
+
The framework provides helpers to extract types from plugins:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import {
|
|
441
|
+
InferService,
|
|
442
|
+
InferSchema,
|
|
443
|
+
InferHandlers,
|
|
444
|
+
InferMiddleware,
|
|
445
|
+
InferDependencies,
|
|
446
|
+
InferConfig,
|
|
447
|
+
} from "./core";
|
|
448
|
+
|
|
449
|
+
import { usersPlugin } from "./plugins/users";
|
|
450
|
+
|
|
451
|
+
// Extract types
|
|
452
|
+
type UsersService = InferService<typeof usersPlugin>;
|
|
453
|
+
type UsersSchema = InferSchema<typeof usersPlugin>;
|
|
454
|
+
type UsersDeps = InferDependencies<typeof usersPlugin>;
|
|
455
|
+
|
|
456
|
+
// Use in your code
|
|
457
|
+
function processUser(service: UsersService) {
|
|
458
|
+
return service.findByEmail("test@example.com");
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Plugin Lifecycle
|
|
465
|
+
|
|
466
|
+
1. **Registration** - `server.registerPlugin(plugin)` adds plugin to manager
|
|
467
|
+
2. **Validation** - At startup, dependencies are validated
|
|
468
|
+
3. **Migration** - `await manager.migrate()` runs all plugin migrations in order
|
|
469
|
+
4. **Initialization** - `await manager.init()` calls each plugin's `service()` function
|
|
470
|
+
5. **Runtime** - Services available via `ctx.plugins` in route handlers
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
Registration → Validation → Migration → Initialization → Runtime
|
|
474
|
+
↓ ↓ ↓ ↓ ↓
|
|
475
|
+
register() check deps run SQL service() ctx.plugins
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Best Practices
|
|
481
|
+
|
|
482
|
+
### 1. Keep Plugins Focused
|
|
483
|
+
Each plugin should have a single responsibility:
|
|
484
|
+
- `users` - User management
|
|
485
|
+
- `auth` - Authentication
|
|
486
|
+
- `email` - Email sending
|
|
487
|
+
- `analytics` - Event tracking
|
|
488
|
+
|
|
489
|
+
### 2. Use Dependencies for Composition
|
|
490
|
+
Instead of importing other plugins directly, declare them as dependencies:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
// Good - explicit dependency
|
|
494
|
+
dependencies: ["users"] as const,
|
|
495
|
+
service: (ctx) => {
|
|
496
|
+
ctx.deps.users.findById(1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Bad - direct import (bypasses dependency resolution)
|
|
500
|
+
import { usersPlugin } from "../users";
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### 3. Leverage Core Services
|
|
504
|
+
Use built-in core services instead of reinventing:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// Good - use built-in cache
|
|
508
|
+
const user = await ctx.core.cache.getOrSet(`user:${id}`, () => fetchUser(id));
|
|
509
|
+
|
|
510
|
+
// Bad - manual caching
|
|
511
|
+
const cached = userCache.get(id);
|
|
512
|
+
if (!cached) { ... }
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### 4. Type Everything
|
|
516
|
+
Use `withSchema()` and `withConfig()` for full type safety:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
// Fully typed plugin
|
|
520
|
+
createPlugin
|
|
521
|
+
.withSchema<MySchema>()
|
|
522
|
+
.withConfig<MyConfig>()
|
|
523
|
+
.define({ ... });
|
|
524
|
+
```
|