@mokup/runtime 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +590 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.mts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.mjs +583 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ice breaker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const paramNamePattern = /^[\w-]+$/;
|
|
4
|
+
const paramPattern = /^\[([^\]/]+)\]$/;
|
|
5
|
+
const catchallPattern = /^\[\.\.\.([^\]/]+)\]$/;
|
|
6
|
+
const optionalCatchallPattern = /^\[\[\.\.\.([^\]/]+)\]\]$/;
|
|
7
|
+
const groupPattern = /^\([^)]+\)$/;
|
|
8
|
+
function decodeSegment(segment) {
|
|
9
|
+
try {
|
|
10
|
+
return decodeURIComponent(segment);
|
|
11
|
+
} catch {
|
|
12
|
+
return segment;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function scoreToken(token) {
|
|
16
|
+
switch (token.type) {
|
|
17
|
+
case "static":
|
|
18
|
+
return 4;
|
|
19
|
+
case "param":
|
|
20
|
+
return 3;
|
|
21
|
+
case "catchall":
|
|
22
|
+
return 2;
|
|
23
|
+
case "optional-catchall":
|
|
24
|
+
return 1;
|
|
25
|
+
default:
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function scoreRouteTokens(tokens) {
|
|
30
|
+
return tokens.map(scoreToken);
|
|
31
|
+
}
|
|
32
|
+
function compareRouteScore(a, b) {
|
|
33
|
+
const min = Math.min(a.length, b.length);
|
|
34
|
+
for (let i = 0; i < min; i += 1) {
|
|
35
|
+
const aValue = a[i] ?? 0;
|
|
36
|
+
const bValue = b[i] ?? 0;
|
|
37
|
+
if (aValue !== bValue) {
|
|
38
|
+
return bValue - aValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (a.length !== b.length) {
|
|
42
|
+
return b.length - a.length;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
function normalizePathname(value) {
|
|
47
|
+
const withoutQuery = value.split("?")[0] ?? "";
|
|
48
|
+
const withoutHash = withoutQuery.split("#")[0] ?? "";
|
|
49
|
+
let normalized = withoutHash.startsWith("/") ? withoutHash : `/${withoutHash}`;
|
|
50
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
51
|
+
normalized = normalized.slice(0, -1);
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
function splitPath(value) {
|
|
56
|
+
return normalizePathname(value).split("/").filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
function parseRouteTemplate(template) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
const warnings = [];
|
|
61
|
+
const normalized = normalizePathname(template);
|
|
62
|
+
const segments = splitPath(normalized);
|
|
63
|
+
const tokens = [];
|
|
64
|
+
const seenParams = /* @__PURE__ */ new Set();
|
|
65
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
66
|
+
const segment = segments[index];
|
|
67
|
+
if (!segment) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (groupPattern.test(segment)) {
|
|
71
|
+
errors.push(`Route groups are not supported: ${segment}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const optionalCatchallMatch = segment.match(optionalCatchallPattern);
|
|
75
|
+
if (optionalCatchallMatch) {
|
|
76
|
+
const name = optionalCatchallMatch[1];
|
|
77
|
+
if (!name) {
|
|
78
|
+
errors.push(`Invalid optional catch-all param name "${segment}"`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!paramNamePattern.test(name)) {
|
|
82
|
+
errors.push(`Invalid optional catch-all param name "${name}"`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (index !== segments.length - 1) {
|
|
86
|
+
errors.push(`Optional catch-all "${segment}" must be the last segment`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (seenParams.has(name)) {
|
|
90
|
+
warnings.push(`Duplicate param name "${name}"`);
|
|
91
|
+
}
|
|
92
|
+
seenParams.add(name);
|
|
93
|
+
tokens.push({ type: "optional-catchall", name });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const catchallMatch = segment.match(catchallPattern);
|
|
97
|
+
if (catchallMatch) {
|
|
98
|
+
const name = catchallMatch[1];
|
|
99
|
+
if (!name) {
|
|
100
|
+
errors.push(`Invalid catch-all param name "${segment}"`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!paramNamePattern.test(name)) {
|
|
104
|
+
errors.push(`Invalid catch-all param name "${name}"`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (index !== segments.length - 1) {
|
|
108
|
+
errors.push(`Catch-all "${segment}" must be the last segment`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (seenParams.has(name)) {
|
|
112
|
+
warnings.push(`Duplicate param name "${name}"`);
|
|
113
|
+
}
|
|
114
|
+
seenParams.add(name);
|
|
115
|
+
tokens.push({ type: "catchall", name });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const paramMatch = segment.match(paramPattern);
|
|
119
|
+
if (paramMatch) {
|
|
120
|
+
const name = paramMatch[1];
|
|
121
|
+
if (!name) {
|
|
122
|
+
errors.push(`Invalid param name "${segment}"`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!paramNamePattern.test(name)) {
|
|
126
|
+
errors.push(`Invalid param name "${name}"`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (seenParams.has(name)) {
|
|
130
|
+
warnings.push(`Duplicate param name "${name}"`);
|
|
131
|
+
}
|
|
132
|
+
seenParams.add(name);
|
|
133
|
+
tokens.push({ type: "param", name });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (segment.includes("[") || segment.includes("]") || segment.includes("(") || segment.includes(")")) {
|
|
137
|
+
errors.push(`Invalid route segment "${segment}"`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
tokens.push({ type: "static", value: segment });
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
template: normalized,
|
|
144
|
+
tokens,
|
|
145
|
+
score: scoreRouteTokens(tokens),
|
|
146
|
+
errors,
|
|
147
|
+
warnings
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function matchRouteTokens(tokens, pathname) {
|
|
151
|
+
const segments = splitPath(pathname);
|
|
152
|
+
const params = {};
|
|
153
|
+
let index = 0;
|
|
154
|
+
for (const token of tokens) {
|
|
155
|
+
if (token.type === "static") {
|
|
156
|
+
const segment = segments[index];
|
|
157
|
+
if (segment !== token.value) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
index += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (token.type === "param") {
|
|
164
|
+
const segment = segments[index];
|
|
165
|
+
if (!segment) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
params[token.name] = decodeSegment(segment);
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (token.type === "catchall") {
|
|
173
|
+
if (index >= segments.length) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
params[token.name] = segments.slice(index).map(decodeSegment);
|
|
177
|
+
index = segments.length;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (token.type === "optional-catchall") {
|
|
181
|
+
params[token.name] = segments.slice(index).map(decodeSegment);
|
|
182
|
+
index = segments.length;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (index !== segments.length) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return { params };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveModuleUrl(modulePath, moduleBase) {
|
|
193
|
+
if (/^(?:data|http|https|file):/.test(modulePath)) {
|
|
194
|
+
return modulePath;
|
|
195
|
+
}
|
|
196
|
+
if (!moduleBase) {
|
|
197
|
+
throw new Error("moduleBase is required for relative module paths.");
|
|
198
|
+
}
|
|
199
|
+
const base = typeof moduleBase === "string" ? moduleBase : moduleBase.href;
|
|
200
|
+
if (/^(?:data|http|https|file):/.test(base)) {
|
|
201
|
+
return new URL(modulePath, base).href;
|
|
202
|
+
}
|
|
203
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
204
|
+
const normalizedModule = modulePath.startsWith("./") ? modulePath.slice(2) : modulePath.startsWith("/") ? modulePath.slice(1) : modulePath;
|
|
205
|
+
return `${normalizedBase}${normalizedModule}`;
|
|
206
|
+
}
|
|
207
|
+
function normalizeRules(value) {
|
|
208
|
+
if (!value) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "function") {
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
response: value
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === "object") {
|
|
222
|
+
return [value];
|
|
223
|
+
}
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
response: value
|
|
227
|
+
}
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
async function executeRule(rule, req, responder, ctx) {
|
|
231
|
+
if (!rule) {
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
const value = rule.response;
|
|
235
|
+
if (typeof value === "function") {
|
|
236
|
+
const handler = value;
|
|
237
|
+
return handler(req, responder, ctx);
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
function extractMiddlewareSource(value) {
|
|
242
|
+
if (value && typeof value === "object" && "middleware" in value) {
|
|
243
|
+
return value.middleware;
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
function normalizeMiddleware(value) {
|
|
248
|
+
const resolved = extractMiddlewareSource(value);
|
|
249
|
+
if (!resolved) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(resolved)) {
|
|
253
|
+
return resolved.filter((entry) => typeof entry === "function");
|
|
254
|
+
}
|
|
255
|
+
if (typeof resolved === "function") {
|
|
256
|
+
return [resolved];
|
|
257
|
+
}
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
async function loadModuleExport(modulePath, exportName, moduleBase, moduleMap) {
|
|
261
|
+
const directMapValue = moduleMap?.[modulePath];
|
|
262
|
+
const resolvedUrl = directMapValue ? void 0 : resolveModuleUrl(modulePath, moduleBase);
|
|
263
|
+
const resolvedMapValue = resolvedUrl ? moduleMap?.[resolvedUrl] : void 0;
|
|
264
|
+
const moduleValue = directMapValue ?? resolvedMapValue;
|
|
265
|
+
const module = moduleValue ?? await import(resolvedUrl ?? modulePath);
|
|
266
|
+
return module[exportName] ?? module.default ?? module;
|
|
267
|
+
}
|
|
268
|
+
function resolveModuleCacheKey(modulePath, exportName, moduleBase, moduleMap) {
|
|
269
|
+
if (moduleMap?.[modulePath]) {
|
|
270
|
+
return `${modulePath}::${exportName}`;
|
|
271
|
+
}
|
|
272
|
+
const resolvedUrl = resolveModuleUrl(modulePath, moduleBase);
|
|
273
|
+
return `${resolvedUrl}::${exportName}`;
|
|
274
|
+
}
|
|
275
|
+
async function loadModuleRule(response, moduleCache, moduleBase, moduleMap) {
|
|
276
|
+
const exportName = response.exportName ?? "default";
|
|
277
|
+
const cacheKey = resolveModuleCacheKey(
|
|
278
|
+
response.module,
|
|
279
|
+
exportName,
|
|
280
|
+
moduleBase,
|
|
281
|
+
moduleMap
|
|
282
|
+
);
|
|
283
|
+
let rules = moduleCache.get(cacheKey);
|
|
284
|
+
if (!rules) {
|
|
285
|
+
const exported = await loadModuleExport(
|
|
286
|
+
response.module,
|
|
287
|
+
exportName,
|
|
288
|
+
moduleBase,
|
|
289
|
+
moduleMap
|
|
290
|
+
);
|
|
291
|
+
rules = normalizeRules(exported);
|
|
292
|
+
moduleCache.set(cacheKey, rules);
|
|
293
|
+
}
|
|
294
|
+
if (typeof response.ruleIndex === "number") {
|
|
295
|
+
return rules[response.ruleIndex];
|
|
296
|
+
}
|
|
297
|
+
return rules[0];
|
|
298
|
+
}
|
|
299
|
+
async function loadModuleMiddleware(middleware, middlewareCache, moduleBase, moduleMap) {
|
|
300
|
+
const exportName = middleware.exportName ?? "default";
|
|
301
|
+
const cacheKey = resolveModuleCacheKey(
|
|
302
|
+
middleware.module,
|
|
303
|
+
exportName,
|
|
304
|
+
moduleBase,
|
|
305
|
+
moduleMap
|
|
306
|
+
);
|
|
307
|
+
let handlers = middlewareCache.get(cacheKey);
|
|
308
|
+
if (!handlers) {
|
|
309
|
+
const exported = await loadModuleExport(
|
|
310
|
+
middleware.module,
|
|
311
|
+
exportName,
|
|
312
|
+
moduleBase,
|
|
313
|
+
moduleMap
|
|
314
|
+
);
|
|
315
|
+
handlers = normalizeMiddleware(exported);
|
|
316
|
+
middlewareCache.set(cacheKey, handlers);
|
|
317
|
+
}
|
|
318
|
+
if (typeof middleware.ruleIndex === "number") {
|
|
319
|
+
return handlers[middleware.ruleIndex];
|
|
320
|
+
}
|
|
321
|
+
return handlers[0];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const methodSet = /* @__PURE__ */ new Set([
|
|
325
|
+
"GET",
|
|
326
|
+
"POST",
|
|
327
|
+
"PUT",
|
|
328
|
+
"PATCH",
|
|
329
|
+
"DELETE",
|
|
330
|
+
"OPTIONS",
|
|
331
|
+
"HEAD"
|
|
332
|
+
]);
|
|
333
|
+
function normalizeMethod(method) {
|
|
334
|
+
if (!method) {
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
const normalized = method.toUpperCase();
|
|
338
|
+
if (methodSet.has(normalized)) {
|
|
339
|
+
return normalized;
|
|
340
|
+
}
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
function mergeHeaders(base, override) {
|
|
344
|
+
if (!override) {
|
|
345
|
+
return base;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...base,
|
|
349
|
+
...override
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function delay(ms) {
|
|
353
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
354
|
+
}
|
|
355
|
+
|
|
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;
|
|
372
|
+
}
|
|
373
|
+
return record;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
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
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (body instanceof Uint8Array) {
|
|
387
|
+
return {
|
|
388
|
+
body,
|
|
389
|
+
contentType: contentType ?? "application/octet-stream"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (body instanceof ArrayBuffer) {
|
|
393
|
+
return {
|
|
394
|
+
body: new Uint8Array(body),
|
|
395
|
+
contentType: contentType ?? "application/octet-stream"
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
body: JSON.stringify(body),
|
|
400
|
+
contentType: contentType ?? "application/json; charset=utf-8"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
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);
|
|
409
|
+
}
|
|
410
|
+
return bytes;
|
|
411
|
+
}
|
|
412
|
+
throw new Error("Base64 decoding is not supported in this runtime.");
|
|
413
|
+
}
|
|
414
|
+
function finalizeStatus(status, body) {
|
|
415
|
+
if (status === 200 && body === null) {
|
|
416
|
+
return 204;
|
|
417
|
+
}
|
|
418
|
+
return status;
|
|
419
|
+
}
|
|
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 () => {
|
|
428
|
+
if (route.response.type === "json") {
|
|
429
|
+
return {
|
|
430
|
+
value: route.response.body,
|
|
431
|
+
contentType: "application/json; charset=utf-8"
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (route.response.type === "text") {
|
|
435
|
+
return {
|
|
436
|
+
value: route.response.body,
|
|
437
|
+
contentType: "text/plain; charset=utf-8"
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (route.response.type === "binary") {
|
|
441
|
+
return {
|
|
442
|
+
value: decodeBase64(route.response.body),
|
|
443
|
+
contentType: "application/octet-stream"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const rule = await loadModuleRule(
|
|
447
|
+
route.response,
|
|
448
|
+
moduleCache,
|
|
449
|
+
moduleBase,
|
|
450
|
+
moduleMap
|
|
451
|
+
);
|
|
452
|
+
const value = await executeRule(rule, req, responder, ctx);
|
|
453
|
+
return { value };
|
|
454
|
+
};
|
|
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);
|
|
481
|
+
};
|
|
482
|
+
const middlewareHandlers = [];
|
|
483
|
+
for (const entry of route.middleware ?? []) {
|
|
484
|
+
const handler = await loadModuleMiddleware(
|
|
485
|
+
entry,
|
|
486
|
+
middlewareCache,
|
|
487
|
+
moduleBase,
|
|
488
|
+
moduleMap
|
|
489
|
+
);
|
|
490
|
+
if (handler) {
|
|
491
|
+
middlewareHandlers.push(handler);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
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);
|
|
499
|
+
}
|
|
500
|
+
const headers = mergeHeaders(responder.toRecord(), route.headers);
|
|
501
|
+
if (contentType && !headers["content-type"]) {
|
|
502
|
+
headers["content-type"] = contentType;
|
|
503
|
+
}
|
|
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;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
status,
|
|
514
|
+
headers,
|
|
515
|
+
body: normalized.body
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function createRuntime(options) {
|
|
519
|
+
let manifestCache = null;
|
|
520
|
+
let compiledRoutes = null;
|
|
521
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
522
|
+
const middlewareCache = /* @__PURE__ */ new Map();
|
|
523
|
+
const getManifest = async () => {
|
|
524
|
+
if (!manifestCache) {
|
|
525
|
+
manifestCache = typeof options.manifest === "function" ? await options.manifest() : options.manifest;
|
|
526
|
+
}
|
|
527
|
+
return manifestCache;
|
|
528
|
+
};
|
|
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));
|
|
549
|
+
}
|
|
550
|
+
compiledRoutes = map;
|
|
551
|
+
return compiledRoutes;
|
|
552
|
+
};
|
|
553
|
+
const handle = async (req) => {
|
|
554
|
+
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) {
|
|
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;
|
|
579
|
+
};
|
|
580
|
+
return {
|
|
581
|
+
handle
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
exports.compareRouteScore = compareRouteScore;
|
|
586
|
+
exports.createRuntime = createRuntime;
|
|
587
|
+
exports.matchRouteTokens = matchRouteTokens;
|
|
588
|
+
exports.normalizePathname = normalizePathname;
|
|
589
|
+
exports.parseRouteTemplate = parseRouteTemplate;
|
|
590
|
+
exports.scoreRouteTokens = scoreRouteTokens;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
type RouteToken = {
|
|
2
|
+
type: 'static';
|
|
3
|
+
value: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'param';
|
|
6
|
+
name: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'catchall';
|
|
9
|
+
name: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'optional-catchall';
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
interface ParsedRouteTemplate {
|
|
15
|
+
template: string;
|
|
16
|
+
tokens: RouteToken[];
|
|
17
|
+
score: number[];
|
|
18
|
+
errors: string[];
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
declare function scoreRouteTokens(tokens: RouteToken[]): (4 | 3 | 2 | 1 | 0)[];
|
|
22
|
+
declare function compareRouteScore(a: number[], b: number[]): number;
|
|
23
|
+
declare function normalizePathname(value: string): string;
|
|
24
|
+
declare function parseRouteTemplate(template: string): ParsedRouteTemplate;
|
|
25
|
+
declare function matchRouteTokens(tokens: RouteToken[], pathname: string): {
|
|
26
|
+
params: Record<string, string | string[]>;
|
|
27
|
+
} | null;
|
|
28
|
+
|
|
29
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
|
30
|
+
interface Manifest {
|
|
31
|
+
version: 1;
|
|
32
|
+
routes: ManifestRoute[];
|
|
33
|
+
}
|
|
34
|
+
interface ManifestRoute {
|
|
35
|
+
method: HttpMethod;
|
|
36
|
+
url: string;
|
|
37
|
+
tokens?: RouteToken[];
|
|
38
|
+
score?: number[];
|
|
39
|
+
source?: string;
|
|
40
|
+
status?: number;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
delay?: number;
|
|
43
|
+
middleware?: ManifestModuleRef[];
|
|
44
|
+
response: ManifestResponse;
|
|
45
|
+
}
|
|
46
|
+
interface ManifestModuleRef {
|
|
47
|
+
module: string;
|
|
48
|
+
exportName?: string;
|
|
49
|
+
ruleIndex?: number;
|
|
50
|
+
}
|
|
51
|
+
type ManifestResponse = {
|
|
52
|
+
type: 'json';
|
|
53
|
+
body: unknown;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'text';
|
|
56
|
+
body: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'binary';
|
|
59
|
+
body: string;
|
|
60
|
+
encoding: 'base64';
|
|
61
|
+
} | ({
|
|
62
|
+
type: 'module';
|
|
63
|
+
} & ManifestModuleRef);
|
|
64
|
+
interface RuntimeRequest {
|
|
65
|
+
method: string;
|
|
66
|
+
path: string;
|
|
67
|
+
query: Record<string, string | string[]>;
|
|
68
|
+
headers: Record<string, string>;
|
|
69
|
+
body: unknown;
|
|
70
|
+
rawBody?: string;
|
|
71
|
+
params?: Record<string, string | string[]>;
|
|
72
|
+
}
|
|
73
|
+
interface RuntimeResult {
|
|
74
|
+
status: number;
|
|
75
|
+
headers: Record<string, string>;
|
|
76
|
+
body: string | Uint8Array | null;
|
|
77
|
+
}
|
|
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>;
|
|
90
|
+
interface RuntimeOptions {
|
|
91
|
+
manifest: Manifest | (() => Promise<Manifest>);
|
|
92
|
+
moduleBase?: string | URL;
|
|
93
|
+
moduleMap?: ModuleMap;
|
|
94
|
+
}
|
|
95
|
+
type ModuleMap = Record<string, Record<string, unknown>>;
|
|
96
|
+
|
|
97
|
+
declare function createRuntime(options: RuntimeOptions): {
|
|
98
|
+
handle: (req: RuntimeRequest) => Promise<RuntimeResult | null>;
|
|
99
|
+
};
|
|
100
|
+
|
|
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 };
|