@webqit/webflo 0.8.77 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/package.json +5 -12
  2. package/src/Cli.js +131 -0
  3. package/src/Configurator.js +97 -0
  4. package/src/Context.js +76 -0
  5. package/src/config-pi/deployment/Env.js +69 -0
  6. package/src/config-pi/deployment/Layout.js +65 -0
  7. package/src/config-pi/deployment/Origins.js +133 -0
  8. package/src/config-pi/deployment/Virtualization.js +65 -0
  9. package/src/config-pi/deployment/index.js +18 -0
  10. package/src/config-pi/index.js +16 -0
  11. package/src/config-pi/runtime/Client.js +59 -0
  12. package/src/config-pi/runtime/Server.js +174 -0
  13. package/src/config-pi/runtime/client/Worker.js +117 -0
  14. package/src/config-pi/runtime/client/index.js +12 -0
  15. package/src/config-pi/runtime/index.js +18 -0
  16. package/src/config-pi/runtime/server/Headers.js +90 -0
  17. package/src/config-pi/runtime/server/Redirects.js +108 -0
  18. package/src/config-pi/runtime/server/index.js +14 -0
  19. package/src/config-pi/static/Manifest.js +321 -0
  20. package/src/config-pi/static/Ssg.js +72 -0
  21. package/src/config-pi/static/index.js +14 -0
  22. package/src/deployment-pi/index.js +10 -0
  23. package/src/{services → deployment-pi}/origins/index.js +88 -58
  24. package/src/index.js +14 -147
  25. package/src/{runtime → runtime-pi}/Router.js +19 -19
  26. package/src/runtime-pi/client/Context.js +7 -0
  27. package/src/{runtime → runtime-pi}/client/Router.js +2 -2
  28. package/src/{runtime/client/Navigator.js → runtime-pi/client/Runtime.js} +148 -103
  29. package/src/runtime-pi/client/RuntimeClient.js +114 -0
  30. package/src/{runtime → runtime-pi}/client/Storage.js +1 -1
  31. package/src/{runtime → runtime-pi}/client/Url.js +2 -6
  32. package/src/{runtime/client/WorkerClient.js → runtime-pi/client/WorkerComm.js} +2 -2
  33. package/src/runtime-pi/client/generate.js +242 -0
  34. package/src/runtime-pi/client/generate.oohtml.js +7 -0
  35. package/src/runtime-pi/client/index.js +18 -0
  36. package/src/runtime-pi/client/whatwag.js +27 -0
  37. package/src/runtime-pi/client/worker/Context.js +7 -0
  38. package/src/runtime-pi/client/worker/Worker.js +243 -0
  39. package/src/runtime-pi/client/worker/WorkerClient.js +46 -0
  40. package/src/runtime-pi/client/worker/index.js +18 -0
  41. package/src/runtime-pi/index.js +14 -0
  42. package/src/runtime-pi/server/Context.js +16 -0
  43. package/src/{runtime → runtime-pi}/server/Router.js +6 -6
  44. package/src/runtime-pi/server/Runtime.js +531 -0
  45. package/src/runtime-pi/server/RuntimeClient.js +103 -0
  46. package/src/runtime-pi/server/index.js +41 -0
  47. package/src/runtime-pi/server/whatwag.js +35 -0
  48. package/src/{runtime → runtime-pi}/util.js +0 -0
  49. package/src/{runtime/_FormData.js → runtime-pi/xFormData.js} +2 -2
  50. package/src/{runtime/_Headers.js → runtime-pi/xHeaders.js} +4 -4
  51. package/src/runtime-pi/xHttpEvent.js +93 -0
  52. package/src/runtime-pi/xHttpMessage.js +179 -0
  53. package/src/runtime-pi/xRequest.js +67 -0
  54. package/src/runtime-pi/xRequestHeaders.js +95 -0
  55. package/src/runtime-pi/xResponse.js +62 -0
  56. package/src/{runtime/_ResponseHeaders.js → runtime-pi/xResponseHeaders.js} +38 -18
  57. package/src/{runtime/_URL.js → runtime-pi/xURL.js} +4 -4
  58. package/src/runtime-pi/xfetch.js +7 -0
  59. package/src/{services → services-pi}/certbot/http-auth-hook.js +0 -0
  60. package/src/{services → services-pi}/certbot/http-cleanup-hook.js +0 -0
  61. package/src/{services → services-pi}/certbot/index.js +21 -15
  62. package/src/services-pi/index.js +9 -0
  63. package/src/static-pi/index.js +11 -0
  64. package/src/webflo.js +33 -0
  65. package/test/index.test.js +26 -0
  66. package/src/build/client/index.js +0 -261
  67. package/src/build/index.js +0 -5
  68. package/src/config/client.js +0 -191
  69. package/src/config/headers.js +0 -121
  70. package/src/config/index.js +0 -14
  71. package/src/config/layout.js +0 -83
  72. package/src/config/manifest.js +0 -341
  73. package/src/config/origins.js +0 -165
  74. package/src/config/prerendering.js +0 -100
  75. package/src/config/redirects.js +0 -137
  76. package/src/config/server.js +0 -201
  77. package/src/config/variables.js +0 -102
  78. package/src/config/vhosts.js +0 -93
  79. package/src/runtime/_MessageStream.js +0 -195
  80. package/src/runtime/_NavigationEvent.js +0 -91
  81. package/src/runtime/_Request.js +0 -59
  82. package/src/runtime/_RequestHeaders.js +0 -72
  83. package/src/runtime/_Response.js +0 -56
  84. package/src/runtime/client/NavigationEvent.js +0 -21
  85. package/src/runtime/client/Runtime.js +0 -126
  86. package/src/runtime/client/Worker.js +0 -317
  87. package/src/runtime/client/archive/Cache.js +0 -38
  88. package/src/runtime/client/archive/Http.js +0 -225
  89. package/src/runtime/client/archive/StdRequest.js +0 -74
  90. package/src/runtime/client/archive/WorkerComm.js +0 -183
  91. package/src/runtime/client/effects/sounds.js +0 -64
  92. package/src/runtime/index.js +0 -5
  93. package/src/runtime/server/NavigationEvent.js +0 -39
  94. package/src/runtime/server/Runtime.js +0 -593
  95. package/src/runtime/server/index.js +0 -183
  96. package/src/runtime/server/index.mjs +0 -10
  97. package/src/services/index.js +0 -6
@@ -0,0 +1,531 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Fs from 'fs';
6
+ import Path from 'path';
7
+ import Http from 'http';
8
+ import Https from 'https';
9
+ import Formidable from 'formidable';
10
+ import Sessions from 'client-sessions';
11
+ import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
+ import { _each } from '@webqit/util/obj/index.js';
13
+ import { _isEmpty } from '@webqit/util/js/index.js';
14
+ import { _from as _arrFrom } from '@webqit/util/arr/index.js';
15
+ import { slice as _streamSlice } from 'stream-slice';
16
+ import * as whatwag from './whatwag.js';
17
+ import xURL from '../xURL.js';
18
+ import xFormData from "../xFormData.js";
19
+ import xRequestHeaders from "../xRequestHeaders.js";
20
+ import xResponseHeaders from "../xResponseHeaders.js";
21
+ import xRequest from "../xRequest.js";
22
+ import xResponse from "../xResponse.js";
23
+ import xfetch from '../xfetch.js';
24
+ import xHttpEvent from '../xHttpEvent.js';
25
+
26
+ const URL = xURL(whatwag.URL);
27
+ const FormData = xFormData(whatwag.FormData);
28
+ const ReadableStream = whatwag.ReadableStream;
29
+ const RequestHeaders = xRequestHeaders(whatwag.Headers);
30
+ const ResponseHeaders = xResponseHeaders(whatwag.Headers);
31
+ const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
32
+ const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
33
+ const fetch = xfetch(whatwag.fetch);
34
+ const HttpEvent = xHttpEvent(Request, Response, URL);
35
+
36
+ export {
37
+ URL,
38
+ FormData,
39
+ ReadableStream,
40
+ RequestHeaders,
41
+ ResponseHeaders,
42
+ Request,
43
+ Response,
44
+ fetch,
45
+ HttpEvent,
46
+ Observer,
47
+ }
48
+
49
+ export default class Runtime {
50
+
51
+ /**
52
+ * Runtime
53
+ *
54
+ * @param Object cx
55
+ * @param Function clientCallback
56
+ *
57
+ * @return void
58
+ */
59
+ constructor(cx, clientCallback) {
60
+ // ---------------
61
+ this.cx = cx;
62
+ this.clients = new Map;
63
+ this.mockSessionStore = {};
64
+ this.ready = (async () => {
65
+
66
+ // ---------------
67
+
68
+ const execClientCallback = (cx, hostname) => {
69
+ let client = clientCallback(cx, hostname);
70
+ if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
71
+ return client;
72
+ };
73
+ const loadContextObj = async cx => {
74
+ const meta = {};
75
+ if (_isEmpty(cx.server)) { cx.server = await (new this.cx.config.runtime.Server(cx)).read(); }
76
+ if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
77
+ if (_isEmpty(cx.env)) {
78
+ let env = await (new this.cx.config.deployment.Env(cx)).read();
79
+ cx.env = env.entries;
80
+ meta.envAutoloading = env.autoload;
81
+ }
82
+ return meta;
83
+ };
84
+ let loadMeta = await loadContextObj(this.cx);
85
+ if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
86
+ if (_isEmpty(this.cx.vcontexts)) {
87
+ this.cx.vcontexts = {};
88
+ const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
89
+ await Promise.all((vhosts.entries || []).map(vhost => async () => {
90
+ this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
91
+ await loadContextObj(this.cx.vcontexts[vhost.host]);
92
+ }));
93
+ }
94
+ _each(this.cx.vcontexts, (host, vcontext) => {
95
+ this.clients.set(vhost.host, execClientCallback(vcontext, host));
96
+ });
97
+ } else {
98
+ this.clients.set('*', execClientCallback(this.cx, '*'));
99
+ }
100
+ // Always populate... regardless whether shared setup
101
+ if (loadMeta.envAutoloading !== false) {
102
+ Object.keys(this.cx.env).forEach(key => {
103
+ if (!(key in process.env)) {
104
+ process.env[key] = this.cx.env[key];
105
+ }
106
+ });
107
+ }
108
+
109
+ // ---------------
110
+
111
+ if (!this.cx.flags['test-only'] && !this.cx.flags['https-only']) {
112
+ Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || this.cx.server.port);
113
+ }
114
+
115
+ // ---------------
116
+
117
+ if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
118
+ const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
119
+ if (this.cx.server.shared) {
120
+ _each(this.cx.vcontexts, (host, vcontext) => {
121
+ if (Fs.existsSync(vcontext.server.https.keyfile)) {
122
+ const cert = {
123
+ key: Fs.readFileSync(vcontext.server.https.keyfile),
124
+ cert: Fs.readFileSync(vcontext.server.https.certfile),
125
+ };
126
+ var domains = _arrFrom(vcontext.server.https.certdoms);
127
+ if (!domains[0] || domains[0].trim() === '*') {
128
+ httpsServer.addContext(host, cert);
129
+ if (vcontext.server.force_www) {
130
+ httpsServer.addContext(host.startsWith('www.') ? host.substr(4) : 'www.' + host, cert);
131
+ }
132
+ } else {
133
+ domains.forEach(domain => {
134
+ httpsServer.addContext(domain, cert);
135
+ });
136
+ }
137
+ }
138
+ });
139
+ } else {
140
+ if (Fs.existsSync(this.cx.server.https.keyfile)) {
141
+ var domains = _arrFrom(this.cx.server.https.certdoms);
142
+ var cert = {
143
+ key: Fs.readFileSync(this.cx.server.https.keyfile),
144
+ cert: Fs.readFileSync(this.cx.server.https.certfile),
145
+ };
146
+ if (!domains[0]) {
147
+ domains = ['*'];
148
+ }
149
+ domains.forEach(domain => {
150
+ httpsServer.addContext(domain, cert);
151
+ });
152
+ }
153
+ }
154
+ httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
155
+ }
156
+
157
+ // ---------------
158
+
159
+ const handleRequest = async (protocol, request, response) => {
160
+ // --------
161
+ // Parse request
162
+ const fullUrl = protocol + '://' + request.headers.host + request.url;
163
+ const requestInit = { method: request.method, headers: request.headers };
164
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
165
+ requestInit.body = await new Promise((resolve, reject) => {
166
+ var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: false, keepExtensions: true });
167
+ formidable.parse(request, (error, fields, files) => {
168
+ if (error) {
169
+ reject(error);
170
+ return;
171
+ }
172
+ if (request.headers['content-type'] === 'application/json') {
173
+ return resolve(fields);
174
+ }
175
+ const formData = new FormData;
176
+ Object.keys(fields).forEach(name => {
177
+ if (Array.isArray(fields[name])) {
178
+ const values = Array.isArray(fields[name][0])
179
+ ? fields[name][0]/* bugly a nested array when there are actually more than entry */
180
+ : fields[name];
181
+ values.forEach(value => {
182
+ formData.append(!name.endsWith(']') ? name + '[]' : name, value);
183
+ });
184
+ } else {
185
+ formData.append(name, fields[name]);
186
+ }
187
+ });
188
+ Object.keys(files).forEach(name => {
189
+ const fileCompat = file => {
190
+ // IMPORTANT
191
+ // Path up the "formidable" file in a way that "formdata-node"
192
+ // to can translate it into its own file instance
193
+ file[Symbol.toStringTag] = 'File';
194
+ file.stream = () => Fs.createReadStream(file.path);
195
+ // Done pathcing
196
+ return file;
197
+ }
198
+ if (Array.isArray(files[name])) {
199
+ files[name].forEach(value => {
200
+ formData.append(name, fileCompat(value));
201
+ });
202
+ } else {
203
+ formData.append(name, fileCompat(files[name]));
204
+ }
205
+ });
206
+ resolve(formData);
207
+ });
208
+ });
209
+ }
210
+
211
+ // --------
212
+ // Run Application
213
+ let clientResponse = await this.go(fullUrl, requestInit, { request, response });
214
+ if (response.headersSent) return;
215
+
216
+ // --------
217
+ // Set headers
218
+ _each(clientResponse.headers.json(), (name, value) => {
219
+ response.setHeader(name, value);
220
+ });
221
+
222
+ // --------
223
+ // Send
224
+ response.statusCode = clientResponse.status;
225
+ response.statusMessage = clientResponse.statusText;
226
+ if (clientResponse.headers.redirect) {
227
+ response.end();
228
+ } else {
229
+ var body = clientResponse.body;
230
+ if ((body instanceof ReadableStream)) {
231
+ body.pipe(response);
232
+ } else {
233
+ // The default
234
+ if (clientResponse.headers.contentType === 'application/json') {
235
+ body += '';
236
+ }
237
+ response.end(body);
238
+ }
239
+ }
240
+ };
241
+
242
+ })();
243
+ // ---------------
244
+ Observer.set(this, 'location', {});
245
+ Observer.set(this, 'network', {});
246
+ // ---------------
247
+ if (this.cx.app.title && this.cx.logger) {
248
+ this.cx.logger.info(`> Server running (${this.cx.app.title || ''})`);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Performs a request.
254
+ *
255
+ * @param object|string url
256
+ * @param object init
257
+ * @param object detail
258
+ *
259
+ * @return Response
260
+ */
261
+ async go(url, init = {}, detail = {}) {
262
+ await this.ready;
263
+
264
+ // ------------
265
+ url = typeof url === 'string' ? new whatwag.URL(url) : url;
266
+ init = { referrer: this.location.href, ...init };
267
+ // ------------
268
+ let _context = this.cx, rdr;
269
+ if (this.cx.server.shared && !(_context = this.cx.vcontexts[url.hostname])
270
+ && !(url.hostname.startsWith('www.') && (_context = this.cx.vcontexts[url.hostname.substr(4)]) && _context.server.force_www)
271
+ && !(!url.hostname.startsWith('www.') && (_context = this.cx.vcontexts['www.' + url.hostname]) && _context.server.force_www)) {
272
+ rdr = { status: 500, statusText: 'Unrecognized host' };
273
+ } else if (url.protocol === 'http:' && _context.server.https.force && !this.cx.flags['http-only'] && /** main server */this.cx.server.https.port) {
274
+ rdr = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
275
+ } else if (url.hostname.startsWith('www.') && _context.server.force_www === 'remove') {
276
+ rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
277
+ } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
278
+ rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
279
+ } else if (_context.config.Redirects && (rdr = await (new _context.config.Redirects(_context)).match(url.href))) {
280
+ rdr = { status: rdr.code || 301 /* Permanent */, headers: { Location: rdr.target } };
281
+ }
282
+ if (rdr) {
283
+ return Response(null, rdr);
284
+ }
285
+ const autoHeaders = _context.config.Headers
286
+ ? await (new _context.config.Headers(_context)).match(url.href)
287
+ : [];
288
+ // ------------
289
+
290
+ // ------------
291
+ Observer.set(this.location, url, { detail: { ...init, ...detail, } });
292
+ Observer.set(this.network, 'redirecting', null);
293
+ // ------------
294
+
295
+ // The request object
296
+ let request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
297
+ // The navigation event
298
+ let httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
299
+ return this.getSession(_context, httpEvent, id, options, callback);
300
+ });
301
+ // Response
302
+ let client = this.clients.get('*');
303
+ if (this.cx.server.shared) {
304
+ client = this.clients.get(url.hostname);
305
+ }
306
+ let response = await client.handle(httpEvent, (...args) => this.remoteFetch(...args));
307
+ let finalResponse = await this.handleResponse(httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
308
+ // Logging
309
+ if (this.cx.logger) {
310
+ let log = this.generateLog(httpEvent, finalResponse);
311
+ this.cx.logger.log(log);
312
+ }
313
+ // Return value
314
+ return finalResponse;
315
+ }
316
+
317
+ // Generates request object
318
+ generateRequest(href, init, autoHeaders = []) {
319
+ let request = new Request(href, init);
320
+ this._autoHeaders(request.headers, autoHeaders);
321
+ return request;
322
+ }
323
+
324
+ // Generates session object
325
+ getSession(cx, e, id, options = {}, callback = null) {
326
+ let baseObject;
327
+ if (!(e.detail.request && e.detail.response)) {
328
+ baseObject = this.mockSessionStore;
329
+ let cookieAvailability = e.request.headers.cookies[id]; // We just want to know availability... not validity, as this is understood to be for testing purposes only
330
+ if (!(this.mockSessionStore[id] && cookieAvailability)) {
331
+ let cookieObj = {};
332
+ Object.defineProperty(this.mockSessionStore, id, {
333
+ get: () => cookieObj,
334
+ set: value => (cookieObj = value),
335
+ });
336
+ }
337
+ } else {
338
+ baseObject = e.detail.request;
339
+ Sessions({
340
+ duration: 0, // how long the session will stay valid in ms
341
+ activeDuration: 0, // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
342
+ ...options,
343
+ cookieName: id, // cookie name dictates the key name added to the request object
344
+ secret: cx.env.SESSION_KEY, // should be a large unguessable string
345
+ })(e.detail.request, e.detail.response, e => {
346
+ if (e) {
347
+ if (!callback) throw e;
348
+ callback(e);
349
+ }
350
+ });
351
+ }
352
+ // Where theres no error, instance is available
353
+ let instance = Object.getOwnPropertyDescriptor(baseObject, id);
354
+ if (!callback) return instance;
355
+ if (instance) callback(null, instance);
356
+ }
357
+
358
+ // Initiates remote fetch and sets the status
359
+ remoteFetch(request) {
360
+ Observer.set(this.network, 'remote', true);
361
+ let _response = fetch(request);
362
+ // This catch() is NOT intended to handle failure of the fetch
363
+ _response.catch(e => Observer.set(this.network, 'error', e.message));
364
+ // Save a reference to this
365
+ return _response.then(async response => {
366
+ // Stop loading status
367
+ Observer.set(this.network, 'remote', false);
368
+ return new Response(response);
369
+ });
370
+ }
371
+
372
+ // Handles response object
373
+ async handleResponse(e, response, autoHeaders = []) {
374
+ if (!(response instanceof Response)) { response = new Response(response); }
375
+ Observer.set(this.network, 'remote', false);
376
+ Observer.set(this.network, 'error', null);
377
+
378
+ // ----------------
379
+ // Mock-Cookies?
380
+ // ----------------
381
+ if (!(e.detail.request && e.detail.response)) {
382
+ for (let cookieName of Object.getOwnPropertyNames(this.mockSessionStore)) {
383
+ response.headers.set('Set-Cookie', `${cookieName}=1`); // We just want to know availability... not validity, as this is understood to be for testing purposes only
384
+ }
385
+ }
386
+
387
+ // ----------------
388
+ // Auto-Headers
389
+ // ----------------
390
+ response.headers.set('Accept-Ranges', 'bytes');
391
+ this._autoHeaders(response.headers, autoHeaders);
392
+
393
+ // ----------------
394
+ // Redirects
395
+ // ----------------
396
+ if (response.headers.redirect) {
397
+ let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
398
+ let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
399
+ let isSameOriginRedirect = (new whatwag.URL(response.headers.location, e.url.origin)).origin === e.url.origin;
400
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
401
+ response.attrs.status = xRedirectCode;
402
+ response.headers.json({
403
+ 'X-Redirect-Code': response.status,
404
+ 'Cache-Control': 'no-store',
405
+ });
406
+ }
407
+ return response;
408
+ }
409
+
410
+ // ----------------
411
+ // 404
412
+ // ----------------
413
+ if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
414
+ response.attrs.status = response.status !== 200 ? response.status : 404;
415
+ response.attrs.statusText = `${e.request.url} not found!`;
416
+ return response;
417
+ }
418
+
419
+ // ----------------
420
+ // Not acceptable
421
+ // ----------------
422
+ if (e.request.headers.get('Accept') && !e.request.headers.accept.match(response.headers.contentType)) {
423
+ response.attrs.status = 406;
424
+ return response;
425
+ }
426
+
427
+ // ----------------
428
+ // Important no-caching
429
+ // for non-"get" requests
430
+ // ----------------
431
+ if (e.request.method !== 'GET' && !response.headers.get('Cache-Control')) {
432
+ response.headers.set('Cache-Control', 'no-store');
433
+ }
434
+
435
+ // ----------------
436
+ // Body
437
+ // ----------------
438
+ let rangeRequest, body = response.body;
439
+ if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
440
+ && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
441
+ // ...in partials
442
+ let totalLength = response.headers.contentLength || 0;
443
+ let ranges = await rangeRequest.reduce(async (_ranges, range) => {
444
+ _ranges = await _ranges;
445
+ if (range[0] < 0 || (totalLength && range[0] > totalLength)
446
+ || (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
447
+ // The range is beyond upper/lower limits
448
+ _ranges.error = true;
449
+ }
450
+ if (!totalLength && range[0] === undefined) {
451
+ // totalLength is unknown and we cant read the trailing size specified in range[1]
452
+ _ranges.error = true;
453
+ }
454
+ if (_ranges.error) return _ranges;
455
+ if (totalLength) { range.clamp(totalLength); }
456
+ let partLength = range[1] - range[0] + 1;
457
+ _ranges.parts.push({
458
+ body: body.pipe(_streamSlice(range[0], range[1] + 1)),
459
+ range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
460
+ length: partLength,
461
+ });
462
+ _ranges.totalLength += partLength;
463
+ return _ranges;
464
+ }, { parts: [], totalLength: 0 });
465
+ if (ranges.error) {
466
+ response.attrs.status = 416;
467
+ response.headers.json({
468
+ 'Content-Range': `bytes */${totalLength || '*'}`,
469
+ 'Content-Length': 0,
470
+ });
471
+ } else {
472
+ // TODO: of ranges.parts is more than one, return multipart/byteranges
473
+ response = new Response(ranges.parts[0].body, {
474
+ status: 206,
475
+ statusText: response.statusText,
476
+ headers: response.headers,
477
+ });
478
+ response.headers.json({
479
+ 'Content-Range': ranges.parts[0].range,
480
+ 'Content-Length': ranges.totalLength,
481
+ });
482
+ }
483
+ }
484
+
485
+ return response;
486
+ }
487
+
488
+ // Generates log
489
+ generateLog(e, response) {
490
+ let log = [];
491
+ // ---------------
492
+ let style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
493
+ let errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
494
+ let xRedirectCode = response.headers.get('X-Redirect-Code');
495
+ let redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
496
+ let statusCode = xRedirectCode || response.status;
497
+ // ---------------
498
+ log.push(`[${style.comment((new Date).toUTCString())}]`);
499
+ log.push(style.keyword(e.request.method));
500
+ log.push(style.url(e.request.url));
501
+ if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
502
+ if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
503
+ if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
504
+ else log.push(style.val(`${statusCode} ${response.statusText}`));
505
+ if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
506
+
507
+ return log.join(' ');
508
+ }
509
+
510
+ // Applies auto headers
511
+ _autoHeaders(headers, autoHeaders) {
512
+ autoHeaders.forEach(header => {
513
+ var headerName = header.name.toLowerCase(),
514
+ headerValue = header.value,
515
+ isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
516
+ isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
517
+ if (isAppend || isPrepend) {
518
+ headerValue = [ headers.get(headerName) || '' , headerValue].filter(v => v);
519
+ headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
520
+ }
521
+ headers.set(headerName, headerValue);
522
+ });
523
+ }
524
+
525
+ }
526
+
527
+ const _streamRead = stream => new Promise(res => {
528
+ let data = '';
529
+ stream.on('data', chunk => data += chunk);
530
+ stream.on('end', () => res(data));
531
+ });
@@ -0,0 +1,103 @@
1
+
2
+ /**
3
+ * imports
4
+ */
5
+ import Fs from 'fs';
6
+ import Path from 'path';
7
+ import QueryString from 'querystring';
8
+ import Router from './Router.js';
9
+
10
+ export default class RuntimeClient {
11
+
12
+ /**
13
+ * RuntimeClient
14
+ *
15
+ * @param Context cx
16
+ */
17
+ constructor(cx) {
18
+ this.cx = cx;
19
+ this.renderFileCache = {};
20
+ }
21
+
22
+ /**
23
+ * Handles navigation events.
24
+ *
25
+ * @param NavigationEvent httpEvent
26
+ * @param Function remoteFetch
27
+ *
28
+ * @return Response
29
+ */
30
+ async handle(httpEvent, remoteFetch) {
31
+ // The app router
32
+ const router = new Router(this.cx, httpEvent.url.pathname);
33
+ const handle = async () => {
34
+ // --------
35
+ // ROUTE FOR DATA
36
+ // --------
37
+ let httpMethodName = httpEvent.request.method.toLowerCase();
38
+ let response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], httpEvent, {}, async event => {
39
+ return router.file(event);
40
+ }, remoteFetch);
41
+ if (!(response instanceof httpEvent.Response)) {
42
+ response = new httpEvent.Response(response);
43
+ }
44
+
45
+ // --------
46
+ // Rendering
47
+ // --------
48
+ if (response.ok && response.bodyAttrs.inputType === 'object' && httpEvent.request.headers.accept.match('text/html')) {
49
+ let rendering = await this.render(httpEvent, router, response);
50
+ if (typeof rendering !== 'string') {
51
+ throw new Error('render() must return a window object or a string response.');
52
+ }
53
+ response = new httpEvent.Response(rendering, {
54
+ status: response.status,
55
+ headers: { ...response.headers.json(), contentType: 'text/html' },
56
+ });
57
+ }
58
+
59
+ return response;
60
+ };
61
+
62
+ // --------
63
+ // PIPE THROUGH MIDDLEWARES
64
+ // --------
65
+ return (this.cx.middlewares || []).concat(handle).reverse().reduce((next, fn) => {
66
+ return () => fn.call(this.cx, httpEvent, router, next);
67
+ }, null)();
68
+ }
69
+
70
+ // Renderer
71
+ async render(httpEvent, router, response) {
72
+ let data = await response.json();
73
+ let rendering = await router.route('render', httpEvent, data, async (httpEvent, data) => {
74
+ var renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
75
+ while ((renderFile = Path.join(this.cx.CWD, this.cx.layout.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
76
+ && (this.renderFileCache[renderFile] === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
77
+ this.renderFileCache[renderFile] = false;
78
+ pathnameSplit.pop();
79
+ }
80
+ const instanceParams = QueryString.stringify({
81
+ SOURCE: renderFile,
82
+ URL: httpEvent.url.href,
83
+ ROOT: this.cx.CWD,
84
+ });
85
+ const { window } = await import('@webqit/oohtml-ssr/instance.js?' + instanceParams);
86
+ // --------
87
+ // OOHTML would waiting for DOM-ready in order to be initialized
88
+ await new Promise(res => window.WebQit.DOM.ready(res));
89
+ await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
90
+ if (!window.document.state.env) {
91
+ window.document.setState({
92
+ env: 'server',
93
+ }, { update: true });
94
+ }
95
+ window.document.setState({ page: data, url: httpEvent.url }, { update: 'merge' });
96
+ window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
97
+ await new Promise(res => setTimeout(res, 10));
98
+ return window;
99
+ });
100
+ return rendering + '';
101
+ }
102
+
103
+ }
@@ -0,0 +1,41 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Context from './Context.js';
6
+ import RuntimeClient from './RuntimeClient.js';
7
+ import Runtime from './Runtime.js';
8
+
9
+ /**
10
+ * @desc
11
+ */
12
+ export const desc = {
13
+ config: {
14
+ headers: 'Configure automatic http headers.',
15
+ prerendering: 'Configure prerendering.',
16
+ redirects: 'Configure automatic redirects.',
17
+ server: 'Configure server settings.',
18
+ vhosts: 'Configure virtual hosts.',
19
+ },
20
+ start: 'Starts the Webflo server.',
21
+ };
22
+
23
+ /**
24
+ * @start
25
+ */
26
+ export async function start(clientCallback = null) {
27
+ const cx = this || {};
28
+ const defaultClientCallback = _cx => new RuntimeClient(_cx);
29
+ return new Runtime(Context.create(cx), ( ...args ) => {
30
+ return clientCallback ? clientCallback( ...args.concat( defaultClientCallback ) ) : defaultClientCallback( ...args );
31
+ });
32
+ }
33
+
34
+ /**
35
+ * @exports
36
+ */
37
+ export {
38
+ Context,
39
+ RuntimeClient,
40
+ Runtime,
41
+ }