@webqit/webflo 0.11.61 → 0.20.2-next.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/.github/FUNDING.yml +12 -0
- package/.github/workflows/publish.yml +48 -0
- package/.gitignore +2 -0
- package/LICENSE +2 -2
- package/README.md +71 -2050
- package/package.json +28 -13
- package/site/-/_.md +139 -0
- package/site/-/docs.old.md +2010 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url 2.js +93 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js 2.map +7 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/site/.vitepress/cache/deps/_metadata 2.json +85 -0
- package/site/.vitepress/cache/deps/_metadata.json +85 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4 2.js +9 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK 2.js +9719 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js +9719 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js.map +7 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7 2.js +12705 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js +12705 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape 2.js +30278 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent 2.js +4710 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js 2.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/site/.vitepress/cache/deps/cytoscape.js 2.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/site/.vitepress/cache/deps/dayjs 2.js +285 -0
- package/site/.vitepress/cache/deps/dayjs.js +285 -0
- package/site/.vitepress/cache/deps/dayjs.js 2.map +7 -0
- package/site/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/site/.vitepress/cache/deps/debug 2.js +453 -0
- package/site/.vitepress/cache/deps/debug.js +453 -0
- package/site/.vitepress/cache/deps/debug.js 2.map +7 -0
- package/site/.vitepress/cache/deps/debug.js.map +7 -0
- package/site/.vitepress/cache/deps/package 2.json +3 -0
- package/site/.vitepress/cache/deps/package.json +3 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api 2.js +4507 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core 2.js +584 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap 2.js +1166 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1166 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js 2.js +1667 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch 2.js +1815 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js +1815 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/site/.vitepress/cache/deps/vue 2.js +344 -0
- package/site/.vitepress/cache/deps/vue.js +344 -0
- package/site/.vitepress/cache/deps/vue.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vue.js.map +7 -0
- package/site/.vitepress/config.ts +147 -0
- package/site/.vitepress/theme/custom.css +50 -0
- package/site/.vitepress/theme/index.ts +6 -0
- package/site/api/webflo-fetch/FormData.md +0 -0
- package/site/api/webflo-fetch/Headers.md +0 -0
- package/site/api/webflo-fetch/LiveResponse.md +0 -0
- package/site/api/webflo-fetch/Request.md +0 -0
- package/site/api/webflo-fetch/Response.md +0 -0
- package/site/api/webflo-fetch/fetch.md +0 -0
- package/site/api/webflo-routing/HttpCookies.md +0 -0
- package/site/api/webflo-routing/HttpEvent/respondWith.md +1 -0
- package/site/api/webflo-routing/HttpEvent/waitUntil.md +1 -0
- package/site/api/webflo-routing/HttpEvent/waitUntilNavigate.md +1 -0
- package/site/api/webflo-routing/HttpEvent.md +30 -0
- package/site/api/webflo-routing/HttpSession.md +0 -0
- package/site/api/webflo-routing/HttpState.md +0 -0
- package/site/api/webflo-routing/HttpUser.md +0 -0
- package/site/api/webflo-routing/handler/fetch.md +42 -0
- package/site/api/webflo-routing/handler/next.md +54 -0
- package/site/api/webflo-routing/handler.md +119 -0
- package/site/api.md +26 -0
- package/site/contributing.md +16 -0
- package/site/docs/advanced/lifecycles.md +20 -0
- package/site/docs/advanced/redirects.md +0 -0
- package/site/docs/advanced/routing.md +1 -0
- package/site/docs/advanced.md +9 -0
- package/site/docs/concepts/realtime.md +637 -0
- package/site/docs/concepts/rendering.md +60 -0
- package/site/docs/concepts/request-response.md +47 -0
- package/site/docs/concepts/routing.md +656 -0
- package/site/docs/concepts/state.md +44 -0
- package/site/docs/concepts/templates.md +48 -0
- package/site/docs/concepts.md +97 -0
- package/site/docs/getting-started.md +378 -0
- package/site/docs/tech-stack.md +56 -0
- package/site/docs.md +100 -0
- package/site/examples/pwa.md +10 -0
- package/site/examples/web.md +11 -0
- package/site/examples.md +10 -0
- package/site/faq.md +13 -0
- package/site/guides/guide-auth.md +13 -0
- package/site/guides/guide-file-upload.md +11 -0
- package/site/guides/guide-service-worker.md +10 -0
- package/site/guides/tutorial-1-todo.md +24 -0
- package/site/guides.md +15 -0
- package/site/index.md +39 -0
- package/site/public/img/brand/logo-670x670.png +0 -0
- package/site/recipes/realtime.md +11 -0
- package/site/recipes/streaming.md +15 -0
- package/site/reference/cli.md +11 -0
- package/site/reference/config.md +13 -0
- package/site/reference/tools.md +9 -0
- package/src/Context.js +3 -11
- package/src/config-pi/deployment/Env.js +6 -19
- package/src/config-pi/deployment/Layout.js +11 -3
- package/src/config-pi/runtime/Client.js +40 -48
- package/src/config-pi/runtime/Server.js +52 -20
- package/src/config-pi/runtime/client/Worker.js +22 -20
- package/src/config-pi/static/Init.js +57 -0
- package/src/config-pi/static/index.js +2 -0
- package/src/deployment-pi/origins/index.js +1 -1
- package/src/deployment-pi/util.js +161 -0
- package/src/index.js +3 -9
- package/src/init-pi/index.js +117 -0
- package/src/init-pi/templates/pwa/app/handler.server.js +8 -0
- package/src/init-pi/templates/pwa/app/page.html +7 -0
- package/src/init-pi/templates/pwa/package.json +19 -0
- package/src/init-pi/templates/pwa/public/assets/app.css +16 -0
- package/src/init-pi/templates/pwa/public/index.html +39 -0
- package/src/init-pi/templates/pwa/public/manifest.json +29 -0
- package/src/init-pi/templates/web/app/handler.server.js +8 -0
- package/src/init-pi/templates/web/app/page.html +7 -0
- package/src/init-pi/templates/web/package.json +19 -0
- package/src/init-pi/templates/web/public/assets/app.css +16 -0
- package/src/init-pi/templates/web/public/index.html +39 -0
- package/src/runtime-pi/WebfloRuntime.js +350 -0
- package/src/runtime-pi/index.js +3 -10
- package/src/runtime-pi/webflo-client/ClientSideCookies.js +17 -0
- package/src/runtime-pi/webflo-client/ClientSideWorkport.js +63 -0
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +213 -0
- package/src/runtime-pi/webflo-client/WebfloClient.js +500 -0
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +206 -0
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +113 -0
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +118 -0
- package/src/runtime-pi/webflo-client/index.js +17 -0
- package/src/runtime-pi/webflo-client/webflo-codegen.js +469 -0
- package/src/runtime-pi/webflo-client/webflo-devmode.js +243 -0
- package/src/runtime-pi/webflo-client/webflo-embedded.js +50 -0
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +437 -0
- package/src/runtime-pi/webflo-fetch/cookies.js +10 -0
- package/src/runtime-pi/webflo-fetch/fetch.js +16 -0
- package/src/runtime-pi/webflo-fetch/formdata.js +54 -0
- package/src/runtime-pi/webflo-fetch/headers.js +151 -0
- package/src/runtime-pi/webflo-fetch/index.js +5 -0
- package/src/runtime-pi/webflo-fetch/message.js +49 -0
- package/src/runtime-pi/webflo-fetch/request.js +62 -0
- package/src/runtime-pi/webflo-fetch/response.js +110 -0
- package/src/runtime-pi/webflo-fetch/util.js +28 -0
- package/src/runtime-pi/webflo-messaging/WQBroadcastChannel.js +10 -0
- package/src/runtime-pi/webflo-messaging/WQMessageChannel.js +26 -0
- package/src/runtime-pi/webflo-messaging/WQMessageEvent.js +87 -0
- package/src/runtime-pi/webflo-messaging/WQMessagePort.js +38 -0
- package/src/runtime-pi/webflo-messaging/WQRelayPort.js +47 -0
- package/src/runtime-pi/webflo-messaging/WQSockPort.js +113 -0
- package/src/runtime-pi/webflo-messaging/WQStarPort.js +104 -0
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +404 -0
- package/src/runtime-pi/webflo-routing/HttpCookies.js +42 -0
- package/src/runtime-pi/webflo-routing/HttpEvent.js +112 -0
- package/src/runtime-pi/webflo-routing/HttpSession.js +11 -0
- package/src/runtime-pi/webflo-routing/HttpState.js +153 -0
- package/src/runtime-pi/webflo-routing/HttpUser.js +54 -0
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +245 -0
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +19 -0
- package/src/runtime-pi/webflo-server/ServerSideSession.js +38 -0
- package/src/runtime-pi/webflo-server/WebfloServer.js +937 -0
- package/src/runtime-pi/webflo-server/index.js +11 -0
- package/src/runtime-pi/webflo-server/messaging/Client.js +27 -0
- package/src/runtime-pi/webflo-server/messaging/ClientRequestRealtime.js +50 -0
- package/src/runtime-pi/webflo-server/messaging/Clients.js +25 -0
- package/src/runtime-pi/webflo-server/webflo-devmode.js +326 -0
- package/src/runtime-pi/{client → webflo-url}/Url.js +27 -76
- package/src/runtime-pi/webflo-url/index.js +1 -0
- package/src/runtime-pi/webflo-url/urlpattern.js +38 -0
- package/src/runtime-pi/{util-url.js → webflo-url/util.js} +5 -43
- package/src/runtime-pi/webflo-url/xURL.js +94 -0
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +234 -0
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +19 -0
- package/src/runtime-pi/webflo-worker/WorkerSideWorkport.js +18 -0
- package/src/runtime-pi/webflo-worker/index.js +11 -0
- package/src/services-pi/index.js +2 -0
- package/src/services-pi/push/index.js +23 -0
- package/src/util.js +10 -0
- package/src/{webflo.js → webflo-cli.js} +4 -4
- package/src/runtime-pi/Application.js +0 -29
- package/src/runtime-pi/Cookies.js +0 -82
- package/src/runtime-pi/HttpEvent.js +0 -107
- package/src/runtime-pi/Router.js +0 -130
- package/src/runtime-pi/Runtime.js +0 -21
- package/src/runtime-pi/client/Application.js +0 -76
- package/src/runtime-pi/client/Context.js +0 -7
- package/src/runtime-pi/client/Router.js +0 -48
- package/src/runtime-pi/client/Runtime.js +0 -525
- package/src/runtime-pi/client/Workport.js +0 -190
- package/src/runtime-pi/client/createStorage.js +0 -58
- package/src/runtime-pi/client/generate.js +0 -481
- package/src/runtime-pi/client/index.js +0 -21
- package/src/runtime-pi/client/worker/Application.js +0 -44
- package/src/runtime-pi/client/worker/Context.js +0 -7
- package/src/runtime-pi/client/worker/Runtime.js +0 -275
- package/src/runtime-pi/client/worker/Workport.js +0 -78
- package/src/runtime-pi/client/worker/index.js +0 -21
- package/src/runtime-pi/server/Application.js +0 -101
- package/src/runtime-pi/server/Context.js +0 -16
- package/src/runtime-pi/server/Router.js +0 -159
- package/src/runtime-pi/server/Runtime.js +0 -558
- package/src/runtime-pi/server/index.js +0 -21
- package/src/runtime-pi/util-http.js +0 -86
- package/src/runtime-pi/xFormData.js +0 -24
- package/src/runtime-pi/xHeaders.js +0 -146
- package/src/runtime-pi/xRequest.js +0 -46
- package/src/runtime-pi/xRequestHeaders.js +0 -109
- package/src/runtime-pi/xResponse.js +0 -33
- package/src/runtime-pi/xResponseHeaders.js +0 -117
- package/src/runtime-pi/xURL.js +0 -105
- package/src/runtime-pi/xfetch.js +0 -23
- package/src/runtime-pi/xxHttpMessage.js +0 -102
- package/src/static-pi/index.js +0 -11
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import Fs from 'fs';
|
|
2
|
+
import Url from 'url';
|
|
3
|
+
import Path from 'path';
|
|
4
|
+
import Http from 'http';
|
|
5
|
+
import Https from 'https';
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import Mime from 'mime-types';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import 'dotenv/config';
|
|
10
|
+
import { Readable } from 'stream';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { Observer } from '@webqit/quantum-js';
|
|
13
|
+
import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
|
|
14
|
+
import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
|
|
15
|
+
import { _each } from '@webqit/util/obj/index.js';
|
|
16
|
+
import { WebfloHMR, openBrowser } from './webflo-devmode.js';
|
|
17
|
+
import { Clients } from './messaging/Clients.js';
|
|
18
|
+
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
19
|
+
import { WQSockPort } from '../webflo-messaging/WQSockPort.js';
|
|
20
|
+
import { ServerSideCookies } from './ServerSideCookies.js';
|
|
21
|
+
import { ServerSideSession } from './ServerSideSession.js';
|
|
22
|
+
import { HttpEvent } from '../webflo-routing/HttpEvent.js';
|
|
23
|
+
import { HttpUser } from '../webflo-routing/HttpUser.js';
|
|
24
|
+
import { createWindow } from '@webqit/oohtml-ssr';
|
|
25
|
+
import {
|
|
26
|
+
readServerConfig,
|
|
27
|
+
readHeadersConfig,
|
|
28
|
+
readRedirectsConfig,
|
|
29
|
+
readLayoutConfig,
|
|
30
|
+
readEnvConfig,
|
|
31
|
+
readProxyConfig,
|
|
32
|
+
readWorkerConfig,
|
|
33
|
+
scanRoots,
|
|
34
|
+
scanRouteHandlers,
|
|
35
|
+
} from '../../deployment-pi/util.js';
|
|
36
|
+
import { _wq } from '../../util.js';
|
|
37
|
+
import '../webflo-fetch/index.js';
|
|
38
|
+
import '../webflo-url/index.js';
|
|
39
|
+
|
|
40
|
+
export class WebfloServer extends WebfloRuntime {
|
|
41
|
+
|
|
42
|
+
static get HttpEvent() { return HttpEvent; }
|
|
43
|
+
|
|
44
|
+
static get HttpCookies() { return ServerSideCookies; }
|
|
45
|
+
|
|
46
|
+
static get HttpSession() { return ServerSideSession; }
|
|
47
|
+
|
|
48
|
+
static get HttpUser() { return HttpUser; }
|
|
49
|
+
|
|
50
|
+
static create(cx) {
|
|
51
|
+
return new this(this.Context.create(cx));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#config;
|
|
55
|
+
get config() { return this.#config; }
|
|
56
|
+
|
|
57
|
+
#routes;
|
|
58
|
+
get routes() { return this.#routes; }
|
|
59
|
+
|
|
60
|
+
#renderFileCache = new Map;
|
|
61
|
+
|
|
62
|
+
#sdk = {};
|
|
63
|
+
get sdk() { return this.#sdk; }
|
|
64
|
+
|
|
65
|
+
#servers = new Map;
|
|
66
|
+
|
|
67
|
+
#clients;
|
|
68
|
+
|
|
69
|
+
#hmrRegistry;
|
|
70
|
+
|
|
71
|
+
env(key) {
|
|
72
|
+
const { ENV } = this.config;
|
|
73
|
+
return key in ENV.mappings
|
|
74
|
+
? process.env[ENV.mappings[key]]
|
|
75
|
+
: process.env[key];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async initialize() {
|
|
79
|
+
const instanceController = await super.initialize();
|
|
80
|
+
const { appMeta: APP_META, flags: FLAGS, logger: LOGGER, } = this.cx;
|
|
81
|
+
this.#config = {
|
|
82
|
+
LAYOUT: await readLayoutConfig(this.cx),
|
|
83
|
+
ENV: await readEnvConfig(this.cx),
|
|
84
|
+
SERVER: await readServerConfig(this.cx),
|
|
85
|
+
HEADERS: await readHeadersConfig(this.cx),
|
|
86
|
+
REDIRECTS: await readRedirectsConfig(this.cx),
|
|
87
|
+
PROXY: await readProxyConfig(this.cx),
|
|
88
|
+
WORKER: await readWorkerConfig(this.cx),
|
|
89
|
+
};
|
|
90
|
+
const { PROXY } = this.config;
|
|
91
|
+
if (FLAGS['dev']) {
|
|
92
|
+
this.#config.DEV_DIR = Path.join(process.cwd(), '.webqit/webflo/@dev');
|
|
93
|
+
this.#config.DEV_LAYOUT = { ...this.config.LAYOUT };
|
|
94
|
+
for (const name of ['CLIENT_DIR', 'WORKER_DIR', 'SERVER_DIR', 'VIEWS_DIR', 'PUBLIC_DIR']) {
|
|
95
|
+
const originalDir = Path.relative(process.cwd(), this.config.LAYOUT[name]);
|
|
96
|
+
this.config.DEV_LAYOUT[name] = `${this.#config.DEV_DIR}/${originalDir}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// -----------------
|
|
100
|
+
this.#routes = {};
|
|
101
|
+
const spaRoots = Fs.existsSync(this.config.LAYOUT.PUBLIC_DIR) ? scanRoots(this.config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
|
|
102
|
+
const serverRoots = PROXY.entries.map((proxy) => proxy.path?.replace(/^\.\//, '')).filter((p) => p);
|
|
103
|
+
scanRouteHandlers(this.#config.DEV_LAYOUT || this.#config.LAYOUT, 'server', (file, route) => {
|
|
104
|
+
this.routes[route] = file;
|
|
105
|
+
}, ''/*offset*/, serverRoots);
|
|
106
|
+
Object.defineProperty(this.#routes, '$root', { value: '' });
|
|
107
|
+
Object.defineProperty(this.#routes, '$sparoots', { value: spaRoots });
|
|
108
|
+
Object.defineProperty(this.#routes, '$serverroots', { value: serverRoots });
|
|
109
|
+
// -----------------
|
|
110
|
+
await this.setupCapabilities();
|
|
111
|
+
this.#clients = new Clients;
|
|
112
|
+
this.control();
|
|
113
|
+
if (FLAGS['dev']) {
|
|
114
|
+
await this.enterDevMode();
|
|
115
|
+
}
|
|
116
|
+
// -----------------
|
|
117
|
+
if (this.#servers.size) {
|
|
118
|
+
// Show server details
|
|
119
|
+
LOGGER?.info(`> Server running! (${APP_META.title || ''}) ✅`);
|
|
120
|
+
for (let [proto, def] of this.#servers) {
|
|
121
|
+
LOGGER?.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
|
|
122
|
+
}
|
|
123
|
+
// Show capabilities
|
|
124
|
+
LOGGER?.info(``);
|
|
125
|
+
LOGGER?.info(`Capabilities: ${Object.keys(this.#sdk).join(', ')}`);
|
|
126
|
+
LOGGER?.info(``);
|
|
127
|
+
} else {
|
|
128
|
+
LOGGER?.info(`> Server not running! No port specified.`);
|
|
129
|
+
}
|
|
130
|
+
// -----------------
|
|
131
|
+
if (PROXY.entries.length) {
|
|
132
|
+
// Show active proxies
|
|
133
|
+
LOGGER?.info(`> Reverse proxies active.`);
|
|
134
|
+
for (const proxy of PROXY.entries) {
|
|
135
|
+
let desc = `> ${proxy.hostnames.join('|')} >>> ${proxy.port || proxy.path}`;
|
|
136
|
+
// Start a proxy recursively?
|
|
137
|
+
if (proxy.path && FLAGS['recursive']) {
|
|
138
|
+
desc += ` ✅`;
|
|
139
|
+
const flags = Object.entries({ ...FLAGS, port: proxy.port }).map(([k, v]) => v === true ? `--${k}` : `--${k}=${v}`);
|
|
140
|
+
spawn('npx', ['webflo', 'start', ...flags], {
|
|
141
|
+
cwd: proxy.path, // target directory
|
|
142
|
+
stdio: 'inherit', // inherit stdio so output streams to parent terminal
|
|
143
|
+
shell: true // for Windows compatibility
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
LOGGER?.info(desc);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return instanceController;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async setupCapabilities() {
|
|
153
|
+
const instanceController = await super.setupCapabilities();
|
|
154
|
+
const { SERVER } = this.config;
|
|
155
|
+
this.#sdk.Observer = Observer;
|
|
156
|
+
// 1. Database capabilities?
|
|
157
|
+
if (SERVER.capabilities?.database) {
|
|
158
|
+
if (SERVER.capabilities.database_dialect !== 'postgres') {
|
|
159
|
+
throw new Error(`Only postgres supported for now for database dialect`);
|
|
160
|
+
}
|
|
161
|
+
if (this.env('DATABASE_URL')) {
|
|
162
|
+
const { SQLClient } = await import('@linked-db/linked-ql/sql');
|
|
163
|
+
const { default: pg } = await import('pg');
|
|
164
|
+
// Obtain pg client
|
|
165
|
+
const pgClient = new pg.Pool({
|
|
166
|
+
connectionString: this.env('DATABASE_URL')
|
|
167
|
+
});
|
|
168
|
+
await (async function connect() {
|
|
169
|
+
pgClient.on('error', (e) => {
|
|
170
|
+
console.log('PG Error', e);
|
|
171
|
+
});
|
|
172
|
+
pgClient.on('end', (e) => {
|
|
173
|
+
console.log('PG End', e);
|
|
174
|
+
});
|
|
175
|
+
pgClient.on('notice', (e) => {
|
|
176
|
+
console.log('PG Notice', e);
|
|
177
|
+
});
|
|
178
|
+
await pgClient.connect();
|
|
179
|
+
})();
|
|
180
|
+
this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
|
|
181
|
+
} else {
|
|
182
|
+
//const { ODBClient } = await import('@linked-db/linked-ql/odb');
|
|
183
|
+
//this.#sdk.db = new ODBClient({ dialect: 'postgres' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// 2. Storage capabilities?
|
|
187
|
+
if (SERVER.capabilities?.redis) {
|
|
188
|
+
const { Redis } = await import('ioredis');
|
|
189
|
+
const redis = this.env('REDIS_URL')
|
|
190
|
+
? new Redis(this.env('REDIS_URL'))
|
|
191
|
+
: new Redis;
|
|
192
|
+
this.#sdk.redis = redis;
|
|
193
|
+
this.#sdk.storage = (namespace, ttl = null) => ({
|
|
194
|
+
async has(key) { return await redis.hexists(namespace, key); },
|
|
195
|
+
async get(key) {
|
|
196
|
+
const value = await redis.hget(namespace, key);
|
|
197
|
+
return typeof value === 'undefined' ? value : JSON.parse(value);
|
|
198
|
+
},
|
|
199
|
+
async set(key, value) {
|
|
200
|
+
const returnValue = await redis.hset(namespace, key, JSON.stringify(value));
|
|
201
|
+
if (!this.ttlApplied && ttl) {
|
|
202
|
+
await redis.expire(namespace, ttl);
|
|
203
|
+
this.ttlApplied = true;
|
|
204
|
+
}
|
|
205
|
+
return returnValue;
|
|
206
|
+
},
|
|
207
|
+
async delete(key) { return await redis.hdel(namespace, key); },
|
|
208
|
+
async clear() { return await redis.del(namespace); },
|
|
209
|
+
async keys() { return await redis.hkeys(namespace); },
|
|
210
|
+
async values() { return (await redis.hvals(namespace) || []).map((value) => typeof value === 'undefined' ? value : JSON.parse(value)); },
|
|
211
|
+
async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, typeof value === 'undefined' ? value : JSON.parse(value)]); },
|
|
212
|
+
get size() { return redis.hlen(namespace); },
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
const inmemSessionRegistry = new Map;
|
|
216
|
+
this.#sdk.storage = (namespace) => {
|
|
217
|
+
if (!inmemSessionRegistry.has(namespace)) {
|
|
218
|
+
inmemSessionRegistry.set(namespace, new Map);
|
|
219
|
+
}
|
|
220
|
+
return inmemSessionRegistry.get(namespace);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// 3. webpush capabilities?
|
|
224
|
+
if (SERVER.capabilities?.webpush) {
|
|
225
|
+
const { default: webpush } = await import('web-push');
|
|
226
|
+
this.#sdk.webpush = webpush;
|
|
227
|
+
if (this.env('VAPID_PUBLIC_KEY') && this.env('VAPID_PRIVATE_KEY')) {
|
|
228
|
+
webpush.setVapidDetails(
|
|
229
|
+
SERVER.capabilities.vapid_subject,
|
|
230
|
+
this.env('VAPID_PUBLIC_KEY'),
|
|
231
|
+
this.env('VAPID_PRIVATE_KEY')
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return instanceController;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async enterDevMode() {
|
|
239
|
+
const { appMeta, flags: FLAGS } = this.cx;
|
|
240
|
+
this.#hmrRegistry = WebfloHMR.manage(this, {
|
|
241
|
+
appMeta,
|
|
242
|
+
buildScripts: {
|
|
243
|
+
['build:html']: FLAGS['build:html'] ?? true,
|
|
244
|
+
['build:css']: FLAGS['build:css'] ?? true,
|
|
245
|
+
['build:js']: FLAGS['build:js'] ?? true,
|
|
246
|
+
},
|
|
247
|
+
buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
|
|
248
|
+
});
|
|
249
|
+
await this.#hmrRegistry.buildJS(true);
|
|
250
|
+
if (FLAGS['open']) {
|
|
251
|
+
for (let [proto, def] of this.#servers) {
|
|
252
|
+
const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
|
|
253
|
+
await openBrowser(url);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
control() {
|
|
259
|
+
const { flags: FLAGS } = this.cx;
|
|
260
|
+
const { SERVER, PROXY } = this.config;
|
|
261
|
+
const instanceController = super.control();
|
|
262
|
+
// ---------------
|
|
263
|
+
if (!FLAGS['test-only'] && !FLAGS['https-only'] && SERVER.port) {
|
|
264
|
+
const httpServer = Http.createServer((request, response) => this.handleNodeHttpRequest(request, response));
|
|
265
|
+
httpServer.listen(FLAGS['port'] || SERVER.port);
|
|
266
|
+
this.#servers.set('http', {
|
|
267
|
+
instance: httpServer,
|
|
268
|
+
hostnames: SERVER.hostnames,
|
|
269
|
+
port: FLAGS['port'] || SERVER.port,
|
|
270
|
+
});
|
|
271
|
+
// Handle WebSocket connections
|
|
272
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
273
|
+
this.handleNodeWsRequest(wss, request, socket, head);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// ---------------
|
|
277
|
+
if (!FLAGS['test-only'] && !FLAGS['http-only'] && SERVER.https.port) {
|
|
278
|
+
const httpsServer = Https.createServer((request, response) => this.handleNodeHttpRequest(request, response));
|
|
279
|
+
httpsServer.listen(SERVER.https.port);
|
|
280
|
+
const addSSLContext = (SERVER) => {
|
|
281
|
+
if (!Fs.existsSync(SERVER.https.keyfile)) return;
|
|
282
|
+
const cert = {
|
|
283
|
+
key: Fs.readFileSync(SERVER.https.keyfile),
|
|
284
|
+
cert: Fs.readFileSync(SERVER.https.certfile),
|
|
285
|
+
};
|
|
286
|
+
SERVER.https.hostnames.forEach((hostname) => {
|
|
287
|
+
httpsServer.addContext(hostname, cert);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.#servers.set('https', {
|
|
291
|
+
instance: httpsServer,
|
|
292
|
+
hostnames: SERVER.https.hostnames,
|
|
293
|
+
port: SERVER.https.port,
|
|
294
|
+
});
|
|
295
|
+
// -------
|
|
296
|
+
addSSLContext(SERVER);
|
|
297
|
+
for (const proxy of PROXY.entries) {
|
|
298
|
+
if (proxy.SERVER) {
|
|
299
|
+
addSSLContext(proxy.SERVER);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Handle WebSocket connections
|
|
303
|
+
httpsServer.on('upgrade', (request, socket, head) => {
|
|
304
|
+
this.handleNodeWsRequest(wss, request, socket, head);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// ---------------
|
|
308
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
309
|
+
// -----------------
|
|
310
|
+
process.on('uncaughtException', (err) => {
|
|
311
|
+
console.error('Uncaught Exception:', err);
|
|
312
|
+
});
|
|
313
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
314
|
+
console.log('Unhandled Rejection', reason, promise);
|
|
315
|
+
});
|
|
316
|
+
return instanceController;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
identifyIncoming(request, autoGenerateID = false) {
|
|
320
|
+
const secret = this.env('SESSION_KEY');
|
|
321
|
+
let clientID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
|
|
322
|
+
if (clientID?.includes('.')) {
|
|
323
|
+
if (secret) {
|
|
324
|
+
const [rand, signature] = clientID.split('.');
|
|
325
|
+
const expectedSignature = crypto.createHmac('sha256', secret)
|
|
326
|
+
.update(rand)
|
|
327
|
+
.digest('hex');
|
|
328
|
+
if (signature !== expectedSignature) {
|
|
329
|
+
clientID = null;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
clientID = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!clientID && autoGenerateID) {
|
|
336
|
+
if (secret) {
|
|
337
|
+
const rand = `${(0 | Math.random() * 9e6).toString(36)}`;
|
|
338
|
+
const signature = crypto.createHmac('sha256', secret)
|
|
339
|
+
.update(rand)
|
|
340
|
+
.digest('hex');
|
|
341
|
+
clientID = `${rand}.${signature}`
|
|
342
|
+
} else {
|
|
343
|
+
clientID = crypto.randomUUID();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return clientID;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async preResolveIncoming({ type, nodeRequest, proxy, reject, handle }) {
|
|
350
|
+
const { SERVER, PROXY, REDIRECTS } = this.config;
|
|
351
|
+
// Derive proto
|
|
352
|
+
const protoDef = type === 'ws' ? { nonSecure: 'ws', secure: 'wss' } : { nonSecure: 'http', secure: 'https' };
|
|
353
|
+
const proto = nodeRequest.connection.encrypted ? protoDef.secure : (nodeRequest.headers['x-forwarded-proto'] || protoDef.nonSecure);
|
|
354
|
+
// Resolve malformed URL: detected when using manual proxy setting in a browser
|
|
355
|
+
let requestUrl = nodeRequest.url;
|
|
356
|
+
if (requestUrl.startsWith(`${proto}://${nodeRequest.headers.host}`)) {
|
|
357
|
+
requestUrl = requestUrl.split(nodeRequest.headers.host)[1];
|
|
358
|
+
}
|
|
359
|
+
const fullUrl = proto + '://' + nodeRequest.headers.host + requestUrl;
|
|
360
|
+
const url = new URL(fullUrl);
|
|
361
|
+
// Begin resolution...
|
|
362
|
+
const hosts = [...this.#servers.values()].reduce((_hosts, server) => _hosts.concat(server.hostnames), []);
|
|
363
|
+
// Level 1 resolution
|
|
364
|
+
for (const $proxy of PROXY.entries) {
|
|
365
|
+
if ($proxy.hostnames.includes(url.hostname) || ($proxy.hostnames.includes('*') && !hosts.includes('*'))) {
|
|
366
|
+
url.port = $proxy.port; // The port forwarding
|
|
367
|
+
if ($proxy.proto) { // Force proto?
|
|
368
|
+
url.protocol = type === 'ws' ? $proxy.proto.replace('http', 'ws') : $proxy.proto;
|
|
369
|
+
}
|
|
370
|
+
return await proxy(url);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Level 2 resolution
|
|
374
|
+
if (!hosts.includes(url.hostname) && !hosts.includes('*')) {
|
|
375
|
+
return reject({
|
|
376
|
+
status: 500,
|
|
377
|
+
statusText: 'Unrecognized host',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (url.protocol === `${protoDef.nonSecure}:` && SERVER.https.port && SERVER.https.force) {
|
|
381
|
+
return reject({
|
|
382
|
+
status: 302,
|
|
383
|
+
statusText: 'Found',
|
|
384
|
+
headers: { Location: (url.protocol = `${protoDef.secure}:`, url.href) }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (url.hostname.startsWith('www.') && SERVER.force_www === 'remove') {
|
|
388
|
+
return reject({
|
|
389
|
+
status: 302,
|
|
390
|
+
statusText: 'Found',
|
|
391
|
+
headers: { Location: (url.hostname = url.hostname.substr(4), url.href) }
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (!url.hostname.startsWith('www.') && SERVER.force_www === 'add') {
|
|
395
|
+
return reject({
|
|
396
|
+
status: 302,
|
|
397
|
+
statusText: 'Found',
|
|
398
|
+
headers: { Location: (url.hostname = `www.${url.hostname}`, url.href) }
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (REDIRECTS) {
|
|
402
|
+
const rejection = REDIRECTS.entries.reduce((_rdr, entry) => {
|
|
403
|
+
return _rdr || ((_rdr = (new URLPattern(entry.from, url.origin)).exec(url.href)) && {
|
|
404
|
+
status: entry.code || 302,
|
|
405
|
+
statusText: entry.code === 301 ? 'Moved Permanently' : 'Found',
|
|
406
|
+
headers: { Location: _rdr.render(entry.to) }
|
|
407
|
+
});
|
|
408
|
+
}, null);
|
|
409
|
+
if (rejection) {
|
|
410
|
+
return reject(rejection);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return handle(url);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async handleNodeWsRequest(wss, nodeRequest, socket, head) {
|
|
417
|
+
const reject = (rejection) => {
|
|
418
|
+
const status = rejection.status || 400;
|
|
419
|
+
const statusText = rejection.statusText || 'Bad Request';
|
|
420
|
+
const headers = rejection.headers || {};
|
|
421
|
+
const body = rejection.body || `${status} ${statusText}`;
|
|
422
|
+
// Write status line and headers
|
|
423
|
+
socket.write(
|
|
424
|
+
`HTTP/1.1 ${status} ${statusText}\r\n` +
|
|
425
|
+
Object.entries(headers).map(([key, value]) => `${key}: ${value}\r\n`).join('') +
|
|
426
|
+
`Content-Type: text/plain\r\n` +
|
|
427
|
+
`Connection: close\r\n` +
|
|
428
|
+
`\r\n` +
|
|
429
|
+
body + `\r\n`
|
|
430
|
+
);
|
|
431
|
+
socket.destroy();
|
|
432
|
+
};
|
|
433
|
+
const proxy = async (destinationURL) => {
|
|
434
|
+
const isSecure = destinationURL.protocol === 'wss:';
|
|
435
|
+
const port = destinationURL.port || (isSecure ? 443 : 80);
|
|
436
|
+
const host = destinationURL.hostname;
|
|
437
|
+
// Connect
|
|
438
|
+
const connect = isSecure
|
|
439
|
+
? (await import('node:tls')).connect
|
|
440
|
+
: (await import('node:net')).connect;
|
|
441
|
+
// Create a TCP or TLS socket to the target WS server and pipe streams
|
|
442
|
+
const proxySocket = connect({ host, port, servername: isSecure ? host : undefined/*required for TLS SNI*/ }, () => {
|
|
443
|
+
// Send raw upgrade HTTP request to the target
|
|
444
|
+
proxySocket.write(`${nodeRequest.method} ${nodeRequest.url} HTTP/${nodeRequest.httpVersion}\r\n`);
|
|
445
|
+
for (const [key, value] of Object.entries(nodeRequest.headers)) {
|
|
446
|
+
proxySocket.write(`${key}: ${value}\r\n`);
|
|
447
|
+
}
|
|
448
|
+
proxySocket.write('\r\n');
|
|
449
|
+
if (head && head.length) {
|
|
450
|
+
proxySocket.write(head);
|
|
451
|
+
}
|
|
452
|
+
// Pipe both sockets together
|
|
453
|
+
socket.pipe(proxySocket).pipe(socket);
|
|
454
|
+
});
|
|
455
|
+
// Handle errors
|
|
456
|
+
proxySocket.on('error', err => {
|
|
457
|
+
console.error('Proxy socket error:', err);
|
|
458
|
+
socket.destroy();
|
|
459
|
+
});
|
|
460
|
+
socket.on('error', () => {
|
|
461
|
+
proxySocket.destroy();
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const handle = (requestURL) => {
|
|
465
|
+
if (requestURL.searchParams.get('rel') === 'hmr') {
|
|
466
|
+
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
|
|
467
|
+
wss.emit('connection', ws, nodeRequest);
|
|
468
|
+
this.#hmrRegistry.clients.add(ws);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (requestURL.searchParams.get('rel') === 'background-messaging') {
|
|
472
|
+
const request = new Request(requestURL.href, { headers: nodeRequest.headers });
|
|
473
|
+
const clientID = this.identifyIncoming(request);
|
|
474
|
+
const client = clientID && this.#clients.getClient(clientID);
|
|
475
|
+
if (!client) {
|
|
476
|
+
return reject({ body: `Lost or invalid clientID` });
|
|
477
|
+
}
|
|
478
|
+
const clientRequestRealtime = client?.getRequestRealtime(requestURL.pathname.split('/').pop());
|
|
479
|
+
if (!clientRequestRealtime) {
|
|
480
|
+
return reject({ body: `Lost or invalid portID` });
|
|
481
|
+
}
|
|
482
|
+
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
|
|
483
|
+
wss.emit('connection', ws, nodeRequest);
|
|
484
|
+
const wsw = new WQSockPort(ws);
|
|
485
|
+
clientRequestRealtime.addPort(wsw);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
return await this.preResolveIncoming({ type: 'ws', nodeRequest, proxy, reject, handle });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async handleNodeHttpRequest(nodeRequest, nodeResponse) {
|
|
493
|
+
// Pipe back response and log
|
|
494
|
+
const respondWith = (response, requestURL) => {
|
|
495
|
+
for (const [name, value] of response.headers) {
|
|
496
|
+
const existing = nodeResponse.getHeader(name);
|
|
497
|
+
if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
|
|
498
|
+
else nodeResponse.setHeader(name, value);
|
|
499
|
+
}
|
|
500
|
+
nodeResponse.statusCode = response.status;
|
|
501
|
+
nodeResponse.statusMessage = response.statusText;
|
|
502
|
+
if (response.body instanceof Readable) {
|
|
503
|
+
response.body.pipe(nodeResponse);
|
|
504
|
+
} else if (response.body instanceof ReadableStream) {
|
|
505
|
+
Readable.fromWeb(response.body).pipe(nodeResponse);
|
|
506
|
+
} else if (response.body) {
|
|
507
|
+
nodeResponse.end(response.body);
|
|
508
|
+
} else {
|
|
509
|
+
nodeResponse.end();
|
|
510
|
+
}
|
|
511
|
+
// Logging
|
|
512
|
+
const { logger: LOGGER } = this.cx;
|
|
513
|
+
if (LOGGER && requestURL) {
|
|
514
|
+
const log = this.generateLog({ url: requestURL.href, method: nodeRequest.method }, response);
|
|
515
|
+
LOGGER.log(log);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
// Reject with error status
|
|
519
|
+
const reject = async (rejection) => {
|
|
520
|
+
respondWith(new Response(null, rejection));
|
|
521
|
+
};
|
|
522
|
+
// Proxy request to a remote/local host
|
|
523
|
+
const proxy = async (destinationURL) => {
|
|
524
|
+
const requestInit = this.parseNodeRequest(nodeRequest);
|
|
525
|
+
requestInit.headers.host = destinationURL.host;
|
|
526
|
+
delete requestInit.headers.connection;
|
|
527
|
+
const response = await fetch(destinationURL, requestInit);
|
|
528
|
+
respondWith(response, destinationURL);
|
|
529
|
+
};
|
|
530
|
+
// Handle
|
|
531
|
+
const handle = async (requestURL) => {
|
|
532
|
+
const requestInit = this.parseNodeRequest(nodeRequest);
|
|
533
|
+
const response = await this.navigate(requestURL, requestInit, {
|
|
534
|
+
request: nodeRequest,
|
|
535
|
+
response: nodeResponse,
|
|
536
|
+
ipAddress: nodeRequest.headers['x-forwarded-for']?.split(',')[0] || nodeRequest.socket.remoteAddress
|
|
537
|
+
});
|
|
538
|
+
respondWith(response, requestURL);
|
|
539
|
+
};
|
|
540
|
+
return await this.preResolveIncoming({ typr: 'http', nodeRequest, reject, proxy, handle });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
parseNodeRequest(nodeRequest, withBody = true) {
|
|
544
|
+
const requestInit = { method: nodeRequest.method, headers: nodeRequest.headers };
|
|
545
|
+
if (withBody && !['GET', 'HEAD'].includes(nodeRequest.method)) {
|
|
546
|
+
nodeRequest[Symbol.toStringTag] = 'ReadableStream'; // Not necessary, but fun
|
|
547
|
+
requestInit.body = nodeRequest;
|
|
548
|
+
requestInit.duplex = 'half'; // See https://github.com/nodejs/node/issues/46221
|
|
549
|
+
}
|
|
550
|
+
return requestInit;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
createRequest(href, init = {}, autoHeaders = []) {
|
|
554
|
+
const request = super.createRequest(href, init);
|
|
555
|
+
this.writeAutoHeaders(request.headers, autoHeaders);
|
|
556
|
+
return request;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
writeAutoHeaders(headers, autoHeaders) {
|
|
560
|
+
autoHeaders.forEach(header => {
|
|
561
|
+
var headerName = header.name.toLowerCase(),
|
|
562
|
+
headerValue = header.value,
|
|
563
|
+
isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
|
|
564
|
+
isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
|
|
565
|
+
if (isAppend || isPrepend) {
|
|
566
|
+
headerValue = [headers.get(headerName) || '', headerValue].filter(v => v);
|
|
567
|
+
headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
|
|
568
|
+
}
|
|
569
|
+
headers.set(headerName, headerValue);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
writeRedirectHeaders(httpEvent, response) {
|
|
574
|
+
const $sparoots = this.#routes.$sparoots;
|
|
575
|
+
const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
|
|
576
|
+
const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
|
|
577
|
+
const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
|
|
578
|
+
const isSameOriginRedirect = destinationURL.origin === httpEvent.url.origin;
|
|
579
|
+
let isSameSpaRedirect = false;
|
|
580
|
+
if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && $sparoots.length) {
|
|
581
|
+
// Longest-first sorting
|
|
582
|
+
const sparoots = $sparoots.sort((a, b) => a.length > b.length ? -1 : 1);
|
|
583
|
+
const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
|
|
584
|
+
isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
|
|
585
|
+
}
|
|
586
|
+
if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
|
|
587
|
+
response.headers.set('X-Redirect-Code', response.status);
|
|
588
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
589
|
+
response.headers.set('Cache-Control', 'no-store');
|
|
590
|
+
const responseMeta = _wq(response, 'meta');
|
|
591
|
+
responseMeta.set('status', xRedirectCode);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async remoteFetch(request, ...args) {
|
|
596
|
+
let href = request;
|
|
597
|
+
if (request instanceof Request) {
|
|
598
|
+
href = request.url;
|
|
599
|
+
} else if (request instanceof URL) {
|
|
600
|
+
href = request.href;
|
|
601
|
+
}
|
|
602
|
+
const _response = fetch(request, ...args);
|
|
603
|
+
// Save a reference to this
|
|
604
|
+
return _response.then(async response => {
|
|
605
|
+
// Stop loading status
|
|
606
|
+
return response;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async localFetch(httpEvent) {
|
|
611
|
+
const { flags: FLAGS } = this.cx;
|
|
612
|
+
const { DEV_LAYOUT, LAYOUT } = this.config;
|
|
613
|
+
const scopeObj = {};
|
|
614
|
+
if (FLAGS['dev']) {
|
|
615
|
+
if (httpEvent.url.pathname === '/@dev') {
|
|
616
|
+
const filename = httpEvent.url.searchParams.get('src').split('?')[0];
|
|
617
|
+
if (filename.endsWith('.js')) {
|
|
618
|
+
scopeObj.filename = Path.join(DEV_LAYOUT.PUBLIC_DIR, filename);
|
|
619
|
+
} else {
|
|
620
|
+
scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, filename);
|
|
621
|
+
}
|
|
622
|
+
} else if (this.#hmrRegistry.options.buildSensitivity === 2) {
|
|
623
|
+
await this.#hmrRegistry.bundleAssetsIfPending();
|
|
624
|
+
scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
|
|
628
|
+
}
|
|
629
|
+
scopeObj.ext = Path.parse(scopeObj.filename).ext;
|
|
630
|
+
const finalizeResponse = (response) => {
|
|
631
|
+
const responseMeta = _wq(response, 'meta');
|
|
632
|
+
responseMeta.set('filename', scopeObj.filename);
|
|
633
|
+
responseMeta.set('static', true);
|
|
634
|
+
responseMeta.set('index', scopeObj.index);
|
|
635
|
+
return response;
|
|
636
|
+
};
|
|
637
|
+
// Pre-encoding support?
|
|
638
|
+
if (scopeObj.preEncodingSupportLevel !== 0) {
|
|
639
|
+
scopeObj.acceptEncs = [];
|
|
640
|
+
scopeObj.supportedEncs = { gzip: '.gz', br: '.br' };
|
|
641
|
+
if ((scopeObj.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
|
|
642
|
+
&& (scopeObj.enc = scopeObj.acceptEncs.reduce((prev, _enc) => prev || (scopeObj.supportedEncs[_enc] && Fs.existsSync(scopeObj.filename + scopeObj.supportedEncs[_enc]) && _enc), null))) {
|
|
643
|
+
// Route to a pre-compressed version of the file
|
|
644
|
+
scopeObj.filename = scopeObj.filename + scopeObj.supportedEncs[scopeObj.enc];
|
|
645
|
+
scopeObj.stats = null;
|
|
646
|
+
} else if (scopeObj.acceptEncs.length) {
|
|
647
|
+
// TODO: Do dynamic encoding
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// if is a directory, search for index file matching the extention
|
|
651
|
+
if (!scopeObj.ext && scopeObj.autoIndexFileSupport !== false && Fs.existsSync(scopeObj.filename) && (scopeObj.stats = Fs.lstatSync(scopeObj.filename)).isDirectory()) {
|
|
652
|
+
scopeObj.ext = '.html';
|
|
653
|
+
scopeObj.index = `index${scopeObj.ext}`;
|
|
654
|
+
scopeObj.filename = Path.join(scopeObj.filename, scopeObj.index);
|
|
655
|
+
scopeObj.stats = null;
|
|
656
|
+
}
|
|
657
|
+
// ------ If we get here, scopeObj.filename has been finalized ------
|
|
658
|
+
// Do file stats
|
|
659
|
+
if (!scopeObj.stats) {
|
|
660
|
+
try { scopeObj.stats = Fs.statSync(scopeObj.filename); } catch (e) {
|
|
661
|
+
if (e.code === 'ENOENT') return finalizeResponse(new Response(null, { status: 404, statusText: 'Not Found' }));
|
|
662
|
+
throw e; // Re-throw other errors
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// ETag support
|
|
666
|
+
scopeObj.stats.etag = `W/"${scopeObj.stats.size}-${scopeObj.stats.mtimeMs}"`;
|
|
667
|
+
const ifNoneMatch = httpEvent.request.headers.get('If-None-Match');
|
|
668
|
+
if (scopeObj.stats.etag && ifNoneMatch === scopeObj.stats.etag) {
|
|
669
|
+
const response = new Response(null, { status: 304, statusText: 'Not Modified' });
|
|
670
|
+
response.headers.set('ETag', scopeObj.stats.etag);
|
|
671
|
+
response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
|
|
672
|
+
response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
673
|
+
return finalizeResponse(response);
|
|
674
|
+
}
|
|
675
|
+
scopeObj.stats.mime = scopeObj.ext && Mime.lookup(scopeObj.ext)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
|
|
676
|
+
// Range support
|
|
677
|
+
const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
|
|
678
|
+
scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
|
|
679
|
+
if (scopeObj.response.status === 416) return finalizeResponse(scopeObj.response);
|
|
680
|
+
// ------ If we get here, it means we're good ------
|
|
681
|
+
if (scopeObj.enc) {
|
|
682
|
+
scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
|
|
683
|
+
}
|
|
684
|
+
// 1. Strong cache validators
|
|
685
|
+
scopeObj.response.headers.set('ETag', scopeObj.stats.etag);
|
|
686
|
+
scopeObj.response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
|
|
687
|
+
// 2. Content presentation and policy
|
|
688
|
+
scopeObj.response.headers.set('Content-Disposition', `inline; filename="${Path.basename(scopeObj.filename)}"`);
|
|
689
|
+
scopeObj.response.headers.set('Referrer-Policy', 'no-referrer-when-downgrade');
|
|
690
|
+
// 3. Cache-Control
|
|
691
|
+
scopeObj.response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
692
|
+
scopeObj.response.headers.set('Vary', 'Accept-Encoding'); // The header that talks to our support for "Accept-Encoding"
|
|
693
|
+
// 4. Security headers
|
|
694
|
+
scopeObj.response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
695
|
+
scopeObj.response.headers.set('Access-Control-Allow-Origin', '*');
|
|
696
|
+
scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
697
|
+
// 5. Partial content support
|
|
698
|
+
scopeObj.response.headers.set('Accept-Ranges', 'bytes');
|
|
699
|
+
// 6. Qualify Service-Worker responses
|
|
700
|
+
if (httpEvent.request.headers.get('Service-Worker') === 'script') {
|
|
701
|
+
scopeObj.response.headers.set('Service-Worker-Allowed', this.#config.WORKER.scope || '/');
|
|
702
|
+
}
|
|
703
|
+
return finalizeResponse(scopeObj.response);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async navigate(url, init = {}, detail = {}) {
|
|
707
|
+
const { HEADERS } = this.config;
|
|
708
|
+
const { flags: FLAGS } = this.cx;
|
|
709
|
+
const scopeObj = { url, init, detail };
|
|
710
|
+
if (typeof scopeObj.url === 'string') {
|
|
711
|
+
scopeObj.url = new URL(scopeObj.url, 'http://localhost');
|
|
712
|
+
}
|
|
713
|
+
// Request processing
|
|
714
|
+
scopeObj.autoHeaders = HEADERS.entries.filter((entry) => (new URLPattern(entry.url, url.origin)).exec(url.href)) || [];
|
|
715
|
+
scopeObj.request = this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
|
|
716
|
+
scopeObj.cookies = this.createHttpCookies({
|
|
717
|
+
request: scopeObj.request
|
|
718
|
+
});
|
|
719
|
+
scopeObj.clientID = this.identifyIncoming(scopeObj.request, true);
|
|
720
|
+
scopeObj.client = this.#clients.getClient(scopeObj.clientID, true);
|
|
721
|
+
scopeObj.realtimePortID = crypto.randomUUID();
|
|
722
|
+
scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.realtimePortID, scopeObj.request.url);
|
|
723
|
+
scopeObj.sessionTTL = this.env('SESSION_TTL') || 2592000/*30days*/;
|
|
724
|
+
scopeObj.session = this.createHttpSession({
|
|
725
|
+
store: this.#sdk.storage?.(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
|
|
726
|
+
request: scopeObj.request,
|
|
727
|
+
sessionID: scopeObj.clientID,
|
|
728
|
+
ttl: scopeObj.sessionTTL
|
|
729
|
+
});
|
|
730
|
+
scopeObj.user = this.createHttpUser({
|
|
731
|
+
store: this.#sdk.storage?.(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
|
|
732
|
+
request: scopeObj.request,
|
|
733
|
+
realtime: scopeObj.clientRequestRealtime,
|
|
734
|
+
session: scopeObj.session,
|
|
735
|
+
});
|
|
736
|
+
scopeObj.httpEvent = this.createHttpEvent({
|
|
737
|
+
request: scopeObj.request,
|
|
738
|
+
realtime: scopeObj.clientRequestRealtime,
|
|
739
|
+
cookies: scopeObj.cookies,
|
|
740
|
+
session: scopeObj.session,
|
|
741
|
+
user: scopeObj.user,
|
|
742
|
+
detail: scopeObj.detail,
|
|
743
|
+
sdk: this.#sdk,
|
|
744
|
+
});
|
|
745
|
+
// Dispatch for response
|
|
746
|
+
scopeObj.response = await this.dispatchNavigationEvent({
|
|
747
|
+
httpEvent: scopeObj.httpEvent,
|
|
748
|
+
crossLayerFetch: (event) => this.localFetch(event),
|
|
749
|
+
responseRealtime: `ws:${scopeObj.httpEvent.realtime.portID}?rel=background-messaging`
|
|
750
|
+
});
|
|
751
|
+
// Reponse handlers
|
|
752
|
+
if (FLAGS['dev']) {
|
|
753
|
+
scopeObj.response.headers.set('X-Webflo-Dev-Mode', 'true'); // Must come before satisfyRequestFormat() sp as to be rendered
|
|
754
|
+
}
|
|
755
|
+
if (scopeObj.response.headers.get('Location')) {
|
|
756
|
+
this.writeRedirectHeaders(scopeObj.httpEvent, scopeObj.response);
|
|
757
|
+
} else {
|
|
758
|
+
scopeObj.response = await this.satisfyRequestFormat(scopeObj.httpEvent, scopeObj.response);
|
|
759
|
+
this.writeAutoHeaders(scopeObj.response.headers, scopeObj.autoHeaders.filter((header) => header.type === 'response'));
|
|
760
|
+
if (scopeObj.httpEvent.request.method !== 'GET' && !scopeObj.response.headers.get('Cache-Control')) {
|
|
761
|
+
scopeObj.response.headers.set('Cache-Control', 'no-store');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return scopeObj.response;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async satisfyRequestFormat(httpEvent, response) {
|
|
768
|
+
if (response.status === 206 || response.status === 416) {
|
|
769
|
+
// If the response is a partial content, we don't need to do anything else
|
|
770
|
+
return response;
|
|
771
|
+
}
|
|
772
|
+
// Satisfy "Accept" header
|
|
773
|
+
const requestAccept = httpEvent.request.headers.get('Accept', true);
|
|
774
|
+
const asHTML = requestAccept?.match('text/html');
|
|
775
|
+
const asIs = requestAccept?.match(response.headers.get('Content-Type'));
|
|
776
|
+
const responseMeta = _wq(response, 'meta');
|
|
777
|
+
if (requestAccept && asHTML >= asIs && !responseMeta.get('static')) {
|
|
778
|
+
response = await this.render(httpEvent, response);
|
|
779
|
+
} else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
|
|
780
|
+
return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
|
|
781
|
+
}
|
|
782
|
+
// ------- With "exception" responses out of the way,
|
|
783
|
+
// let's set the header that talks to our support for "Accept"
|
|
784
|
+
if (!responseMeta.get('static')) {
|
|
785
|
+
response.headers.append('Vary', 'Accept');
|
|
786
|
+
}
|
|
787
|
+
// Satisfy "Range" header
|
|
788
|
+
const requestRange = httpEvent.request.headers.get('Range', true);
|
|
789
|
+
if (requestRange.length && response.headers.get('Content-Length')) {
|
|
790
|
+
const stats = {
|
|
791
|
+
size: parseInt(response.headers.get('Content-Length')),
|
|
792
|
+
mime: response.headers.get('Content-Type') || 'application/octet-stream',
|
|
793
|
+
};
|
|
794
|
+
const headersBefore = response.headers;
|
|
795
|
+
response = this.createStreamingResponse(
|
|
796
|
+
httpEvent,
|
|
797
|
+
(params) => this.streamSlice(response.body, { ...params }),
|
|
798
|
+
stats
|
|
799
|
+
);
|
|
800
|
+
for (const [name, value] of headersBefore) {
|
|
801
|
+
if (/Content-Length|Content-Type/i.test(name)) continue;
|
|
802
|
+
response.headers.append(name, value);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return response;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async render(httpEvent, response) {
|
|
809
|
+
const { LAYOUT } = this.config;
|
|
810
|
+
const scopeObj = {};
|
|
811
|
+
scopeObj.router = new this.constructor.Router(this, httpEvent.url.pathname);
|
|
812
|
+
scopeObj.rendering = await scopeObj.router.route('render', httpEvent, async (httpEvent) => {
|
|
813
|
+
let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
|
|
814
|
+
while ((renderFile = Path.join(LAYOUT.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
|
|
815
|
+
&& (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
|
|
816
|
+
this.#renderFileCache.set(renderFile, false);
|
|
817
|
+
pathnameSplit.pop();
|
|
818
|
+
}
|
|
819
|
+
const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(LAYOUT.PUBLIC_DIR)));
|
|
820
|
+
const instanceParams = /*QueryString.stringify*/({
|
|
821
|
+
//file: renderFile,
|
|
822
|
+
url: dirPublic.href,// httpEvent.url.href,
|
|
823
|
+
});
|
|
824
|
+
const { window, document } = createWindow(renderFile, instanceParams);
|
|
825
|
+
//const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
|
|
826
|
+
await new Promise((res) => {
|
|
827
|
+
if (document.readyState === 'complete') return res(1);
|
|
828
|
+
document.addEventListener('load', res);
|
|
829
|
+
});
|
|
830
|
+
const data = await response.parse();
|
|
831
|
+
if (window.webqit?.oohtml?.config) {
|
|
832
|
+
// Await rendering engine
|
|
833
|
+
if (window.webqit?.$qCompilerWorker) {
|
|
834
|
+
window.webqit.$qCompilerWorker.postMessage({ source: '1+1', params: {} }, []);
|
|
835
|
+
await new Promise(res => {
|
|
836
|
+
window.webqit.$qCompilerImport.then(res);
|
|
837
|
+
setTimeout(() => res(1), 1000);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
const {
|
|
841
|
+
HTML_IMPORTS: { attr: modulesContextAttrs } = {},
|
|
842
|
+
BINDINGS_API: { api: bindingsConfig } = {},
|
|
843
|
+
} = window.webqit.oohtml.config;
|
|
844
|
+
if (modulesContextAttrs) {
|
|
845
|
+
const newRoute = '/' + `app/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
|
|
846
|
+
document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
|
|
847
|
+
}
|
|
848
|
+
if (bindingsConfig) {
|
|
849
|
+
document[bindingsConfig.bind]({
|
|
850
|
+
state: {},
|
|
851
|
+
data,
|
|
852
|
+
env: 'server',
|
|
853
|
+
navigator: null,
|
|
854
|
+
location: this.location,
|
|
855
|
+
network: null,
|
|
856
|
+
transition: null,
|
|
857
|
+
background: null
|
|
858
|
+
}, { diff: true });
|
|
859
|
+
}
|
|
860
|
+
await new Promise(res => setTimeout(res, 300));
|
|
861
|
+
}
|
|
862
|
+
for (const name of ['X-Background-Messaging-Port', 'X-Live-Response-Message-ID', 'X-Webflo-Dev-Mode']) {
|
|
863
|
+
document.querySelector(`meta[name="${name}"]`)?.remove();
|
|
864
|
+
if (!response.headers.get(name)) continue;
|
|
865
|
+
const metaElement = document.createElement('meta');
|
|
866
|
+
metaElement.setAttribute('name', name);
|
|
867
|
+
metaElement.setAttribute('content', response.headers.get(name));
|
|
868
|
+
document.head.prepend(metaElement);
|
|
869
|
+
}
|
|
870
|
+
// Append hydration data
|
|
871
|
+
for (const [rel, content] of [['hydration', data]]) {
|
|
872
|
+
document.querySelector(`script[rel="${rel}"][type="application/json"]`)?.remove();
|
|
873
|
+
const dataScript = document.createElement('script');
|
|
874
|
+
dataScript.setAttribute('type', 'application/json');
|
|
875
|
+
dataScript.setAttribute('rel', rel);
|
|
876
|
+
dataScript.textContent = JSON.stringify(content);
|
|
877
|
+
document.body.append(dataScript);
|
|
878
|
+
}
|
|
879
|
+
const rendering = window.toString();
|
|
880
|
+
document.documentElement.remove();
|
|
881
|
+
document.writeln('');
|
|
882
|
+
try { window.close(); } catch (e) {}
|
|
883
|
+
return rendering;
|
|
884
|
+
});
|
|
885
|
+
// Validate rendering
|
|
886
|
+
if (typeof scopeObj.rendering !== 'string' && !(typeof scopeObj.rendering?.toString === 'function')) {
|
|
887
|
+
throw new Error('render() must return a string response or an object that implements toString()..');
|
|
888
|
+
}
|
|
889
|
+
// Convert back to response
|
|
890
|
+
scopeObj.response = new Response(scopeObj.rendering, {
|
|
891
|
+
headers: response.headers,
|
|
892
|
+
status: response.status,
|
|
893
|
+
statusText: response.statusText,
|
|
894
|
+
});
|
|
895
|
+
scopeObj.response.headers.set('Content-Type', 'text/html');
|
|
896
|
+
scopeObj.response.headers.set('Content-Length', (new Blob([scopeObj.rendering])).size);
|
|
897
|
+
return scopeObj.response;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
generateLog(request, response, isproxy = false) {
|
|
901
|
+
const { logger: LOGGER } = this.cx;
|
|
902
|
+
const log = [];
|
|
903
|
+
// ---------------
|
|
904
|
+
const style = LOGGER.style || { keyword: (str) => str, comment: (str) => str, url: (str) => str, val: (str) => str, err: (str) => str, };
|
|
905
|
+
const errorCode = response.status >= 400 && response.status < 500 ? response.status : 0;
|
|
906
|
+
const xRedirectCode = response.headers.get('X-Redirect-Code');
|
|
907
|
+
const isRedirect = (xRedirectCode || response.status + '').startsWith('3') && (xRedirectCode || response.status) !== 304;
|
|
908
|
+
const statusCode = xRedirectCode && `${xRedirectCode} (${response.status})` || response.status;
|
|
909
|
+
const responseMeta = _wq(response, 'meta');
|
|
910
|
+
// ---------------
|
|
911
|
+
log.push(`[${style.comment((new Date).toUTCString())}]`);
|
|
912
|
+
log.push(style.keyword(request.method));
|
|
913
|
+
if (isproxy) log.push(style.keyword('>>'));
|
|
914
|
+
log.push(style.url(request.url));
|
|
915
|
+
if (responseMeta.has('hint')) log.push(`(${style.comment(responseMeta.get('hint'))})`);
|
|
916
|
+
const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length') && this.formatBytes(response.headers.get('Content-Length'))].filter((x) => x);
|
|
917
|
+
if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
|
|
918
|
+
if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
|
|
919
|
+
if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
|
|
920
|
+
else log.push(style.val(`${statusCode} ${response.statusText}`));
|
|
921
|
+
if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
|
|
922
|
+
return log.join(' ');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
formatBytes(bytes, decimals = 5, locale = 'en', withSpace = true) {
|
|
926
|
+
if (bytes + '' === '0') return `0${withSpace ? ' ' : ''}B`;
|
|
927
|
+
const k = 1024;
|
|
928
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
929
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
930
|
+
const rawValue = bytes / Math.pow(k, i);
|
|
931
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
932
|
+
minimumFractionDigits: 0,
|
|
933
|
+
maximumFractionDigits: decimals,
|
|
934
|
+
});
|
|
935
|
+
return `${formatter.format(rawValue)}${withSpace ? ' ' : ''}${sizes[i]}`;
|
|
936
|
+
}
|
|
937
|
+
}
|