@hyperspan/framework 2.0.0 → 2.0.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/actions.js +109 -0
- package/dist/callsites.js +18 -0
- package/dist/client/css.js +6 -0
- package/dist/client/js.js +314 -0
- package/dist/client/manifest.js +55 -0
- package/dist/cookies.js +142 -0
- package/dist/dev-bindings.js +15 -0
- package/dist/fetch-handler.js +108 -0
- package/dist/html.js +1 -0
- package/dist/index.js +50 -0
- package/dist/island.js +17 -0
- package/dist/layout.js +42 -0
- package/dist/middleware.js +89 -0
- package/dist/register-routes.js +41 -0
- package/dist/server.js +580 -0
- package/dist/ssr/install-server-dom-mock.js +2 -0
- package/dist/ssr/mock-dom.js +370 -0
- package/dist/utils.js +157 -0
- package/package.json +25 -18
- package/.claude/settings.local.json +0 -9
- package/tsconfig.json +0 -25
package/dist/server.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
var __freeze = Object.freeze;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", { value: __freeze(raw || cooked.slice()) }));
|
|
4
|
+
var _a;
|
|
5
|
+
import "./ssr/install-server-dom-mock.js";
|
|
6
|
+
import { html, isHSHtml, renderStream, renderAsync, render, _typeOf } from "./html.js";
|
|
7
|
+
import { isbot } from "isbot";
|
|
8
|
+
import { executeMiddleware } from "./middleware.js";
|
|
9
|
+
import { buildUrl, parsePath, removeUndefined } from "./utils.js";
|
|
10
|
+
import { Cookies } from "./cookies.js";
|
|
11
|
+
import { nodeAdapter } from "@hyperspan/adapter-node";
|
|
12
|
+
const IS_PROD = process.env.NODE_ENV === "production";
|
|
13
|
+
function hyperspanDisableStreaming(context) {
|
|
14
|
+
return isbot(context.req.raw.headers.get("user-agent") ?? "");
|
|
15
|
+
}
|
|
16
|
+
const disableStreamingOptions = { hyperspanDisableStreaming };
|
|
17
|
+
function shouldDisableStreaming(disableStreaming, context) {
|
|
18
|
+
return disableStreaming?.(context, disableStreamingOptions) ?? false;
|
|
19
|
+
}
|
|
20
|
+
class HTTPResponseException extends Error {
|
|
21
|
+
_error;
|
|
22
|
+
_response;
|
|
23
|
+
constructor(body, options) {
|
|
24
|
+
super(body instanceof Error ? body.message : body);
|
|
25
|
+
this._error = body instanceof Error ? body : void 0;
|
|
26
|
+
this._response = new Response(body instanceof Error ? body.message : body, options);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function createConfig(config = {}) {
|
|
30
|
+
const defaultConfig = {
|
|
31
|
+
appDir: "./app",
|
|
32
|
+
publicDir: "./public",
|
|
33
|
+
plugins: [],
|
|
34
|
+
deployAdapter: config.deployAdapter ?? nodeAdapter(),
|
|
35
|
+
responseOptions: {
|
|
36
|
+
disableStreaming: hyperspanDisableStreaming
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
...defaultConfig,
|
|
41
|
+
...config,
|
|
42
|
+
deployAdapter: config.deployAdapter ?? defaultConfig.deployAdapter,
|
|
43
|
+
responseOptions: {
|
|
44
|
+
...defaultConfig.responseOptions,
|
|
45
|
+
...config.responseOptions
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createContext(req, route) {
|
|
50
|
+
const url = new URL(req.url);
|
|
51
|
+
const query = new URLSearchParams(url.search);
|
|
52
|
+
const method = req.method.toUpperCase();
|
|
53
|
+
const requestHeaders = new Headers(req.headers);
|
|
54
|
+
const responseHeaders = new Headers();
|
|
55
|
+
const path = route?._path() || "/";
|
|
56
|
+
const requestParams = req.params;
|
|
57
|
+
const params = Object.assign(
|
|
58
|
+
{},
|
|
59
|
+
requestParams || {},
|
|
60
|
+
removeUndefined(route?._config.params || {})
|
|
61
|
+
);
|
|
62
|
+
const catchAllParam = Object.keys(params).find((key) => key.startsWith("..."));
|
|
63
|
+
if (catchAllParam && path.includes("/*")) {
|
|
64
|
+
const catchAllValue = url.pathname.split(path.replace("/*", "/")).pop();
|
|
65
|
+
params[catchAllParam.replace("...", "")] = catchAllValue;
|
|
66
|
+
delete params[catchAllParam];
|
|
67
|
+
}
|
|
68
|
+
let status = void 0;
|
|
69
|
+
const copyHeaders = (source, target) => {
|
|
70
|
+
source.forEach((value, key) => {
|
|
71
|
+
if (key.toLowerCase() === "set-cookie") return;
|
|
72
|
+
target.set(key, value);
|
|
73
|
+
});
|
|
74
|
+
const cookies = typeof source.getSetCookie === "function" ? source.getSetCookie() : [];
|
|
75
|
+
for (const cookie of cookies) {
|
|
76
|
+
target.append("Set-Cookie", cookie);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const merge = async (response) => {
|
|
80
|
+
const mergedHeaders = new Headers();
|
|
81
|
+
copyHeaders(responseHeaders, mergedHeaders);
|
|
82
|
+
copyHeaders(response.headers, mergedHeaders);
|
|
83
|
+
return new Response(await response.text(), {
|
|
84
|
+
status: context.res.status ?? response.status,
|
|
85
|
+
headers: mergedHeaders
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const context = {
|
|
89
|
+
vars: {},
|
|
90
|
+
url: (diff, options) => {
|
|
91
|
+
const next = buildUrl(url, diff, options);
|
|
92
|
+
return `${next.pathname}${next.search}${next.hash}`;
|
|
93
|
+
},
|
|
94
|
+
route: {
|
|
95
|
+
name: route?._config.name || void 0,
|
|
96
|
+
path,
|
|
97
|
+
params,
|
|
98
|
+
cssImports: route ? route._config.cssImports ?? [] : []
|
|
99
|
+
},
|
|
100
|
+
req: {
|
|
101
|
+
raw: req,
|
|
102
|
+
url,
|
|
103
|
+
method,
|
|
104
|
+
headers: requestHeaders,
|
|
105
|
+
query,
|
|
106
|
+
cookies: new Cookies(req),
|
|
107
|
+
async text() {
|
|
108
|
+
return req.clone().text();
|
|
109
|
+
},
|
|
110
|
+
async json() {
|
|
111
|
+
return await req.clone().json();
|
|
112
|
+
},
|
|
113
|
+
async formData() {
|
|
114
|
+
return await req.clone().formData();
|
|
115
|
+
},
|
|
116
|
+
async urlencoded() {
|
|
117
|
+
return new URLSearchParams(await req.clone().text());
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
res: {
|
|
121
|
+
cookies: new Cookies(req, responseHeaders),
|
|
122
|
+
headers: responseHeaders,
|
|
123
|
+
status,
|
|
124
|
+
html: (html2, options) => merge(
|
|
125
|
+
new Response(html2, {
|
|
126
|
+
...options,
|
|
127
|
+
headers: { "Content-Type": "text/html; charset=UTF-8", ...options?.headers }
|
|
128
|
+
})
|
|
129
|
+
),
|
|
130
|
+
json: (json, options) => merge(
|
|
131
|
+
new Response(JSON.stringify(json), {
|
|
132
|
+
...options,
|
|
133
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
134
|
+
})
|
|
135
|
+
),
|
|
136
|
+
text: (text, options) => merge(
|
|
137
|
+
new Response(text, {
|
|
138
|
+
...options,
|
|
139
|
+
headers: { "Content-Type": "text/plain; charset=UTF-8", ...options?.headers }
|
|
140
|
+
})
|
|
141
|
+
),
|
|
142
|
+
redirect: (url2, options) => merge(new Response(null, { status: 302, headers: { Location: url2, ...options?.headers } })),
|
|
143
|
+
error: (error, options) => merge(new Response(error.message, { status: 500, ...options })),
|
|
144
|
+
notFound: (options) => merge(new Response("Not Found", { status: 404, ...options })),
|
|
145
|
+
merge
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return context;
|
|
149
|
+
}
|
|
150
|
+
function noContentResponse(context) {
|
|
151
|
+
return new Response(null, {
|
|
152
|
+
status: 204,
|
|
153
|
+
headers: context.res.headers
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function createRoute(config = {}) {
|
|
157
|
+
const _handlers = {};
|
|
158
|
+
let _errorHandler = void 0;
|
|
159
|
+
const api = {
|
|
160
|
+
_kind: "hsRoute",
|
|
161
|
+
_config: config,
|
|
162
|
+
_middleware: {
|
|
163
|
+
GET: [],
|
|
164
|
+
POST: [],
|
|
165
|
+
PUT: [],
|
|
166
|
+
PATCH: [],
|
|
167
|
+
DELETE: [],
|
|
168
|
+
HEAD: [],
|
|
169
|
+
OPTIONS: [],
|
|
170
|
+
"*": []
|
|
171
|
+
},
|
|
172
|
+
_methods: () => Object.keys(_handlers),
|
|
173
|
+
_path() {
|
|
174
|
+
if (this._config.path) {
|
|
175
|
+
const { path } = parsePath(this._config.path);
|
|
176
|
+
return path;
|
|
177
|
+
}
|
|
178
|
+
return "/";
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Add a GET route handler (primary page display)
|
|
182
|
+
*/
|
|
183
|
+
get(handler, handlerOptions) {
|
|
184
|
+
_handlers["GET"] = handler;
|
|
185
|
+
api._middleware["GET"] = handlerOptions?.middleware || [];
|
|
186
|
+
return api;
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* Add a POST route handler (typically to process form data)
|
|
190
|
+
*/
|
|
191
|
+
post(handler, handlerOptions) {
|
|
192
|
+
_handlers["POST"] = handler;
|
|
193
|
+
api._middleware["POST"] = handlerOptions?.middleware || [];
|
|
194
|
+
return api;
|
|
195
|
+
},
|
|
196
|
+
/**
|
|
197
|
+
* Add a PUT route handler (typically to update existing data)
|
|
198
|
+
*/
|
|
199
|
+
put(handler, handlerOptions) {
|
|
200
|
+
_handlers["PUT"] = handler;
|
|
201
|
+
api._middleware["PUT"] = handlerOptions?.middleware || [];
|
|
202
|
+
return api;
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* Add a PATCH route handler (typically to update existing data)
|
|
206
|
+
*/
|
|
207
|
+
patch(handler, handlerOptions) {
|
|
208
|
+
_handlers["PATCH"] = handler;
|
|
209
|
+
api._middleware["PATCH"] = handlerOptions?.middleware || [];
|
|
210
|
+
return api;
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* Add a DELETE route handler (typically to delete existing data)
|
|
214
|
+
*/
|
|
215
|
+
delete(handler, handlerOptions) {
|
|
216
|
+
_handlers["DELETE"] = handler;
|
|
217
|
+
api._middleware["DELETE"] = handlerOptions?.middleware || [];
|
|
218
|
+
return api;
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* Add a OPTIONS route handler (typically to handle CORS preflight requests)
|
|
222
|
+
*/
|
|
223
|
+
options(handler, handlerOptions) {
|
|
224
|
+
_handlers["OPTIONS"] = handler;
|
|
225
|
+
api._middleware["OPTIONS"] = handlerOptions?.middleware || [];
|
|
226
|
+
return api;
|
|
227
|
+
},
|
|
228
|
+
/**
|
|
229
|
+
* Add a ALL route handler (typically to handle all HTTP methods)
|
|
230
|
+
*/
|
|
231
|
+
all(handler, handlerOptions) {
|
|
232
|
+
_handlers["*"] = handler;
|
|
233
|
+
api._middleware["*"] = handlerOptions?.middleware || [];
|
|
234
|
+
return api;
|
|
235
|
+
},
|
|
236
|
+
/**
|
|
237
|
+
* Set a custom error handler for this route to fall back to if the route handler throws an error
|
|
238
|
+
*/
|
|
239
|
+
errorHandler(handler) {
|
|
240
|
+
_errorHandler = handler;
|
|
241
|
+
return api;
|
|
242
|
+
},
|
|
243
|
+
/**
|
|
244
|
+
* Add a middleware function to this route (for all HTTP methods) (non-destructive)
|
|
245
|
+
*/
|
|
246
|
+
use(middleware, opts = {}) {
|
|
247
|
+
if (opts?.methods) {
|
|
248
|
+
opts.methods.forEach((method) => {
|
|
249
|
+
api._middleware[method].push(middleware);
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
api._middleware["*"].push(middleware);
|
|
253
|
+
}
|
|
254
|
+
return api;
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* Set the complete middleware stack for this route (for all HTTP methods) (destructive)
|
|
258
|
+
* NOTE: This will override the middleware stack for this route
|
|
259
|
+
*/
|
|
260
|
+
middleware(middleware, opts = {}) {
|
|
261
|
+
if (opts?.methods) {
|
|
262
|
+
opts.methods.forEach((method) => {
|
|
263
|
+
api._middleware[method] = middleware;
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
api._middleware["*"] = middleware;
|
|
267
|
+
}
|
|
268
|
+
return api;
|
|
269
|
+
},
|
|
270
|
+
/**
|
|
271
|
+
* Fetch - handle a direct HTTP request
|
|
272
|
+
*/
|
|
273
|
+
async fetch(request) {
|
|
274
|
+
const context = createContext(request, api);
|
|
275
|
+
const method = context.req.method;
|
|
276
|
+
const globalMiddleware = api._middleware["*"] || [];
|
|
277
|
+
const methodMiddleware = api._middleware[method] || [];
|
|
278
|
+
const methodHandler = async (context2) => {
|
|
279
|
+
if (method === "OPTIONS" && !_handlers["OPTIONS"]) {
|
|
280
|
+
return context2.res.html(
|
|
281
|
+
render(html`
|
|
282
|
+
<!DOCTYPE html>
|
|
283
|
+
<html lang="en"></html>
|
|
284
|
+
`),
|
|
285
|
+
{
|
|
286
|
+
status: 200,
|
|
287
|
+
headers: {
|
|
288
|
+
"Access-Control-Allow-Origin": "*",
|
|
289
|
+
"Access-Control-Allow-Methods": ["HEAD", "OPTIONS", ...Object.keys(_handlers)].join(
|
|
290
|
+
", "
|
|
291
|
+
),
|
|
292
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const handler = (method === "HEAD" ? _handlers["GET"] : _handlers[method]) ?? _handlers["*"];
|
|
298
|
+
if (!handler) {
|
|
299
|
+
return context2.res.error(new Error("Method not allowed"), { status: 405 });
|
|
300
|
+
}
|
|
301
|
+
let routeContent;
|
|
302
|
+
try {
|
|
303
|
+
routeContent = await handler(context2);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
if (e instanceof HTTPResponseException) {
|
|
306
|
+
routeContent = e._response;
|
|
307
|
+
} else {
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (routeContent instanceof Response) {
|
|
312
|
+
return routeContent;
|
|
313
|
+
}
|
|
314
|
+
if (isHTMLContent(routeContent)) {
|
|
315
|
+
const responseOptions = {
|
|
316
|
+
...api._serverConfig?.responseOptions ?? {},
|
|
317
|
+
...api._config?.responseOptions ?? {}
|
|
318
|
+
};
|
|
319
|
+
return returnHTMLResponse(context2, () => routeContent, responseOptions);
|
|
320
|
+
}
|
|
321
|
+
const streamOptions = {
|
|
322
|
+
status: context2.res.status ?? 200,
|
|
323
|
+
headers: Object.fromEntries(context2.res.headers.entries())
|
|
324
|
+
};
|
|
325
|
+
const contentType = _typeOf(routeContent);
|
|
326
|
+
if (contentType === "generator") {
|
|
327
|
+
return new StreamResponse(routeContent, streamOptions);
|
|
328
|
+
}
|
|
329
|
+
if (routeContent instanceof ReadableStream) {
|
|
330
|
+
return new StreamResponse(routeContent, streamOptions);
|
|
331
|
+
}
|
|
332
|
+
if (routeContent === void 0 || routeContent === null) {
|
|
333
|
+
return noContentResponse(context2);
|
|
334
|
+
}
|
|
335
|
+
return routeContent;
|
|
336
|
+
};
|
|
337
|
+
try {
|
|
338
|
+
return await executeMiddleware(context, [
|
|
339
|
+
...globalMiddleware,
|
|
340
|
+
...methodMiddleware,
|
|
341
|
+
methodHandler
|
|
342
|
+
]);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (_errorHandler !== void 0) {
|
|
345
|
+
const responseOptions = {
|
|
346
|
+
...api._serverConfig?.responseOptions ?? {},
|
|
347
|
+
...api._config?.responseOptions ?? {}
|
|
348
|
+
};
|
|
349
|
+
return returnHTMLResponse(
|
|
350
|
+
context,
|
|
351
|
+
() => _errorHandler(context, e),
|
|
352
|
+
responseOptions
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
throw e;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
return api;
|
|
360
|
+
}
|
|
361
|
+
async function createServer(config = {}) {
|
|
362
|
+
const _routes = [];
|
|
363
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
364
|
+
await Promise.all(config.plugins.map((plugin) => plugin(config)));
|
|
365
|
+
}
|
|
366
|
+
const api = {
|
|
367
|
+
_config: config,
|
|
368
|
+
_routes,
|
|
369
|
+
_middleware: {
|
|
370
|
+
GET: [],
|
|
371
|
+
POST: [],
|
|
372
|
+
PUT: [],
|
|
373
|
+
PATCH: [],
|
|
374
|
+
DELETE: [],
|
|
375
|
+
HEAD: [],
|
|
376
|
+
OPTIONS: [],
|
|
377
|
+
"*": []
|
|
378
|
+
},
|
|
379
|
+
use(middleware, opts) {
|
|
380
|
+
if (opts?.methods) {
|
|
381
|
+
opts.methods.forEach((method) => {
|
|
382
|
+
api._middleware[method].push(middleware);
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
api._middleware["*"].push(middleware);
|
|
386
|
+
}
|
|
387
|
+
return this;
|
|
388
|
+
},
|
|
389
|
+
get(path, handler, handlerOptions) {
|
|
390
|
+
const route = createRoute().get(handler, handlerOptions);
|
|
391
|
+
route._config.path = path;
|
|
392
|
+
_routes.push(route);
|
|
393
|
+
return route;
|
|
394
|
+
},
|
|
395
|
+
post(path, handler, handlerOptions) {
|
|
396
|
+
const route = createRoute().post(handler, handlerOptions);
|
|
397
|
+
route._config.path = path;
|
|
398
|
+
_routes.push(route);
|
|
399
|
+
return route;
|
|
400
|
+
},
|
|
401
|
+
put(path, handler, handlerOptions) {
|
|
402
|
+
const route = createRoute().put(handler, handlerOptions);
|
|
403
|
+
route._config.path = path;
|
|
404
|
+
_routes.push(route);
|
|
405
|
+
return route;
|
|
406
|
+
},
|
|
407
|
+
delete(path, handler, handlerOptions) {
|
|
408
|
+
const route = createRoute().delete(handler, handlerOptions);
|
|
409
|
+
route._config.path = path;
|
|
410
|
+
_routes.push(route);
|
|
411
|
+
return route;
|
|
412
|
+
},
|
|
413
|
+
patch(path, handler, handlerOptions) {
|
|
414
|
+
const route = createRoute().patch(handler, handlerOptions);
|
|
415
|
+
route._config.path = path;
|
|
416
|
+
_routes.push(route);
|
|
417
|
+
return route;
|
|
418
|
+
},
|
|
419
|
+
options(path, handler, handlerOptions) {
|
|
420
|
+
const route = createRoute().options(handler, handlerOptions);
|
|
421
|
+
route._config.path = path;
|
|
422
|
+
_routes.push(route);
|
|
423
|
+
return route;
|
|
424
|
+
},
|
|
425
|
+
all(path, handler, handlerOptions) {
|
|
426
|
+
const route = createRoute().all(handler, handlerOptions);
|
|
427
|
+
route._config.path = path;
|
|
428
|
+
_routes.push(route);
|
|
429
|
+
return route;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
return api;
|
|
433
|
+
}
|
|
434
|
+
async function initServerRoutes(server, config, addRoutes) {
|
|
435
|
+
await config.beforeRoutesAdded?.(server);
|
|
436
|
+
await addRoutes(server);
|
|
437
|
+
await config.afterRoutesAdded?.(server);
|
|
438
|
+
}
|
|
439
|
+
function isHTMLContent(response) {
|
|
440
|
+
const hasHTMLContentType = response instanceof Response && response.headers.get("Content-Type") === "text/html";
|
|
441
|
+
const isHTMLTemplate = isHSHtml(response);
|
|
442
|
+
const isHTMLString = typeof response === "string" && response.trim().startsWith("<");
|
|
443
|
+
return hasHTMLContentType || isHTMLTemplate || isHTMLString;
|
|
444
|
+
}
|
|
445
|
+
async function returnHTMLResponse(context, handlerFn, responseOptions) {
|
|
446
|
+
try {
|
|
447
|
+
const routeContent = await handlerFn();
|
|
448
|
+
if (routeContent instanceof Response) {
|
|
449
|
+
return routeContent;
|
|
450
|
+
}
|
|
451
|
+
if (isHSHtml(routeContent)) {
|
|
452
|
+
const disableStreaming = shouldDisableStreaming(responseOptions?.disableStreaming, context);
|
|
453
|
+
if (!disableStreaming && routeContent.asyncContent?.length > 0) {
|
|
454
|
+
return new StreamResponse(
|
|
455
|
+
renderStream(routeContent, {
|
|
456
|
+
renderChunk: (chunk) => {
|
|
457
|
+
return html(_a || (_a = __template(['\n <template id="', '_content">', "<!--end--></template>\n <script>\n window._hsc = window._hsc || [];\n window._hsc.push({ id: '", "' });\n <\/script>\n <!--/hs:chunk-->\n "])), chunk.id, html.raw(chunk.content), chunk.id);
|
|
458
|
+
}
|
|
459
|
+
}),
|
|
460
|
+
responseOptions
|
|
461
|
+
);
|
|
462
|
+
} else {
|
|
463
|
+
const output = await renderAsync(routeContent);
|
|
464
|
+
return context.res.html(output, responseOptions);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return context.res.html(String(routeContent), responseOptions);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
!IS_PROD && console.error(e);
|
|
470
|
+
return await showErrorReponse(context, e, responseOptions);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function getRunnableRoute(route, routeConfig) {
|
|
474
|
+
if (isRunnableRoute(route)) {
|
|
475
|
+
return route;
|
|
476
|
+
}
|
|
477
|
+
const kind = typeof route;
|
|
478
|
+
if (kind === "object" && "default" in route) {
|
|
479
|
+
return getRunnableRoute(route.default, routeConfig);
|
|
480
|
+
}
|
|
481
|
+
throw new Error(
|
|
482
|
+
`Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route).join(", ")}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function isRunnableRoute(route) {
|
|
486
|
+
if (typeof route !== "object") {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const obj = route;
|
|
490
|
+
return typeof obj?._kind === "string" && "fetch" in obj;
|
|
491
|
+
}
|
|
492
|
+
async function showErrorReponse(context, err, responseOptions) {
|
|
493
|
+
let status = 500;
|
|
494
|
+
const message = err.message || "Internal Server Error";
|
|
495
|
+
if (err instanceof HTTPResponseException) {
|
|
496
|
+
status = err._response?.status ?? 500;
|
|
497
|
+
}
|
|
498
|
+
const stack = !IS_PROD && err.stack ? err.stack.split("\n").slice(1).join("\n") : "";
|
|
499
|
+
if (context.req.headers.get("X-Request-Type") === "partial") {
|
|
500
|
+
const output2 = render(html`
|
|
501
|
+
<section style="padding: 20px;">
|
|
502
|
+
<p style="margin-bottom: 10px;"><strong>Error</strong></p>
|
|
503
|
+
<strong>${message}</strong>
|
|
504
|
+
${stack ? html`<pre>${stack}</pre>` : ""}
|
|
505
|
+
</section>
|
|
506
|
+
`);
|
|
507
|
+
return context.res.html(output2, Object.assign({ status }, responseOptions));
|
|
508
|
+
}
|
|
509
|
+
const output = render(html`
|
|
510
|
+
<!DOCTYPE html>
|
|
511
|
+
<html lang="en">
|
|
512
|
+
<head>
|
|
513
|
+
<meta charset="UTF-8" />
|
|
514
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
515
|
+
<title>Application Error</title>
|
|
516
|
+
</head>
|
|
517
|
+
<body>
|
|
518
|
+
<main>
|
|
519
|
+
<h1>Application Error</h1>
|
|
520
|
+
<strong>${message}</strong>
|
|
521
|
+
${stack ? html`<pre>${stack}</pre>` : ""}
|
|
522
|
+
</main>
|
|
523
|
+
</body>
|
|
524
|
+
</html>
|
|
525
|
+
`);
|
|
526
|
+
return context.res.html(output, Object.assign({ status }, responseOptions));
|
|
527
|
+
}
|
|
528
|
+
class StreamResponse extends Response {
|
|
529
|
+
constructor(body, options = {}) {
|
|
530
|
+
super();
|
|
531
|
+
const { status, headers, ...restOptions } = options;
|
|
532
|
+
const stream = body instanceof ReadableStream ? body : createReadableStreamFromAsyncGenerator(body);
|
|
533
|
+
const defaultContentType = body instanceof ReadableStream ? "application/octet-stream" : "text/html; charset=UTF-8";
|
|
534
|
+
const mergedHeaders = new Headers({
|
|
535
|
+
"Transfer-Encoding": "chunked",
|
|
536
|
+
"Content-Type": defaultContentType,
|
|
537
|
+
"Content-Encoding": "Identity"
|
|
538
|
+
});
|
|
539
|
+
if (headers) {
|
|
540
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
541
|
+
mergedHeaders.set(key, value);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return new Response(stream, {
|
|
545
|
+
status: status ?? 200,
|
|
546
|
+
headers: mergedHeaders,
|
|
547
|
+
...restOptions
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function createReadableStreamFromAsyncGenerator(output) {
|
|
552
|
+
const encoder = new TextEncoder();
|
|
553
|
+
return new ReadableStream({
|
|
554
|
+
async start(controller) {
|
|
555
|
+
while (true) {
|
|
556
|
+
const { done, value } = await output.next();
|
|
557
|
+
if (done) {
|
|
558
|
+
controller.close();
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
controller.enqueue(encoder.encode(value));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
export {
|
|
567
|
+
HTTPResponseException,
|
|
568
|
+
IS_PROD,
|
|
569
|
+
StreamResponse,
|
|
570
|
+
createConfig,
|
|
571
|
+
createContext,
|
|
572
|
+
createReadableStreamFromAsyncGenerator,
|
|
573
|
+
createRoute,
|
|
574
|
+
createServer,
|
|
575
|
+
getRunnableRoute,
|
|
576
|
+
hyperspanDisableStreaming,
|
|
577
|
+
initServerRoutes,
|
|
578
|
+
isRunnableRoute,
|
|
579
|
+
returnHTMLResponse
|
|
580
|
+
};
|