@mokup/runtime 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @mokup/runtime
2
+
3
+ English | [Chinese](./README.zh-CN.md)
4
+
5
+ ## Overview
6
+
7
+ The Mokup runtime evaluates manifests and resolves mock responses at request time. It is shared by server adapters and Workers. For detailed usage, see https://mokup.icebreaker.top.
@@ -0,0 +1,7 @@
1
+ # @mokup/runtime
2
+
3
+ [English](./README.md) | 中文
4
+
5
+ ## 概览
6
+
7
+ Mokup runtime 用于在请求时读取 manifest 并返回 mock 响应,是 Server 适配器与 Worker 的共享核心。完整文档见 https://mokup.icebreaker.top。
package/dist/index.cjs CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const hono = require('hono');
4
+ const patternRouter = require('hono/router/pattern-router');
5
+
3
6
  const paramNamePattern = /^[\w-]+$/;
4
7
  const paramPattern = /^\[([^\]/]+)\]$/;
5
8
  const catchallPattern = /^\[\.\.\.([^\]/]+)\]$/;
@@ -227,14 +230,14 @@ function normalizeRules(value) {
227
230
  }
228
231
  ];
229
232
  }
230
- async function executeRule(rule, req, responder, ctx) {
233
+ async function executeRule(rule, context) {
231
234
  if (!rule) {
232
235
  return void 0;
233
236
  }
234
237
  const value = rule.response;
235
238
  if (typeof value === "function") {
236
239
  const handler = value;
237
- return handler(req, responder, ctx);
240
+ return handler(context);
238
241
  }
239
242
  return value;
240
243
  }
@@ -340,108 +343,153 @@ function normalizeMethod(method) {
340
343
  }
341
344
  return void 0;
342
345
  }
343
- function mergeHeaders(base, override) {
344
- if (!override) {
345
- return base;
346
- }
347
- return {
348
- ...base,
349
- ...override
350
- };
351
- }
352
346
  function delay(ms) {
353
347
  return new Promise((resolve) => setTimeout(resolve, ms));
354
348
  }
355
349
 
356
- class ResponseController {
357
- statusCode = 200;
358
- headers = /* @__PURE__ */ new Map();
359
- setHeader(key, value) {
360
- this.headers.set(key.toLowerCase(), value);
361
- }
362
- getHeader(key) {
363
- return this.headers.get(key.toLowerCase());
364
- }
365
- removeHeader(key) {
366
- this.headers.delete(key.toLowerCase());
367
- }
368
- toRecord() {
369
- const record = {};
370
- for (const [key, value] of this.headers.entries()) {
371
- record[key] = value;
350
+ function decodeBase64(value) {
351
+ if (typeof atob === "function") {
352
+ const binary = atob(value);
353
+ const bytes = new Uint8Array(binary.length);
354
+ for (let i = 0; i < binary.length; i += 1) {
355
+ bytes[i] = binary.charCodeAt(i);
372
356
  }
373
- return record;
357
+ return bytes;
374
358
  }
359
+ throw new Error("Base64 decoding is not supported in this runtime.");
375
360
  }
376
- function normalizeBody(body, contentType) {
377
- if (typeof body === "undefined") {
378
- return { body: null };
379
- }
380
- if (typeof body === "string") {
381
- return {
382
- body,
383
- contentType: contentType ?? "text/plain; charset=utf-8"
384
- };
361
+
362
+ function toHonoPath(tokens) {
363
+ if (!tokens || tokens.length === 0) {
364
+ return "/";
385
365
  }
386
- if (body instanceof Uint8Array) {
366
+ const segments = tokens.map((token) => {
367
+ if (token.type === "static") {
368
+ return token.value;
369
+ }
370
+ if (token.type === "param") {
371
+ return `:${token.name}`;
372
+ }
373
+ if (token.type === "catchall") {
374
+ return `:${token.name}{.+}`;
375
+ }
376
+ return `:${token.name}{.+}?`;
377
+ });
378
+ return `/${segments.join("/")}`;
379
+ }
380
+ function compileRoutes(manifest) {
381
+ const compiled = [];
382
+ for (const route of manifest.routes) {
383
+ const method = normalizeMethod(route.method) ?? "GET";
384
+ const parsed = route.tokens ? {
385
+ tokens: route.tokens,
386
+ score: route.score ?? scoreRouteTokens(route.tokens),
387
+ errors: []
388
+ } : parseRouteTemplate(route.url);
389
+ if (parsed.errors.length > 0) {
390
+ continue;
391
+ }
392
+ compiled.push({
393
+ route,
394
+ method,
395
+ tokens: route.tokens ?? parsed.tokens,
396
+ score: route.score ?? parsed.score
397
+ });
398
+ }
399
+ return compiled.sort((a, b) => {
400
+ if (a.method !== b.method) {
401
+ return a.method.localeCompare(b.method);
402
+ }
403
+ return compareRouteScore(a.score, b.score);
404
+ });
405
+ }
406
+ function shouldTreatAsText(contentType) {
407
+ const normalized = contentType.toLowerCase();
408
+ return normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("xml") || normalized.includes("javascript");
409
+ }
410
+ async function toRuntimeResult(response) {
411
+ const headers = {};
412
+ response.headers.forEach((value, key) => {
413
+ headers[key.toLowerCase()] = value;
414
+ });
415
+ if (!response.body || [204, 205, 304].includes(response.status)) {
387
416
  return {
388
- body,
389
- contentType: contentType ?? "application/octet-stream"
417
+ status: response.status,
418
+ headers,
419
+ body: null
390
420
  };
391
421
  }
392
- if (body instanceof ArrayBuffer) {
422
+ const contentType = headers["content-type"] ?? "";
423
+ if (shouldTreatAsText(contentType)) {
393
424
  return {
394
- body: new Uint8Array(body),
395
- contentType: contentType ?? "application/octet-stream"
425
+ status: response.status,
426
+ headers,
427
+ body: await response.text()
396
428
  };
397
429
  }
430
+ const buffer = new Uint8Array(await response.arrayBuffer());
398
431
  return {
399
- body: JSON.stringify(body),
400
- contentType: contentType ?? "application/json; charset=utf-8"
432
+ status: response.status,
433
+ headers,
434
+ body: buffer
401
435
  };
402
436
  }
403
- function decodeBase64(value) {
404
- if (typeof atob === "function") {
405
- const binary = atob(value);
406
- const bytes = new Uint8Array(binary.length);
407
- for (let i = 0; i < binary.length; i += 1) {
408
- bytes[i] = binary.charCodeAt(i);
437
+ function applyRouteOverrides(response, route) {
438
+ const headers = new Headers(response.headers);
439
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
440
+ if (route.headers) {
441
+ for (const [key, value] of Object.entries(route.headers)) {
442
+ headers.set(key, value);
409
443
  }
410
- return bytes;
411
444
  }
412
- throw new Error("Base64 decoding is not supported in this runtime.");
445
+ const status = route.status ?? response.status;
446
+ if (status === response.status && !hasHeaders) {
447
+ return response;
448
+ }
449
+ return new Response(response.body, { status, headers });
413
450
  }
414
- function finalizeStatus(status, body) {
415
- if (status === 200 && body === null) {
416
- return 204;
451
+ function normalizeHandlerValue(c, value) {
452
+ if (value instanceof Response) {
453
+ return value;
454
+ }
455
+ if (typeof value === "undefined") {
456
+ const response = c.body(null);
457
+ if (response.status === 200) {
458
+ return new Response(response.body, {
459
+ status: 204,
460
+ headers: response.headers
461
+ });
462
+ }
463
+ return response;
464
+ }
465
+ if (typeof value === "string") {
466
+ return c.text(value);
417
467
  }
418
- return status;
468
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
469
+ if (!c.res.headers.get("content-type")) {
470
+ c.header("content-type", "application/octet-stream");
471
+ }
472
+ const data = value instanceof ArrayBuffer ? value : new Uint8Array(value);
473
+ return c.body(data);
474
+ }
475
+ return c.json(value);
419
476
  }
420
-
421
- async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase, moduleMap) {
422
- const responder = new ResponseController();
423
- const ctx = {
424
- delay,
425
- json: (data) => data
426
- };
427
- const runHandler = async () => {
477
+ function createRouteHandler(params) {
478
+ const { route, moduleCache, moduleBase, moduleMap } = params;
479
+ return async (c) => {
428
480
  if (route.response.type === "json") {
429
- return {
430
- value: route.response.body,
431
- contentType: "application/json; charset=utf-8"
432
- };
481
+ if (typeof route.response.body === "undefined") {
482
+ return normalizeHandlerValue(c, void 0);
483
+ }
484
+ return c.json(route.response.body);
433
485
  }
434
486
  if (route.response.type === "text") {
435
- return {
436
- value: route.response.body,
437
- contentType: "text/plain; charset=utf-8"
438
- };
487
+ return c.text(route.response.body);
439
488
  }
440
489
  if (route.response.type === "binary") {
441
- return {
442
- value: decodeBase64(route.response.body),
443
- contentType: "application/octet-stream"
444
- };
490
+ const data = new Uint8Array(decodeBase64(route.response.body));
491
+ c.header("content-type", "application/octet-stream");
492
+ return c.body(data);
445
493
  }
446
494
  const rule = await loadModuleRule(
447
495
  route.response,
@@ -449,75 +497,106 @@ async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase
449
497
  moduleBase,
450
498
  moduleMap
451
499
  );
452
- const value = await executeRule(rule, req, responder, ctx);
453
- return { value };
500
+ const value = await executeRule(rule, c);
501
+ return normalizeHandlerValue(c, value);
454
502
  };
455
- const runMiddlewares = async (middlewares) => {
456
- let lastIndex = -1;
457
- const dispatch = async (index) => {
458
- if (index <= lastIndex) {
459
- throw new Error("Middleware next() called multiple times.");
460
- }
461
- lastIndex = index;
462
- const handler = middlewares[index];
463
- if (!handler) {
464
- return runHandler();
465
- }
466
- let nextResult;
467
- const next = async () => {
468
- nextResult = await dispatch(index + 1);
469
- return nextResult.value;
470
- };
471
- const value = await handler(req, responder, ctx, next);
472
- if (typeof value !== "undefined") {
473
- return { value };
474
- }
475
- if (nextResult) {
476
- return nextResult;
477
- }
478
- return { value: void 0 };
479
- };
480
- return dispatch(0);
503
+ }
504
+ function createFinalizeMiddleware(route) {
505
+ return async (c, next) => {
506
+ const response = await next();
507
+ const resolved = response ?? c.res;
508
+ if (route.delay && route.delay > 0) {
509
+ await delay(route.delay);
510
+ }
511
+ return applyRouteOverrides(resolved, route);
481
512
  };
482
- const middlewareHandlers = [];
483
- for (const entry of route.middleware ?? []) {
484
- const handler = await loadModuleMiddleware(
485
- entry,
486
- middlewareCache,
487
- moduleBase,
488
- moduleMap
513
+ }
514
+ async function buildApp(params) {
515
+ const { manifest, moduleCache, middlewareCache, moduleBase, moduleMap } = params;
516
+ const app = new hono.Hono({ router: new patternRouter.PatternRouter(), strict: false });
517
+ const compiled = compileRoutes(manifest);
518
+ for (const entry of compiled) {
519
+ const middlewares = [];
520
+ for (const middleware of entry.route.middleware ?? []) {
521
+ const handler2 = await loadModuleMiddleware(
522
+ middleware,
523
+ middlewareCache,
524
+ moduleBase,
525
+ moduleMap
526
+ );
527
+ if (handler2) {
528
+ middlewares.push(handler2);
529
+ }
530
+ }
531
+ const handler = createRouteHandler({
532
+ route: entry.route,
533
+ moduleCache,
534
+ ...typeof moduleBase !== "undefined" ? { moduleBase } : {},
535
+ ...typeof moduleMap !== "undefined" ? { moduleMap } : {}
536
+ });
537
+ app.on(
538
+ entry.method,
539
+ toHonoPath(entry.tokens),
540
+ createFinalizeMiddleware(entry.route),
541
+ ...middlewares,
542
+ handler
489
543
  );
490
- if (handler) {
491
- middlewareHandlers.push(handler);
544
+ }
545
+ return app;
546
+ }
547
+ function appendQueryParams(url, query) {
548
+ for (const [key, value] of Object.entries(query)) {
549
+ if (Array.isArray(value)) {
550
+ for (const entry of value) {
551
+ url.searchParams.append(key, entry);
552
+ }
553
+ } else {
554
+ url.searchParams.append(key, value);
492
555
  }
493
556
  }
494
- const result = middlewareHandlers.length > 0 ? await runMiddlewares(middlewareHandlers) : await runHandler();
495
- const responseValue = result.value;
496
- const contentType = result.contentType;
497
- if (route.delay && route.delay > 0) {
498
- await delay(route.delay);
557
+ }
558
+ function resolveRequestBody(req, contentType) {
559
+ if (typeof req.rawBody !== "undefined") {
560
+ return req.rawBody;
499
561
  }
500
- const headers = mergeHeaders(responder.toRecord(), route.headers);
501
- if (contentType && !headers["content-type"]) {
502
- headers["content-type"] = contentType;
562
+ const body = req.body;
563
+ if (typeof body === "undefined") {
564
+ return void 0;
503
565
  }
504
- const normalized = normalizeBody(responseValue, headers["content-type"]);
505
- const status = finalizeStatus(
506
- route.status ?? responder.statusCode,
507
- normalized.body
508
- );
509
- if (normalized.contentType && !headers["content-type"]) {
510
- headers["content-type"] = normalized.contentType;
566
+ if (typeof body === "string") {
567
+ return body;
511
568
  }
512
- return {
513
- status,
514
- headers,
515
- body: normalized.body
516
- };
569
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
570
+ return body;
571
+ }
572
+ if (typeof body === "object") {
573
+ const normalized = contentType.toLowerCase();
574
+ if (normalized.includes("json")) {
575
+ return JSON.stringify(body);
576
+ }
577
+ return JSON.stringify(body);
578
+ }
579
+ return String(body);
580
+ }
581
+ function toFetchRequest(req) {
582
+ const url = new URL(req.path, "http://mokup.local");
583
+ appendQueryParams(url, req.query);
584
+ const headers = new Headers();
585
+ for (const [key, value] of Object.entries(req.headers)) {
586
+ headers.set(key, value);
587
+ }
588
+ const method = normalizeMethod(req.method) ?? "GET";
589
+ const contentType = headers.get("content-type") ?? "";
590
+ const body = resolveRequestBody(req, contentType);
591
+ const init = { method, headers };
592
+ if (typeof body !== "undefined" && method !== "GET" && method !== "HEAD") {
593
+ init.body = body;
594
+ }
595
+ return new Request(url.toString(), init);
517
596
  }
518
597
  function createRuntime(options) {
519
598
  let manifestCache = null;
520
- let compiledRoutes = null;
599
+ let appPromise = null;
521
600
  const moduleCache = /* @__PURE__ */ new Map();
522
601
  const middlewareCache = /* @__PURE__ */ new Map();
523
602
  const getManifest = async () => {
@@ -526,56 +605,31 @@ function createRuntime(options) {
526
605
  }
527
606
  return manifestCache;
528
607
  };
529
- const getRouteList = async () => {
530
- if (compiledRoutes) {
531
- return compiledRoutes;
532
- }
533
- const manifest = await getManifest();
534
- const map = /* @__PURE__ */ new Map();
535
- for (const route of manifest.routes) {
536
- const method = normalizeMethod(route.method) ?? "GET";
537
- const parsed = route.tokens ? { tokens: route.tokens, score: route.score ?? scoreRouteTokens(route.tokens), errors: [] } : parseRouteTemplate(route.url);
538
- if (parsed.errors.length > 0) {
539
- continue;
540
- }
541
- const tokens = route.tokens ?? parsed.tokens;
542
- const score = route.score ?? parsed.score;
543
- const list = map.get(method) ?? [];
544
- list.push({ route, tokens, score });
545
- map.set(method, list);
546
- }
547
- for (const list of map.values()) {
548
- list.sort((a, b) => compareRouteScore(a.score, b.score));
608
+ const getApp = async () => {
609
+ if (!appPromise) {
610
+ appPromise = (async () => {
611
+ const manifest = await getManifest();
612
+ return buildApp({
613
+ manifest,
614
+ moduleCache,
615
+ middlewareCache,
616
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
617
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
618
+ });
619
+ })();
549
620
  }
550
- compiledRoutes = map;
551
- return compiledRoutes;
621
+ return appPromise;
552
622
  };
553
623
  const handle = async (req) => {
624
+ const app = await getApp();
554
625
  const method = normalizeMethod(req.method) ?? "GET";
555
- const map = await getRouteList();
556
- const list = map.get(method);
557
- if (!list || list.length === 0) {
626
+ const matchMethod = method === "HEAD" ? "GET" : method;
627
+ const match = app.router.match(matchMethod, req.path);
628
+ if (!match || match[0].length === 0) {
558
629
  return null;
559
630
  }
560
- for (const entry of list) {
561
- const matched = matchRouteTokens(entry.tokens, req.path);
562
- if (!matched) {
563
- continue;
564
- }
565
- const requestWithParams = {
566
- ...req,
567
- params: matched.params
568
- };
569
- return executeRoute(
570
- entry.route,
571
- requestWithParams,
572
- moduleCache,
573
- middlewareCache,
574
- options.moduleBase,
575
- options.moduleMap
576
- );
577
- }
578
- return null;
631
+ const response = await app.fetch(toFetchRequest(req));
632
+ return await toRuntimeResult(response);
579
633
  };
580
634
  return {
581
635
  handle
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Context, MiddlewareHandler } from 'hono';
2
+
1
3
  type RouteToken = {
2
4
  type: 'static';
3
5
  value: string;
@@ -75,18 +77,9 @@ interface RuntimeResult {
75
77
  headers: Record<string, string>;
76
78
  body: string | Uint8Array | null;
77
79
  }
78
- interface MockContext {
79
- delay: (ms: number) => Promise<void>;
80
- json: <T>(data: T) => T;
81
- }
82
- interface MockResponder {
83
- statusCode: number;
84
- setHeader: (key: string, value: string) => void;
85
- getHeader: (key: string) => string | undefined;
86
- removeHeader: (key: string) => void;
87
- }
88
- type MockMiddleware = (req: RuntimeRequest, res: MockResponder, ctx: MockContext, next: () => Promise<unknown>) => unknown | Promise<unknown>;
89
- type MockResponseHandler = (req: RuntimeRequest, res: MockResponder, ctx: MockContext) => unknown | Promise<unknown>;
80
+ type MockContext = Context;
81
+ type MockMiddleware = MiddlewareHandler;
82
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
83
  interface RuntimeOptions {
91
84
  manifest: Manifest | (() => Promise<Manifest>);
92
85
  moduleBase?: string | URL;
@@ -99,4 +92,4 @@ declare function createRuntime(options: RuntimeOptions): {
99
92
  };
100
93
 
101
94
  export { compareRouteScore, createRuntime, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
102
- export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponder, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
95
+ export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Context, MiddlewareHandler } from 'hono';
2
+
1
3
  type RouteToken = {
2
4
  type: 'static';
3
5
  value: string;
@@ -75,18 +77,9 @@ interface RuntimeResult {
75
77
  headers: Record<string, string>;
76
78
  body: string | Uint8Array | null;
77
79
  }
78
- interface MockContext {
79
- delay: (ms: number) => Promise<void>;
80
- json: <T>(data: T) => T;
81
- }
82
- interface MockResponder {
83
- statusCode: number;
84
- setHeader: (key: string, value: string) => void;
85
- getHeader: (key: string) => string | undefined;
86
- removeHeader: (key: string) => void;
87
- }
88
- type MockMiddleware = (req: RuntimeRequest, res: MockResponder, ctx: MockContext, next: () => Promise<unknown>) => unknown | Promise<unknown>;
89
- type MockResponseHandler = (req: RuntimeRequest, res: MockResponder, ctx: MockContext) => unknown | Promise<unknown>;
80
+ type MockContext = Context;
81
+ type MockMiddleware = MiddlewareHandler;
82
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
83
  interface RuntimeOptions {
91
84
  manifest: Manifest | (() => Promise<Manifest>);
92
85
  moduleBase?: string | URL;
@@ -99,4 +92,4 @@ declare function createRuntime(options: RuntimeOptions): {
99
92
  };
100
93
 
101
94
  export { compareRouteScore, createRuntime, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
102
- export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponder, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
95
+ export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Context, MiddlewareHandler } from 'hono';
2
+
1
3
  type RouteToken = {
2
4
  type: 'static';
3
5
  value: string;
@@ -75,18 +77,9 @@ interface RuntimeResult {
75
77
  headers: Record<string, string>;
76
78
  body: string | Uint8Array | null;
77
79
  }
78
- interface MockContext {
79
- delay: (ms: number) => Promise<void>;
80
- json: <T>(data: T) => T;
81
- }
82
- interface MockResponder {
83
- statusCode: number;
84
- setHeader: (key: string, value: string) => void;
85
- getHeader: (key: string) => string | undefined;
86
- removeHeader: (key: string) => void;
87
- }
88
- type MockMiddleware = (req: RuntimeRequest, res: MockResponder, ctx: MockContext, next: () => Promise<unknown>) => unknown | Promise<unknown>;
89
- type MockResponseHandler = (req: RuntimeRequest, res: MockResponder, ctx: MockContext) => unknown | Promise<unknown>;
80
+ type MockContext = Context;
81
+ type MockMiddleware = MiddlewareHandler;
82
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
83
  interface RuntimeOptions {
91
84
  manifest: Manifest | (() => Promise<Manifest>);
92
85
  moduleBase?: string | URL;
@@ -99,4 +92,4 @@ declare function createRuntime(options: RuntimeOptions): {
99
92
  };
100
93
 
101
94
  export { compareRouteScore, createRuntime, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
102
- export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponder, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
95
+ export type { HttpMethod, Manifest, ManifestModuleRef, ManifestResponse, ManifestRoute, MockContext, MockMiddleware, MockResponseHandler, ModuleMap, ParsedRouteTemplate, RouteToken, RuntimeOptions, RuntimeRequest, RuntimeResult };
package/dist/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import { PatternRouter } from 'hono/router/pattern-router';
3
+
1
4
  const paramNamePattern = /^[\w-]+$/;
2
5
  const paramPattern = /^\[([^\]/]+)\]$/;
3
6
  const catchallPattern = /^\[\.\.\.([^\]/]+)\]$/;
@@ -225,14 +228,14 @@ function normalizeRules(value) {
225
228
  }
226
229
  ];
227
230
  }
228
- async function executeRule(rule, req, responder, ctx) {
231
+ async function executeRule(rule, context) {
229
232
  if (!rule) {
230
233
  return void 0;
231
234
  }
232
235
  const value = rule.response;
233
236
  if (typeof value === "function") {
234
237
  const handler = value;
235
- return handler(req, responder, ctx);
238
+ return handler(context);
236
239
  }
237
240
  return value;
238
241
  }
@@ -338,108 +341,153 @@ function normalizeMethod(method) {
338
341
  }
339
342
  return void 0;
340
343
  }
341
- function mergeHeaders(base, override) {
342
- if (!override) {
343
- return base;
344
- }
345
- return {
346
- ...base,
347
- ...override
348
- };
349
- }
350
344
  function delay(ms) {
351
345
  return new Promise((resolve) => setTimeout(resolve, ms));
352
346
  }
353
347
 
354
- class ResponseController {
355
- statusCode = 200;
356
- headers = /* @__PURE__ */ new Map();
357
- setHeader(key, value) {
358
- this.headers.set(key.toLowerCase(), value);
359
- }
360
- getHeader(key) {
361
- return this.headers.get(key.toLowerCase());
362
- }
363
- removeHeader(key) {
364
- this.headers.delete(key.toLowerCase());
365
- }
366
- toRecord() {
367
- const record = {};
368
- for (const [key, value] of this.headers.entries()) {
369
- record[key] = value;
348
+ function decodeBase64(value) {
349
+ if (typeof atob === "function") {
350
+ const binary = atob(value);
351
+ const bytes = new Uint8Array(binary.length);
352
+ for (let i = 0; i < binary.length; i += 1) {
353
+ bytes[i] = binary.charCodeAt(i);
370
354
  }
371
- return record;
355
+ return bytes;
372
356
  }
357
+ throw new Error("Base64 decoding is not supported in this runtime.");
373
358
  }
374
- function normalizeBody(body, contentType) {
375
- if (typeof body === "undefined") {
376
- return { body: null };
377
- }
378
- if (typeof body === "string") {
379
- return {
380
- body,
381
- contentType: contentType ?? "text/plain; charset=utf-8"
382
- };
359
+
360
+ function toHonoPath(tokens) {
361
+ if (!tokens || tokens.length === 0) {
362
+ return "/";
383
363
  }
384
- if (body instanceof Uint8Array) {
364
+ const segments = tokens.map((token) => {
365
+ if (token.type === "static") {
366
+ return token.value;
367
+ }
368
+ if (token.type === "param") {
369
+ return `:${token.name}`;
370
+ }
371
+ if (token.type === "catchall") {
372
+ return `:${token.name}{.+}`;
373
+ }
374
+ return `:${token.name}{.+}?`;
375
+ });
376
+ return `/${segments.join("/")}`;
377
+ }
378
+ function compileRoutes(manifest) {
379
+ const compiled = [];
380
+ for (const route of manifest.routes) {
381
+ const method = normalizeMethod(route.method) ?? "GET";
382
+ const parsed = route.tokens ? {
383
+ tokens: route.tokens,
384
+ score: route.score ?? scoreRouteTokens(route.tokens),
385
+ errors: []
386
+ } : parseRouteTemplate(route.url);
387
+ if (parsed.errors.length > 0) {
388
+ continue;
389
+ }
390
+ compiled.push({
391
+ route,
392
+ method,
393
+ tokens: route.tokens ?? parsed.tokens,
394
+ score: route.score ?? parsed.score
395
+ });
396
+ }
397
+ return compiled.sort((a, b) => {
398
+ if (a.method !== b.method) {
399
+ return a.method.localeCompare(b.method);
400
+ }
401
+ return compareRouteScore(a.score, b.score);
402
+ });
403
+ }
404
+ function shouldTreatAsText(contentType) {
405
+ const normalized = contentType.toLowerCase();
406
+ return normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("xml") || normalized.includes("javascript");
407
+ }
408
+ async function toRuntimeResult(response) {
409
+ const headers = {};
410
+ response.headers.forEach((value, key) => {
411
+ headers[key.toLowerCase()] = value;
412
+ });
413
+ if (!response.body || [204, 205, 304].includes(response.status)) {
385
414
  return {
386
- body,
387
- contentType: contentType ?? "application/octet-stream"
415
+ status: response.status,
416
+ headers,
417
+ body: null
388
418
  };
389
419
  }
390
- if (body instanceof ArrayBuffer) {
420
+ const contentType = headers["content-type"] ?? "";
421
+ if (shouldTreatAsText(contentType)) {
391
422
  return {
392
- body: new Uint8Array(body),
393
- contentType: contentType ?? "application/octet-stream"
423
+ status: response.status,
424
+ headers,
425
+ body: await response.text()
394
426
  };
395
427
  }
428
+ const buffer = new Uint8Array(await response.arrayBuffer());
396
429
  return {
397
- body: JSON.stringify(body),
398
- contentType: contentType ?? "application/json; charset=utf-8"
430
+ status: response.status,
431
+ headers,
432
+ body: buffer
399
433
  };
400
434
  }
401
- function decodeBase64(value) {
402
- if (typeof atob === "function") {
403
- const binary = atob(value);
404
- const bytes = new Uint8Array(binary.length);
405
- for (let i = 0; i < binary.length; i += 1) {
406
- bytes[i] = binary.charCodeAt(i);
435
+ function applyRouteOverrides(response, route) {
436
+ const headers = new Headers(response.headers);
437
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
438
+ if (route.headers) {
439
+ for (const [key, value] of Object.entries(route.headers)) {
440
+ headers.set(key, value);
407
441
  }
408
- return bytes;
409
442
  }
410
- throw new Error("Base64 decoding is not supported in this runtime.");
443
+ const status = route.status ?? response.status;
444
+ if (status === response.status && !hasHeaders) {
445
+ return response;
446
+ }
447
+ return new Response(response.body, { status, headers });
411
448
  }
412
- function finalizeStatus(status, body) {
413
- if (status === 200 && body === null) {
414
- return 204;
449
+ function normalizeHandlerValue(c, value) {
450
+ if (value instanceof Response) {
451
+ return value;
452
+ }
453
+ if (typeof value === "undefined") {
454
+ const response = c.body(null);
455
+ if (response.status === 200) {
456
+ return new Response(response.body, {
457
+ status: 204,
458
+ headers: response.headers
459
+ });
460
+ }
461
+ return response;
462
+ }
463
+ if (typeof value === "string") {
464
+ return c.text(value);
415
465
  }
416
- return status;
466
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
467
+ if (!c.res.headers.get("content-type")) {
468
+ c.header("content-type", "application/octet-stream");
469
+ }
470
+ const data = value instanceof ArrayBuffer ? value : new Uint8Array(value);
471
+ return c.body(data);
472
+ }
473
+ return c.json(value);
417
474
  }
418
-
419
- async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase, moduleMap) {
420
- const responder = new ResponseController();
421
- const ctx = {
422
- delay,
423
- json: (data) => data
424
- };
425
- const runHandler = async () => {
475
+ function createRouteHandler(params) {
476
+ const { route, moduleCache, moduleBase, moduleMap } = params;
477
+ return async (c) => {
426
478
  if (route.response.type === "json") {
427
- return {
428
- value: route.response.body,
429
- contentType: "application/json; charset=utf-8"
430
- };
479
+ if (typeof route.response.body === "undefined") {
480
+ return normalizeHandlerValue(c, void 0);
481
+ }
482
+ return c.json(route.response.body);
431
483
  }
432
484
  if (route.response.type === "text") {
433
- return {
434
- value: route.response.body,
435
- contentType: "text/plain; charset=utf-8"
436
- };
485
+ return c.text(route.response.body);
437
486
  }
438
487
  if (route.response.type === "binary") {
439
- return {
440
- value: decodeBase64(route.response.body),
441
- contentType: "application/octet-stream"
442
- };
488
+ const data = new Uint8Array(decodeBase64(route.response.body));
489
+ c.header("content-type", "application/octet-stream");
490
+ return c.body(data);
443
491
  }
444
492
  const rule = await loadModuleRule(
445
493
  route.response,
@@ -447,75 +495,106 @@ async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase
447
495
  moduleBase,
448
496
  moduleMap
449
497
  );
450
- const value = await executeRule(rule, req, responder, ctx);
451
- return { value };
498
+ const value = await executeRule(rule, c);
499
+ return normalizeHandlerValue(c, value);
452
500
  };
453
- const runMiddlewares = async (middlewares) => {
454
- let lastIndex = -1;
455
- const dispatch = async (index) => {
456
- if (index <= lastIndex) {
457
- throw new Error("Middleware next() called multiple times.");
458
- }
459
- lastIndex = index;
460
- const handler = middlewares[index];
461
- if (!handler) {
462
- return runHandler();
463
- }
464
- let nextResult;
465
- const next = async () => {
466
- nextResult = await dispatch(index + 1);
467
- return nextResult.value;
468
- };
469
- const value = await handler(req, responder, ctx, next);
470
- if (typeof value !== "undefined") {
471
- return { value };
472
- }
473
- if (nextResult) {
474
- return nextResult;
475
- }
476
- return { value: void 0 };
477
- };
478
- return dispatch(0);
501
+ }
502
+ function createFinalizeMiddleware(route) {
503
+ return async (c, next) => {
504
+ const response = await next();
505
+ const resolved = response ?? c.res;
506
+ if (route.delay && route.delay > 0) {
507
+ await delay(route.delay);
508
+ }
509
+ return applyRouteOverrides(resolved, route);
479
510
  };
480
- const middlewareHandlers = [];
481
- for (const entry of route.middleware ?? []) {
482
- const handler = await loadModuleMiddleware(
483
- entry,
484
- middlewareCache,
485
- moduleBase,
486
- moduleMap
511
+ }
512
+ async function buildApp(params) {
513
+ const { manifest, moduleCache, middlewareCache, moduleBase, moduleMap } = params;
514
+ const app = new Hono({ router: new PatternRouter(), strict: false });
515
+ const compiled = compileRoutes(manifest);
516
+ for (const entry of compiled) {
517
+ const middlewares = [];
518
+ for (const middleware of entry.route.middleware ?? []) {
519
+ const handler2 = await loadModuleMiddleware(
520
+ middleware,
521
+ middlewareCache,
522
+ moduleBase,
523
+ moduleMap
524
+ );
525
+ if (handler2) {
526
+ middlewares.push(handler2);
527
+ }
528
+ }
529
+ const handler = createRouteHandler({
530
+ route: entry.route,
531
+ moduleCache,
532
+ ...typeof moduleBase !== "undefined" ? { moduleBase } : {},
533
+ ...typeof moduleMap !== "undefined" ? { moduleMap } : {}
534
+ });
535
+ app.on(
536
+ entry.method,
537
+ toHonoPath(entry.tokens),
538
+ createFinalizeMiddleware(entry.route),
539
+ ...middlewares,
540
+ handler
487
541
  );
488
- if (handler) {
489
- middlewareHandlers.push(handler);
542
+ }
543
+ return app;
544
+ }
545
+ function appendQueryParams(url, query) {
546
+ for (const [key, value] of Object.entries(query)) {
547
+ if (Array.isArray(value)) {
548
+ for (const entry of value) {
549
+ url.searchParams.append(key, entry);
550
+ }
551
+ } else {
552
+ url.searchParams.append(key, value);
490
553
  }
491
554
  }
492
- const result = middlewareHandlers.length > 0 ? await runMiddlewares(middlewareHandlers) : await runHandler();
493
- const responseValue = result.value;
494
- const contentType = result.contentType;
495
- if (route.delay && route.delay > 0) {
496
- await delay(route.delay);
555
+ }
556
+ function resolveRequestBody(req, contentType) {
557
+ if (typeof req.rawBody !== "undefined") {
558
+ return req.rawBody;
497
559
  }
498
- const headers = mergeHeaders(responder.toRecord(), route.headers);
499
- if (contentType && !headers["content-type"]) {
500
- headers["content-type"] = contentType;
560
+ const body = req.body;
561
+ if (typeof body === "undefined") {
562
+ return void 0;
501
563
  }
502
- const normalized = normalizeBody(responseValue, headers["content-type"]);
503
- const status = finalizeStatus(
504
- route.status ?? responder.statusCode,
505
- normalized.body
506
- );
507
- if (normalized.contentType && !headers["content-type"]) {
508
- headers["content-type"] = normalized.contentType;
564
+ if (typeof body === "string") {
565
+ return body;
509
566
  }
510
- return {
511
- status,
512
- headers,
513
- body: normalized.body
514
- };
567
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
568
+ return body;
569
+ }
570
+ if (typeof body === "object") {
571
+ const normalized = contentType.toLowerCase();
572
+ if (normalized.includes("json")) {
573
+ return JSON.stringify(body);
574
+ }
575
+ return JSON.stringify(body);
576
+ }
577
+ return String(body);
578
+ }
579
+ function toFetchRequest(req) {
580
+ const url = new URL(req.path, "http://mokup.local");
581
+ appendQueryParams(url, req.query);
582
+ const headers = new Headers();
583
+ for (const [key, value] of Object.entries(req.headers)) {
584
+ headers.set(key, value);
585
+ }
586
+ const method = normalizeMethod(req.method) ?? "GET";
587
+ const contentType = headers.get("content-type") ?? "";
588
+ const body = resolveRequestBody(req, contentType);
589
+ const init = { method, headers };
590
+ if (typeof body !== "undefined" && method !== "GET" && method !== "HEAD") {
591
+ init.body = body;
592
+ }
593
+ return new Request(url.toString(), init);
515
594
  }
516
595
  function createRuntime(options) {
517
596
  let manifestCache = null;
518
- let compiledRoutes = null;
597
+ let appPromise = null;
519
598
  const moduleCache = /* @__PURE__ */ new Map();
520
599
  const middlewareCache = /* @__PURE__ */ new Map();
521
600
  const getManifest = async () => {
@@ -524,56 +603,31 @@ function createRuntime(options) {
524
603
  }
525
604
  return manifestCache;
526
605
  };
527
- const getRouteList = async () => {
528
- if (compiledRoutes) {
529
- return compiledRoutes;
530
- }
531
- const manifest = await getManifest();
532
- const map = /* @__PURE__ */ new Map();
533
- for (const route of manifest.routes) {
534
- const method = normalizeMethod(route.method) ?? "GET";
535
- const parsed = route.tokens ? { tokens: route.tokens, score: route.score ?? scoreRouteTokens(route.tokens), errors: [] } : parseRouteTemplate(route.url);
536
- if (parsed.errors.length > 0) {
537
- continue;
538
- }
539
- const tokens = route.tokens ?? parsed.tokens;
540
- const score = route.score ?? parsed.score;
541
- const list = map.get(method) ?? [];
542
- list.push({ route, tokens, score });
543
- map.set(method, list);
544
- }
545
- for (const list of map.values()) {
546
- list.sort((a, b) => compareRouteScore(a.score, b.score));
606
+ const getApp = async () => {
607
+ if (!appPromise) {
608
+ appPromise = (async () => {
609
+ const manifest = await getManifest();
610
+ return buildApp({
611
+ manifest,
612
+ moduleCache,
613
+ middlewareCache,
614
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
615
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
616
+ });
617
+ })();
547
618
  }
548
- compiledRoutes = map;
549
- return compiledRoutes;
619
+ return appPromise;
550
620
  };
551
621
  const handle = async (req) => {
622
+ const app = await getApp();
552
623
  const method = normalizeMethod(req.method) ?? "GET";
553
- const map = await getRouteList();
554
- const list = map.get(method);
555
- if (!list || list.length === 0) {
624
+ const matchMethod = method === "HEAD" ? "GET" : method;
625
+ const match = app.router.match(matchMethod, req.path);
626
+ if (!match || match[0].length === 0) {
556
627
  return null;
557
628
  }
558
- for (const entry of list) {
559
- const matched = matchRouteTokens(entry.tokens, req.path);
560
- if (!matched) {
561
- continue;
562
- }
563
- const requestWithParams = {
564
- ...req,
565
- params: matched.params
566
- };
567
- return executeRoute(
568
- entry.route,
569
- requestWithParams,
570
- moduleCache,
571
- middlewareCache,
572
- options.moduleBase,
573
- options.moduleMap
574
- );
575
- }
576
- return null;
629
+ const response = await app.fetch(toFetchRequest(req));
630
+ return await toRuntimeResult(response);
577
631
  };
578
632
  return {
579
633
  handle
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@mokup/runtime",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.1.0",
5
5
  "description": "Cross-runtime mock matching and response handling for mokup.",
6
6
  "license": "MIT",
7
+ "homepage": "https://mokup.icebreaker.top",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/sonofmagic/mokup.git",
@@ -25,7 +26,9 @@
25
26
  "files": [
26
27
  "dist"
27
28
  ],
28
- "dependencies": {},
29
+ "dependencies": {
30
+ "hono": "^4.11.4"
31
+ },
29
32
  "devDependencies": {
30
33
  "typescript": "^5.9.3",
31
34
  "unbuild": "^3.6.1"