@mokup/runtime 1.0.2 → 1.0.3

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/dist/index.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { Context, Hono } from '@mokup/shared/hono';
2
2
  export { Context, MiddlewareHandler, handle } from '@mokup/shared/hono';
3
3
 
4
+ /**
5
+ * Tokenized route segment for matching.
6
+ *
7
+ * @example
8
+ * import type { RouteToken } from '@mokup/runtime'
9
+ *
10
+ * const token: RouteToken = { type: 'param', name: 'id' }
11
+ */
4
12
  type RouteToken = {
5
13
  type: 'static';
6
14
  value: string;
@@ -14,6 +22,20 @@ type RouteToken = {
14
22
  type: 'optional-catchall';
15
23
  name: string;
16
24
  };
25
+ /**
26
+ * Parsed route template with tokens and score.
27
+ *
28
+ * @example
29
+ * import type { ParsedRouteTemplate } from '@mokup/runtime'
30
+ *
31
+ * const parsed: ParsedRouteTemplate = {
32
+ * template: '/users/[id]',
33
+ * tokens: [{ type: 'param', name: 'id' }],
34
+ * score: [3],
35
+ * errors: [],
36
+ * warnings: [],
37
+ * }
38
+ */
17
39
  interface ParsedRouteTemplate {
18
40
  template: string;
19
41
  tokens: RouteToken[];
@@ -21,36 +43,196 @@ interface ParsedRouteTemplate {
21
43
  errors: string[];
22
44
  warnings: string[];
23
45
  }
46
+ /**
47
+ * Compute a score array for route tokens.
48
+ *
49
+ * @param tokens - Parsed route tokens.
50
+ * @returns A numeric score array used for sorting.
51
+ *
52
+ * @example
53
+ * import { scoreRouteTokens } from '@mokup/runtime'
54
+ *
55
+ * const score = scoreRouteTokens([{ type: 'static', value: 'users' }])
56
+ */
24
57
  declare function scoreRouteTokens(tokens: RouteToken[]): (4 | 3 | 2 | 1 | 0)[];
58
+ /**
59
+ * Compare two route scores (higher wins).
60
+ *
61
+ * @param a - Score array for route A.
62
+ * @param b - Score array for route B.
63
+ * @returns Negative when A is higher priority.
64
+ *
65
+ * @example
66
+ * import { compareRouteScore } from '@mokup/runtime'
67
+ *
68
+ * const order = compareRouteScore([4], [3])
69
+ */
25
70
  declare function compareRouteScore(a: number[], b: number[]): number;
71
+ /**
72
+ * Normalize a URL path by stripping query/hash and trailing slash.
73
+ *
74
+ * @param value - Raw path or URL.
75
+ * @returns Normalized pathname.
76
+ *
77
+ * @example
78
+ * import { normalizePathname } from '@mokup/runtime'
79
+ *
80
+ * const pathname = normalizePathname('/users/?q=1')
81
+ */
26
82
  declare function normalizePathname(value: string): string;
83
+ /**
84
+ * Parse a route template into tokens, score, and diagnostics.
85
+ *
86
+ * @param template - Route template string.
87
+ * @returns Parsed template details.
88
+ *
89
+ * @example
90
+ * import { parseRouteTemplate } from '@mokup/runtime'
91
+ *
92
+ * const parsed = parseRouteTemplate('/users/[id]')
93
+ */
27
94
  declare function parseRouteTemplate(template: string): ParsedRouteTemplate;
95
+ /**
96
+ * Match route tokens against a pathname and extract params.
97
+ *
98
+ * @param tokens - Tokens for the template.
99
+ * @param pathname - Request pathname.
100
+ * @returns Params object or null if no match.
101
+ *
102
+ * @example
103
+ * import { matchRouteTokens } from '@mokup/runtime'
104
+ *
105
+ * const match = matchRouteTokens(
106
+ * [{ type: 'param', name: 'id' }],
107
+ * '/123',
108
+ * )
109
+ */
28
110
  declare function matchRouteTokens(tokens: RouteToken[], pathname: string): {
29
111
  params: Record<string, string | string[]>;
30
112
  } | null;
31
113
 
114
+ /**
115
+ * Supported HTTP methods for mock routes.
116
+ *
117
+ * @example
118
+ * import type { HttpMethod } from '@mokup/runtime'
119
+ *
120
+ * const method: HttpMethod = 'GET'
121
+ */
32
122
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
123
+ /**
124
+ * Serialized manifest describing all mock routes.
125
+ *
126
+ * @example
127
+ * import type { Manifest } from '@mokup/runtime'
128
+ *
129
+ * const manifest: Manifest = {
130
+ * version: 1,
131
+ * routes: [],
132
+ * }
133
+ */
33
134
  interface Manifest {
135
+ /**
136
+ * Manifest schema version.
137
+ *
138
+ * @default 1
139
+ */
34
140
  version: 1;
141
+ /**
142
+ * Route entries in this manifest.
143
+ *
144
+ * @default []
145
+ */
35
146
  routes: ManifestRoute[];
36
147
  }
148
+ /**
149
+ * One route entry inside the manifest.
150
+ *
151
+ * @example
152
+ * import type { ManifestRoute } from '@mokup/runtime'
153
+ *
154
+ * const route: ManifestRoute = {
155
+ * method: 'GET',
156
+ * url: '/api/ping',
157
+ * response: { type: 'json', body: { ok: true } },
158
+ * }
159
+ */
37
160
  interface ManifestRoute {
161
+ /** HTTP method of the route. */
38
162
  method: HttpMethod;
163
+ /** URL template for the route. */
39
164
  url: string;
165
+ /** Pre-parsed route tokens (optional optimization). */
40
166
  tokens?: RouteToken[];
167
+ /** Route score used for sorting and matching. */
41
168
  score?: number[];
169
+ /** Source file path for the route handler. */
42
170
  source?: string;
171
+ /**
172
+ * Override response status code.
173
+ *
174
+ * @default 200
175
+ */
43
176
  status?: number;
177
+ /**
178
+ * Additional headers to apply to the response.
179
+ *
180
+ * @default {}
181
+ */
44
182
  headers?: Record<string, string>;
183
+ /**
184
+ * Delay in milliseconds before responding.
185
+ *
186
+ * @default 0
187
+ */
45
188
  delay?: number;
189
+ /**
190
+ * Middleware module references to apply before the handler.
191
+ *
192
+ * @default []
193
+ */
46
194
  middleware?: ManifestModuleRef[];
195
+ /** Response definition for this route. */
47
196
  response: ManifestResponse;
48
197
  }
198
+ /**
199
+ * Reference to a module export produced by mokup build.
200
+ *
201
+ * @example
202
+ * import type { ManifestModuleRef } from '@mokup/runtime'
203
+ *
204
+ * const ref: ManifestModuleRef = {
205
+ * module: './mokup-handlers/mock/ping.get.mjs',
206
+ * exportName: 'default',
207
+ * }
208
+ */
49
209
  interface ManifestModuleRef {
210
+ /** Module path (absolute or relative to moduleBase). */
50
211
  module: string;
212
+ /**
213
+ * Named export to load from the module.
214
+ *
215
+ * @default "default"
216
+ */
51
217
  exportName?: string;
218
+ /**
219
+ * Optional rule index when multiple rules exist in one module.
220
+ *
221
+ * @default 0
222
+ */
52
223
  ruleIndex?: number;
53
224
  }
225
+ /**
226
+ * Response descriptor stored in the manifest.
227
+ *
228
+ * @example
229
+ * import type { ManifestResponse } from '@mokup/runtime'
230
+ *
231
+ * const response: ManifestResponse = {
232
+ * type: 'text',
233
+ * body: 'ok',
234
+ * }
235
+ */
54
236
  type ManifestResponse = {
55
237
  type: 'json';
56
238
  body: unknown;
@@ -64,33 +246,170 @@ type ManifestResponse = {
64
246
  } | ({
65
247
  type: 'module';
66
248
  } & ManifestModuleRef);
249
+ /**
250
+ * Normalized request input for runtime execution.
251
+ *
252
+ * @example
253
+ * import type { RuntimeRequest } from '@mokup/runtime'
254
+ *
255
+ * const request: RuntimeRequest = {
256
+ * method: 'GET',
257
+ * path: '/api/ping',
258
+ * query: {},
259
+ * headers: {},
260
+ * body: null,
261
+ * }
262
+ */
67
263
  interface RuntimeRequest {
264
+ /** HTTP method. */
68
265
  method: string;
266
+ /** Request path (no origin). */
69
267
  path: string;
268
+ /** Parsed query parameters. */
70
269
  query: Record<string, string | string[]>;
270
+ /** Normalized request headers. */
71
271
  headers: Record<string, string>;
272
+ /** Parsed request body. */
72
273
  body: unknown;
274
+ /** Raw body text (if available). */
73
275
  rawBody?: string;
276
+ /** Path params captured during matching. */
74
277
  params?: Record<string, string | string[]>;
75
278
  }
279
+ /**
280
+ * Normalized runtime response output.
281
+ *
282
+ * @example
283
+ * import type { RuntimeResult } from '@mokup/runtime'
284
+ *
285
+ * const result: RuntimeResult = {
286
+ * status: 200,
287
+ * headers: { 'content-type': 'application/json' },
288
+ * body: '{"ok":true}',
289
+ * }
290
+ */
76
291
  interface RuntimeResult {
292
+ /**
293
+ * HTTP status code.
294
+ *
295
+ * @default 200
296
+ */
77
297
  status: number;
298
+ /**
299
+ * Response headers.
300
+ *
301
+ * @default {}
302
+ */
78
303
  headers: Record<string, string>;
304
+ /** Response body as text or binary. */
79
305
  body: string | Uint8Array | null;
80
306
  }
307
+ /**
308
+ * Static response values supported by route handlers.
309
+ *
310
+ * @example
311
+ * import type { RouteStaticResponse } from '@mokup/runtime'
312
+ *
313
+ * const value: RouteStaticResponse = { ok: true }
314
+ */
81
315
  type RouteStaticResponse = string | number | boolean | bigint | symbol | null | undefined | object;
316
+ /**
317
+ * Allowed return values from a route handler.
318
+ *
319
+ * @example
320
+ * import type { RouteHandlerResult } from '@mokup/runtime'
321
+ *
322
+ * const result: RouteHandlerResult = 'ok'
323
+ */
82
324
  type RouteHandlerResult = RouteStaticResponse | Response;
325
+ /**
326
+ * Function signature for request handlers.
327
+ *
328
+ * @example
329
+ * import type { RequestHandler } from '@mokup/runtime'
330
+ *
331
+ * const handler: RequestHandler = (c) => {
332
+ * return { ok: true }
333
+ * }
334
+ */
83
335
  type RequestHandler = (context: Context) => RouteHandlerResult | Promise<RouteHandlerResult>;
336
+ /**
337
+ * Route response as a static value or handler function.
338
+ *
339
+ * @example
340
+ * import type { RouteResponse } from '@mokup/runtime'
341
+ *
342
+ * const response: RouteResponse = (c) => c.text('ok')
343
+ */
84
344
  type RouteResponse = RouteStaticResponse | RequestHandler;
85
345
 
346
+ /**
347
+ * Runtime configuration options.
348
+ *
349
+ * @example
350
+ * import type { RuntimeOptions } from '@mokup/runtime'
351
+ *
352
+ * const options: RuntimeOptions = {
353
+ * manifest: { version: 1, routes: [] },
354
+ * }
355
+ */
86
356
  interface RuntimeOptions {
357
+ /**
358
+ * Manifest object or async loader.
359
+ */
87
360
  manifest: Manifest | (() => Promise<Manifest>);
361
+ /**
362
+ * Base directory for resolving module paths.
363
+ *
364
+ * @default undefined
365
+ */
88
366
  moduleBase?: string | URL;
367
+ /**
368
+ * Map of module exports for in-memory execution.
369
+ *
370
+ * @default undefined
371
+ */
89
372
  moduleMap?: ModuleMap;
90
373
  }
374
+ /**
375
+ * Map of module path to exported members.
376
+ *
377
+ * @example
378
+ * import type { ModuleMap } from '@mokup/runtime'
379
+ *
380
+ * const moduleMap: ModuleMap = {
381
+ * './handlers/ping.mjs': { default: () => ({ ok: true }) },
382
+ * }
383
+ */
91
384
  type ModuleMap = Record<string, Record<string, unknown>>;
92
385
 
386
+ /**
387
+ * Build a Hono app from a manifest, loading module handlers as needed.
388
+ *
389
+ * @param options - Runtime options including the manifest.
390
+ * @returns A Hono app with routes registered.
391
+ *
392
+ * @example
393
+ * import { createRuntimeApp } from '@mokup/runtime'
394
+ *
395
+ * const app = await createRuntimeApp({
396
+ * manifest: { version: 1, routes: [] },
397
+ * })
398
+ */
93
399
  declare function createRuntimeApp(options: RuntimeOptions): Promise<Hono>;
400
+ /**
401
+ * Create a cached runtime handler for fetching and request simulation.
402
+ *
403
+ * @param options - Runtime options including the manifest.
404
+ * @returns Runtime helper with fetch and match helpers.
405
+ *
406
+ * @example
407
+ * import { createRuntime } from '@mokup/runtime'
408
+ *
409
+ * const runtime = createRuntime({
410
+ * manifest: { version: 1, routes: [] },
411
+ * })
412
+ */
94
413
  declare function createRuntime(options: RuntimeOptions): {
95
414
  handle: (req: RuntimeRequest) => Promise<RuntimeResult | null>;
96
415
  };
package/dist/index.mjs CHANGED
@@ -190,6 +190,29 @@ function matchRouteTokens(tokens, pathname) {
190
190
  return { params };
191
191
  }
192
192
 
193
+ const methodSet = /* @__PURE__ */ new Set([
194
+ "GET",
195
+ "POST",
196
+ "PUT",
197
+ "PATCH",
198
+ "DELETE",
199
+ "OPTIONS",
200
+ "HEAD"
201
+ ]);
202
+ function normalizeMethod(method) {
203
+ if (!method) {
204
+ return void 0;
205
+ }
206
+ const normalized = method.toUpperCase();
207
+ if (methodSet.has(normalized)) {
208
+ return normalized;
209
+ }
210
+ return void 0;
211
+ }
212
+ function delay(ms) {
213
+ return new Promise((resolve) => setTimeout(resolve, ms));
214
+ }
215
+
193
216
  function resolveModuleUrl(modulePath, moduleBase) {
194
217
  if (/^(?:data|http|https|file):/.test(modulePath)) {
195
218
  return modulePath;
@@ -325,29 +348,6 @@ async function loadModuleMiddleware(middleware, middlewareCache, moduleBase, mod
325
348
  return handlers[0];
326
349
  }
327
350
 
328
- const methodSet = /* @__PURE__ */ new Set([
329
- "GET",
330
- "POST",
331
- "PUT",
332
- "PATCH",
333
- "DELETE",
334
- "OPTIONS",
335
- "HEAD"
336
- ]);
337
- function normalizeMethod(method) {
338
- if (!method) {
339
- return void 0;
340
- }
341
- const normalized = method.toUpperCase();
342
- if (methodSet.has(normalized)) {
343
- return normalized;
344
- }
345
- return void 0;
346
- }
347
- function delay(ms) {
348
- return new Promise((resolve) => setTimeout(resolve, ms));
349
- }
350
-
351
351
  function decodeBase64(value) {
352
352
  if (typeof atob === "function") {
353
353
  const binary = atob(value);
@@ -360,50 +360,6 @@ function decodeBase64(value) {
360
360
  throw new Error("Base64 decoding is not supported in this runtime.");
361
361
  }
362
362
 
363
- function toHonoPath(tokens) {
364
- if (!tokens || tokens.length === 0) {
365
- return "/";
366
- }
367
- const segments = tokens.map((token) => {
368
- if (token.type === "static") {
369
- return token.value;
370
- }
371
- if (token.type === "param") {
372
- return `:${token.name}`;
373
- }
374
- if (token.type === "catchall") {
375
- return `:${token.name}{.+}`;
376
- }
377
- return `:${token.name}{.+}?`;
378
- });
379
- return `/${segments.join("/")}`;
380
- }
381
- function compileRoutes(manifest) {
382
- const compiled = [];
383
- for (const route of manifest.routes) {
384
- const method = normalizeMethod(route.method) ?? "GET";
385
- const parsed = route.tokens ? {
386
- tokens: route.tokens,
387
- score: route.score ?? scoreRouteTokens(route.tokens),
388
- errors: []
389
- } : parseRouteTemplate(route.url);
390
- if (parsed.errors.length > 0) {
391
- continue;
392
- }
393
- compiled.push({
394
- route,
395
- method,
396
- tokens: route.tokens ?? parsed.tokens,
397
- score: route.score ?? parsed.score
398
- });
399
- }
400
- return compiled.sort((a, b) => {
401
- if (a.method !== b.method) {
402
- return a.method.localeCompare(b.method);
403
- }
404
- return compareRouteScore(a.score, b.score);
405
- });
406
- }
407
363
  function shouldTreatAsText(contentType) {
408
364
  const normalized = contentType.toLowerCase();
409
365
  if (!normalized) {
@@ -485,6 +441,52 @@ function resolveResponse(value, fallback) {
485
441
  }
486
442
  return fallback;
487
443
  }
444
+
445
+ function toHonoPath(tokens) {
446
+ if (!tokens || tokens.length === 0) {
447
+ return "/";
448
+ }
449
+ const segments = tokens.map((token) => {
450
+ if (token.type === "static") {
451
+ return token.value;
452
+ }
453
+ if (token.type === "param") {
454
+ return `:${token.name}`;
455
+ }
456
+ if (token.type === "catchall") {
457
+ return `:${token.name}{.+}`;
458
+ }
459
+ return `:${token.name}{.+}?`;
460
+ });
461
+ return `/${segments.join("/")}`;
462
+ }
463
+ function compileRoutes(manifest) {
464
+ const compiled = [];
465
+ for (const route of manifest.routes) {
466
+ const method = normalizeMethod(route.method) ?? "GET";
467
+ const parsed = route.tokens ? {
468
+ tokens: route.tokens,
469
+ score: route.score ?? scoreRouteTokens(route.tokens),
470
+ errors: []
471
+ } : parseRouteTemplate(route.url);
472
+ if (parsed.errors.length > 0) {
473
+ continue;
474
+ }
475
+ compiled.push({
476
+ route,
477
+ method,
478
+ tokens: route.tokens ?? parsed.tokens,
479
+ score: route.score ?? parsed.score
480
+ });
481
+ }
482
+ return compiled.sort((a, b) => {
483
+ if (a.method !== b.method) {
484
+ return a.method.localeCompare(b.method);
485
+ }
486
+ return compareRouteScore(a.score, b.score);
487
+ });
488
+ }
489
+
488
490
  function normalizeHandlerValue(c, value) {
489
491
  if (value instanceof Response) {
490
492
  return value;
@@ -551,7 +553,7 @@ function createFinalizeMiddleware(route) {
551
553
  };
552
554
  }
553
555
  async function buildApp(params) {
554
- const { manifest, moduleCache, middlewareCache, moduleBase, moduleMap } = params;
556
+ const manifest = typeof params.manifest === "function" ? await params.manifest() : params.manifest;
555
557
  const app = new Hono({ router: new PatternRouter(), strict: false });
556
558
  const compiled = compileRoutes(manifest);
557
559
  for (const entry of compiled) {
@@ -559,9 +561,9 @@ async function buildApp(params) {
559
561
  for (const middleware of entry.route.middleware ?? []) {
560
562
  const handler2 = await loadModuleMiddleware(
561
563
  middleware,
562
- middlewareCache,
563
- moduleBase,
564
- moduleMap
564
+ params.middlewareCache,
565
+ params.moduleBase,
566
+ params.moduleMap
565
567
  );
566
568
  if (handler2) {
567
569
  middlewares.push(handler2);
@@ -569,9 +571,9 @@ async function buildApp(params) {
569
571
  }
570
572
  const handler = createRouteHandler({
571
573
  route: entry.route,
572
- moduleCache,
573
- ...typeof moduleBase !== "undefined" ? { moduleBase } : {},
574
- ...typeof moduleMap !== "undefined" ? { moduleMap } : {}
574
+ moduleCache: params.moduleCache,
575
+ ...typeof params.moduleBase !== "undefined" ? { moduleBase: params.moduleBase } : {},
576
+ ...typeof params.moduleMap !== "undefined" ? { moduleMap: params.moduleMap } : {}
575
577
  });
576
578
  app.on(
577
579
  entry.method,
@@ -583,18 +585,7 @@ async function buildApp(params) {
583
585
  }
584
586
  return app;
585
587
  }
586
- async function createRuntimeApp(options) {
587
- const manifest = typeof options.manifest === "function" ? await options.manifest() : options.manifest;
588
- const moduleCache = /* @__PURE__ */ new Map();
589
- const middlewareCache = /* @__PURE__ */ new Map();
590
- return await buildApp({
591
- manifest,
592
- moduleCache,
593
- middlewareCache,
594
- ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
595
- ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
596
- });
597
- }
588
+
598
589
  function appendQueryParams(url, query) {
599
590
  for (const [key, value] of Object.entries(query)) {
600
591
  if (Array.isArray(value)) {
@@ -670,6 +661,18 @@ function routeNeedsModuleBase(route, moduleMap) {
670
661
  }
671
662
  return false;
672
663
  }
664
+
665
+ async function createRuntimeApp(options) {
666
+ const moduleCache = /* @__PURE__ */ new Map();
667
+ const middlewareCache = /* @__PURE__ */ new Map();
668
+ return await buildApp({
669
+ manifest: options.manifest,
670
+ moduleCache,
671
+ middlewareCache,
672
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
673
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
674
+ });
675
+ }
673
676
  function createRuntime(options) {
674
677
  let manifestCache = null;
675
678
  let appPromise = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/runtime",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "description": "Cross-runtime mock matching and response handling for mokup.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -27,7 +27,7 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@mokup/shared": "1.0.1"
30
+ "@mokup/shared": "1.0.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "typescript": "^5.9.3",