@mokup/runtime 0.0.1 → 0.1.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/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const hono = require('@mokup/shared/hono');
4
+
3
5
  const paramNamePattern = /^[\w-]+$/;
4
6
  const paramPattern = /^\[([^\]/]+)\]$/;
5
7
  const catchallPattern = /^\[\.\.\.([^\]/]+)\]$/;
@@ -227,14 +229,14 @@ function normalizeRules(value) {
227
229
  }
228
230
  ];
229
231
  }
230
- async function executeRule(rule, req, responder, ctx) {
232
+ async function executeRule(rule, context) {
231
233
  if (!rule) {
232
234
  return void 0;
233
235
  }
234
236
  const value = rule.response;
235
237
  if (typeof value === "function") {
236
238
  const handler = value;
237
- return handler(req, responder, ctx);
239
+ return handler(context);
238
240
  }
239
241
  return value;
240
242
  }
@@ -262,7 +264,10 @@ async function loadModuleExport(modulePath, exportName, moduleBase, moduleMap) {
262
264
  const resolvedUrl = directMapValue ? void 0 : resolveModuleUrl(modulePath, moduleBase);
263
265
  const resolvedMapValue = resolvedUrl ? moduleMap?.[resolvedUrl] : void 0;
264
266
  const moduleValue = directMapValue ?? resolvedMapValue;
265
- const module = moduleValue ?? await import(resolvedUrl ?? modulePath);
267
+ const module = moduleValue ?? await import(
268
+ /* @vite-ignore */
269
+ resolvedUrl ?? modulePath
270
+ );
266
271
  return module[exportName] ?? module.default ?? module;
267
272
  }
268
273
  function resolveModuleCacheKey(modulePath, exportName, moduleBase, moduleMap) {
@@ -340,108 +345,177 @@ function normalizeMethod(method) {
340
345
  }
341
346
  return void 0;
342
347
  }
343
- function mergeHeaders(base, override) {
344
- if (!override) {
345
- return base;
346
- }
347
- return {
348
- ...base,
349
- ...override
350
- };
351
- }
352
348
  function delay(ms) {
353
349
  return new Promise((resolve) => setTimeout(resolve, ms));
354
350
  }
355
351
 
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());
352
+ function decodeBase64(value) {
353
+ if (typeof atob === "function") {
354
+ const binary = atob(value);
355
+ const bytes = new Uint8Array(binary.length);
356
+ for (let i = 0; i < binary.length; i += 1) {
357
+ bytes[i] = binary.charCodeAt(i);
358
+ }
359
+ return bytes;
364
360
  }
365
- removeHeader(key) {
366
- this.headers.delete(key.toLowerCase());
361
+ throw new Error("Base64 decoding is not supported in this runtime.");
362
+ }
363
+
364
+ function toHonoPath(tokens) {
365
+ if (!tokens || tokens.length === 0) {
366
+ return "/";
367
367
  }
368
- toRecord() {
369
- const record = {};
370
- for (const [key, value] of this.headers.entries()) {
371
- record[key] = value;
368
+ const segments = tokens.map((token) => {
369
+ if (token.type === "static") {
370
+ return token.value;
372
371
  }
373
- return record;
374
- }
372
+ if (token.type === "param") {
373
+ return `:${token.name}`;
374
+ }
375
+ if (token.type === "catchall") {
376
+ return `:${token.name}{.+}`;
377
+ }
378
+ return `:${token.name}{.+}?`;
379
+ });
380
+ return `/${segments.join("/")}`;
375
381
  }
376
- function normalizeBody(body, contentType) {
377
- if (typeof body === "undefined") {
378
- return { body: null };
382
+ function compileRoutes(manifest) {
383
+ const compiled = [];
384
+ for (const route of manifest.routes) {
385
+ const method = normalizeMethod(route.method) ?? "GET";
386
+ const parsed = route.tokens ? {
387
+ tokens: route.tokens,
388
+ score: route.score ?? scoreRouteTokens(route.tokens),
389
+ errors: []
390
+ } : parseRouteTemplate(route.url);
391
+ if (parsed.errors.length > 0) {
392
+ continue;
393
+ }
394
+ compiled.push({
395
+ route,
396
+ method,
397
+ tokens: route.tokens ?? parsed.tokens,
398
+ score: route.score ?? parsed.score
399
+ });
400
+ }
401
+ return compiled.sort((a, b) => {
402
+ if (a.method !== b.method) {
403
+ return a.method.localeCompare(b.method);
404
+ }
405
+ return compareRouteScore(a.score, b.score);
406
+ });
407
+ }
408
+ function shouldTreatAsText(contentType) {
409
+ const normalized = contentType.toLowerCase();
410
+ if (!normalized) {
411
+ return true;
379
412
  }
380
- if (typeof body === "string") {
381
- return {
382
- body,
383
- contentType: contentType ?? "text/plain; charset=utf-8"
384
- };
413
+ if (normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("xml") || normalized.includes("javascript")) {
414
+ return true;
385
415
  }
386
- if (body instanceof Uint8Array) {
416
+ if (normalized.startsWith("image/") || normalized.startsWith("audio/") || normalized.startsWith("video/") || normalized.includes("octet-stream")) {
417
+ return false;
418
+ }
419
+ if (normalized.startsWith("application/") && (normalized.includes("pdf") || normalized.includes("zip") || normalized.includes("gzip"))) {
420
+ return false;
421
+ }
422
+ return true;
423
+ }
424
+ async function toRuntimeResult(response) {
425
+ const headers = {};
426
+ response.headers.forEach((value, key) => {
427
+ headers[key.toLowerCase()] = value;
428
+ });
429
+ if (!response.body || [204, 205, 304].includes(response.status)) {
387
430
  return {
388
- body,
389
- contentType: contentType ?? "application/octet-stream"
431
+ status: response.status,
432
+ headers,
433
+ body: null
390
434
  };
391
435
  }
392
- if (body instanceof ArrayBuffer) {
436
+ const contentType = headers["content-type"] ?? "";
437
+ if (shouldTreatAsText(contentType)) {
393
438
  return {
394
- body: new Uint8Array(body),
395
- contentType: contentType ?? "application/octet-stream"
439
+ status: response.status,
440
+ headers,
441
+ body: await response.text()
396
442
  };
397
443
  }
444
+ const buffer = new Uint8Array(await response.arrayBuffer());
398
445
  return {
399
- body: JSON.stringify(body),
400
- contentType: contentType ?? "application/json; charset=utf-8"
446
+ status: response.status,
447
+ headers,
448
+ body: buffer
401
449
  };
402
450
  }
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);
451
+ function isValidStatus(status) {
452
+ return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
453
+ }
454
+ function resolveStatus(routeStatus, responseStatus) {
455
+ if (isValidStatus(routeStatus)) {
456
+ return routeStatus;
457
+ }
458
+ if (isValidStatus(responseStatus)) {
459
+ return responseStatus;
460
+ }
461
+ return 200;
462
+ }
463
+ function applyRouteOverrides(response, route) {
464
+ const headers = new Headers(response.headers);
465
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
466
+ if (route.headers) {
467
+ for (const [key, value] of Object.entries(route.headers)) {
468
+ headers.set(key, value);
409
469
  }
410
- return bytes;
411
470
  }
412
- throw new Error("Base64 decoding is not supported in this runtime.");
471
+ const status = resolveStatus(route.status, response.status);
472
+ if (status === response.status && !hasHeaders) {
473
+ return response;
474
+ }
475
+ return new Response(response.body, { status, headers });
413
476
  }
414
- function finalizeStatus(status, body) {
415
- if (status === 200 && body === null) {
416
- return 204;
477
+ function normalizeHandlerValue(c, value) {
478
+ if (value instanceof Response) {
479
+ return value;
480
+ }
481
+ if (typeof value === "undefined") {
482
+ const response = c.body(null);
483
+ if (response.status === 200) {
484
+ return new Response(response.body, {
485
+ status: 204,
486
+ headers: response.headers
487
+ });
488
+ }
489
+ return response;
490
+ }
491
+ if (typeof value === "string") {
492
+ return c.text(value);
417
493
  }
418
- return status;
494
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
495
+ if (!c.res.headers.get("content-type")) {
496
+ c.header("content-type", "application/octet-stream");
497
+ }
498
+ const data = value instanceof ArrayBuffer ? value : new Uint8Array(value);
499
+ return c.body(data);
500
+ }
501
+ return c.json(value);
419
502
  }
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 () => {
503
+ function createRouteHandler(params) {
504
+ const { route, moduleCache, moduleBase, moduleMap } = params;
505
+ return async (c) => {
428
506
  if (route.response.type === "json") {
429
- return {
430
- value: route.response.body,
431
- contentType: "application/json; charset=utf-8"
432
- };
507
+ if (typeof route.response.body === "undefined") {
508
+ return normalizeHandlerValue(c, void 0);
509
+ }
510
+ return c.json(route.response.body);
433
511
  }
434
512
  if (route.response.type === "text") {
435
- return {
436
- value: route.response.body,
437
- contentType: "text/plain; charset=utf-8"
438
- };
513
+ return c.text(route.response.body);
439
514
  }
440
515
  if (route.response.type === "binary") {
441
- return {
442
- value: decodeBase64(route.response.body),
443
- contentType: "application/octet-stream"
444
- };
516
+ const data = new Uint8Array(decodeBase64(route.response.body));
517
+ c.header("content-type", "application/octet-stream");
518
+ return c.body(data);
445
519
  }
446
520
  const rule = await loadModuleRule(
447
521
  route.response,
@@ -449,75 +523,144 @@ async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase
449
523
  moduleBase,
450
524
  moduleMap
451
525
  );
452
- const value = await executeRule(rule, req, responder, ctx);
453
- return { value };
526
+ const value = await executeRule(rule, c);
527
+ return normalizeHandlerValue(c, value);
454
528
  };
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);
529
+ }
530
+ function createFinalizeMiddleware(route) {
531
+ return async (c, next) => {
532
+ const response = await next();
533
+ const resolved = response ?? c.res;
534
+ if (route.delay && route.delay > 0) {
535
+ await delay(route.delay);
536
+ }
537
+ return applyRouteOverrides(resolved, route);
481
538
  };
482
- const middlewareHandlers = [];
483
- for (const entry of route.middleware ?? []) {
484
- const handler = await loadModuleMiddleware(
485
- entry,
486
- middlewareCache,
487
- moduleBase,
488
- moduleMap
539
+ }
540
+ async function buildApp(params) {
541
+ const { manifest, moduleCache, middlewareCache, moduleBase, moduleMap } = params;
542
+ const app = new hono.Hono({ router: new hono.PatternRouter(), strict: false });
543
+ const compiled = compileRoutes(manifest);
544
+ for (const entry of compiled) {
545
+ const middlewares = [];
546
+ for (const middleware of entry.route.middleware ?? []) {
547
+ const handler2 = await loadModuleMiddleware(
548
+ middleware,
549
+ middlewareCache,
550
+ moduleBase,
551
+ moduleMap
552
+ );
553
+ if (handler2) {
554
+ middlewares.push(handler2);
555
+ }
556
+ }
557
+ const handler = createRouteHandler({
558
+ route: entry.route,
559
+ moduleCache,
560
+ ...typeof moduleBase !== "undefined" ? { moduleBase } : {},
561
+ ...typeof moduleMap !== "undefined" ? { moduleMap } : {}
562
+ });
563
+ app.on(
564
+ entry.method,
565
+ toHonoPath(entry.tokens),
566
+ createFinalizeMiddleware(entry.route),
567
+ ...middlewares,
568
+ handler
489
569
  );
490
- if (handler) {
491
- middlewareHandlers.push(handler);
570
+ }
571
+ return app;
572
+ }
573
+ async function createRuntimeApp(options) {
574
+ const manifest = typeof options.manifest === "function" ? await options.manifest() : options.manifest;
575
+ const moduleCache = /* @__PURE__ */ new Map();
576
+ const middlewareCache = /* @__PURE__ */ new Map();
577
+ return await buildApp({
578
+ manifest,
579
+ moduleCache,
580
+ middlewareCache,
581
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
582
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
583
+ });
584
+ }
585
+ function appendQueryParams(url, query) {
586
+ for (const [key, value] of Object.entries(query)) {
587
+ if (Array.isArray(value)) {
588
+ for (const entry of value) {
589
+ url.searchParams.append(key, entry);
590
+ }
591
+ } else {
592
+ url.searchParams.append(key, value);
492
593
  }
493
594
  }
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);
595
+ }
596
+ function resolveRequestBody(req, contentType) {
597
+ if (typeof req.rawBody !== "undefined") {
598
+ return req.rawBody;
499
599
  }
500
- const headers = mergeHeaders(responder.toRecord(), route.headers);
501
- if (contentType && !headers["content-type"]) {
502
- headers["content-type"] = contentType;
600
+ const body = req.body;
601
+ if (typeof body === "undefined") {
602
+ return void 0;
503
603
  }
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;
604
+ if (typeof body === "string") {
605
+ return body;
511
606
  }
512
- return {
513
- status,
514
- headers,
515
- body: normalized.body
516
- };
607
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
608
+ return body;
609
+ }
610
+ if (typeof body === "object") {
611
+ const normalized = contentType.toLowerCase();
612
+ if (normalized.includes("json")) {
613
+ return JSON.stringify(body);
614
+ }
615
+ return JSON.stringify(body);
616
+ }
617
+ return String(body);
618
+ }
619
+ function toFetchRequest(req) {
620
+ const url = new URL(normalizePathname(req.path), "http://mokup.local");
621
+ appendQueryParams(url, req.query);
622
+ const headers = new Headers();
623
+ for (const [key, value] of Object.entries(req.headers)) {
624
+ headers.set(key, value);
625
+ }
626
+ const method = normalizeMethod(req.method) ?? "GET";
627
+ const contentType = headers.get("content-type") ?? "";
628
+ const body = resolveRequestBody(req, contentType);
629
+ const init = { method, headers };
630
+ if (typeof body !== "undefined" && method !== "GET" && method !== "HEAD") {
631
+ init.body = body;
632
+ }
633
+ return new Request(url.toString(), init);
634
+ }
635
+ function isRelativeModulePath(modulePath) {
636
+ return !/^(?:data|http|https|file):/.test(modulePath);
637
+ }
638
+ function requiresModuleBase(modulePath, moduleMap) {
639
+ if (!isRelativeModulePath(modulePath)) {
640
+ return false;
641
+ }
642
+ if (moduleMap && Object.prototype.hasOwnProperty.call(moduleMap, modulePath)) {
643
+ return false;
644
+ }
645
+ return true;
646
+ }
647
+ function routeNeedsModuleBase(route, moduleMap) {
648
+ if (route.response.type === "module" && requiresModuleBase(route.response.module, moduleMap)) {
649
+ return true;
650
+ }
651
+ if (route.middleware) {
652
+ for (const middleware of route.middleware) {
653
+ if (requiresModuleBase(middleware.module, moduleMap)) {
654
+ return true;
655
+ }
656
+ }
657
+ }
658
+ return false;
517
659
  }
518
660
  function createRuntime(options) {
519
661
  let manifestCache = null;
520
- let compiledRoutes = null;
662
+ let appPromise = null;
663
+ let compiledCache = null;
521
664
  const moduleCache = /* @__PURE__ */ new Map();
522
665
  const middlewareCache = /* @__PURE__ */ new Map();
523
666
  const getManifest = async () => {
@@ -526,64 +669,62 @@ function createRuntime(options) {
526
669
  }
527
670
  return manifestCache;
528
671
  };
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);
672
+ const getApp = async () => {
673
+ if (!appPromise) {
674
+ appPromise = (async () => {
675
+ const manifest = await getManifest();
676
+ return buildApp({
677
+ manifest,
678
+ moduleCache,
679
+ middlewareCache,
680
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
681
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
682
+ });
683
+ })();
546
684
  }
547
- for (const list of map.values()) {
548
- list.sort((a, b) => compareRouteScore(a.score, b.score));
685
+ return appPromise;
686
+ };
687
+ const getCompiled = async () => {
688
+ if (!compiledCache) {
689
+ compiledCache = compileRoutes(await getManifest());
549
690
  }
550
- compiledRoutes = map;
551
- return compiledRoutes;
691
+ return compiledCache;
552
692
  };
553
693
  const handle = async (req) => {
554
694
  const method = normalizeMethod(req.method) ?? "GET";
555
- const map = await getRouteList();
556
- const list = map.get(method);
557
- if (!list || list.length === 0) {
558
- return null;
559
- }
560
- for (const entry of list) {
561
- const matched = matchRouteTokens(entry.tokens, req.path);
562
- if (!matched) {
695
+ const matchMethod = method === "HEAD" ? "GET" : method;
696
+ const pathname = normalizePathname(req.path);
697
+ const compiled = await getCompiled();
698
+ let matchedRoute = null;
699
+ for (const entry of compiled) {
700
+ if (entry.method !== matchMethod) {
563
701
  continue;
564
702
  }
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
- );
703
+ if (matchRouteTokens(entry.tokens, pathname)) {
704
+ matchedRoute = entry.route;
705
+ break;
706
+ }
577
707
  }
578
- return null;
708
+ if (!matchedRoute) {
709
+ return null;
710
+ }
711
+ if (typeof options.moduleBase === "undefined" && routeNeedsModuleBase(matchedRoute, options.moduleMap)) {
712
+ throw new Error("moduleBase is required for relative module paths.");
713
+ }
714
+ const app = await getApp();
715
+ const response = await app.fetch(toFetchRequest(req));
716
+ const resolvedResponse = applyRouteOverrides(response, matchedRoute);
717
+ return await toRuntimeResult(resolvedResponse);
579
718
  };
580
719
  return {
581
720
  handle
582
721
  };
583
722
  }
584
723
 
724
+ exports.handle = hono.handle;
585
725
  exports.compareRouteScore = compareRouteScore;
586
726
  exports.createRuntime = createRuntime;
727
+ exports.createRuntimeApp = createRuntimeApp;
587
728
  exports.matchRouteTokens = matchRouteTokens;
588
729
  exports.normalizePathname = normalizePathname;
589
730
  exports.parseRouteTemplate = parseRouteTemplate;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,6 @@
1
+ import { Context, MiddlewareHandler, Hono } from '@mokup/shared/hono';
2
+ export { handle } from '@mokup/shared/hono';
3
+
1
4
  type RouteToken = {
2
5
  type: 'static';
3
6
  value: string;
@@ -75,18 +78,9 @@ interface RuntimeResult {
75
78
  headers: Record<string, string>;
76
79
  body: string | Uint8Array | null;
77
80
  }
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>;
81
+ type MockContext = Context;
82
+ type MockMiddleware = MiddlewareHandler;
83
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
84
  interface RuntimeOptions {
91
85
  manifest: Manifest | (() => Promise<Manifest>);
92
86
  moduleBase?: string | URL;
@@ -94,9 +88,10 @@ interface RuntimeOptions {
94
88
  }
95
89
  type ModuleMap = Record<string, Record<string, unknown>>;
96
90
 
91
+ declare function createRuntimeApp(options: RuntimeOptions): Promise<Hono>;
97
92
  declare function createRuntime(options: RuntimeOptions): {
98
93
  handle: (req: RuntimeRequest) => Promise<RuntimeResult | null>;
99
94
  };
100
95
 
101
- 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 };
96
+ export { compareRouteScore, createRuntime, createRuntimeApp, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
97
+ 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,6 @@
1
+ import { Context, MiddlewareHandler, Hono } from '@mokup/shared/hono';
2
+ export { handle } from '@mokup/shared/hono';
3
+
1
4
  type RouteToken = {
2
5
  type: 'static';
3
6
  value: string;
@@ -75,18 +78,9 @@ interface RuntimeResult {
75
78
  headers: Record<string, string>;
76
79
  body: string | Uint8Array | null;
77
80
  }
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>;
81
+ type MockContext = Context;
82
+ type MockMiddleware = MiddlewareHandler;
83
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
84
  interface RuntimeOptions {
91
85
  manifest: Manifest | (() => Promise<Manifest>);
92
86
  moduleBase?: string | URL;
@@ -94,9 +88,10 @@ interface RuntimeOptions {
94
88
  }
95
89
  type ModuleMap = Record<string, Record<string, unknown>>;
96
90
 
91
+ declare function createRuntimeApp(options: RuntimeOptions): Promise<Hono>;
97
92
  declare function createRuntime(options: RuntimeOptions): {
98
93
  handle: (req: RuntimeRequest) => Promise<RuntimeResult | null>;
99
94
  };
100
95
 
101
- 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 };
96
+ export { compareRouteScore, createRuntime, createRuntimeApp, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
97
+ 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,6 @@
1
+ import { Context, MiddlewareHandler, Hono } from '@mokup/shared/hono';
2
+ export { handle } from '@mokup/shared/hono';
3
+
1
4
  type RouteToken = {
2
5
  type: 'static';
3
6
  value: string;
@@ -75,18 +78,9 @@ interface RuntimeResult {
75
78
  headers: Record<string, string>;
76
79
  body: string | Uint8Array | null;
77
80
  }
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>;
81
+ type MockContext = Context;
82
+ type MockMiddleware = MiddlewareHandler;
83
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
90
84
  interface RuntimeOptions {
91
85
  manifest: Manifest | (() => Promise<Manifest>);
92
86
  moduleBase?: string | URL;
@@ -94,9 +88,10 @@ interface RuntimeOptions {
94
88
  }
95
89
  type ModuleMap = Record<string, Record<string, unknown>>;
96
90
 
91
+ declare function createRuntimeApp(options: RuntimeOptions): Promise<Hono>;
97
92
  declare function createRuntime(options: RuntimeOptions): {
98
93
  handle: (req: RuntimeRequest) => Promise<RuntimeResult | null>;
99
94
  };
100
95
 
101
- 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 };
96
+ export { compareRouteScore, createRuntime, createRuntimeApp, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
97
+ 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, PatternRouter } from '@mokup/shared/hono';
2
+ export { handle } from '@mokup/shared/hono';
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
  }
@@ -260,7 +263,10 @@ async function loadModuleExport(modulePath, exportName, moduleBase, moduleMap) {
260
263
  const resolvedUrl = directMapValue ? void 0 : resolveModuleUrl(modulePath, moduleBase);
261
264
  const resolvedMapValue = resolvedUrl ? moduleMap?.[resolvedUrl] : void 0;
262
265
  const moduleValue = directMapValue ?? resolvedMapValue;
263
- const module = moduleValue ?? await import(resolvedUrl ?? modulePath);
266
+ const module = moduleValue ?? await import(
267
+ /* @vite-ignore */
268
+ resolvedUrl ?? modulePath
269
+ );
264
270
  return module[exportName] ?? module.default ?? module;
265
271
  }
266
272
  function resolveModuleCacheKey(modulePath, exportName, moduleBase, moduleMap) {
@@ -338,108 +344,177 @@ function normalizeMethod(method) {
338
344
  }
339
345
  return void 0;
340
346
  }
341
- function mergeHeaders(base, override) {
342
- if (!override) {
343
- return base;
344
- }
345
- return {
346
- ...base,
347
- ...override
348
- };
349
- }
350
347
  function delay(ms) {
351
348
  return new Promise((resolve) => setTimeout(resolve, ms));
352
349
  }
353
350
 
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());
351
+ function decodeBase64(value) {
352
+ if (typeof atob === "function") {
353
+ const binary = atob(value);
354
+ const bytes = new Uint8Array(binary.length);
355
+ for (let i = 0; i < binary.length; i += 1) {
356
+ bytes[i] = binary.charCodeAt(i);
357
+ }
358
+ return bytes;
362
359
  }
363
- removeHeader(key) {
364
- this.headers.delete(key.toLowerCase());
360
+ throw new Error("Base64 decoding is not supported in this runtime.");
361
+ }
362
+
363
+ function toHonoPath(tokens) {
364
+ if (!tokens || tokens.length === 0) {
365
+ return "/";
365
366
  }
366
- toRecord() {
367
- const record = {};
368
- for (const [key, value] of this.headers.entries()) {
369
- record[key] = value;
367
+ const segments = tokens.map((token) => {
368
+ if (token.type === "static") {
369
+ return token.value;
370
370
  }
371
- return record;
372
- }
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("/")}`;
373
380
  }
374
- function normalizeBody(body, contentType) {
375
- if (typeof body === "undefined") {
376
- return { body: null };
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
+ function shouldTreatAsText(contentType) {
408
+ const normalized = contentType.toLowerCase();
409
+ if (!normalized) {
410
+ return true;
377
411
  }
378
- if (typeof body === "string") {
379
- return {
380
- body,
381
- contentType: contentType ?? "text/plain; charset=utf-8"
382
- };
412
+ if (normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("xml") || normalized.includes("javascript")) {
413
+ return true;
383
414
  }
384
- if (body instanceof Uint8Array) {
415
+ if (normalized.startsWith("image/") || normalized.startsWith("audio/") || normalized.startsWith("video/") || normalized.includes("octet-stream")) {
416
+ return false;
417
+ }
418
+ if (normalized.startsWith("application/") && (normalized.includes("pdf") || normalized.includes("zip") || normalized.includes("gzip"))) {
419
+ return false;
420
+ }
421
+ return true;
422
+ }
423
+ async function toRuntimeResult(response) {
424
+ const headers = {};
425
+ response.headers.forEach((value, key) => {
426
+ headers[key.toLowerCase()] = value;
427
+ });
428
+ if (!response.body || [204, 205, 304].includes(response.status)) {
385
429
  return {
386
- body,
387
- contentType: contentType ?? "application/octet-stream"
430
+ status: response.status,
431
+ headers,
432
+ body: null
388
433
  };
389
434
  }
390
- if (body instanceof ArrayBuffer) {
435
+ const contentType = headers["content-type"] ?? "";
436
+ if (shouldTreatAsText(contentType)) {
391
437
  return {
392
- body: new Uint8Array(body),
393
- contentType: contentType ?? "application/octet-stream"
438
+ status: response.status,
439
+ headers,
440
+ body: await response.text()
394
441
  };
395
442
  }
443
+ const buffer = new Uint8Array(await response.arrayBuffer());
396
444
  return {
397
- body: JSON.stringify(body),
398
- contentType: contentType ?? "application/json; charset=utf-8"
445
+ status: response.status,
446
+ headers,
447
+ body: buffer
399
448
  };
400
449
  }
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);
450
+ function isValidStatus(status) {
451
+ return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
452
+ }
453
+ function resolveStatus(routeStatus, responseStatus) {
454
+ if (isValidStatus(routeStatus)) {
455
+ return routeStatus;
456
+ }
457
+ if (isValidStatus(responseStatus)) {
458
+ return responseStatus;
459
+ }
460
+ return 200;
461
+ }
462
+ function applyRouteOverrides(response, route) {
463
+ const headers = new Headers(response.headers);
464
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
465
+ if (route.headers) {
466
+ for (const [key, value] of Object.entries(route.headers)) {
467
+ headers.set(key, value);
407
468
  }
408
- return bytes;
409
469
  }
410
- throw new Error("Base64 decoding is not supported in this runtime.");
470
+ const status = resolveStatus(route.status, response.status);
471
+ if (status === response.status && !hasHeaders) {
472
+ return response;
473
+ }
474
+ return new Response(response.body, { status, headers });
411
475
  }
412
- function finalizeStatus(status, body) {
413
- if (status === 200 && body === null) {
414
- return 204;
476
+ function normalizeHandlerValue(c, value) {
477
+ if (value instanceof Response) {
478
+ return value;
479
+ }
480
+ if (typeof value === "undefined") {
481
+ const response = c.body(null);
482
+ if (response.status === 200) {
483
+ return new Response(response.body, {
484
+ status: 204,
485
+ headers: response.headers
486
+ });
487
+ }
488
+ return response;
489
+ }
490
+ if (typeof value === "string") {
491
+ return c.text(value);
415
492
  }
416
- return status;
493
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
494
+ if (!c.res.headers.get("content-type")) {
495
+ c.header("content-type", "application/octet-stream");
496
+ }
497
+ const data = value instanceof ArrayBuffer ? value : new Uint8Array(value);
498
+ return c.body(data);
499
+ }
500
+ return c.json(value);
417
501
  }
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 () => {
502
+ function createRouteHandler(params) {
503
+ const { route, moduleCache, moduleBase, moduleMap } = params;
504
+ return async (c) => {
426
505
  if (route.response.type === "json") {
427
- return {
428
- value: route.response.body,
429
- contentType: "application/json; charset=utf-8"
430
- };
506
+ if (typeof route.response.body === "undefined") {
507
+ return normalizeHandlerValue(c, void 0);
508
+ }
509
+ return c.json(route.response.body);
431
510
  }
432
511
  if (route.response.type === "text") {
433
- return {
434
- value: route.response.body,
435
- contentType: "text/plain; charset=utf-8"
436
- };
512
+ return c.text(route.response.body);
437
513
  }
438
514
  if (route.response.type === "binary") {
439
- return {
440
- value: decodeBase64(route.response.body),
441
- contentType: "application/octet-stream"
442
- };
515
+ const data = new Uint8Array(decodeBase64(route.response.body));
516
+ c.header("content-type", "application/octet-stream");
517
+ return c.body(data);
443
518
  }
444
519
  const rule = await loadModuleRule(
445
520
  route.response,
@@ -447,75 +522,144 @@ async function executeRoute(route, req, moduleCache, middlewareCache, moduleBase
447
522
  moduleBase,
448
523
  moduleMap
449
524
  );
450
- const value = await executeRule(rule, req, responder, ctx);
451
- return { value };
525
+ const value = await executeRule(rule, c);
526
+ return normalizeHandlerValue(c, value);
452
527
  };
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);
528
+ }
529
+ function createFinalizeMiddleware(route) {
530
+ return async (c, next) => {
531
+ const response = await next();
532
+ const resolved = response ?? c.res;
533
+ if (route.delay && route.delay > 0) {
534
+ await delay(route.delay);
535
+ }
536
+ return applyRouteOverrides(resolved, route);
479
537
  };
480
- const middlewareHandlers = [];
481
- for (const entry of route.middleware ?? []) {
482
- const handler = await loadModuleMiddleware(
483
- entry,
484
- middlewareCache,
485
- moduleBase,
486
- moduleMap
538
+ }
539
+ async function buildApp(params) {
540
+ const { manifest, moduleCache, middlewareCache, moduleBase, moduleMap } = params;
541
+ const app = new Hono({ router: new PatternRouter(), strict: false });
542
+ const compiled = compileRoutes(manifest);
543
+ for (const entry of compiled) {
544
+ const middlewares = [];
545
+ for (const middleware of entry.route.middleware ?? []) {
546
+ const handler2 = await loadModuleMiddleware(
547
+ middleware,
548
+ middlewareCache,
549
+ moduleBase,
550
+ moduleMap
551
+ );
552
+ if (handler2) {
553
+ middlewares.push(handler2);
554
+ }
555
+ }
556
+ const handler = createRouteHandler({
557
+ route: entry.route,
558
+ moduleCache,
559
+ ...typeof moduleBase !== "undefined" ? { moduleBase } : {},
560
+ ...typeof moduleMap !== "undefined" ? { moduleMap } : {}
561
+ });
562
+ app.on(
563
+ entry.method,
564
+ toHonoPath(entry.tokens),
565
+ createFinalizeMiddleware(entry.route),
566
+ ...middlewares,
567
+ handler
487
568
  );
488
- if (handler) {
489
- middlewareHandlers.push(handler);
569
+ }
570
+ return app;
571
+ }
572
+ async function createRuntimeApp(options) {
573
+ const manifest = typeof options.manifest === "function" ? await options.manifest() : options.manifest;
574
+ const moduleCache = /* @__PURE__ */ new Map();
575
+ const middlewareCache = /* @__PURE__ */ new Map();
576
+ return await buildApp({
577
+ manifest,
578
+ moduleCache,
579
+ middlewareCache,
580
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
581
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
582
+ });
583
+ }
584
+ function appendQueryParams(url, query) {
585
+ for (const [key, value] of Object.entries(query)) {
586
+ if (Array.isArray(value)) {
587
+ for (const entry of value) {
588
+ url.searchParams.append(key, entry);
589
+ }
590
+ } else {
591
+ url.searchParams.append(key, value);
490
592
  }
491
593
  }
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);
594
+ }
595
+ function resolveRequestBody(req, contentType) {
596
+ if (typeof req.rawBody !== "undefined") {
597
+ return req.rawBody;
497
598
  }
498
- const headers = mergeHeaders(responder.toRecord(), route.headers);
499
- if (contentType && !headers["content-type"]) {
500
- headers["content-type"] = contentType;
599
+ const body = req.body;
600
+ if (typeof body === "undefined") {
601
+ return void 0;
501
602
  }
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;
603
+ if (typeof body === "string") {
604
+ return body;
509
605
  }
510
- return {
511
- status,
512
- headers,
513
- body: normalized.body
514
- };
606
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
607
+ return body;
608
+ }
609
+ if (typeof body === "object") {
610
+ const normalized = contentType.toLowerCase();
611
+ if (normalized.includes("json")) {
612
+ return JSON.stringify(body);
613
+ }
614
+ return JSON.stringify(body);
615
+ }
616
+ return String(body);
617
+ }
618
+ function toFetchRequest(req) {
619
+ const url = new URL(normalizePathname(req.path), "http://mokup.local");
620
+ appendQueryParams(url, req.query);
621
+ const headers = new Headers();
622
+ for (const [key, value] of Object.entries(req.headers)) {
623
+ headers.set(key, value);
624
+ }
625
+ const method = normalizeMethod(req.method) ?? "GET";
626
+ const contentType = headers.get("content-type") ?? "";
627
+ const body = resolveRequestBody(req, contentType);
628
+ const init = { method, headers };
629
+ if (typeof body !== "undefined" && method !== "GET" && method !== "HEAD") {
630
+ init.body = body;
631
+ }
632
+ return new Request(url.toString(), init);
633
+ }
634
+ function isRelativeModulePath(modulePath) {
635
+ return !/^(?:data|http|https|file):/.test(modulePath);
636
+ }
637
+ function requiresModuleBase(modulePath, moduleMap) {
638
+ if (!isRelativeModulePath(modulePath)) {
639
+ return false;
640
+ }
641
+ if (moduleMap && Object.prototype.hasOwnProperty.call(moduleMap, modulePath)) {
642
+ return false;
643
+ }
644
+ return true;
645
+ }
646
+ function routeNeedsModuleBase(route, moduleMap) {
647
+ if (route.response.type === "module" && requiresModuleBase(route.response.module, moduleMap)) {
648
+ return true;
649
+ }
650
+ if (route.middleware) {
651
+ for (const middleware of route.middleware) {
652
+ if (requiresModuleBase(middleware.module, moduleMap)) {
653
+ return true;
654
+ }
655
+ }
656
+ }
657
+ return false;
515
658
  }
516
659
  function createRuntime(options) {
517
660
  let manifestCache = null;
518
- let compiledRoutes = null;
661
+ let appPromise = null;
662
+ let compiledCache = null;
519
663
  const moduleCache = /* @__PURE__ */ new Map();
520
664
  const middlewareCache = /* @__PURE__ */ new Map();
521
665
  const getManifest = async () => {
@@ -524,60 +668,56 @@ function createRuntime(options) {
524
668
  }
525
669
  return manifestCache;
526
670
  };
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);
671
+ const getApp = async () => {
672
+ if (!appPromise) {
673
+ appPromise = (async () => {
674
+ const manifest = await getManifest();
675
+ return buildApp({
676
+ manifest,
677
+ moduleCache,
678
+ middlewareCache,
679
+ ...typeof options.moduleBase !== "undefined" ? { moduleBase: options.moduleBase } : {},
680
+ ...typeof options.moduleMap !== "undefined" ? { moduleMap: options.moduleMap } : {}
681
+ });
682
+ })();
544
683
  }
545
- for (const list of map.values()) {
546
- list.sort((a, b) => compareRouteScore(a.score, b.score));
684
+ return appPromise;
685
+ };
686
+ const getCompiled = async () => {
687
+ if (!compiledCache) {
688
+ compiledCache = compileRoutes(await getManifest());
547
689
  }
548
- compiledRoutes = map;
549
- return compiledRoutes;
690
+ return compiledCache;
550
691
  };
551
692
  const handle = async (req) => {
552
693
  const method = normalizeMethod(req.method) ?? "GET";
553
- const map = await getRouteList();
554
- const list = map.get(method);
555
- if (!list || list.length === 0) {
556
- return null;
557
- }
558
- for (const entry of list) {
559
- const matched = matchRouteTokens(entry.tokens, req.path);
560
- if (!matched) {
694
+ const matchMethod = method === "HEAD" ? "GET" : method;
695
+ const pathname = normalizePathname(req.path);
696
+ const compiled = await getCompiled();
697
+ let matchedRoute = null;
698
+ for (const entry of compiled) {
699
+ if (entry.method !== matchMethod) {
561
700
  continue;
562
701
  }
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
- );
702
+ if (matchRouteTokens(entry.tokens, pathname)) {
703
+ matchedRoute = entry.route;
704
+ break;
705
+ }
575
706
  }
576
- return null;
707
+ if (!matchedRoute) {
708
+ return null;
709
+ }
710
+ if (typeof options.moduleBase === "undefined" && routeNeedsModuleBase(matchedRoute, options.moduleMap)) {
711
+ throw new Error("moduleBase is required for relative module paths.");
712
+ }
713
+ const app = await getApp();
714
+ const response = await app.fetch(toFetchRequest(req));
715
+ const resolvedResponse = applyRouteOverrides(response, matchedRoute);
716
+ return await toRuntimeResult(resolvedResponse);
577
717
  };
578
718
  return {
579
719
  handle
580
720
  };
581
721
  }
582
722
 
583
- export { compareRouteScore, createRuntime, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
723
+ export { compareRouteScore, createRuntime, createRuntimeApp, matchRouteTokens, normalizePathname, parseRouteTemplate, scoreRouteTokens };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/runtime",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.1.1",
5
5
  "description": "Cross-runtime mock matching and response handling for mokup.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -26,7 +26,9 @@
26
26
  "files": [
27
27
  "dist"
28
28
  ],
29
- "dependencies": {},
29
+ "dependencies": {
30
+ "@mokup/shared": "0.1.0"
31
+ },
30
32
  "devDependencies": {
31
33
  "typescript": "^5.9.3",
32
34
  "unbuild": "^3.6.1"