@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.
Files changed (235) hide show
  1. package/.github/FUNDING.yml +12 -0
  2. package/.github/workflows/publish.yml +48 -0
  3. package/.gitignore +2 -0
  4. package/LICENSE +2 -2
  5. package/README.md +71 -2050
  6. package/package.json +28 -13
  7. package/site/-/_.md +139 -0
  8. package/site/-/docs.old.md +2010 -0
  9. package/site/.vitepress/cache/deps/@braintree_sanitize-url 2.js +93 -0
  10. package/site/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  11. package/site/.vitepress/cache/deps/@braintree_sanitize-url.js 2.map +7 -0
  12. package/site/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  13. package/site/.vitepress/cache/deps/_metadata 2.json +85 -0
  14. package/site/.vitepress/cache/deps/_metadata.json +85 -0
  15. package/site/.vitepress/cache/deps/chunk-BUSYA2B4 2.js +9 -0
  16. package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  17. package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js 2.map +7 -0
  18. package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  19. package/site/.vitepress/cache/deps/chunk-Q2AYPHVK 2.js +9719 -0
  20. package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js +9719 -0
  21. package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js 2.map +7 -0
  22. package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js.map +7 -0
  23. package/site/.vitepress/cache/deps/chunk-QAXAIFA7 2.js +12705 -0
  24. package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js +12705 -0
  25. package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js 2.map +7 -0
  26. package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js.map +7 -0
  27. package/site/.vitepress/cache/deps/cytoscape 2.js +30278 -0
  28. package/site/.vitepress/cache/deps/cytoscape-cose-bilkent 2.js +4710 -0
  29. package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  30. package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js 2.map +7 -0
  31. package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  32. package/site/.vitepress/cache/deps/cytoscape.js +30278 -0
  33. package/site/.vitepress/cache/deps/cytoscape.js 2.map +7 -0
  34. package/site/.vitepress/cache/deps/cytoscape.js.map +7 -0
  35. package/site/.vitepress/cache/deps/dayjs 2.js +285 -0
  36. package/site/.vitepress/cache/deps/dayjs.js +285 -0
  37. package/site/.vitepress/cache/deps/dayjs.js 2.map +7 -0
  38. package/site/.vitepress/cache/deps/dayjs.js.map +7 -0
  39. package/site/.vitepress/cache/deps/debug 2.js +453 -0
  40. package/site/.vitepress/cache/deps/debug.js +453 -0
  41. package/site/.vitepress/cache/deps/debug.js 2.map +7 -0
  42. package/site/.vitepress/cache/deps/debug.js.map +7 -0
  43. package/site/.vitepress/cache/deps/package 2.json +3 -0
  44. package/site/.vitepress/cache/deps/package.json +3 -0
  45. package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api 2.js +4507 -0
  46. package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  47. package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js 2.map +7 -0
  48. package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  49. package/site/.vitepress/cache/deps/vitepress___@vueuse_core 2.js +584 -0
  50. package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  51. package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js 2.map +7 -0
  52. package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  53. package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap 2.js +1166 -0
  54. package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1166 -0
  55. package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js 2.map +7 -0
  56. package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  57. package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js 2.js +1667 -0
  58. package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  59. package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js 2.map +7 -0
  60. package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  61. package/site/.vitepress/cache/deps/vitepress___minisearch 2.js +1815 -0
  62. package/site/.vitepress/cache/deps/vitepress___minisearch.js +1815 -0
  63. package/site/.vitepress/cache/deps/vitepress___minisearch.js 2.map +7 -0
  64. package/site/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  65. package/site/.vitepress/cache/deps/vue 2.js +344 -0
  66. package/site/.vitepress/cache/deps/vue.js +344 -0
  67. package/site/.vitepress/cache/deps/vue.js 2.map +7 -0
  68. package/site/.vitepress/cache/deps/vue.js.map +7 -0
  69. package/site/.vitepress/config.ts +147 -0
  70. package/site/.vitepress/theme/custom.css +50 -0
  71. package/site/.vitepress/theme/index.ts +6 -0
  72. package/site/api/webflo-fetch/FormData.md +0 -0
  73. package/site/api/webflo-fetch/Headers.md +0 -0
  74. package/site/api/webflo-fetch/LiveResponse.md +0 -0
  75. package/site/api/webflo-fetch/Request.md +0 -0
  76. package/site/api/webflo-fetch/Response.md +0 -0
  77. package/site/api/webflo-fetch/fetch.md +0 -0
  78. package/site/api/webflo-routing/HttpCookies.md +0 -0
  79. package/site/api/webflo-routing/HttpEvent/respondWith.md +1 -0
  80. package/site/api/webflo-routing/HttpEvent/waitUntil.md +1 -0
  81. package/site/api/webflo-routing/HttpEvent/waitUntilNavigate.md +1 -0
  82. package/site/api/webflo-routing/HttpEvent.md +30 -0
  83. package/site/api/webflo-routing/HttpSession.md +0 -0
  84. package/site/api/webflo-routing/HttpState.md +0 -0
  85. package/site/api/webflo-routing/HttpUser.md +0 -0
  86. package/site/api/webflo-routing/handler/fetch.md +42 -0
  87. package/site/api/webflo-routing/handler/next.md +54 -0
  88. package/site/api/webflo-routing/handler.md +119 -0
  89. package/site/api.md +26 -0
  90. package/site/contributing.md +16 -0
  91. package/site/docs/advanced/lifecycles.md +20 -0
  92. package/site/docs/advanced/redirects.md +0 -0
  93. package/site/docs/advanced/routing.md +1 -0
  94. package/site/docs/advanced.md +9 -0
  95. package/site/docs/concepts/realtime.md +637 -0
  96. package/site/docs/concepts/rendering.md +60 -0
  97. package/site/docs/concepts/request-response.md +47 -0
  98. package/site/docs/concepts/routing.md +656 -0
  99. package/site/docs/concepts/state.md +44 -0
  100. package/site/docs/concepts/templates.md +48 -0
  101. package/site/docs/concepts.md +97 -0
  102. package/site/docs/getting-started.md +378 -0
  103. package/site/docs/tech-stack.md +56 -0
  104. package/site/docs.md +100 -0
  105. package/site/examples/pwa.md +10 -0
  106. package/site/examples/web.md +11 -0
  107. package/site/examples.md +10 -0
  108. package/site/faq.md +13 -0
  109. package/site/guides/guide-auth.md +13 -0
  110. package/site/guides/guide-file-upload.md +11 -0
  111. package/site/guides/guide-service-worker.md +10 -0
  112. package/site/guides/tutorial-1-todo.md +24 -0
  113. package/site/guides.md +15 -0
  114. package/site/index.md +39 -0
  115. package/site/public/img/brand/logo-670x670.png +0 -0
  116. package/site/recipes/realtime.md +11 -0
  117. package/site/recipes/streaming.md +15 -0
  118. package/site/reference/cli.md +11 -0
  119. package/site/reference/config.md +13 -0
  120. package/site/reference/tools.md +9 -0
  121. package/src/Context.js +3 -11
  122. package/src/config-pi/deployment/Env.js +6 -19
  123. package/src/config-pi/deployment/Layout.js +11 -3
  124. package/src/config-pi/runtime/Client.js +40 -48
  125. package/src/config-pi/runtime/Server.js +52 -20
  126. package/src/config-pi/runtime/client/Worker.js +22 -20
  127. package/src/config-pi/static/Init.js +57 -0
  128. package/src/config-pi/static/index.js +2 -0
  129. package/src/deployment-pi/origins/index.js +1 -1
  130. package/src/deployment-pi/util.js +161 -0
  131. package/src/index.js +3 -9
  132. package/src/init-pi/index.js +117 -0
  133. package/src/init-pi/templates/pwa/app/handler.server.js +8 -0
  134. package/src/init-pi/templates/pwa/app/page.html +7 -0
  135. package/src/init-pi/templates/pwa/package.json +19 -0
  136. package/src/init-pi/templates/pwa/public/assets/app.css +16 -0
  137. package/src/init-pi/templates/pwa/public/index.html +39 -0
  138. package/src/init-pi/templates/pwa/public/manifest.json +29 -0
  139. package/src/init-pi/templates/web/app/handler.server.js +8 -0
  140. package/src/init-pi/templates/web/app/page.html +7 -0
  141. package/src/init-pi/templates/web/package.json +19 -0
  142. package/src/init-pi/templates/web/public/assets/app.css +16 -0
  143. package/src/init-pi/templates/web/public/index.html +39 -0
  144. package/src/runtime-pi/WebfloRuntime.js +350 -0
  145. package/src/runtime-pi/index.js +3 -10
  146. package/src/runtime-pi/webflo-client/ClientSideCookies.js +17 -0
  147. package/src/runtime-pi/webflo-client/ClientSideWorkport.js +63 -0
  148. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +213 -0
  149. package/src/runtime-pi/webflo-client/WebfloClient.js +500 -0
  150. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +206 -0
  151. package/src/runtime-pi/webflo-client/WebfloRootClient2.js +113 -0
  152. package/src/runtime-pi/webflo-client/WebfloSubClient.js +118 -0
  153. package/src/runtime-pi/webflo-client/index.js +17 -0
  154. package/src/runtime-pi/webflo-client/webflo-codegen.js +469 -0
  155. package/src/runtime-pi/webflo-client/webflo-devmode.js +243 -0
  156. package/src/runtime-pi/webflo-client/webflo-embedded.js +50 -0
  157. package/src/runtime-pi/webflo-fetch/LiveResponse.js +437 -0
  158. package/src/runtime-pi/webflo-fetch/cookies.js +10 -0
  159. package/src/runtime-pi/webflo-fetch/fetch.js +16 -0
  160. package/src/runtime-pi/webflo-fetch/formdata.js +54 -0
  161. package/src/runtime-pi/webflo-fetch/headers.js +151 -0
  162. package/src/runtime-pi/webflo-fetch/index.js +5 -0
  163. package/src/runtime-pi/webflo-fetch/message.js +49 -0
  164. package/src/runtime-pi/webflo-fetch/request.js +62 -0
  165. package/src/runtime-pi/webflo-fetch/response.js +110 -0
  166. package/src/runtime-pi/webflo-fetch/util.js +28 -0
  167. package/src/runtime-pi/webflo-messaging/WQBroadcastChannel.js +10 -0
  168. package/src/runtime-pi/webflo-messaging/WQMessageChannel.js +26 -0
  169. package/src/runtime-pi/webflo-messaging/WQMessageEvent.js +87 -0
  170. package/src/runtime-pi/webflo-messaging/WQMessagePort.js +38 -0
  171. package/src/runtime-pi/webflo-messaging/WQRelayPort.js +47 -0
  172. package/src/runtime-pi/webflo-messaging/WQSockPort.js +113 -0
  173. package/src/runtime-pi/webflo-messaging/WQStarPort.js +104 -0
  174. package/src/runtime-pi/webflo-messaging/wq-message-port.js +404 -0
  175. package/src/runtime-pi/webflo-routing/HttpCookies.js +42 -0
  176. package/src/runtime-pi/webflo-routing/HttpEvent.js +112 -0
  177. package/src/runtime-pi/webflo-routing/HttpSession.js +11 -0
  178. package/src/runtime-pi/webflo-routing/HttpState.js +153 -0
  179. package/src/runtime-pi/webflo-routing/HttpUser.js +54 -0
  180. package/src/runtime-pi/webflo-routing/WebfloRouter.js +245 -0
  181. package/src/runtime-pi/webflo-server/ServerSideCookies.js +19 -0
  182. package/src/runtime-pi/webflo-server/ServerSideSession.js +38 -0
  183. package/src/runtime-pi/webflo-server/WebfloServer.js +937 -0
  184. package/src/runtime-pi/webflo-server/index.js +11 -0
  185. package/src/runtime-pi/webflo-server/messaging/Client.js +27 -0
  186. package/src/runtime-pi/webflo-server/messaging/ClientRequestRealtime.js +50 -0
  187. package/src/runtime-pi/webflo-server/messaging/Clients.js +25 -0
  188. package/src/runtime-pi/webflo-server/webflo-devmode.js +326 -0
  189. package/src/runtime-pi/{client → webflo-url}/Url.js +27 -76
  190. package/src/runtime-pi/webflo-url/index.js +1 -0
  191. package/src/runtime-pi/webflo-url/urlpattern.js +38 -0
  192. package/src/runtime-pi/{util-url.js → webflo-url/util.js} +5 -43
  193. package/src/runtime-pi/webflo-url/xURL.js +94 -0
  194. package/src/runtime-pi/webflo-worker/WebfloWorker.js +234 -0
  195. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +19 -0
  196. package/src/runtime-pi/webflo-worker/WorkerSideWorkport.js +18 -0
  197. package/src/runtime-pi/webflo-worker/index.js +11 -0
  198. package/src/services-pi/index.js +2 -0
  199. package/src/services-pi/push/index.js +23 -0
  200. package/src/util.js +10 -0
  201. package/src/{webflo.js → webflo-cli.js} +4 -4
  202. package/src/runtime-pi/Application.js +0 -29
  203. package/src/runtime-pi/Cookies.js +0 -82
  204. package/src/runtime-pi/HttpEvent.js +0 -107
  205. package/src/runtime-pi/Router.js +0 -130
  206. package/src/runtime-pi/Runtime.js +0 -21
  207. package/src/runtime-pi/client/Application.js +0 -76
  208. package/src/runtime-pi/client/Context.js +0 -7
  209. package/src/runtime-pi/client/Router.js +0 -48
  210. package/src/runtime-pi/client/Runtime.js +0 -525
  211. package/src/runtime-pi/client/Workport.js +0 -190
  212. package/src/runtime-pi/client/createStorage.js +0 -58
  213. package/src/runtime-pi/client/generate.js +0 -481
  214. package/src/runtime-pi/client/index.js +0 -21
  215. package/src/runtime-pi/client/worker/Application.js +0 -44
  216. package/src/runtime-pi/client/worker/Context.js +0 -7
  217. package/src/runtime-pi/client/worker/Runtime.js +0 -275
  218. package/src/runtime-pi/client/worker/Workport.js +0 -78
  219. package/src/runtime-pi/client/worker/index.js +0 -21
  220. package/src/runtime-pi/server/Application.js +0 -101
  221. package/src/runtime-pi/server/Context.js +0 -16
  222. package/src/runtime-pi/server/Router.js +0 -159
  223. package/src/runtime-pi/server/Runtime.js +0 -558
  224. package/src/runtime-pi/server/index.js +0 -21
  225. package/src/runtime-pi/util-http.js +0 -86
  226. package/src/runtime-pi/xFormData.js +0 -24
  227. package/src/runtime-pi/xHeaders.js +0 -146
  228. package/src/runtime-pi/xRequest.js +0 -46
  229. package/src/runtime-pi/xRequestHeaders.js +0 -109
  230. package/src/runtime-pi/xResponse.js +0 -33
  231. package/src/runtime-pi/xResponseHeaders.js +0 -117
  232. package/src/runtime-pi/xURL.js +0 -105
  233. package/src/runtime-pi/xfetch.js +0 -23
  234. package/src/runtime-pi/xxHttpMessage.js +0 -102
  235. 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
+ }