@moku-labs/worker 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ export const app = createApp({
33
33
  server: {
34
34
  endpoints: [
35
35
  endpoint("/health").get(() => new Response("ok", { status: 200 })),
36
- endpoint("/api/data/{lang?}").get(({ params }) =>
36
+ endpoint("/api/data/{lang:?}").get(({ params }) =>
37
37
  Response.json({ lang: params.lang ?? "en" })
38
38
  ),
39
39
  endpoint("/users/{userId}").get(
@@ -0,0 +1,63 @@
1
+ import { PluginCtx } from "@moku-labs/core";
2
+
3
+ //#region src/config.d.ts
4
+ /** Per-request Cloudflare bindings object (env). Framework-level shared type. */
5
+ type WorkerEnv = Record<string, unknown>;
6
+ /** Global framework config — flat, with complete defaults. */
7
+ type WorkerConfig = {
8
+ stage: "production" | "development" | "test";
9
+ name: string;
10
+ compatibilityDate: string;
11
+ };
12
+ /** Global framework events — declared once, visible to every plugin. */
13
+ type WorkerEvents = {
14
+ "request:start": {
15
+ method: string;
16
+ path: string;
17
+ requestId: string;
18
+ };
19
+ "request:end": {
20
+ method: string;
21
+ path: string;
22
+ status: number;
23
+ ms: number;
24
+ };
25
+ "deploy:phase": {
26
+ phase: string;
27
+ detail?: string;
28
+ };
29
+ "deploy:complete": {
30
+ url: string;
31
+ };
32
+ "provision:resource": {
33
+ kind: "kv" | "r2" | "d1" | "queue" | "do";
34
+ name: string;
35
+ };
36
+ };
37
+ /**
38
+ * Worker-bound plugin context for Layer-3 consumer plugins. Aliases the core
39
+ * {@link PluginCtx} with the global {@link WorkerEvents} pre-merged into the event
40
+ * map, so a consumer plugin types its own `config`/`state`/`emit` by passing only
41
+ * its OWN event map — never hand-merging `WorkerEvents`, and never importing from
42
+ * `@moku-labs/core` (a Layer-1 boundary the spec validator flags for consumers).
43
+ *
44
+ * A plugin that resolves sibling plugins also needs a `require` field; intersect the
45
+ * public `Server.RequireFn` for it, exactly as this framework's own plugins do. When
46
+ * you need the unaliased shape (e.g. a different global event map), use the raw
47
+ * re-exported {@link PluginCtx} instead.
48
+ *
49
+ * @template Config - This plugin's own flat configuration object.
50
+ * @template State - This plugin's mutable state (use `Record<string, never>` when stateless).
51
+ * @template Events - This plugin's own event map, merged on top of {@link WorkerEvents}; defaults to none.
52
+ * @example
53
+ * ```typescript
54
+ * import type { Server, WorkerPluginCtx } from "@moku-labs/worker";
55
+ * type MyEvents = { "my:done": { id: string } };
56
+ * export type MyCtx = WorkerPluginCtx<MyConfig, Record<string, never>, MyEvents> & {
57
+ * require: Server.RequireFn;
58
+ * };
59
+ * ```
60
+ */
61
+ type WorkerPluginCtx<Config, State, Events extends Record<string, unknown> = Record<never, never>> = PluginCtx<Config, State, WorkerEvents & Events>;
62
+ //#endregion
63
+ export { WorkerPluginCtx as i, WorkerEnv as n, WorkerEvents as r, WorkerConfig as t };
@@ -0,0 +1,63 @@
1
+ import { PluginCtx } from "@moku-labs/core";
2
+
3
+ //#region src/config.d.ts
4
+ /** Per-request Cloudflare bindings object (env). Framework-level shared type. */
5
+ type WorkerEnv = Record<string, unknown>;
6
+ /** Global framework config — flat, with complete defaults. */
7
+ type WorkerConfig = {
8
+ stage: "production" | "development" | "test";
9
+ name: string;
10
+ compatibilityDate: string;
11
+ };
12
+ /** Global framework events — declared once, visible to every plugin. */
13
+ type WorkerEvents = {
14
+ "request:start": {
15
+ method: string;
16
+ path: string;
17
+ requestId: string;
18
+ };
19
+ "request:end": {
20
+ method: string;
21
+ path: string;
22
+ status: number;
23
+ ms: number;
24
+ };
25
+ "deploy:phase": {
26
+ phase: string;
27
+ detail?: string;
28
+ };
29
+ "deploy:complete": {
30
+ url: string;
31
+ };
32
+ "provision:resource": {
33
+ kind: "kv" | "r2" | "d1" | "queue" | "do";
34
+ name: string;
35
+ };
36
+ };
37
+ /**
38
+ * Worker-bound plugin context for Layer-3 consumer plugins. Aliases the core
39
+ * {@link PluginCtx} with the global {@link WorkerEvents} pre-merged into the event
40
+ * map, so a consumer plugin types its own `config`/`state`/`emit` by passing only
41
+ * its OWN event map — never hand-merging `WorkerEvents`, and never importing from
42
+ * `@moku-labs/core` (a Layer-1 boundary the spec validator flags for consumers).
43
+ *
44
+ * A plugin that resolves sibling plugins also needs a `require` field; intersect the
45
+ * public `Server.RequireFn` for it, exactly as this framework's own plugins do. When
46
+ * you need the unaliased shape (e.g. a different global event map), use the raw
47
+ * re-exported {@link PluginCtx} instead.
48
+ *
49
+ * @template Config - This plugin's own flat configuration object.
50
+ * @template State - This plugin's mutable state (use `Record<string, never>` when stateless).
51
+ * @template Events - This plugin's own event map, merged on top of {@link WorkerEvents}; defaults to none.
52
+ * @example
53
+ * ```typescript
54
+ * import type { Server, WorkerPluginCtx } from "@moku-labs/worker";
55
+ * type MyEvents = { "my:done": { id: string } };
56
+ * export type MyCtx = WorkerPluginCtx<MyConfig, Record<string, never>, MyEvents> & {
57
+ * require: Server.RequireFn;
58
+ * };
59
+ * ```
60
+ */
61
+ type WorkerPluginCtx<Config, State, Events extends Record<string, unknown> = Record<never, never>> = PluginCtx<Config, State, WorkerEvents & Events>;
62
+ //#endregion
63
+ export { WorkerPluginCtx as i, WorkerEnv as n, WorkerEvents as r, WorkerConfig as t };
package/dist/index.cjs CHANGED
@@ -121,17 +121,21 @@ const makeEndpoint = (path, method, handler) => ({
121
121
  handler
122
122
  });
123
123
  /**
124
- * Build a typed `Endpoint`. `{name}` → required param; `{name?}` → optional param.
124
+ * Build a typed `Endpoint`. `{name}` → required param (`string`); `{name:?}` →
125
+ * optional param (`string | undefined`). The path template flows into each
126
+ * handler's `ctx.params` ({@link PathParams}), so a required `{id}` is typed
127
+ * `string` — no `?? ""` fallback needed.
125
128
  *
126
129
  * PURE factory (spec/03 §1): no ctx, no lifecycle, no side effects; safe to run
127
130
  * before `createApp`. Each verb method (`get`, `post`, …, `all`) returns the
128
131
  * truthful Endpoint value — `method: "ALL"` is never used as a `"get"` sentinel.
129
132
  *
130
- * @param path - Endpoint path, optionally with `{name}` / `{name?}` params.
133
+ * @template Path - The path template literal, inferred from `path`.
134
+ * @param path - Endpoint path, optionally with `{name}` / `{name:?}` params.
131
135
  * @returns A builder whose verb methods each return a typed `Endpoint`.
132
136
  * @example
133
137
  * ```typescript
134
- * endpoint("/api/data/{lang?}").get(({ params }) =>
138
+ * endpoint("/api/data/{lang:?}").get(({ params }) =>
135
139
  * Response.json({ lang: params.lang ?? "en" })
136
140
  * );
137
141
  * ```
@@ -232,27 +236,28 @@ const endpoint = (path) => ({
232
236
  const LITERAL_WEIGHT = 2;
233
237
  /** Specificity weight for a required param segment `{name}`. */
234
238
  const REQUIRED_PARAM_WEIGHT = 1;
235
- /** Specificity weight for an optional param segment `{name?}`. */
239
+ /** Specificity weight for an optional param segment `{name:?}`. */
236
240
  const OPTIONAL_PARAM_WEIGHT = 0;
237
241
  /**
238
242
  * Parse one path segment string into a typed `PathSegment`.
239
243
  *
240
- * `{name}` → required param; `{name?}` → optional param; anything else → literal.
244
+ * `{name}` → required param; `{name:?}` → optional param; anything else → literal.
245
+ * The `:?` optional suffix matches the `@moku-labs/web` router pattern.
241
246
  *
242
247
  * @param raw - A single path segment token (no leading slash).
243
248
  * @returns The parsed `PathSegment`.
244
249
  * @example
245
250
  * ```typescript
246
- * parseSegment("{id}") // → { value: "id", param: true, optional: false }
247
- * parseSegment("{id?}") // → { value: "id", param: true, optional: true }
248
- * parseSegment("api") // → { value: "api", param: false, optional: false }
251
+ * parseSegment("{id}") // → { value: "id", param: true, optional: false }
252
+ * parseSegment("{id:?}") // → { value: "id", param: true, optional: true }
253
+ * parseSegment("api") // → { value: "api", param: false, optional: false }
249
254
  * ```
250
255
  */
251
256
  const parseSegment = (raw) => {
252
257
  if (raw.startsWith("{") && raw.endsWith("}")) {
253
258
  const inner = raw.slice(1, -1);
254
- if (inner.endsWith("?")) return {
255
- value: inner.slice(0, -1),
259
+ if (inner.endsWith(":?")) return {
260
+ value: inner.slice(0, -2),
256
261
  param: true,
257
262
  optional: true
258
263
  };
@@ -347,7 +352,7 @@ const tryMatchEndpoint = (compiled, method, tokens) => {
347
352
  * @example
348
353
  * ```typescript
349
354
  * const a = compileEndpoint(endpoint("/api/{id}").get(handler)); // specificity 3
350
- * const b = compileEndpoint(endpoint("/api/{id?}").get(handler)); // specificity 2
355
+ * const b = compileEndpoint(endpoint("/api/{id:?}").get(handler)); // specificity 2
351
356
  * [b, a].sort(bySpecificityDesc); // → [a, b] — higher specificity first
352
357
  * ```
353
358
  */
@@ -390,10 +395,12 @@ const findBestMatch = (table, method, tokens) => {
390
395
  *
391
396
  * Called by `onInit` — the one-time per-isolate setup. Sorts `state.table` by
392
397
  * specificity (descending), validates that no endpoint path contains duplicate
393
- * `{param}` names, and sets `state.compiled = true` to guard re-entry.
398
+ * `{param}` names or the retired `{name?}` optional syntax, and sets
399
+ * `state.compiled = true` to guard re-entry.
394
400
  *
395
401
  * @param state - The mutable server state whose `table` should be compiled.
396
- * @throws {Error} With `[moku-worker]` prefix when a path has duplicate param names.
402
+ * @throws {Error} With `[moku-worker]` prefix when a path has duplicate param
403
+ * names, or uses the old `{name?}` optional syntax (now `{name:?}`).
397
404
  * @example
398
405
  * ```typescript
399
406
  * // Called inside serverPlugin.onInit:
@@ -407,6 +414,10 @@ const compileServerState = (state) => {
407
414
  const seen = /* @__PURE__ */ new Set();
408
415
  for (const segment of compiled.segments) {
409
416
  if (!segment.param) continue;
417
+ if (segment.value.endsWith("?")) {
418
+ const name = segment.value.slice(0, -1);
419
+ throw new Error(`[moku-worker] endpoint path "${compiled.endpoint.path}" uses the old optional-param syntax "{${segment.value}}".\n Optional params now use the colon form (matching @moku-labs/web): write "{${name}:?}" instead of "{${name}?}".`);
420
+ }
410
421
  if (seen.has(segment.value)) throw new Error(`[moku-worker] endpoint path "${compiled.endpoint.path}" has duplicate param "{${segment.value}}".\n Each {param} name in a path must be unique.`);
411
422
  seen.add(segment.value);
412
423
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as WorkerEnv, r as WorkerEvents, t as WorkerConfig } from "./config-AjH57AmD.cjs";
2
- import { PluginCtx, PluginInstance } from "@moku-labs/core";
1
+ import { i as WorkerPluginCtx, n as WorkerEnv, r as WorkerEvents, t as WorkerConfig } from "./config-Bj3GUJT_.cjs";
2
+ import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
3
3
  import { envPlugin, logPlugin } from "@moku-labs/common";
4
4
 
5
5
  //#region \0rolldown/runtime.js
@@ -50,7 +50,7 @@ type BindingsApi = {
50
50
  * api-factory context. State slot is Record<string, never> — bindings holds NO
51
51
  * state (F4). Type-argument order is PluginCtx<Config, State, Events>.
52
52
  */
53
- type Context = PluginCtx<Config$4, Record<string, never>, WorkerEvents>;
53
+ type Context = PluginCtx$1<Config$4, Record<string, never>, WorkerEvents>;
54
54
  //#endregion
55
55
  //#region src/plugins/bindings/index.d.ts
56
56
  /**
@@ -64,19 +64,19 @@ type Context = PluginCtx<Config$4, Record<string, never>, WorkerEvents>;
64
64
  */
65
65
  declare const bindingsPlugin: import("@moku-labs/core").PluginInstance<"bindings", Config$4, Record<string, never>, BindingsApi, {}> & Record<never, never>;
66
66
  declare namespace types_d_exports$4 {
67
- export { Api$3 as Api, CompiledEndpoint, Endpoint, EndpointHandler, MatchResult, Method, PathSegment, RequestContext, RequireFn, ServerConfig, ServerCtx, ServerEvents, ServerState };
67
+ export { Api$3 as Api, CompiledEndpoint, Endpoint, EndpointHandler, MatchResult, Method, PathParams, PathSegment, RequestContext, RequireFn, ServerConfig, ServerCtx, ServerEvents, ServerState };
68
68
  }
69
69
  /** HTTP method an endpoint matches; "ALL" matches any verb. */
70
70
  type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL";
71
- /** One parsed path segment: a literal, a required `{name}`, or an optional `{name?}`. */
71
+ /** One parsed path segment: a literal, a required `{name}`, or an optional `{name:?}`. */
72
72
  type PathSegment = {
73
- /** The literal text, or the param name when a param. */readonly value: string; /** Whether this segment is a `{name}` / `{name?}` parameter. */
74
- readonly param: boolean; /** Whether the param is optional (`{name?}`). */
73
+ /** The literal text, or the param name when a param. */readonly value: string; /** Whether this segment is a `{name}` / `{name:?}` parameter. */
74
+ readonly param: boolean; /** Whether the param is optional (`{name:?}`). */
75
75
  readonly optional: boolean;
76
76
  };
77
77
  /** A declarative endpoint produced by the pure endpoint() builder. */
78
78
  type Endpoint = {
79
- /** Endpoint path, optionally with `{name}` / `{name?}` params. */readonly path: string; /** HTTP method or "ALL". */
79
+ /** Endpoint path, optionally with `{name}` / `{name:?}` params. */readonly path: string; /** HTTP method or "ALL". */
80
80
  readonly method: Method; /** The handler invoked on a match. */
81
81
  readonly handler: EndpointHandler;
82
82
  };
@@ -125,14 +125,55 @@ type ApiOf<P> = P extends {
125
125
  } ? A : never;
126
126
  /** Cross-plugin reach used inside handlers: require(plugin) returns that plugin's API. Mirrors ctx.require. */
127
127
  type RequireFn = <P extends AnyPlugin>(plugin: P) => ApiOf<P>;
128
- /** A request handler: receives the per-request context, returns a Response. */
129
- type EndpointHandler = (ctx: RequestContext) => Response | Promise<Response>;
130
- /** Fresh per-request object threaded to each EndpointHandler. */
131
- type RequestContext = {
128
+ /**
129
+ * Prettify an intersection into a single flat object type for readable hovers.
130
+ * Homomorphic mapped type preserves each property's `?` optionality modifier.
131
+ */
132
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
133
+ /**
134
+ * Required param names in a path template, as a string union (`never` if none).
135
+ * Walks each `{...}` segment: collects a bare `{name}`, skips an optional `{name:?}`.
136
+ */
137
+ type RequiredParamNames<Path extends string> = Path extends `${string}{${infer Rest}` ? Rest extends `${infer Body}}${infer Tail}` ? Body extends `${string}:?` ? RequiredParamNames<Tail> : Body | RequiredParamNames<Tail> : never : never;
138
+ /**
139
+ * Optional param names in a path template with the `:?` suffix stripped, as a
140
+ * string union (`never` if none). Collects `{name:?}`, skips a bare `{name}`.
141
+ */
142
+ type OptionalParamNames<Path extends string> = Path extends `${string}{${infer Rest}` ? Rest extends `${infer Body}}${infer Tail}` ? Body extends `${infer Name}:?` ? Name | OptionalParamNames<Tail> : OptionalParamNames<Tail> : never : never;
143
+ /**
144
+ * Map a path template to its typed `params` object: a required `{name}` becomes
145
+ * `name: string`; an optional `{name:?}` becomes `name?: string`. A non-literal
146
+ * `string` path (e.g. one assembled at runtime) widens to the permissive
147
+ * `Record<string, string | undefined>`.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * type P = PathParams<"/boards/{id}/data/{lang:?}">;
152
+ * // { id: string; lang?: string }
153
+ * ```
154
+ */
155
+ type PathParams<Path extends string> = string extends Path ? Record<string, string | undefined> : Prettify<{ [K in RequiredParamNames<Path>]: string } & { [K in OptionalParamNames<Path>]?: string }>;
156
+ /**
157
+ * A request handler: receives the per-request context, returns a Response.
158
+ *
159
+ * @template Params - Path-params shape, inferred by the `endpoint()` builder
160
+ * from the path template ({@link PathParams}) — a required `{name}` is
161
+ * `string`, an optional `{name:?}` is `string | undefined`. Defaults to the
162
+ * permissive `Record<string, string | undefined>` for hand-written handler types.
163
+ */
164
+ type EndpointHandler<Params = Record<string, string | undefined>> = (ctx: RequestContext<Params>) => Response | Promise<Response>;
165
+ /**
166
+ * Fresh per-request object threaded to each EndpointHandler.
167
+ *
168
+ * @template Params - Path-params shape for `params`, inferred from the path
169
+ * template by the `endpoint()` builder ({@link PathParams}). Defaults to the
170
+ * permissive `Record<string, string | undefined>`.
171
+ */
172
+ type RequestContext<Params = Record<string, string | undefined>> = {
132
173
  /** The incoming request. */readonly request: Request; /** Per-request Cloudflare bindings — threaded on the stack, NEVER stored in state. */
133
174
  readonly env: WorkerEnv; /** waitUntil / passThroughOnException. */
134
- readonly exec: ExecutionContext; /** Path params extracted from the matched endpoint. */
135
- readonly params: Record<string, string | undefined>; /** Parsed request URL. */
175
+ readonly exec: ExecutionContext; /** Path params extracted from the matched endpoint, typed from the path template. */
176
+ readonly params: Params; /** Parsed request URL. */
136
177
  readonly url: URL; /** Cross-plugin reach for handlers (e.g. require(bindingsPlugin)). */
137
178
  readonly require: RequireFn; /** Presence check for an optional plugin. */
138
179
  readonly has: (name: string) => boolean;
@@ -145,7 +186,7 @@ type ServerEvents = {
145
186
  };
146
187
  };
147
188
  /** Full server plugin context (own config + state + merged events + cross-plugin reach). */
148
- type ServerCtx = PluginCtx<ServerConfig, ServerState, WorkerEvents & ServerEvents> & {
189
+ type ServerCtx = PluginCtx$1<ServerConfig, ServerState, WorkerEvents & ServerEvents> & {
149
190
  /** Cross-plugin require threaded into each RequestContext. */require: RequireFn; /** Presence check for an optional plugin. */
150
191
  has: (name: string) => boolean;
151
192
  };
@@ -175,12 +216,14 @@ type Api$3 = {
175
216
  /**
176
217
  * Fluent builder whose verb methods each return a typed `Endpoint`.
177
218
  *
219
+ * @template Path - The path template literal, used to infer each handler's
220
+ * typed `ctx.params` ({@link PathParams}).
178
221
  * @example
179
222
  * ```typescript
180
- * const e = endpoint("/api/{id}").get(handler);
223
+ * const e = endpoint("/api/{id}").get(({ params }) => Response.json({ id: params.id }));
181
224
  * ```
182
225
  */
183
- type EndpointBuilder = {
226
+ type EndpointBuilder<Path extends string> = {
184
227
  /**
185
228
  * Build a GET endpoint bound to this path.
186
229
  *
@@ -191,7 +234,7 @@ type EndpointBuilder = {
191
234
  * endpoint("/health").get(() => new Response("ok"));
192
235
  * ```
193
236
  */
194
- get(handler: EndpointHandler): Endpoint;
237
+ get(handler: EndpointHandler<PathParams<Path>>): Endpoint;
195
238
  /**
196
239
  * Build a POST endpoint bound to this path.
197
240
  *
@@ -202,7 +245,7 @@ type EndpointBuilder = {
202
245
  * endpoint("/users").post(({ request }) => Response.json({ created: true }, { status: 201 }));
203
246
  * ```
204
247
  */
205
- post(handler: EndpointHandler): Endpoint;
248
+ post(handler: EndpointHandler<PathParams<Path>>): Endpoint;
206
249
  /**
207
250
  * Build a PUT endpoint bound to this path.
208
251
  *
@@ -213,7 +256,7 @@ type EndpointBuilder = {
213
256
  * endpoint("/users/{id}").put(({ params }) => Response.json({ updated: params.id }));
214
257
  * ```
215
258
  */
216
- put(handler: EndpointHandler): Endpoint;
259
+ put(handler: EndpointHandler<PathParams<Path>>): Endpoint;
217
260
  /**
218
261
  * Build a PATCH endpoint bound to this path.
219
262
  *
@@ -224,7 +267,7 @@ type EndpointBuilder = {
224
267
  * endpoint("/users/{id}").patch(({ params }) => Response.json({ patched: params.id }));
225
268
  * ```
226
269
  */
227
- patch(handler: EndpointHandler): Endpoint;
270
+ patch(handler: EndpointHandler<PathParams<Path>>): Endpoint;
228
271
  /**
229
272
  * Build a DELETE endpoint bound to this path.
230
273
  *
@@ -235,7 +278,7 @@ type EndpointBuilder = {
235
278
  * endpoint("/users/{id}").delete(() => new Response(null, { status: 204 }));
236
279
  * ```
237
280
  */
238
- delete(handler: EndpointHandler): Endpoint;
281
+ delete(handler: EndpointHandler<PathParams<Path>>): Endpoint;
239
282
  /**
240
283
  * Build a HEAD endpoint bound to this path.
241
284
  *
@@ -246,7 +289,7 @@ type EndpointBuilder = {
246
289
  * endpoint("/health").head(() => new Response(null, { status: 200 }));
247
290
  * ```
248
291
  */
249
- head(handler: EndpointHandler): Endpoint;
292
+ head(handler: EndpointHandler<PathParams<Path>>): Endpoint;
250
293
  /**
251
294
  * Build an OPTIONS endpoint bound to this path.
252
295
  *
@@ -257,7 +300,7 @@ type EndpointBuilder = {
257
300
  * endpoint("/api").options(() => new Response(null, { headers: { Allow: "GET, POST" } }));
258
301
  * ```
259
302
  */
260
- options(handler: EndpointHandler): Endpoint;
303
+ options(handler: EndpointHandler<PathParams<Path>>): Endpoint;
261
304
  /**
262
305
  * Build an ALL-method endpoint bound to this path (`method: "ALL"` — matches any verb).
263
306
  *
@@ -268,25 +311,29 @@ type EndpointBuilder = {
268
311
  * endpoint("0 * * * *").all(async () => new Response("cron done"));
269
312
  * ```
270
313
  */
271
- all(handler: EndpointHandler): Endpoint;
314
+ all(handler: EndpointHandler<PathParams<Path>>): Endpoint;
272
315
  };
273
316
  /**
274
- * Build a typed `Endpoint`. `{name}` → required param; `{name?}` → optional param.
317
+ * Build a typed `Endpoint`. `{name}` → required param (`string`); `{name:?}` →
318
+ * optional param (`string | undefined`). The path template flows into each
319
+ * handler's `ctx.params` ({@link PathParams}), so a required `{id}` is typed
320
+ * `string` — no `?? ""` fallback needed.
275
321
  *
276
322
  * PURE factory (spec/03 §1): no ctx, no lifecycle, no side effects; safe to run
277
323
  * before `createApp`. Each verb method (`get`, `post`, …, `all`) returns the
278
324
  * truthful Endpoint value — `method: "ALL"` is never used as a `"get"` sentinel.
279
325
  *
280
- * @param path - Endpoint path, optionally with `{name}` / `{name?}` params.
326
+ * @template Path - The path template literal, inferred from `path`.
327
+ * @param path - Endpoint path, optionally with `{name}` / `{name:?}` params.
281
328
  * @returns A builder whose verb methods each return a typed `Endpoint`.
282
329
  * @example
283
330
  * ```typescript
284
- * endpoint("/api/data/{lang?}").get(({ params }) =>
331
+ * endpoint("/api/data/{lang:?}").get(({ params }) =>
285
332
  * Response.json({ lang: params.lang ?? "en" })
286
333
  * );
287
334
  * ```
288
335
  */
289
- declare const endpoint: (path: string) => EndpointBuilder;
336
+ declare const endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
290
337
  declare namespace types_d_exports$1 {
291
338
  export { Api$2 as Api, Config$3 as Config, D1Ctx, DeployManifest$2 as DeployManifest };
292
339
  }
@@ -370,7 +417,7 @@ type Api$2 = {
370
417
  * Internal context type — own config first, no state, no d1-local events.
371
418
  * Intersected with a narrow `require` typed to the one dependency d1 resolves.
372
419
  */
373
- type D1Ctx = PluginCtx<Config$3, Record<string, never>, WorkerEvents> & {
420
+ type D1Ctx = PluginCtx$1<Config$3, Record<string, never>, WorkerEvents> & {
374
421
  /**
375
422
  * Resolve a dependency plugin's api. d1 only ever resolves `bindingsPlugin`.
376
423
  *
@@ -446,7 +493,7 @@ type Api$1 = {
446
493
  * Internal context type — own config first, no state, no DO events.
447
494
  * Intersected with a narrow `require` typed to the one dependency durableObjects resolves.
448
495
  */
449
- type Ctx$1 = PluginCtx<Config$2, Record<string, never>, WorkerEvents> & {
496
+ type Ctx$1 = PluginCtx$1<Config$2, Record<string, never>, WorkerEvents> & {
450
497
  /**
451
498
  * Resolve a dependency plugin's api. durableObjects only ever resolves `bindingsPlugin`.
452
499
  *
@@ -685,7 +732,7 @@ type Api = {
685
732
  * (core's "advanced composition" note), typed to the one dependency queues resolves —
686
733
  * `require(bindingsPlugin)` → `BindingsApi`. Core does not export `RequireFunction`.
687
734
  */
688
- type Ctx = PluginCtx<Config$1, Record<string, never>, WorkerEvents & QueueEvents> & {
735
+ type Ctx = PluginCtx$1<Config$1, Record<string, never>, WorkerEvents & QueueEvents> & {
689
736
  /**
690
737
  * Resolve a dependency plugin's api. queues only ever resolves `bindingsPlugin`.
691
738
  *
@@ -735,7 +782,7 @@ declare const serverPlugin: import("@moku-labs/core").PluginInstance<"server", S
735
782
  method: string;
736
783
  };
737
784
  }> & {
738
- endpoint: (path: string) => EndpointBuilder;
785
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
739
786
  };
740
787
  //#endregion
741
788
  //#region src/plugins/storage/providers/types.d.ts
@@ -831,7 +878,7 @@ type StorageApi = {
831
878
  * resolves — mirrors the kv/api.ts pattern (PluginCtx has no `require` by
832
879
  * default; core does not export a generic RequireFunction).
833
880
  */
834
- type StorageCtx = PluginCtx<StorageConfig, Record<string, never>, WorkerEvents> & {
881
+ type StorageCtx = PluginCtx$1<StorageConfig, Record<string, never>, WorkerEvents> & {
835
882
  /**
836
883
  * Resolve a dependency plugin's api. storage only ever resolves bindingsPlugin.
837
884
  *
@@ -953,7 +1000,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
953
1000
  method: string;
954
1001
  };
955
1002
  }> & {
956
- endpoint: (path: string) => EndpointBuilder;
1003
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
957
1004
  }) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>, import("@moku-labs/core").CorePluginInstance<"stage", {
958
1005
  stage: "production" | "development" | "test";
959
1006
  }, Record<string, never>, {
@@ -966,7 +1013,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
966
1013
  method: string;
967
1014
  };
968
1015
  }> & {
969
- endpoint: (path: string) => EndpointBuilder;
1016
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
970
1017
  }) | ExtraPlugins[number], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>, import("@moku-labs/core").CorePluginInstance<"stage", {
971
1018
  stage: "production" | "development" | "test";
972
1019
  }, Record<string, never>, {
@@ -981,4 +1028,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
981
1028
  current: () => "production" | "development" | "test";
982
1029
  }>]>>;
983
1030
  //#endregion
984
- export { type types_d_exports as Bindings, type types_d_exports$1 as D1, type types_d_exports$2 as DurableObjects, type types_d_exports$3 as Queues, type types_d_exports$4 as Server, type StageApi, type types_d_exports$5 as Storage, type WorkerConfig, type WorkerEnv, type WorkerEvents, bindingsPlugin, createApp, createPlugin, d1Plugin, defineDurableObject, durableObjectsPlugin, endpoint, envPlugin, kvPlugin, logPlugin, queuesPlugin, serverPlugin, stagePlugin, storagePlugin };
1031
+ export { type types_d_exports as Bindings, type types_d_exports$1 as D1, type types_d_exports$2 as DurableObjects, type PluginCtx, type types_d_exports$3 as Queues, type types_d_exports$4 as Server, type StageApi, type types_d_exports$5 as Storage, type WorkerConfig, type WorkerEnv, type WorkerEvents, type WorkerPluginCtx, bindingsPlugin, createApp, createPlugin, d1Plugin, defineDurableObject, durableObjectsPlugin, endpoint, envPlugin, kvPlugin, logPlugin, queuesPlugin, serverPlugin, stagePlugin, storagePlugin };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { n as WorkerEnv, r as WorkerEvents, t as WorkerConfig } from "./config-AjH57AmD.mjs";
1
+ import { i as WorkerPluginCtx, n as WorkerEnv, r as WorkerEvents, t as WorkerConfig } from "./config-Bj3GUJT_.mjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
- import { PluginCtx, PluginInstance } from "@moku-labs/core";
3
+ import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
4
4
 
5
5
  //#region \0rolldown/runtime.js
6
6
  declare namespace types_d_exports {
@@ -50,7 +50,7 @@ type BindingsApi = {
50
50
  * api-factory context. State slot is Record<string, never> — bindings holds NO
51
51
  * state (F4). Type-argument order is PluginCtx<Config, State, Events>.
52
52
  */
53
- type Context = PluginCtx<Config$4, Record<string, never>, WorkerEvents>;
53
+ type Context = PluginCtx$1<Config$4, Record<string, never>, WorkerEvents>;
54
54
  //#endregion
55
55
  //#region src/plugins/bindings/index.d.ts
56
56
  /**
@@ -64,19 +64,19 @@ type Context = PluginCtx<Config$4, Record<string, never>, WorkerEvents>;
64
64
  */
65
65
  declare const bindingsPlugin: import("@moku-labs/core").PluginInstance<"bindings", Config$4, Record<string, never>, BindingsApi, {}> & Record<never, never>;
66
66
  declare namespace types_d_exports$4 {
67
- export { Api$3 as Api, CompiledEndpoint, Endpoint, EndpointHandler, MatchResult, Method, PathSegment, RequestContext, RequireFn, ServerConfig, ServerCtx, ServerEvents, ServerState };
67
+ export { Api$3 as Api, CompiledEndpoint, Endpoint, EndpointHandler, MatchResult, Method, PathParams, PathSegment, RequestContext, RequireFn, ServerConfig, ServerCtx, ServerEvents, ServerState };
68
68
  }
69
69
  /** HTTP method an endpoint matches; "ALL" matches any verb. */
70
70
  type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL";
71
- /** One parsed path segment: a literal, a required `{name}`, or an optional `{name?}`. */
71
+ /** One parsed path segment: a literal, a required `{name}`, or an optional `{name:?}`. */
72
72
  type PathSegment = {
73
- /** The literal text, or the param name when a param. */readonly value: string; /** Whether this segment is a `{name}` / `{name?}` parameter. */
74
- readonly param: boolean; /** Whether the param is optional (`{name?}`). */
73
+ /** The literal text, or the param name when a param. */readonly value: string; /** Whether this segment is a `{name}` / `{name:?}` parameter. */
74
+ readonly param: boolean; /** Whether the param is optional (`{name:?}`). */
75
75
  readonly optional: boolean;
76
76
  };
77
77
  /** A declarative endpoint produced by the pure endpoint() builder. */
78
78
  type Endpoint = {
79
- /** Endpoint path, optionally with `{name}` / `{name?}` params. */readonly path: string; /** HTTP method or "ALL". */
79
+ /** Endpoint path, optionally with `{name}` / `{name:?}` params. */readonly path: string; /** HTTP method or "ALL". */
80
80
  readonly method: Method; /** The handler invoked on a match. */
81
81
  readonly handler: EndpointHandler;
82
82
  };
@@ -125,14 +125,55 @@ type ApiOf<P> = P extends {
125
125
  } ? A : never;
126
126
  /** Cross-plugin reach used inside handlers: require(plugin) returns that plugin's API. Mirrors ctx.require. */
127
127
  type RequireFn = <P extends AnyPlugin>(plugin: P) => ApiOf<P>;
128
- /** A request handler: receives the per-request context, returns a Response. */
129
- type EndpointHandler = (ctx: RequestContext) => Response | Promise<Response>;
130
- /** Fresh per-request object threaded to each EndpointHandler. */
131
- type RequestContext = {
128
+ /**
129
+ * Prettify an intersection into a single flat object type for readable hovers.
130
+ * Homomorphic mapped type preserves each property's `?` optionality modifier.
131
+ */
132
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
133
+ /**
134
+ * Required param names in a path template, as a string union (`never` if none).
135
+ * Walks each `{...}` segment: collects a bare `{name}`, skips an optional `{name:?}`.
136
+ */
137
+ type RequiredParamNames<Path extends string> = Path extends `${string}{${infer Rest}` ? Rest extends `${infer Body}}${infer Tail}` ? Body extends `${string}:?` ? RequiredParamNames<Tail> : Body | RequiredParamNames<Tail> : never : never;
138
+ /**
139
+ * Optional param names in a path template with the `:?` suffix stripped, as a
140
+ * string union (`never` if none). Collects `{name:?}`, skips a bare `{name}`.
141
+ */
142
+ type OptionalParamNames<Path extends string> = Path extends `${string}{${infer Rest}` ? Rest extends `${infer Body}}${infer Tail}` ? Body extends `${infer Name}:?` ? Name | OptionalParamNames<Tail> : OptionalParamNames<Tail> : never : never;
143
+ /**
144
+ * Map a path template to its typed `params` object: a required `{name}` becomes
145
+ * `name: string`; an optional `{name:?}` becomes `name?: string`. A non-literal
146
+ * `string` path (e.g. one assembled at runtime) widens to the permissive
147
+ * `Record<string, string | undefined>`.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * type P = PathParams<"/boards/{id}/data/{lang:?}">;
152
+ * // { id: string; lang?: string }
153
+ * ```
154
+ */
155
+ type PathParams<Path extends string> = string extends Path ? Record<string, string | undefined> : Prettify<{ [K in RequiredParamNames<Path>]: string } & { [K in OptionalParamNames<Path>]?: string }>;
156
+ /**
157
+ * A request handler: receives the per-request context, returns a Response.
158
+ *
159
+ * @template Params - Path-params shape, inferred by the `endpoint()` builder
160
+ * from the path template ({@link PathParams}) — a required `{name}` is
161
+ * `string`, an optional `{name:?}` is `string | undefined`. Defaults to the
162
+ * permissive `Record<string, string | undefined>` for hand-written handler types.
163
+ */
164
+ type EndpointHandler<Params = Record<string, string | undefined>> = (ctx: RequestContext<Params>) => Response | Promise<Response>;
165
+ /**
166
+ * Fresh per-request object threaded to each EndpointHandler.
167
+ *
168
+ * @template Params - Path-params shape for `params`, inferred from the path
169
+ * template by the `endpoint()` builder ({@link PathParams}). Defaults to the
170
+ * permissive `Record<string, string | undefined>`.
171
+ */
172
+ type RequestContext<Params = Record<string, string | undefined>> = {
132
173
  /** The incoming request. */readonly request: Request; /** Per-request Cloudflare bindings — threaded on the stack, NEVER stored in state. */
133
174
  readonly env: WorkerEnv; /** waitUntil / passThroughOnException. */
134
- readonly exec: ExecutionContext; /** Path params extracted from the matched endpoint. */
135
- readonly params: Record<string, string | undefined>; /** Parsed request URL. */
175
+ readonly exec: ExecutionContext; /** Path params extracted from the matched endpoint, typed from the path template. */
176
+ readonly params: Params; /** Parsed request URL. */
136
177
  readonly url: URL; /** Cross-plugin reach for handlers (e.g. require(bindingsPlugin)). */
137
178
  readonly require: RequireFn; /** Presence check for an optional plugin. */
138
179
  readonly has: (name: string) => boolean;
@@ -145,7 +186,7 @@ type ServerEvents = {
145
186
  };
146
187
  };
147
188
  /** Full server plugin context (own config + state + merged events + cross-plugin reach). */
148
- type ServerCtx = PluginCtx<ServerConfig, ServerState, WorkerEvents & ServerEvents> & {
189
+ type ServerCtx = PluginCtx$1<ServerConfig, ServerState, WorkerEvents & ServerEvents> & {
149
190
  /** Cross-plugin require threaded into each RequestContext. */require: RequireFn; /** Presence check for an optional plugin. */
150
191
  has: (name: string) => boolean;
151
192
  };
@@ -175,12 +216,14 @@ type Api$3 = {
175
216
  /**
176
217
  * Fluent builder whose verb methods each return a typed `Endpoint`.
177
218
  *
219
+ * @template Path - The path template literal, used to infer each handler's
220
+ * typed `ctx.params` ({@link PathParams}).
178
221
  * @example
179
222
  * ```typescript
180
- * const e = endpoint("/api/{id}").get(handler);
223
+ * const e = endpoint("/api/{id}").get(({ params }) => Response.json({ id: params.id }));
181
224
  * ```
182
225
  */
183
- type EndpointBuilder = {
226
+ type EndpointBuilder<Path extends string> = {
184
227
  /**
185
228
  * Build a GET endpoint bound to this path.
186
229
  *
@@ -191,7 +234,7 @@ type EndpointBuilder = {
191
234
  * endpoint("/health").get(() => new Response("ok"));
192
235
  * ```
193
236
  */
194
- get(handler: EndpointHandler): Endpoint;
237
+ get(handler: EndpointHandler<PathParams<Path>>): Endpoint;
195
238
  /**
196
239
  * Build a POST endpoint bound to this path.
197
240
  *
@@ -202,7 +245,7 @@ type EndpointBuilder = {
202
245
  * endpoint("/users").post(({ request }) => Response.json({ created: true }, { status: 201 }));
203
246
  * ```
204
247
  */
205
- post(handler: EndpointHandler): Endpoint;
248
+ post(handler: EndpointHandler<PathParams<Path>>): Endpoint;
206
249
  /**
207
250
  * Build a PUT endpoint bound to this path.
208
251
  *
@@ -213,7 +256,7 @@ type EndpointBuilder = {
213
256
  * endpoint("/users/{id}").put(({ params }) => Response.json({ updated: params.id }));
214
257
  * ```
215
258
  */
216
- put(handler: EndpointHandler): Endpoint;
259
+ put(handler: EndpointHandler<PathParams<Path>>): Endpoint;
217
260
  /**
218
261
  * Build a PATCH endpoint bound to this path.
219
262
  *
@@ -224,7 +267,7 @@ type EndpointBuilder = {
224
267
  * endpoint("/users/{id}").patch(({ params }) => Response.json({ patched: params.id }));
225
268
  * ```
226
269
  */
227
- patch(handler: EndpointHandler): Endpoint;
270
+ patch(handler: EndpointHandler<PathParams<Path>>): Endpoint;
228
271
  /**
229
272
  * Build a DELETE endpoint bound to this path.
230
273
  *
@@ -235,7 +278,7 @@ type EndpointBuilder = {
235
278
  * endpoint("/users/{id}").delete(() => new Response(null, { status: 204 }));
236
279
  * ```
237
280
  */
238
- delete(handler: EndpointHandler): Endpoint;
281
+ delete(handler: EndpointHandler<PathParams<Path>>): Endpoint;
239
282
  /**
240
283
  * Build a HEAD endpoint bound to this path.
241
284
  *
@@ -246,7 +289,7 @@ type EndpointBuilder = {
246
289
  * endpoint("/health").head(() => new Response(null, { status: 200 }));
247
290
  * ```
248
291
  */
249
- head(handler: EndpointHandler): Endpoint;
292
+ head(handler: EndpointHandler<PathParams<Path>>): Endpoint;
250
293
  /**
251
294
  * Build an OPTIONS endpoint bound to this path.
252
295
  *
@@ -257,7 +300,7 @@ type EndpointBuilder = {
257
300
  * endpoint("/api").options(() => new Response(null, { headers: { Allow: "GET, POST" } }));
258
301
  * ```
259
302
  */
260
- options(handler: EndpointHandler): Endpoint;
303
+ options(handler: EndpointHandler<PathParams<Path>>): Endpoint;
261
304
  /**
262
305
  * Build an ALL-method endpoint bound to this path (`method: "ALL"` — matches any verb).
263
306
  *
@@ -268,25 +311,29 @@ type EndpointBuilder = {
268
311
  * endpoint("0 * * * *").all(async () => new Response("cron done"));
269
312
  * ```
270
313
  */
271
- all(handler: EndpointHandler): Endpoint;
314
+ all(handler: EndpointHandler<PathParams<Path>>): Endpoint;
272
315
  };
273
316
  /**
274
- * Build a typed `Endpoint`. `{name}` → required param; `{name?}` → optional param.
317
+ * Build a typed `Endpoint`. `{name}` → required param (`string`); `{name:?}` →
318
+ * optional param (`string | undefined`). The path template flows into each
319
+ * handler's `ctx.params` ({@link PathParams}), so a required `{id}` is typed
320
+ * `string` — no `?? ""` fallback needed.
275
321
  *
276
322
  * PURE factory (spec/03 §1): no ctx, no lifecycle, no side effects; safe to run
277
323
  * before `createApp`. Each verb method (`get`, `post`, …, `all`) returns the
278
324
  * truthful Endpoint value — `method: "ALL"` is never used as a `"get"` sentinel.
279
325
  *
280
- * @param path - Endpoint path, optionally with `{name}` / `{name?}` params.
326
+ * @template Path - The path template literal, inferred from `path`.
327
+ * @param path - Endpoint path, optionally with `{name}` / `{name:?}` params.
281
328
  * @returns A builder whose verb methods each return a typed `Endpoint`.
282
329
  * @example
283
330
  * ```typescript
284
- * endpoint("/api/data/{lang?}").get(({ params }) =>
331
+ * endpoint("/api/data/{lang:?}").get(({ params }) =>
285
332
  * Response.json({ lang: params.lang ?? "en" })
286
333
  * );
287
334
  * ```
288
335
  */
289
- declare const endpoint: (path: string) => EndpointBuilder;
336
+ declare const endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
290
337
  declare namespace types_d_exports$1 {
291
338
  export { Api$2 as Api, Config$3 as Config, D1Ctx, DeployManifest$2 as DeployManifest };
292
339
  }
@@ -370,7 +417,7 @@ type Api$2 = {
370
417
  * Internal context type — own config first, no state, no d1-local events.
371
418
  * Intersected with a narrow `require` typed to the one dependency d1 resolves.
372
419
  */
373
- type D1Ctx = PluginCtx<Config$3, Record<string, never>, WorkerEvents> & {
420
+ type D1Ctx = PluginCtx$1<Config$3, Record<string, never>, WorkerEvents> & {
374
421
  /**
375
422
  * Resolve a dependency plugin's api. d1 only ever resolves `bindingsPlugin`.
376
423
  *
@@ -446,7 +493,7 @@ type Api$1 = {
446
493
  * Internal context type — own config first, no state, no DO events.
447
494
  * Intersected with a narrow `require` typed to the one dependency durableObjects resolves.
448
495
  */
449
- type Ctx$1 = PluginCtx<Config$2, Record<string, never>, WorkerEvents> & {
496
+ type Ctx$1 = PluginCtx$1<Config$2, Record<string, never>, WorkerEvents> & {
450
497
  /**
451
498
  * Resolve a dependency plugin's api. durableObjects only ever resolves `bindingsPlugin`.
452
499
  *
@@ -685,7 +732,7 @@ type Api = {
685
732
  * (core's "advanced composition" note), typed to the one dependency queues resolves —
686
733
  * `require(bindingsPlugin)` → `BindingsApi`. Core does not export `RequireFunction`.
687
734
  */
688
- type Ctx = PluginCtx<Config$1, Record<string, never>, WorkerEvents & QueueEvents> & {
735
+ type Ctx = PluginCtx$1<Config$1, Record<string, never>, WorkerEvents & QueueEvents> & {
689
736
  /**
690
737
  * Resolve a dependency plugin's api. queues only ever resolves `bindingsPlugin`.
691
738
  *
@@ -735,7 +782,7 @@ declare const serverPlugin: import("@moku-labs/core").PluginInstance<"server", S
735
782
  method: string;
736
783
  };
737
784
  }> & {
738
- endpoint: (path: string) => EndpointBuilder;
785
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
739
786
  };
740
787
  //#endregion
741
788
  //#region src/plugins/storage/providers/types.d.ts
@@ -831,7 +878,7 @@ type StorageApi = {
831
878
  * resolves — mirrors the kv/api.ts pattern (PluginCtx has no `require` by
832
879
  * default; core does not export a generic RequireFunction).
833
880
  */
834
- type StorageCtx = PluginCtx<StorageConfig, Record<string, never>, WorkerEvents> & {
881
+ type StorageCtx = PluginCtx$1<StorageConfig, Record<string, never>, WorkerEvents> & {
835
882
  /**
836
883
  * Resolve a dependency plugin's api. storage only ever resolves bindingsPlugin.
837
884
  *
@@ -953,7 +1000,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
953
1000
  method: string;
954
1001
  };
955
1002
  }> & {
956
- endpoint: (path: string) => EndpointBuilder;
1003
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
957
1004
  }) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>, import("@moku-labs/core").CorePluginInstance<"stage", {
958
1005
  stage: "production" | "development" | "test";
959
1006
  }, Record<string, never>, {
@@ -966,7 +1013,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
966
1013
  method: string;
967
1014
  };
968
1015
  }> & {
969
- endpoint: (path: string) => EndpointBuilder;
1016
+ endpoint: <Path extends string>(path: Path) => EndpointBuilder<Path>;
970
1017
  }) | ExtraPlugins[number], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>, import("@moku-labs/core").CorePluginInstance<"stage", {
971
1018
  stage: "production" | "development" | "test";
972
1019
  }, Record<string, never>, {
@@ -981,4 +1028,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
981
1028
  current: () => "production" | "development" | "test";
982
1029
  }>]>>;
983
1030
  //#endregion
984
- export { type types_d_exports as Bindings, type types_d_exports$1 as D1, type types_d_exports$2 as DurableObjects, type types_d_exports$3 as Queues, type types_d_exports$4 as Server, type StageApi, type types_d_exports$5 as Storage, type WorkerConfig, type WorkerEnv, type WorkerEvents, bindingsPlugin, createApp, createPlugin, d1Plugin, defineDurableObject, durableObjectsPlugin, endpoint, envPlugin, kvPlugin, logPlugin, queuesPlugin, serverPlugin, stagePlugin, storagePlugin };
1031
+ export { type types_d_exports as Bindings, type types_d_exports$1 as D1, type types_d_exports$2 as DurableObjects, type PluginCtx, type types_d_exports$3 as Queues, type types_d_exports$4 as Server, type StageApi, type types_d_exports$5 as Storage, type WorkerConfig, type WorkerEnv, type WorkerEvents, type WorkerPluginCtx, bindingsPlugin, createApp, createPlugin, d1Plugin, defineDurableObject, durableObjectsPlugin, endpoint, envPlugin, kvPlugin, logPlugin, queuesPlugin, serverPlugin, stagePlugin, storagePlugin };
package/dist/index.mjs CHANGED
@@ -120,17 +120,21 @@ const makeEndpoint = (path, method, handler) => ({
120
120
  handler
121
121
  });
122
122
  /**
123
- * Build a typed `Endpoint`. `{name}` → required param; `{name?}` → optional param.
123
+ * Build a typed `Endpoint`. `{name}` → required param (`string`); `{name:?}` →
124
+ * optional param (`string | undefined`). The path template flows into each
125
+ * handler's `ctx.params` ({@link PathParams}), so a required `{id}` is typed
126
+ * `string` — no `?? ""` fallback needed.
124
127
  *
125
128
  * PURE factory (spec/03 §1): no ctx, no lifecycle, no side effects; safe to run
126
129
  * before `createApp`. Each verb method (`get`, `post`, …, `all`) returns the
127
130
  * truthful Endpoint value — `method: "ALL"` is never used as a `"get"` sentinel.
128
131
  *
129
- * @param path - Endpoint path, optionally with `{name}` / `{name?}` params.
132
+ * @template Path - The path template literal, inferred from `path`.
133
+ * @param path - Endpoint path, optionally with `{name}` / `{name:?}` params.
130
134
  * @returns A builder whose verb methods each return a typed `Endpoint`.
131
135
  * @example
132
136
  * ```typescript
133
- * endpoint("/api/data/{lang?}").get(({ params }) =>
137
+ * endpoint("/api/data/{lang:?}").get(({ params }) =>
134
138
  * Response.json({ lang: params.lang ?? "en" })
135
139
  * );
136
140
  * ```
@@ -231,27 +235,28 @@ const endpoint = (path) => ({
231
235
  const LITERAL_WEIGHT = 2;
232
236
  /** Specificity weight for a required param segment `{name}`. */
233
237
  const REQUIRED_PARAM_WEIGHT = 1;
234
- /** Specificity weight for an optional param segment `{name?}`. */
238
+ /** Specificity weight for an optional param segment `{name:?}`. */
235
239
  const OPTIONAL_PARAM_WEIGHT = 0;
236
240
  /**
237
241
  * Parse one path segment string into a typed `PathSegment`.
238
242
  *
239
- * `{name}` → required param; `{name?}` → optional param; anything else → literal.
243
+ * `{name}` → required param; `{name:?}` → optional param; anything else → literal.
244
+ * The `:?` optional suffix matches the `@moku-labs/web` router pattern.
240
245
  *
241
246
  * @param raw - A single path segment token (no leading slash).
242
247
  * @returns The parsed `PathSegment`.
243
248
  * @example
244
249
  * ```typescript
245
- * parseSegment("{id}") // → { value: "id", param: true, optional: false }
246
- * parseSegment("{id?}") // → { value: "id", param: true, optional: true }
247
- * parseSegment("api") // → { value: "api", param: false, optional: false }
250
+ * parseSegment("{id}") // → { value: "id", param: true, optional: false }
251
+ * parseSegment("{id:?}") // → { value: "id", param: true, optional: true }
252
+ * parseSegment("api") // → { value: "api", param: false, optional: false }
248
253
  * ```
249
254
  */
250
255
  const parseSegment = (raw) => {
251
256
  if (raw.startsWith("{") && raw.endsWith("}")) {
252
257
  const inner = raw.slice(1, -1);
253
- if (inner.endsWith("?")) return {
254
- value: inner.slice(0, -1),
258
+ if (inner.endsWith(":?")) return {
259
+ value: inner.slice(0, -2),
255
260
  param: true,
256
261
  optional: true
257
262
  };
@@ -346,7 +351,7 @@ const tryMatchEndpoint = (compiled, method, tokens) => {
346
351
  * @example
347
352
  * ```typescript
348
353
  * const a = compileEndpoint(endpoint("/api/{id}").get(handler)); // specificity 3
349
- * const b = compileEndpoint(endpoint("/api/{id?}").get(handler)); // specificity 2
354
+ * const b = compileEndpoint(endpoint("/api/{id:?}").get(handler)); // specificity 2
350
355
  * [b, a].sort(bySpecificityDesc); // → [a, b] — higher specificity first
351
356
  * ```
352
357
  */
@@ -389,10 +394,12 @@ const findBestMatch = (table, method, tokens) => {
389
394
  *
390
395
  * Called by `onInit` — the one-time per-isolate setup. Sorts `state.table` by
391
396
  * specificity (descending), validates that no endpoint path contains duplicate
392
- * `{param}` names, and sets `state.compiled = true` to guard re-entry.
397
+ * `{param}` names or the retired `{name?}` optional syntax, and sets
398
+ * `state.compiled = true` to guard re-entry.
393
399
  *
394
400
  * @param state - The mutable server state whose `table` should be compiled.
395
- * @throws {Error} With `[moku-worker]` prefix when a path has duplicate param names.
401
+ * @throws {Error} With `[moku-worker]` prefix when a path has duplicate param
402
+ * names, or uses the old `{name?}` optional syntax (now `{name:?}`).
396
403
  * @example
397
404
  * ```typescript
398
405
  * // Called inside serverPlugin.onInit:
@@ -406,6 +413,10 @@ const compileServerState = (state) => {
406
413
  const seen = /* @__PURE__ */ new Set();
407
414
  for (const segment of compiled.segments) {
408
415
  if (!segment.param) continue;
416
+ if (segment.value.endsWith("?")) {
417
+ const name = segment.value.slice(0, -1);
418
+ throw new Error(`[moku-worker] endpoint path "${compiled.endpoint.path}" uses the old optional-param syntax "{${segment.value}}".\n Optional params now use the colon form (matching @moku-labs/web): write "{${name}:?}" instead of "{${name}?}".`);
419
+ }
409
420
  if (seen.has(segment.value)) throw new Error(`[moku-worker] endpoint path "${compiled.endpoint.path}" has duplicate param "{${segment.value}}".\n Each {param} name in a path must be unique.`);
410
421
  seen.add(segment.value);
411
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/worker",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Cloudflare Worker framework for Moku — Durable Objects, Queues, R2, D1, and KV plugins that compose with Moku Web.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,36 +0,0 @@
1
- //#region src/config.d.ts
2
- /** Per-request Cloudflare bindings object (env). Framework-level shared type. */
3
- type WorkerEnv = Record<string, unknown>;
4
- /** Global framework config — flat, with complete defaults. */
5
- type WorkerConfig = {
6
- stage: "production" | "development" | "test";
7
- name: string;
8
- compatibilityDate: string;
9
- };
10
- /** Global framework events — declared once, visible to every plugin. */
11
- type WorkerEvents = {
12
- "request:start": {
13
- method: string;
14
- path: string;
15
- requestId: string;
16
- };
17
- "request:end": {
18
- method: string;
19
- path: string;
20
- status: number;
21
- ms: number;
22
- };
23
- "deploy:phase": {
24
- phase: string;
25
- detail?: string;
26
- };
27
- "deploy:complete": {
28
- url: string;
29
- };
30
- "provision:resource": {
31
- kind: "kv" | "r2" | "d1" | "queue" | "do";
32
- name: string;
33
- };
34
- };
35
- //#endregion
36
- export { WorkerEnv as n, WorkerEvents as r, WorkerConfig as t };
@@ -1,36 +0,0 @@
1
- //#region src/config.d.ts
2
- /** Per-request Cloudflare bindings object (env). Framework-level shared type. */
3
- type WorkerEnv = Record<string, unknown>;
4
- /** Global framework config — flat, with complete defaults. */
5
- type WorkerConfig = {
6
- stage: "production" | "development" | "test";
7
- name: string;
8
- compatibilityDate: string;
9
- };
10
- /** Global framework events — declared once, visible to every plugin. */
11
- type WorkerEvents = {
12
- "request:start": {
13
- method: string;
14
- path: string;
15
- requestId: string;
16
- };
17
- "request:end": {
18
- method: string;
19
- path: string;
20
- status: number;
21
- ms: number;
22
- };
23
- "deploy:phase": {
24
- phase: string;
25
- detail?: string;
26
- };
27
- "deploy:complete": {
28
- url: string;
29
- };
30
- "provision:resource": {
31
- kind: "kv" | "r2" | "d1" | "queue" | "do";
32
- name: string;
33
- };
34
- };
35
- //#endregion
36
- export { WorkerEnv as n, WorkerEvents as r, WorkerConfig as t };