@moku-labs/worker 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/README.md +376 -0
- package/dist/cli.cjs +743 -0
- package/dist/cli.d.cts +139 -0
- package/dist/cli.d.mts +139 -0
- package/dist/cli.mjs +740 -0
- package/dist/config-AjH57AmD.d.cts +36 -0
- package/dist/config-AjH57AmD.d.mts +36 -0
- package/dist/index.cjs +544 -0
- package/dist/index.d.cts +962 -0
- package/dist/index.d.mts +960 -0
- package/dist/index.mjs +491 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/storage-BaQ6BBtl.cjs +990 -0
- package/dist/storage-bL-U_fkA.mjs +882 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# @moku-labs/worker
|
|
2
|
+
|
|
3
|
+
> Server-side Cloudflare Workers app + deploy framework, built on [`@moku-labs/core`](https://github.com/moku-labs/core). Durable Objects, Queues, R2, D1, and KV — each as its own composable Moku plugin — designed to compose alongside Moku Web.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@moku-labs/worker` models a Cloudflare Worker as a small set of **composable Moku plugins**. Each Cloudflare primitive (KV, D1, R2, Queues, Durable Objects) is a plugin that resolves its binding **per request** off the Cloudflare `env`, and a `server` plugin owns the HTTP routing and request dispatch. Deploy tooling (`deploy`, `cli`) is built from the same plugin model but kept strictly **out of the runtime bundle**.
|
|
8
|
+
|
|
9
|
+
Two design facts shape everything below:
|
|
10
|
+
|
|
11
|
+
1. **Runtime vs. node-only split.** The request-time surface is imported from `@moku-labs/worker`; the build-time deploy/CLI surface is imported from `@moku-labs/worker/cli`. The `./cli` entry reaches for `node:child_process` and `node:fs`, so it must never enter the deployed Worker bundle — the two entry points enforce that boundary.
|
|
12
|
+
2. **Env per request, never stored.** One Cloudflare isolate serves concurrent requests. Bindings (`env`) are threaded as a **call argument** to every plugin method and live only on the call stack — they are never captured in plugin state, so concurrent requests cannot leak each other's bindings.
|
|
13
|
+
|
|
14
|
+
This framework supplies the **server-side** Cloudflare primitives. Moku Web (`@moku-labs/web`) supplies the request/island layer; the two compose.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
Install (this project uses **bun** as its package manager):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @moku-labs/worker
|
|
22
|
+
bun add -d @cloudflare/workers-types
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A minimal Worker that routes HTTP requests. This shape is taken directly from the framework's own passing server integration test (`src/plugins/server/__tests__/integration/server.test.ts`):
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// app.ts
|
|
29
|
+
import { createApp, endpoint } from "@moku-labs/worker";
|
|
30
|
+
|
|
31
|
+
export const app = createApp({
|
|
32
|
+
pluginConfigs: {
|
|
33
|
+
server: {
|
|
34
|
+
endpoints: [
|
|
35
|
+
endpoint("/health").get(() => new Response("ok", { status: 200 })),
|
|
36
|
+
endpoint("/api/data/{lang?}").get(({ params }) =>
|
|
37
|
+
Response.json({ lang: params.lang ?? "en" })
|
|
38
|
+
),
|
|
39
|
+
endpoint("/users/{userId}").get(
|
|
40
|
+
({ params }) => new Response(`user=${params.userId}`, { status: 200 })
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// worker.ts — the default export is hand-assembled; no plugin produces it.
|
|
50
|
+
import { app } from "./app";
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
fetch: (request: Request, env: Record<string, unknown>, ctx: ExecutionContext) =>
|
|
54
|
+
app.server.handle(request, env, ctx)
|
|
55
|
+
} satisfies ExportedHandler;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`createApp` is synchronous, built once per isolate at module load, and frozen. `bindingsPlugin` and `serverPlugin` are wired into the framework by default — you do not list them in `plugins`. A request to `/api/data/fr` returns `{ "lang": "fr" }`; `/api/data` returns `{ "lang": "en" }`; an unmatched path returns `404`.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun add @moku-labs/worker
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Dependency | Why |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `@moku-labs/core@0.1.4` | The micro-kernel this framework is built on. Installed transitively. |
|
|
69
|
+
| `@moku-labs/common@0.1.1` | Supplies the `log` and `env` core plugins. Installed transitively. |
|
|
70
|
+
| `@cloudflare/workers-types` (dev) | Ambient Cloudflare runtime types (`KVNamespace`, `D1Database`, `R2Bucket`, `Queue`, `DurableObjectNamespace`, `ExecutionContext`, …). Type-only — never bundled. Add to your tsconfig `types`. |
|
|
71
|
+
| `wrangler` (peer/dev) | Required **only** for the node-only deploy/CLI path (`@moku-labs/worker/cli`). Invoked as a subprocess; never bundled. |
|
|
72
|
+
|
|
73
|
+
Requires Node `>=24` for the build/deploy tooling and bun `>=1.3.14`.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### Creating an app
|
|
78
|
+
|
|
79
|
+
`createApp` is the Layer-3 consumer entry. Resource plugins are added to `plugins`; their configuration goes under `pluginConfigs.<name>`:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createApp, kvPlugin } from "@moku-labs/worker";
|
|
83
|
+
|
|
84
|
+
const app = createApp({
|
|
85
|
+
config: { name: "my-api", stage: "production", compatibilityDate: "2026-06-17" },
|
|
86
|
+
plugins: [kvPlugin],
|
|
87
|
+
pluginConfigs: {
|
|
88
|
+
bindings: { required: ["MY_KV"] },
|
|
89
|
+
kv: { binding: "MY_KV" }
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**The final plugin list is `[...frameworkDefaults, ...yourPlugins]`** (spec/02 §4). This framework's defaults are the core plugins (`log`, `env`, `stage`) plus `bindingsPlugin` and `serverPlugin`, registered first and in order; your `plugins` are appended after. So:
|
|
95
|
+
|
|
96
|
+
- **Do not re-list `bindingsPlugin` or `serverPlugin`** — they are already defaults. Re-listing a default collides on name and throws `TypeError: [moku-worker] Duplicate plugin name: "bindings"` during init (spec/11 §Part 1 — no merge, no "last wins").
|
|
97
|
+
- **`depends: [bindingsPlugin]` is satisfied automatically.** `bindings` is a default ordered ahead of every consumer plugin, so any resource plugin you append (which declares `depends: [bindingsPlugin]`) resolves correctly without you listing `bindings`. List only the resource plugins you are adding.
|
|
98
|
+
- **`pluginConfigs` is keyed by plugin name**, so you can still configure a default plugin (e.g. `bindings: { required: [...] }`) without putting it in `plugins`.
|
|
99
|
+
|
|
100
|
+
### Accessing plugin APIs
|
|
101
|
+
|
|
102
|
+
Regular plugins mount their api on `app.<name>`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
app.server.handle(request, env, exec); // route one HTTP request → Response
|
|
106
|
+
app.kv.get(env, "feature-flags"); // env-first KV read
|
|
107
|
+
app.d1.query(env, "SELECT 1"); // env-first D1 query
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The core plugins are **flat-injected** on every plugin's `ctx` — `ctx.log`, `ctx.env`, `ctx.stage` — which is the ergonomic way to use them from inside plugin code. Like every plugin, they are also mounted on the app surface, so `app.log`, `app.env`, and `app.stage` exist alongside `app.server`, `app.bindings`, and the resource plugins.
|
|
111
|
+
|
|
112
|
+
### Env-per-request threading
|
|
113
|
+
|
|
114
|
+
Every binding-resolving method takes the per-request Cloudflare `env` as its **first argument**. Inside a `server` endpoint handler you receive `env` (and a cross-plugin `require`) on the per-request `RequestContext`, and thread `env` into each call:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { createApp, endpoint, kvPlugin } from "@moku-labs/worker";
|
|
118
|
+
|
|
119
|
+
const app = createApp({
|
|
120
|
+
plugins: [kvPlugin],
|
|
121
|
+
pluginConfigs: {
|
|
122
|
+
kv: { binding: "MY_KV" },
|
|
123
|
+
server: {
|
|
124
|
+
endpoints: [
|
|
125
|
+
endpoint("/cache/{key}").get(async ({ params, env, require, has }) => {
|
|
126
|
+
if (!has("kv")) return new Response("kv not configured", { status: 501 });
|
|
127
|
+
const value = await require(kvPlugin).get(env, params.key ?? "");
|
|
128
|
+
return value === null
|
|
129
|
+
? new Response("miss", { status: 404 })
|
|
130
|
+
: new Response(value);
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Wiring the Worker entry
|
|
139
|
+
|
|
140
|
+
The Cloudflare default export (`{ fetch, scheduled, queue }`) is **not** produced by any plugin — you hand-assemble it from the relevant `app.*` methods. `fetch` / `scheduled` / `queue` are Cloudflare runtime callbacks (not Moku lifecycle phases); each threads the per-invocation `env` on the stack:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// worker.ts
|
|
144
|
+
import { app } from "./app";
|
|
145
|
+
import type { ExecutionContext, ExportedHandler, MessageBatch, ScheduledController } from "@cloudflare/workers-types";
|
|
146
|
+
|
|
147
|
+
export default {
|
|
148
|
+
fetch: (request: Request, env: Record<string, unknown>, ctx: ExecutionContext) =>
|
|
149
|
+
app.server.handle(request, env, ctx),
|
|
150
|
+
scheduled: (controller: ScheduledController, env: Record<string, unknown>, ctx: ExecutionContext) =>
|
|
151
|
+
app.server.scheduled(controller, env, ctx),
|
|
152
|
+
queue: (batch: MessageBatch, env: Record<string, unknown>, ctx: ExecutionContext) =>
|
|
153
|
+
app.queues.consume(batch, env, ctx)
|
|
154
|
+
} satisfies ExportedHandler;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A stateless Worker never calls `app.start()` / `app.stop()` — no plugin opens a long-lived connection, so there is no lifecycle to run.
|
|
158
|
+
|
|
159
|
+
## Plugins
|
|
160
|
+
|
|
161
|
+
Plugin **name strings** are bare (`"server"`, `"kv"`, `"durableObjects"`); the **exported instances** carry the `Plugin` suffix (`serverPlugin`, `kvPlugin`, `durableObjectsPlugin`).
|
|
162
|
+
|
|
163
|
+
| Plugin | Description | Tier | Entry | Key APIs |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| [`bindings`](src/plugins/bindings/README.md) | Resolves Cloudflare bindings off the per-request `env`; the binding-family dependency root. | Micro | `@moku-labs/worker` | `require(env, name)`, `has(env, name)` |
|
|
166
|
+
| [`server`](src/plugins/server/README.md) | HTTP routing + request/scheduled dispatch; the Worker-entry surface. | Standard | `@moku-labs/worker` | `handle`, `scheduled`, `endpoint` |
|
|
167
|
+
| [`kv`](src/plugins/kv/README.md) | Thin env-first wrapper over one KV namespace. | Micro | `@moku-labs/worker` | `get`, `put`, `delete`, `list`, `deployManifest` |
|
|
168
|
+
| [`d1`](src/plugins/d1/README.md) | Typed wrappers over D1's `prepare().bind()` (`query`/`first`/`run`/`batch`). Not an ORM. | Standard | `@moku-labs/worker` | `query`, `first`, `run`, `batch`, `prepare`, `deployManifest` |
|
|
169
|
+
| [`queues`](src/plugins/queues/README.md) | Cloudflare Queues producer + consumer. | Standard | `@moku-labs/worker` | `send`, `sendBatch`, `consume`, `deployManifest` |
|
|
170
|
+
| [`storage`](src/plugins/storage/README.md) | R2 object storage behind a provider-adapter seam. | Complex | `@moku-labs/worker` | `get`, `put`, `delete`, `list`, `deployManifest` |
|
|
171
|
+
| [`durableObjects`](src/plugins/durable-objects/README.md) | Resolves DO stubs off `env`; ships `defineDurableObject` base-class helper. | Standard | `@moku-labs/worker` | `get`, `deployManifest`, `defineDurableObject` |
|
|
172
|
+
| [`stage`](src/plugins/stage/README.md) | Deployment-stage / dev-mode detection. Core plugin, flat-injected as `ctx.stage`. | Nano | `@moku-labs/worker` | `isDev`, `isProduction`, `current` |
|
|
173
|
+
| [`deploy`](src/plugins/deploy/README.md) | Build-time deploy orchestrator: detect → provision → wrangler-config → upload → deploy. **Node-only.** | Complex | `@moku-labs/worker/cli` | `run`, `dev`, `init` |
|
|
174
|
+
| [`cli`](src/plugins/cli/README.md) | Developer-facing `dev` / `deploy` verbs + live progress TUI. Thin passthroughs to `deploy`. **Node-only.** | Standard | `@moku-labs/worker/cli` | `dev`, `deploy` |
|
|
175
|
+
|
|
176
|
+
> The `log` and `env` **core plugins are not authored here** — they come from `@moku-labs/common` and are re-exported (`logPlugin`, `envPlugin`) for completeness. `env` is environment-**variable** access (`get`/`require`/`has`), distinct from `stage` (dev/production detection).
|
|
177
|
+
|
|
178
|
+
Helpers (also from `@moku-labs/worker`): `endpoint(path)` (server route builder) and `defineDurableObject(name)` (DO base-class factory).
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
### `WorkerConfig`
|
|
183
|
+
|
|
184
|
+
The global framework config, passed as `createApp({ config })`. Flat, with complete defaults:
|
|
185
|
+
|
|
186
|
+
| Field | Type | Default | Description |
|
|
187
|
+
|---|---|---|---|
|
|
188
|
+
| `stage` | `"production" \| "development" \| "test"` | `"production"` | Deployment stage. Production-safe default. Forwarded into the `stage` plugin (`ctx.stage`). |
|
|
189
|
+
| `name` | `string` | `"moku-worker"` | Worker name. Used by `deploy` as the wrangler `name` (`ctx.global.name`). |
|
|
190
|
+
| `compatibilityDate` | `string` | `""` | Cloudflare compatibility date. Used by `deploy` as the wrangler `compatibility_date`. |
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const app = createApp({
|
|
194
|
+
config: { name: "my-api", stage: "production", compatibilityDate: "2026-06-17" }
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Per-plugin config (`pluginConfigs`)
|
|
199
|
+
|
|
200
|
+
Each plugin's config is supplied under its name key. All configs are flat with complete defaults (overriding one key never drops siblings) and **frozen** after `createApp`:
|
|
201
|
+
|
|
202
|
+
| Plugin | Key fields (default) |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `bindings` | `required: string[]` (`[]`) |
|
|
205
|
+
| `server` | `endpoints: Endpoint[]` (`[]`) |
|
|
206
|
+
| `kv` | `binding: string` (`"KV"`) |
|
|
207
|
+
| `d1` | `binding: string` (`"DB"`), `migrations: string` (`""`) |
|
|
208
|
+
| `queues` | `producers: string[]` (`[]`), `onMessage: (message, env) => Promise<void>` (no-op) |
|
|
209
|
+
| `storage` | `bucket: string` (`"ASSETS"`), `upload: string` (`""`) |
|
|
210
|
+
| `durableObjects` | `bindings: Record<string, string>` (`{}`) — logical → CF binding name |
|
|
211
|
+
| `stage` | `stage: "production" \| "development" \| "test"` (`"production"`) — fed from `WorkerConfig.stage` |
|
|
212
|
+
| `deploy` | `configFile: string` (`"wrangler.jsonc"`), `ci: boolean` (`false`) |
|
|
213
|
+
| `cli` | `port: number` (`8787`) |
|
|
214
|
+
|
|
215
|
+
See each plugin's README for the full field reference.
|
|
216
|
+
|
|
217
|
+
## Events
|
|
218
|
+
|
|
219
|
+
Events are fire-and-forget observability — the kernel cannot carry a return value through an event, so all request/response and deploy **work** flows through api return values, never through `emit`. Two scopes exist:
|
|
220
|
+
|
|
221
|
+
### Global events (`WorkerEvents`, declared in `src/config.ts`)
|
|
222
|
+
|
|
223
|
+
Visible to every plugin; hookable without a `depends` edge.
|
|
224
|
+
|
|
225
|
+
| Event | Payload | Emitted by | When |
|
|
226
|
+
|---|---|---|---|
|
|
227
|
+
| `request:start` | `{ method: string; path: string; requestId: string }` | `server` | Start of `handle`, before matching. `requestId` is a fresh `crypto.randomUUID()`. |
|
|
228
|
+
| `request:end` | `{ method: string; path: string; status: number; ms: number }` | `server` | After the handler returns, with final status + elapsed ms. |
|
|
229
|
+
| `deploy:phase` | `{ phase: string; detail?: string }` | `deploy` | Each pipeline stage: `detect`, `provision`, `wrangler-config`, `upload` (`detail: "<n> files"`), `deploy`. |
|
|
230
|
+
| `provision:resource` | `{ kind: "kv" \| "r2" \| "d1" \| "queue" \| "do"; name: string }` | `deploy` | Once per provisioned resource. |
|
|
231
|
+
| `deploy:complete` | `{ url: string }` | `deploy` | After `wrangler deploy` succeeds. |
|
|
232
|
+
|
|
233
|
+
### Plugin-local events
|
|
234
|
+
|
|
235
|
+
Declared on the producing plugin; observers reach them via `depends: [<plugin>]`.
|
|
236
|
+
|
|
237
|
+
| Event | Scope | Payload | Emitted by | When |
|
|
238
|
+
|---|---|---|---|---|
|
|
239
|
+
| `server:matched` | `Server.ServerEvents` | `{ path: string; method: string }` | `server` | After a request matches an endpoint, before the handler runs. Not emitted on `404`. |
|
|
240
|
+
| `queue:message` | `Queues.QueueEvents` | `{ queue: string; messageId: string }` | `queues` | After `config.onMessage` settles for a message inside `consume`. |
|
|
241
|
+
|
|
242
|
+
Subscribe from a plugin's `hooks`:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
hooks: (register) => {
|
|
246
|
+
register("deploy:phase", ({ phase, detail }) =>
|
|
247
|
+
console.log(`▸ ${phase}${detail ? ` (${detail})` : ""}`)
|
|
248
|
+
);
|
|
249
|
+
register("deploy:complete", ({ url }) => console.log(`✓ ${url}`));
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Architecture
|
|
254
|
+
|
|
255
|
+
### Three-layer Moku model
|
|
256
|
+
|
|
257
|
+
| Layer | File | Produces |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| 1 — config + events | `src/config.ts` | `createCoreConfig` → `WorkerConfig`, `WorkerEvents`, registers core plugins (`log`, `env`, `stage`) |
|
|
260
|
+
| 2 — framework + plugins | `src/index.ts` | `createCore` → exposes `createApp` / `createPlugin`; wires `bindings` + `server` defaults |
|
|
261
|
+
| 3 — consumer app | your code | `createApp({ ... })` |
|
|
262
|
+
|
|
263
|
+
### Plugin dependency graph
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
bindings (root — depends on nothing)
|
|
267
|
+
├── server
|
|
268
|
+
├── kv
|
|
269
|
+
├── d1
|
|
270
|
+
├── queues
|
|
271
|
+
├── storage
|
|
272
|
+
└── durableObjects
|
|
273
|
+
|
|
274
|
+
deploy → depends on [storage, kv, d1, queues, durableObjects] (node-only)
|
|
275
|
+
cli → depends on [deploy] (node-only)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Each resource plugin exposes a `deployManifest()` that `deploy` reads via `ctx.require` — `deploy` never inspects sibling `pluginConfigs` (a plugin sees only `ctx.global` + its own `ctx.config`; `require` returns a plugin's api, not its config). Init order is a topological sort of this graph; `bindings` initializes first.
|
|
279
|
+
|
|
280
|
+
### Event flow
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
fetch → app.server.handle
|
|
284
|
+
├─ emit request:start (global)
|
|
285
|
+
├─ match endpoint
|
|
286
|
+
├─ emit server:matched (local) ─ skipped on 404
|
|
287
|
+
├─ run handler → Response
|
|
288
|
+
└─ emit request:end (global)
|
|
289
|
+
|
|
290
|
+
deploy → app.deploy.run
|
|
291
|
+
├─ emit deploy:phase {detect}
|
|
292
|
+
├─ emit deploy:phase {provision} → per resource: emit provision:resource
|
|
293
|
+
├─ emit deploy:phase {wrangler-config}
|
|
294
|
+
├─ emit deploy:phase {upload} (only if R2 upload dir)
|
|
295
|
+
├─ emit deploy:phase {deploy}
|
|
296
|
+
└─ emit deploy:complete {url}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Runtime vs. node-only boundary
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
@moku-labs/worker (. → src/index.ts) runtime — ships in the Worker bundle
|
|
303
|
+
createApp, createPlugin
|
|
304
|
+
bindingsPlugin, serverPlugin, kvPlugin, d1Plugin,
|
|
305
|
+
queuesPlugin, storagePlugin, durableObjectsPlugin, stagePlugin
|
|
306
|
+
endpoint, defineDurableObject
|
|
307
|
+
envPlugin, logPlugin
|
|
308
|
+
WorkerConfig, WorkerEvents, WorkerEnv, + type namespaces (Server, D1, Queues, Storage, DurableObjects)
|
|
309
|
+
|
|
310
|
+
@moku-labs/worker/cli (./cli → src/cli.ts) node-only — NEVER in the Worker bundle
|
|
311
|
+
deployPlugin, cliPlugin
|
|
312
|
+
ExternalManifest, ResourceManifest
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`deploy` and `cli` import `node:child_process` / `node:fs`, which cannot run in the Cloudflare isolate. The `./cli` entry exists to keep those Node built-ins out of the deployed bundle (design constraint HC11). Importing `deployPlugin` from `@moku-labs/worker` is intentionally impossible. The specs reference a `@moku-labs/worker/worker` subpath — it does **not** exist; the real entries are `.` and `./cli`.
|
|
316
|
+
|
|
317
|
+
## Development
|
|
318
|
+
|
|
319
|
+
Scripts (run with **bun** — never npm/yarn/pnpm):
|
|
320
|
+
|
|
321
|
+
| Script | Command | Purpose |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `bun run build` | `tsdown` | Build the package (`dist/`). |
|
|
324
|
+
| `bun run lint` | `biome check . && eslint .` | Biome + ESLint. |
|
|
325
|
+
| `bun run lint:fix` | `biome check --write . && eslint --fix .` | Auto-fix. |
|
|
326
|
+
| `bun run format` | `biome format --write .` | Format. |
|
|
327
|
+
| `bun run test` | `vitest run` | All tests (unit + integration). |
|
|
328
|
+
| `bun run test:unit` | `vitest run --project unit` | Unit tests only. |
|
|
329
|
+
| `bun run test:integration` | `vitest run --project integration` | Integration tests only. |
|
|
330
|
+
| `bun run test:coverage` | `vitest run … --coverage` | Tests with coverage (90% threshold). |
|
|
331
|
+
| `bun run validate` | `publint && attw …` | Package-publish validation. |
|
|
332
|
+
|
|
333
|
+
### Test layout
|
|
334
|
+
|
|
335
|
+
Tests are **colocated inside each plugin**: `src/plugins/<name>/__tests__/unit/` and `src/plugins/<name>/__tests__/integration/`. Framework-level cross-plugin tests live in root `tests/unit/` and `tests/integration/`. Never put plugin-specific tests in the root `tests/`.
|
|
336
|
+
|
|
337
|
+
### Adding a plugin
|
|
338
|
+
|
|
339
|
+
1. Create `src/plugins/<name>/` (see [moku-plugin tiers](src/plugins/server/README.md) for file layout by complexity).
|
|
340
|
+
2. Author the plugin with `createPlugin("<name>", { … })` (no explicit generics — they are inferred).
|
|
341
|
+
3. Re-export the instance (and any type namespace) from `src/plugins/index.ts` for the runtime entry, or `src/cli.ts` for a node-only plugin.
|
|
342
|
+
4. Add colocated `__tests__/`.
|
|
343
|
+
|
|
344
|
+
Custom plugin skeleton:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { createPlugin } from "@moku-labs/worker";
|
|
348
|
+
import { bindingsPlugin } from "@moku-labs/worker";
|
|
349
|
+
import type { WorkerEnv } from "@moku-labs/worker";
|
|
350
|
+
|
|
351
|
+
export const cachePlugin = createPlugin("cache", {
|
|
352
|
+
depends: [bindingsPlugin] as const,
|
|
353
|
+
config: { binding: "CACHE" },
|
|
354
|
+
api: (ctx) => ({
|
|
355
|
+
read: (env: WorkerEnv, key: string) =>
|
|
356
|
+
ctx.require(bindingsPlugin).require<KVNamespace>(env, ctx.config.binding).get(key)
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## API Reference
|
|
362
|
+
|
|
363
|
+
Per-plugin READMEs (authoritative API/config/events for each):
|
|
364
|
+
|
|
365
|
+
- [`bindings`](src/plugins/bindings/README.md)
|
|
366
|
+
- [`server`](src/plugins/server/README.md)
|
|
367
|
+
- [`kv`](src/plugins/kv/README.md)
|
|
368
|
+
- [`d1`](src/plugins/d1/README.md)
|
|
369
|
+
- [`queues`](src/plugins/queues/README.md)
|
|
370
|
+
- [`storage`](src/plugins/storage/README.md)
|
|
371
|
+
- [`durable-objects`](src/plugins/durable-objects/README.md)
|
|
372
|
+
- [`stage`](src/plugins/stage/README.md)
|
|
373
|
+
- [`deploy`](src/plugins/deploy/README.md)
|
|
374
|
+
- [`cli`](src/plugins/cli/README.md)
|
|
375
|
+
|
|
376
|
+
For the underlying kernel model (`createCoreConfig`, `createCore`, `createApp`, lifecycle, events), see the [Moku Core specification](https://github.com/moku-labs/core/tree/main/specification).
|