@moku-labs/worker 0.9.2 → 0.11.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 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
- > 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.
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
- ## Overview
11
+ [![npm](https://img.shields.io/npm/v/@moku-labs/worker?logo=npm&color=cb3837&label=npm)](https://www.npmjs.com/package/@moku-labs/worker)
12
+ [![CI](https://github.com/moku-labs/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/moku-labs/worker/actions/workflows/ci.yml)
13
+ [![types](https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white)](#requirements)
14
+ [![node](https://img.shields.io/badge/node-%3E%3D24-339933?logo=node.js&logoColor=white)](#requirements)
15
+ [![for @moku-labs/core](https://img.shields.io/badge/for-%40moku--labs%2Fcore-0b7285)](https://github.com/moku-labs/core)
16
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
6
17
 
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**.
18
+ <br/>
8
19
 
9
- Two design facts shape everything below:
20
+ [Why](#why-moku-labsworker) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Plugins](#plugins) · [Configuration](#configuration) · [Scripts](#scripts)
10
21
 
11
- 1. **Runtime vs. node-only surface.** Everything ships from `@moku-labs/worker`, including the build-time `deployPlugin`/`cliPlugin` (the `@moku-labs/worker/cli` subpath remains as a back-compat alias). Those two reach for `node:child_process`/`node:fs`, so they enter a bundle **only when a consumer actually lists them in `createApp({ plugins })`** — `"sideEffects": false` tree-shakes them out of any request-time Worker bundle that doesn't.
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
- This framework supplies the **server-side** Cloudflare primitives. Moku Web (`@moku-labs/web`) supplies the request/island layer; the two compose.
24
+ ---
15
25
 
16
- ## Quick Start
26
+ ## Why @moku-labs/worker
17
27
 
18
- Install (this project uses **bun** as its package manager):
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
- ```bash
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
- 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`):
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", { status: 200 })),
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}`, { status: 200 })
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. `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** 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
- ## Usage
80
+ ## How it works
76
81
 
77
- ### Creating an app
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
- `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
- });
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
- **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:
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
- - **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`.
102
+ ## Env is the contract
99
103
 
100
- ### Accessing plugin APIs
104
+ > The binding lives on the request, not on the plugin.
101
105
 
102
- Regular plugins mount their api on `app.<name>`:
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.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
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
- 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:
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
- 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
- }
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
- ### 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.
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
- 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` 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
- > 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`
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 global framework config, passed as `createApp({ config })`. Flat, with complete defaults:
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
- | 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`. |
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
- config: { name: "my-api", stage: "production", compatibilityDate: "2026-06-17" }
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
- ### Per-plugin config (`pluginConfigs`)
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
- 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`:
162
+ ## Runtime vs. node-only
201
163
 
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`) |
164
+ Deploy tooling is built from the same plugin model but kept strictly out of the request-time bundle.
214
165
 
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 |
166
+ | Surface | Entry | In the Worker bundle? | Carries |
226
167
  |---|---|---|---|
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. |
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
- ### Plugin-local events
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
- Declared on the producing plugin; observers reach them via `depends: [<plugin>]`.
174
+ ## Configuration
236
175
 
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`. |
176
+ The global `WorkerConfig`, passed as `createApp({ config })` flat, with complete defaults:
241
177
 
242
- Subscribe from a plugin's `hooks`:
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
- ```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
- ```
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
- ## Architecture
186
+ ## Events
254
187
 
255
- ### Three-layer Moku model
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
- | Layer | File | Produces |
190
+ | Event(s) | Emitted by | When |
258
191
  |---|---|---|
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
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
- 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)
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
- 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.
208
+ ## Scripts
279
209
 
280
- ### Event flow
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
- Scripts (run with **bun** — never npm/yarn/pnpm):
225
+ ## Requirements
321
226
 
322
- | Script | Command | Purpose |
323
- |---|---|---|
324
- | `bun run build` | `tsdown` | Build the package (`dist/`). |
325
- | `bun run lint` | `biome check . && eslint .` | Biome + ESLint. |
326
- | `bun run lint:fix` | `biome check --write . && eslint --fix .` | Auto-fix. |
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
- ## API Reference
233
+ ## Docs
363
234
 
364
- Per-plugin READMEs (authoritative API/config/events for each):
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
- - [`bindings`](src/plugins/bindings/README.md)
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
- For the underlying kernel model (`createCoreConfig`, `createCore`, `createApp`, lifecycle, events), see the [Moku Core specification](https://github.com/moku-labs/core/tree/main/specification).
240
+ [MIT](./LICENSE) © [moku-labs](https://github.com/moku-labs)