@moku-labs/worker 0.9.2 → 0.10.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 +142 -279
- package/dist/{cli--EPl98vG.mjs → cli-D6i-Kugx.mjs} +427 -256
- package/dist/{cli-imQGo0tc.cjs → cli-Dnb-P_pp.cjs} +427 -256
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 moku-labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,43 +1,63 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @moku-labs/worker
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**The Cloudflare Workers backend for Moku — Durable Objects, Queues, R2, D1, and KV, each a composable plugin.**
|
|
6
|
+
|
|
7
|
+
Every Cloudflare primitive is a Moku plugin that resolves its binding **per request** off the Worker `env` — never stored, never shared across the concurrent requests one isolate serves. A `server` plugin owns HTTP routing and dispatch; build-time `deploy`/`cli` ship the Worker but stay out of the runtime bundle. Not an ORM, not a router framework, not a replacement for Wrangler — the thin, typed seam between your handlers and Cloudflare's runtime, built on [`@moku-labs/core`](https://github.com/moku-labs/core) and designed to compose with [`@moku-labs/web`](https://github.com/moku-labs/web).
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/@moku-labs/worker)
|
|
12
|
+
[](https://github.com/moku-labs/worker/actions/workflows/ci.yml)
|
|
13
|
+
[](#requirements)
|
|
14
|
+
[](#requirements)
|
|
15
|
+
[](https://github.com/moku-labs/core)
|
|
16
|
+
[](./LICENSE)
|
|
6
17
|
|
|
7
|
-
|
|
18
|
+
<br/>
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
[Why](#why-moku-labsworker) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Plugins](#plugins) · [Configuration](#configuration) · [Scripts](#scripts)
|
|
10
21
|
|
|
11
|
-
|
|
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.
|
|
22
|
+
</div>
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
---
|
|
15
25
|
|
|
16
|
-
##
|
|
26
|
+
## Why @moku-labs/worker
|
|
17
27
|
|
|
18
|
-
|
|
28
|
+
- **Every primitive is a plugin.** KV, D1, R2, Queues, and Durable Objects each compose into `createApp` — add only what you use; the rest tree-shakes away.
|
|
29
|
+
- **`env` is a call argument, never state.** Bindings are threaded per request and live only on the call stack, so one isolate serving concurrent requests can never leak bindings between them.
|
|
30
|
+
- **One bundle, no Node leakage.** The build-time `deploy`/`cli` plugins reach for `node:fs` and `node:child_process`, but `"sideEffects": false` keeps them out of any request-time Worker bundle that doesn't list them.
|
|
31
|
+
- **Not an ORM, not a router framework.** Thin, typed wrappers over the real Cloudflare APIs (`prepare().bind()`, `KVNamespace`, `R2Bucket`) — no abstraction to learn on top of the platform.
|
|
32
|
+
- **The server half of Moku.** `@moku-labs/web` supplies the request/island layer; this supplies the Cloudflare primitives — same kernel, same plugin model, no second code path to keep in sync.
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```sh
|
|
21
37
|
bun add @moku-labs/worker
|
|
22
38
|
bun add -d @cloudflare/workers-types
|
|
23
39
|
```
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
> [!NOTE]
|
|
42
|
+
> **Status: `0.x` — early.** API may shift before `1.0`. Built on `@moku-labs/core` (`1.x`, semver-compliant). `wrangler` is an **optional** peer — needed only when you add the `deploy`/`cli` plugins.
|
|
43
|
+
|
|
44
|
+
Declare your routes as data, then hand-assemble the Worker entry from `app.server`:
|
|
26
45
|
|
|
27
46
|
```typescript
|
|
28
47
|
// app.ts
|
|
29
48
|
import { createApp, endpoint } from "@moku-labs/worker";
|
|
30
49
|
|
|
31
50
|
export const app = createApp({
|
|
51
|
+
config: { name: "my-api", compatibilityDate: "2026-06-17" },
|
|
32
52
|
pluginConfigs: {
|
|
33
53
|
server: {
|
|
34
54
|
endpoints: [
|
|
35
|
-
endpoint("/health").get(() => new Response("ok"
|
|
55
|
+
endpoint("/health").get(() => new Response("ok")),
|
|
36
56
|
endpoint("/api/data/{lang:?}").get(({ params }) =>
|
|
37
57
|
Response.json({ lang: params.lang ?? "en" })
|
|
38
58
|
),
|
|
39
59
|
endpoint("/users/{userId}").get(
|
|
40
|
-
({ params }) => new Response(`user=${params.userId}
|
|
60
|
+
({ params }) => new Response(`user=${params.userId}`)
|
|
41
61
|
)
|
|
42
62
|
]
|
|
43
63
|
}
|
|
@@ -55,323 +75,166 @@ export default {
|
|
|
55
75
|
} satisfies ExportedHandler;
|
|
56
76
|
```
|
|
57
77
|
|
|
58
|
-
`createApp` is synchronous, built once per isolate at module load, and frozen. `
|
|
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** when you add the node-only `deployPlugin`/`cliPlugin`. Invoked as a subprocess; never bundled. |
|
|
72
|
-
|
|
73
|
-
Requires Node `>=24` for the build/deploy tooling and bun `>=1.3.14`.
|
|
78
|
+
`createApp` is synchronous, built once per isolate at module load, and frozen. `bindings` and `server` are wired in by default — you never list them. A request to `/api/data/fr` returns `{ "lang": "fr" }`; `/api/data` returns `{ "lang": "en" }`; an unmatched path returns `404`. Path params mirror `@moku-labs/web`: `{name}` is required (typed `string`), `{name:?}` is optional (typed `string | undefined`).
|
|
74
79
|
|
|
75
|
-
##
|
|
80
|
+
## How it works
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
Three layers, one kernel. `createCoreConfig` declares config + events and registers the core plugins; `createCore` wires the framework defaults; your code calls `createApp`. At runtime, each Cloudflare invocation threads its `env` down through the entry into `app.server` and out to whichever resource plugins a handler reaches:
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
kv: { binding: "MY_KV" }
|
|
90
|
-
}
|
|
91
|
-
});
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
REQ["fetch · scheduled · queue<br/>(env per invocation)"] --> ENTRY["your worker.ts<br/>default export"]
|
|
87
|
+
ENTRY --> APP["app.server.handle<br/>matches & dispatches"]
|
|
88
|
+
APP --> RES["kv · d1 · r2 · queues · DO<br/>resolve binding off env"]
|
|
89
|
+
RES --> OUT["Response"]
|
|
90
|
+
classDef io fill:#0b7285,stroke:#08525f,color:#fff;
|
|
91
|
+
classDef mach fill:#1864ab,stroke:#0d3d6e,color:#fff;
|
|
92
|
+
class REQ,OUT io
|
|
93
|
+
class ENTRY,APP,RES mach
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
| Layer | File | Produces |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| 1 — config + events | `src/config.ts` | `createCoreConfig` → `WorkerConfig`, `WorkerEvents`; registers core plugins (`log`, `env`, `stage`) |
|
|
99
|
+
| 2 — framework + plugins | `src/index.ts` | `createCore` → `createApp` / `createPlugin`; wires `bindings` + `server` defaults |
|
|
100
|
+
| 3 — consumer app | your code | `createApp({ … })` |
|
|
95
101
|
|
|
96
|
-
|
|
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`.
|
|
102
|
+
## Env is the contract
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
> The binding lives on the request, not on the plugin.
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
One Cloudflare isolate serves many concurrent requests, so every binding-resolving method takes the per-request `env` as its **first argument** and reads it on the call stack — `env` is never captured in plugin state:
|
|
103
107
|
|
|
104
108
|
```typescript
|
|
105
|
-
app.
|
|
106
|
-
app.
|
|
107
|
-
app.
|
|
109
|
+
app.kv.get(env, "feature-flags"); // env-first KV read
|
|
110
|
+
app.d1.query(env, "SELECT 1"); // env-first D1 query
|
|
111
|
+
app.durableObjects.get(env, "board", "room-42"); // env-first DO stub
|
|
108
112
|
```
|
|
109
113
|
|
|
110
|
-
|
|
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:
|
|
114
|
+
Inside a `server` handler you receive `env` (plus a cross-plugin `require` and an `has` presence check) on the per-request `RequestContext`, and thread it onward — so two requests in flight at once can never observe each other's bindings:
|
|
115
115
|
|
|
116
116
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
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
|
-
}
|
|
117
|
+
endpoint("/cache/{key}").get(async ({ params, env, require, has }) => {
|
|
118
|
+
if (!has("kv")) return new Response("kv not configured", { status: 501 });
|
|
119
|
+
const value = await require(kvPlugin).get(env, params.key);
|
|
120
|
+
return value === null ? new Response("miss", { status: 404 }) : new Response(value);
|
|
135
121
|
});
|
|
136
122
|
```
|
|
137
123
|
|
|
138
|
-
|
|
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.
|
|
124
|
+
The core plugins are **flat-injected** on every plugin's `ctx` — `ctx.log`, `ctx.env`, `ctx.stage` — and also mounted on the app surface (`app.log`, `app.env`, `app.stage`).
|
|
158
125
|
|
|
159
126
|
## Plugins
|
|
160
127
|
|
|
161
|
-
|
|
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` alias) | `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` alias) | `dev`, `deploy` |
|
|
128
|
+
Name **strings** are bare (`"server"`, `"kv"`); the exported **instances** carry the `Plugin` suffix (`serverPlugin`, `kvPlugin`). Everything ships from the `@moku-labs/worker` root.
|
|
175
129
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
130
|
+
| Plugin | Tier | Responsibility | Key API |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| [`bindings`](src/plugins/bindings/README.md) | Standard | Resolves Cloudflare bindings off the per-request `env`; the binding-family dependency root. | `require(env, name)`, `has(env, name)` |
|
|
133
|
+
| [`server`](src/plugins/server/README.md) | Standard | HTTP routing + request/scheduled dispatch; the Worker-entry surface. | `handle`, `scheduled`, `endpoint` |
|
|
134
|
+
| [`kv`](src/plugins/kv/README.md) | Micro | Thin env-first wrapper over one KV namespace. | `get`, `put`, `delete`, `list` |
|
|
135
|
+
| [`d1`](src/plugins/d1/README.md) | Standard | Typed wrappers over D1's `prepare().bind()`. Not an ORM. | `query`, `first`, `run`, `batch`, `prepare` |
|
|
136
|
+
| [`queues`](src/plugins/queues/README.md) | Standard | Cloudflare Queues producer + consumer. | `send`, `sendBatch`, `consume` |
|
|
137
|
+
| [`storage`](src/plugins/storage/README.md) | Complex | R2 object storage behind a provider-adapter seam. | `get`, `put`, `delete`, `list` |
|
|
138
|
+
| [`durableObjects`](src/plugins/durable-objects/README.md) | Standard | Resolves DO stubs off `env`; ships `defineDurableObject`. | `get`, `defineDurableObject` |
|
|
139
|
+
| [`stage`](src/plugins/stage/README.md) | Nano (core) | Deployment-stage / dev-mode detection, flat-injected as `ctx.stage`. | `isDev`, `isProduction`, `current` |
|
|
140
|
+
| [`deploy`](src/plugins/deploy/README.md) | Complex | Build-time orchestrator: detect → provision → wrangler-config → upload → deploy. **Node-only.** | `run`, `dev`, `init` |
|
|
141
|
+
| [`cli`](src/plugins/cli/README.md) | Standard | Developer-facing `dev` / `deploy` verbs + live progress TUI. **Node-only.** | `dev`, `deploy` |
|
|
183
142
|
|
|
184
|
-
The
|
|
143
|
+
> The `log` and `env` core plugins are **not authored here** — they come from [`@moku-labs/common`](https://github.com/moku-labs/common) and are re-exported (`logPlugin`, `envPlugin`). `env` is environment-**variable** access, distinct from `stage` (dev/production detection). Helpers `endpoint(path)` and `defineDurableObject(name)` ship from the root too.
|
|
185
144
|
|
|
186
|
-
|
|
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`. |
|
|
145
|
+
Add a resource plugin by appending it — defaults stay implicit:
|
|
191
146
|
|
|
192
147
|
```typescript
|
|
148
|
+
import { createApp, kvPlugin } from "@moku-labs/worker";
|
|
149
|
+
|
|
193
150
|
const app = createApp({
|
|
194
|
-
|
|
151
|
+
plugins: [kvPlugin], // append only what you add
|
|
152
|
+
pluginConfigs: {
|
|
153
|
+
bindings: { required: ["MY_KV"] }, // fail fast if the binding is missing
|
|
154
|
+
kv: { binding: "MY_KV" }
|
|
155
|
+
}
|
|
195
156
|
});
|
|
196
157
|
```
|
|
197
158
|
|
|
198
|
-
|
|
159
|
+
> [!IMPORTANT]
|
|
160
|
+
> The final plugin list is `[…frameworkDefaults, …yourPlugins]`. Defaults are `[log, env, stage, bindings, server]`, registered first; your `plugins` append after. **Do not re-list a default** — re-listing collides on name and throws `TypeError: [moku-worker] Duplicate plugin name: "bindings"` at init. `pluginConfigs` is keyed by name, so you can still configure a default (e.g. `bindings.required`) without listing it.
|
|
199
161
|
|
|
200
|
-
|
|
162
|
+
## Runtime vs. node-only
|
|
201
163
|
|
|
202
|
-
|
|
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`) |
|
|
164
|
+
Deploy tooling is built from the same plugin model but kept strictly out of the request-time bundle.
|
|
214
165
|
|
|
215
|
-
|
|
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 |
|
|
166
|
+
| Surface | Entry | In the Worker bundle? | Carries |
|
|
226
167
|
|---|---|---|---|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
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. |
|
|
168
|
+
| Runtime | `@moku-labs/worker` (`.`) | Always | `createApp`, `createPlugin`, all resource plugins, `server`, helpers, types |
|
|
169
|
+
| Node-only | `@moku-labs/worker` → `deployPlugin` / `cliPlugin` | Only if you add them | `deploy` + `cli`; pulls in `node:fs` / `node:child_process` |
|
|
232
170
|
|
|
233
|
-
|
|
171
|
+
> [!TIP]
|
|
172
|
+
> Everything ships from the root entry — including `deployPlugin`/`cliPlugin`. Because the package is `"sideEffects": false`, a Worker that imports `createApp` and never lists those two tree-shakes the Node built-ins away, with no separate entry point. The `./cli` subpath remains as a back-compat alias.
|
|
234
173
|
|
|
235
|
-
|
|
174
|
+
## Configuration
|
|
236
175
|
|
|
237
|
-
|
|
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`. |
|
|
176
|
+
The global `WorkerConfig`, passed as `createApp({ config })` — flat, with complete defaults:
|
|
241
177
|
|
|
242
|
-
|
|
178
|
+
| Field | Type | Default | Notes |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
| `name` | `string` | `"moku-worker"` | Worker name; `deploy` uses it as the wrangler `name`. |
|
|
181
|
+
| `stage` | `"production" \| "development" \| "test"` | `"production"` | Production-safe default; bridged into the `stage` plugin so `ctx.stage` and `ctx.global.stage` stay in lockstep. |
|
|
182
|
+
| `compatibilityDate` | `string` | `""` | Cloudflare compatibility date; `deploy` uses it as `compatibility_date`. |
|
|
243
183
|
|
|
244
|
-
|
|
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
|
-
```
|
|
184
|
+
Per-plugin config goes under `pluginConfigs.<name>` (e.g. `server.endpoints`, `kv.binding`, `bindings.required`, `deploy.configFile`). Every config is flat with complete defaults — overriding one key never drops siblings — and **frozen** after `createApp`. Each field is documented in that plugin's README, linked in the [Plugins](#plugins) table.
|
|
252
185
|
|
|
253
|
-
##
|
|
186
|
+
## Events
|
|
254
187
|
|
|
255
|
-
|
|
188
|
+
Events are fire-and-forget observability — request/response and deploy **work** flows through API return values, never through `emit`. Global events live on `WorkerEvents` (`src/config.ts`) and are visible to every plugin; plugin-local events are reached via `depends: [<plugin>]`.
|
|
256
189
|
|
|
257
|
-
|
|
|
190
|
+
| Event(s) | Emitted by | When |
|
|
258
191
|
|---|---|---|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
|
262
|
-
|
|
263
|
-
|
|
192
|
+
| `request:start` · `request:end` | `server` | Around each `handle` — start (fresh `requestId`), end (final `status` + `ms`). |
|
|
193
|
+
| `server:matched` *(local)* | `server` | After a request matches an endpoint, before the handler runs. Not on `404`. |
|
|
194
|
+
| `queue:message` *(local)* | `queues` | After `config.onMessage` settles for a message inside `consume`. |
|
|
195
|
+
| `deploy:phase` · `deploy:complete` | `deploy` | Each pipeline stage; final deployed `url`. |
|
|
196
|
+
| `provision:plan` · `provision:resource` · `provision:skip` | `deploy` | Provisioning plan, then per-resource create or skip. |
|
|
197
|
+
| `auth:verified` | `deploy` | After Cloudflare auth resolves (`account`, `accountId`, `scopes`). |
|
|
198
|
+
| `dev:phase` · `dev:rebuilt` · `dev:error` | `deploy` | Local `dev` server: stage, incremental rebuild (`files` + `ms`), error. |
|
|
264
199
|
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
├── storage
|
|
272
|
-
└── durableObjects
|
|
273
|
-
|
|
274
|
-
deploy → depends on [storage, kv, d1, queues, durableObjects] (node-only)
|
|
275
|
-
cli → depends on [deploy] (node-only)
|
|
200
|
+
```typescript
|
|
201
|
+
// A plugin's `hooks` factory receives `ctx` and returns an event → handler map.
|
|
202
|
+
hooks: (ctx) => ({
|
|
203
|
+
"deploy:phase": ({ phase, detail }) => ctx.log.info(`▸ ${phase}${detail ? ` (${detail})` : ""}`),
|
|
204
|
+
"deploy:complete": ({ url }) => ctx.log.info(`✓ ${url}`)
|
|
205
|
+
})
|
|
276
206
|
```
|
|
277
207
|
|
|
278
|
-
|
|
208
|
+
## Scripts
|
|
279
209
|
|
|
280
|
-
|
|
210
|
+
Run with **bun** — never npm/yarn/pnpm.
|
|
281
211
|
|
|
212
|
+
```sh
|
|
213
|
+
bun run build # build with tsdown → dist/
|
|
214
|
+
bun run test # all tests (vitest)
|
|
215
|
+
bun run test:unit # unit tests only
|
|
216
|
+
bun run test:integration # integration tests only
|
|
217
|
+
bun run test:coverage # tests with coverage (90% threshold)
|
|
218
|
+
bun run typecheck # tsc --noEmit
|
|
219
|
+
bun run lint # biome check + eslint
|
|
220
|
+
bun run lint:fix # auto-fix lint issues
|
|
221
|
+
bun run format # biome format --write
|
|
222
|
+
bun run validate # publint + are-the-types-wrong
|
|
282
223
|
```
|
|
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) one entry for everything
|
|
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
|
-
deployPlugin, cliPlugin node-only — tree-shaken unless you add them
|
|
310
|
-
ExternalManifest, ResourceManifest
|
|
311
|
-
|
|
312
|
-
@moku-labs/worker/cli (./cli → src/cli.ts) back-compat alias for the two node-only plugins
|
|
313
|
-
deployPlugin, cliPlugin, ExternalManifest, ResourceManifest
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
`deploy` and `cli` import `node:child_process` / `node:fs`, which cannot run in the Cloudflare isolate — but they reach a bundle **only** when a consumer lists them in `createApp({ plugins })`. Because the package is `"sideEffects": false`, a request-time Worker that imports `createApp` (and never adds those two) tree-shakes them away, so the Node built-ins stay out of the deployed bundle without a separate entry point. The `./cli` subpath remains as a back-compat alias. The specs reference a `@moku-labs/worker/worker` subpath — it does **not** exist; the real entries are `.` and `./cli`.
|
|
317
|
-
|
|
318
|
-
## Development
|
|
319
224
|
|
|
320
|
-
|
|
225
|
+
## Requirements
|
|
321
226
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
| `bun run format` | `biome format --write .` | Format. |
|
|
328
|
-
| `bun run test` | `vitest run` | All tests (unit + integration). |
|
|
329
|
-
| `bun run test:unit` | `vitest run --project unit` | Unit tests only. |
|
|
330
|
-
| `bun run test:integration` | `vitest run --project integration` | Integration tests only. |
|
|
331
|
-
| `bun run test:coverage` | `vitest run … --coverage` | Tests with coverage (90% threshold). |
|
|
332
|
-
| `bun run validate` | `publint && attw …` | Package-publish validation. |
|
|
333
|
-
|
|
334
|
-
### Test layout
|
|
335
|
-
|
|
336
|
-
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/`.
|
|
337
|
-
|
|
338
|
-
### Adding a plugin
|
|
339
|
-
|
|
340
|
-
1. Create `src/plugins/<name>/` (see [moku-plugin tiers](src/plugins/server/README.md) for file layout by complexity).
|
|
341
|
-
2. Author the plugin with `createPlugin("<name>", { … })` (no explicit generics — they are inferred).
|
|
342
|
-
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.
|
|
343
|
-
4. Add colocated `__tests__/`.
|
|
344
|
-
|
|
345
|
-
Custom plugin skeleton:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import { createPlugin } from "@moku-labs/worker";
|
|
349
|
-
import { bindingsPlugin } from "@moku-labs/worker";
|
|
350
|
-
import type { WorkerEnv } from "@moku-labs/worker";
|
|
351
|
-
|
|
352
|
-
export const cachePlugin = createPlugin("cache", {
|
|
353
|
-
depends: [bindingsPlugin] as const,
|
|
354
|
-
config: { binding: "CACHE" },
|
|
355
|
-
api: (ctx) => ({
|
|
356
|
-
read: (env: WorkerEnv, key: string) =>
|
|
357
|
-
ctx.require(bindingsPlugin).require<KVNamespace>(env, ctx.config.binding).get(key)
|
|
358
|
-
})
|
|
359
|
-
});
|
|
360
|
-
```
|
|
227
|
+
- **Node `>= 24`** and **Bun `>= 1.3.14`** — use `bun` exclusively (never npm/yarn/pnpm).
|
|
228
|
+
- **TypeScript** in strict mode, with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
|
|
229
|
+
- **[`@moku-labs/core`](https://github.com/moku-labs/core)** — the micro-kernel this framework is built on (installed transitively, with `@moku-labs/common`).
|
|
230
|
+
- **`@cloudflare/workers-types`** *(dev)* — ambient runtime types (`KVNamespace`, `D1Database`, `R2Bucket`, `ExecutionContext`, …); add to your tsconfig `types`. Type-only, never bundled.
|
|
231
|
+
- **`wrangler`** *(optional peer)* — required only when you add `deployPlugin`/`cliPlugin`. Invoked as a subprocess; never bundled.
|
|
361
232
|
|
|
362
|
-
##
|
|
233
|
+
## Docs
|
|
363
234
|
|
|
364
|
-
Per-plugin READMEs
|
|
235
|
+
- **Per-plugin READMEs** — authoritative API, config, and events for each plugin, linked in the [Plugins](#plugins) table.
|
|
236
|
+
- **[Moku Core specification](https://github.com/moku-labs/core/tree/main/specification)** — the underlying kernel model: `createCoreConfig`, `createCore`, `createApp`, lifecycle, events.
|
|
365
237
|
|
|
366
|
-
|
|
367
|
-
- [`server`](src/plugins/server/README.md)
|
|
368
|
-
- [`kv`](src/plugins/kv/README.md)
|
|
369
|
-
- [`d1`](src/plugins/d1/README.md)
|
|
370
|
-
- [`queues`](src/plugins/queues/README.md)
|
|
371
|
-
- [`storage`](src/plugins/storage/README.md)
|
|
372
|
-
- [`durable-objects`](src/plugins/durable-objects/README.md)
|
|
373
|
-
- [`stage`](src/plugins/stage/README.md)
|
|
374
|
-
- [`deploy`](src/plugins/deploy/README.md)
|
|
375
|
-
- [`cli`](src/plugins/cli/README.md)
|
|
238
|
+
## License
|
|
376
239
|
|
|
377
|
-
|
|
240
|
+
[MIT](./LICENSE) © [moku-labs](https://github.com/moku-labs)
|