@webqit/webflo 0.8.45 → 0.8.49
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/docker/Dockerfile +25 -0
- package/docker/README.md +69 -0
- package/package.json +9 -5
- package/src/cmd/client.js +68 -97
- package/src/cmd/origins.js +2 -2
- package/src/modules/Router.js +130 -0
- package/src/modules/_FormData.js +60 -0
- package/src/modules/_Headers.js +88 -0
- package/src/modules/_MessageStream.js +191 -0
- package/src/modules/_NavigationEvent.js +89 -0
- package/src/modules/_Request.js +61 -0
- package/src/modules/_RequestHeaders.js +72 -0
- package/src/modules/_Response.js +56 -0
- package/src/modules/_ResponseHeaders.js +81 -0
- package/src/modules/_URL.js +111 -0
- package/src/modules/client/Cache.js +38 -0
- package/src/modules/client/Client.js +51 -22
- package/src/modules/client/Http.js +26 -11
- package/src/modules/client/NavigationEvent.js +20 -0
- package/src/modules/client/Router.js +30 -104
- package/src/modules/client/StdRequest.js +34 -33
- package/src/modules/client/Storage.js +56 -0
- package/src/modules/client/Url.js +1 -1
- package/src/modules/client/Worker.js +74 -68
- package/src/modules/client/WorkerClient.js +102 -0
- package/src/modules/client/WorkerComm.js +183 -0
- package/src/modules/client/effects/sounds.js +64 -0
- package/src/modules/server/NavigationEvent.js +38 -0
- package/src/modules/server/Router.js +53 -124
- package/src/modules/server/Server.js +195 -87
- package/src/modules/util.js +7 -7
- package/src/modules/NavigationEvent.js +0 -32
- package/src/modules/Response.js +0 -98
- package/src/modules/XURL.js +0 -125
- package/src/modules/client/ClientNavigationEvent.js +0 -22
- package/src/modules/client/Push.js +0 -84
- package/src/modules/server/ServerNavigationEvent.js +0 -23
- package/src/modules/server/StdIncomingMessage.js +0 -70
|
@@ -6,17 +6,21 @@ import Fs from 'fs';
|
|
|
6
6
|
import Path from 'path';
|
|
7
7
|
import Http from 'http';
|
|
8
8
|
import Https from 'https';
|
|
9
|
+
import Formidable from 'formidable';
|
|
9
10
|
import QueryString from 'querystring';
|
|
11
|
+
import Sessions from 'client-sessions';
|
|
10
12
|
import _each from '@webqit/util/obj/each.js';
|
|
11
13
|
import _arrFrom from '@webqit/util/arr/from.js';
|
|
12
14
|
import _promise from '@webqit/util/js/promise.js';
|
|
13
15
|
import _isObject from '@webqit/util/js/isObject.js';
|
|
14
16
|
import _isArray from '@webqit/util/js/isArray.js';
|
|
15
|
-
import
|
|
17
|
+
import { _isString, _isPlainObject, _isPlainArray } from '@webqit/util/js/index.js';
|
|
18
|
+
import _delay from '@webqit/util/js/delay.js';
|
|
19
|
+
import { slice as _streamSlice } from 'stream-slice';
|
|
20
|
+
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
|
|
16
21
|
import * as config from '../../config/index.js';
|
|
17
22
|
import * as cmd from '../../cmd/index.js';
|
|
18
|
-
import
|
|
19
|
-
import StdIncomingMessage from './StdIncomingMessage.js';
|
|
23
|
+
import NavigationEvent from './NavigationEvent.js';
|
|
20
24
|
import Router from './Router.js';
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -36,12 +40,22 @@ export default async function(Ui, flags = {}) {
|
|
|
36
40
|
variables: await config.variables.read(flags, layout),
|
|
37
41
|
};
|
|
38
42
|
|
|
39
|
-
if (setup.variables.autoload !== false
|
|
43
|
+
if (!setup.server.shared && setup.variables.autoload !== false) {
|
|
40
44
|
Object.keys(setup.variables.entries).forEach(key => {
|
|
41
45
|
process.env[key] = setup.variables.entries[key];
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
const getSessionInitializer = (sesskey, hostname = null) => {
|
|
50
|
+
const secret = sesskey || (hostname ? uuidv5(hostname, uuidv4()) : uuidv4());
|
|
51
|
+
return Sessions({
|
|
52
|
+
cookieName: '_session', // cookie name dictates the key name added to the request object
|
|
53
|
+
secret, // should be a large unguessable string
|
|
54
|
+
duration: 24 * 60 * 60 * 1000, // how long the session will stay valid in ms
|
|
55
|
+
activeDuration: 1000 * 60 * 5 // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
const instanceSetup = setup;
|
|
46
60
|
|
|
47
61
|
const v_setup = {};
|
|
@@ -54,9 +68,12 @@ export default async function(Ui, flags = {}) {
|
|
|
54
68
|
variables: await config.variables.read(flags, vlayout),
|
|
55
69
|
vh,
|
|
56
70
|
};
|
|
71
|
+
v_setup[vh.host].sessionInit = getSessionInitializer(v_setup[vh.host].variables.entries.sesskey, vh.host),
|
|
57
72
|
resolve();
|
|
58
73
|
})));
|
|
59
|
-
}
|
|
74
|
+
} else {
|
|
75
|
+
setup.sessionInit = getSessionInitializer(setup.variables.entries.sesskey);
|
|
76
|
+
}
|
|
60
77
|
|
|
61
78
|
// ---------------------------------------------
|
|
62
79
|
|
|
@@ -84,6 +101,7 @@ export default async function(Ui, flags = {}) {
|
|
|
84
101
|
response.setHeader('Location', protocol + '://www.' + hostname + request.url);
|
|
85
102
|
response.end();
|
|
86
103
|
} else {
|
|
104
|
+
setup.sessionInit(request, response, () => {});
|
|
87
105
|
run(instanceSetup, setup, request, response, Ui, flags, protocol);
|
|
88
106
|
}
|
|
89
107
|
};
|
|
@@ -92,7 +110,7 @@ export default async function(Ui, flags = {}) {
|
|
|
92
110
|
|
|
93
111
|
if (!flags['https-only']) {
|
|
94
112
|
|
|
95
|
-
Http.createServer(
|
|
113
|
+
Http.createServer((request, response) => {
|
|
96
114
|
if (setup.server.shared) {
|
|
97
115
|
var _setup;
|
|
98
116
|
if (_setup = getVSetup(request, response)) {
|
|
@@ -119,7 +137,7 @@ export default async function(Ui, flags = {}) {
|
|
|
119
137
|
|
|
120
138
|
if (!flags['http-only'] && setup.server.https.port) {
|
|
121
139
|
|
|
122
|
-
const httpsServer = Https.createServer(
|
|
140
|
+
const httpsServer = Https.createServer((request, response) => {
|
|
123
141
|
if (setup.server.shared) {
|
|
124
142
|
var _setup;
|
|
125
143
|
if (_setup = getVSetup(request, response)) {
|
|
@@ -189,7 +207,58 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
189
207
|
// Request parsing
|
|
190
208
|
// --------
|
|
191
209
|
|
|
192
|
-
const
|
|
210
|
+
const fullUrl = protocol + '://' + request.headers.host + request.url;
|
|
211
|
+
const requestInit = { method: request.method, headers: request.headers };
|
|
212
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
213
|
+
requestInit.body = await new Promise((resolve, reject) => {
|
|
214
|
+
var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: false, keepExtensions: true });
|
|
215
|
+
formidable.parse(request, (error, fields, files) => {
|
|
216
|
+
if (error) {
|
|
217
|
+
reject(error);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (request.headers['content-type'] === 'application/json') {
|
|
221
|
+
return resolve(fields);
|
|
222
|
+
}
|
|
223
|
+
const formData = new NavigationEvent.globals.FormData;
|
|
224
|
+
Object.keys(fields).forEach(name => {
|
|
225
|
+
if (Array.isArray(fields[name])) {
|
|
226
|
+
const values = Array.isArray(fields[name][0])
|
|
227
|
+
? fields[name][0]/* bugly a nested array when there are actually more than entry */
|
|
228
|
+
: fields[name];
|
|
229
|
+
values.forEach(value => {
|
|
230
|
+
formData.append(!name.endsWith(']') ? name + '[]' : name, value);
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
formData.append(name, fields[name]);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
Object.keys(files).forEach(name => {
|
|
237
|
+
const fileCompat = file => {
|
|
238
|
+
// IMPORTANT
|
|
239
|
+
// Path up the "formidable" file in a way that "formdata-node"
|
|
240
|
+
// to can translate it into its own file instance
|
|
241
|
+
file[Symbol.toStringTag] = 'File';
|
|
242
|
+
file.stream = () => Fs.createReadStream(file.path);
|
|
243
|
+
// Done pathcing
|
|
244
|
+
return file;
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(files[name])) {
|
|
247
|
+
files[name].forEach(value => {
|
|
248
|
+
formData.append(name, fileCompat(value));
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
formData.append(name, fileCompat(files[name]));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
resolve(formData);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// The Formidabble thing in NavigationEvent class would still need
|
|
259
|
+
// a reference to the Nodejs request
|
|
260
|
+
const _request = new NavigationEvent.Request(fullUrl, requestInit);
|
|
261
|
+
const serverNavigationEvent = new NavigationEvent(_request, request._session);
|
|
193
262
|
const $context = {
|
|
194
263
|
rdr: null,
|
|
195
264
|
layout: hostSetup.layout,
|
|
@@ -210,10 +279,10 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
210
279
|
isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
|
|
211
280
|
isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
|
|
212
281
|
if (isAppend || isPrepend) {
|
|
213
|
-
headerValue = [ serverNavigationEvent.request.headers
|
|
282
|
+
headerValue = [ serverNavigationEvent.request.headers.get(headerName) || '' , headerValue].filter(v => v);
|
|
214
283
|
headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
|
|
215
284
|
}
|
|
216
|
-
return { name:headerName, value:headerValue };
|
|
285
|
+
return { name: headerName, value: headerValue };
|
|
217
286
|
}
|
|
218
287
|
|
|
219
288
|
// -------------------
|
|
@@ -235,7 +304,7 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
235
304
|
|
|
236
305
|
$context.headers.filter(header => header.type === 'request').forEach(header => {
|
|
237
306
|
const { name, value } = resolveSetHeader(header);
|
|
238
|
-
serverNavigationEvent.request.headers
|
|
307
|
+
serverNavigationEvent.request.headers.set(name, value);
|
|
239
308
|
});
|
|
240
309
|
|
|
241
310
|
// -------------------
|
|
@@ -246,22 +315,17 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
246
315
|
|
|
247
316
|
try {
|
|
248
317
|
|
|
249
|
-
// -------------------
|
|
250
|
-
// Handle autodeploy events
|
|
251
|
-
// -------------------
|
|
252
|
-
|
|
253
318
|
// The app router
|
|
254
319
|
const router = new Router(serverNavigationEvent.url.pathname, hostSetup.layout, $context);
|
|
255
320
|
|
|
256
321
|
// --------
|
|
257
322
|
// ROUTE FOR DEPLOY
|
|
258
323
|
// --------
|
|
259
|
-
|
|
260
324
|
if (cmd.origins) {
|
|
261
325
|
await cmd.origins.hook(Ui, serverNavigationEvent, async (payload, defaultPeployFn) => {
|
|
262
|
-
var exitCode = await router.route('deploy',
|
|
326
|
+
var exitCode = await router.route('deploy', serverNavigationEvent, payload, function(event, _payload) {
|
|
263
327
|
return defaultPeployFn(_payload);
|
|
264
|
-
}
|
|
328
|
+
});
|
|
265
329
|
// -----------
|
|
266
330
|
response.statusCode = 200;
|
|
267
331
|
response.end(exitCode);
|
|
@@ -273,48 +337,46 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
273
337
|
// --------
|
|
274
338
|
// ROUTE FOR DATA
|
|
275
339
|
// --------
|
|
276
|
-
|
|
277
340
|
const httpMethodName = serverNavigationEvent.request.method.toLowerCase();
|
|
278
|
-
$context.response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'],
|
|
279
|
-
var file = await router.fetch(
|
|
341
|
+
$context.response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], serverNavigationEvent, null, async function(event) {
|
|
342
|
+
var file = await router.fetch(event);
|
|
280
343
|
// JSON request should ignore static files
|
|
281
|
-
if (file && !
|
|
344
|
+
if (file && !event.request.headers.accept.match(file.headers.contentType)) {
|
|
282
345
|
return;
|
|
283
346
|
}
|
|
284
347
|
// ----------------
|
|
285
348
|
// PRE-RENDERING
|
|
286
349
|
// ----------------
|
|
287
|
-
if (file && file.contentType === 'text/html' && (file.body + '').startsWith(`<!-- PRE-RENDERED -->`)) {
|
|
288
|
-
|
|
289
|
-
if (!prerenderMatch) {
|
|
350
|
+
if (file && file.headers.contentType === 'text/html' && (file.body + '').startsWith(`<!-- PRE-RENDERED -->`)) {
|
|
351
|
+
if (config.prerendering && !(await !config.prerendering.match(serverNavigationEvent.url.pathname, flags, hostSetup.layout))) {
|
|
290
352
|
router.deletePreRendered(file.filename);
|
|
291
353
|
return;
|
|
292
354
|
}
|
|
293
355
|
}
|
|
294
356
|
return file;
|
|
295
|
-
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// --------
|
|
296
360
|
if (!($context.response instanceof serverNavigationEvent.Response)) {
|
|
297
|
-
$context.response = new serverNavigationEvent.Response(
|
|
361
|
+
$context.response = new serverNavigationEvent.Response($context.response);
|
|
298
362
|
}
|
|
363
|
+
// --------
|
|
299
364
|
|
|
300
365
|
// --------
|
|
301
366
|
// API CALL OR PAGE REQUEST?
|
|
302
367
|
// --------
|
|
303
368
|
|
|
304
|
-
if (!$context.response.
|
|
305
|
-
|
|
306
|
-
&& !$context.response.static
|
|
307
|
-
&& _isTypeObject($context.response.body)) {
|
|
308
|
-
if (serverNavigationEvent.request.accepts.type('text/html')) {
|
|
369
|
+
if (!$context.response.meta.static && (_isPlainObject($context.response.original) || _isPlainArray($context.response.original))) {
|
|
370
|
+
if (serverNavigationEvent.request.headers.accept.match('text/html')) {
|
|
309
371
|
// --------
|
|
310
372
|
// Render
|
|
311
373
|
// --------
|
|
312
|
-
|
|
374
|
+
var rendering = await router.route('render', serverNavigationEvent, $context.response.original, async function(event, data) {
|
|
313
375
|
// --------
|
|
314
376
|
if (!hostSetup.layout.renderFileCache) {
|
|
315
377
|
hostSetup.layout.renderFileCache = {};
|
|
316
378
|
}
|
|
317
|
-
var renderFile, pathnameSplit =
|
|
379
|
+
var renderFile, pathnameSplit = event.url.pathname.split('/');
|
|
318
380
|
while ((renderFile = Path.join(hostSetup.layout.ROOT, hostSetup.layout.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
|
|
319
381
|
&& pathnameSplit.length && (hostSetup.layout.renderFileCache[renderFile] === false || !(hostSetup.layout.renderFileCache[renderFile] && Fs.existsSync(renderFile)))) {
|
|
320
382
|
hostSetup.layout.renderFileCache[renderFile] === false;
|
|
@@ -323,7 +385,7 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
323
385
|
hostSetup.layout.renderFileCache[renderFile] === true;
|
|
324
386
|
const instanceParams = QueryString.stringify({
|
|
325
387
|
SOURCE: renderFile,
|
|
326
|
-
URL:
|
|
388
|
+
URL: event.url.href,
|
|
327
389
|
ROOT: hostSetup.layout.ROOT,
|
|
328
390
|
});
|
|
329
391
|
const { window } = await import('@webqit/pseudo-browser/instance.js?' + instanceParams);
|
|
@@ -335,39 +397,27 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
335
397
|
env: 'server',
|
|
336
398
|
}, {update: true});
|
|
337
399
|
}
|
|
338
|
-
window.document.setState({page: data, url:
|
|
339
|
-
window.document.body.setAttribute('template', 'page/' +
|
|
400
|
+
window.document.setState({page: data, url: event.url}, {update: 'merge'});
|
|
401
|
+
window.document.body.setAttribute('template', 'page/' + event.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
|
|
340
402
|
return new Promise(res => {
|
|
341
403
|
window.document.addEventListener('templatesreadystatechange', () => res(window));
|
|
342
404
|
if (window.document.templatesReadyState === 'complete') {
|
|
343
405
|
res(window);
|
|
344
406
|
}
|
|
345
407
|
});
|
|
346
|
-
}
|
|
408
|
+
});
|
|
347
409
|
// --------
|
|
348
410
|
// Serialize rendering?
|
|
349
411
|
// --------
|
|
350
412
|
if (_isObject(rendering) && rendering.document) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
resolve(new serverNavigationEvent.Response({
|
|
354
|
-
...$context.response,
|
|
355
|
-
contentType: 'text/html',
|
|
356
|
-
body: rendering.print(),
|
|
357
|
-
}));
|
|
358
|
-
}, 1000);
|
|
359
|
-
});
|
|
360
|
-
} else {
|
|
361
|
-
if (!(rendering instanceof serverNavigationEvent.Response)) {
|
|
362
|
-
throw new Error('render() must return a window object or a response Object corresponding to event.Response')
|
|
363
|
-
}
|
|
364
|
-
$context.response = rendering;
|
|
413
|
+
await _delay(1000);
|
|
414
|
+
rendering = rendering.print();
|
|
365
415
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
416
|
+
if (!_isString(rendering)) throw new Error('render() must return a window object or a string response.')
|
|
417
|
+
$context.response = new serverNavigationEvent.Response(rendering, {
|
|
418
|
+
status: $context.response.status,
|
|
419
|
+
headers: { ...$context.response.headers.json(), contentType: 'text/html' },
|
|
420
|
+
});
|
|
371
421
|
}
|
|
372
422
|
}
|
|
373
423
|
|
|
@@ -376,17 +426,32 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
376
426
|
// --------
|
|
377
427
|
|
|
378
428
|
if (!response.headersSent) {
|
|
429
|
+
|
|
430
|
+
// -------------------
|
|
431
|
+
// Streaming headers
|
|
432
|
+
// -------------------
|
|
433
|
+
// Chrome needs this for audio elements to play
|
|
434
|
+
response.setHeader('Accept-Ranges', 'bytes');
|
|
435
|
+
/*
|
|
436
|
+
if ($context.response.headers.contentLength && !$context.response.headers.contentRange) {
|
|
437
|
+
$context.response.headers.contentRange = `bytes 0-${$context.response.headers.contentLength}/${$context.response.headers.contentLength}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
*/
|
|
379
441
|
// -------------------
|
|
380
442
|
// Automatic response headers
|
|
381
443
|
// -------------------
|
|
444
|
+
//response.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
445
|
+
//response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
382
446
|
$context.headers.filter(header => header.type === 'response').forEach(header => {
|
|
383
447
|
const { name, value } = resolveSetHeader(header);
|
|
384
448
|
response.setHeader(name, value);
|
|
385
449
|
});
|
|
450
|
+
|
|
386
451
|
// -------------------
|
|
387
|
-
// Route response
|
|
452
|
+
// Route response cookies
|
|
388
453
|
// -------------------
|
|
389
|
-
const cookieAtts = ['Expires', 'Max-Age', 'Domain', 'Path', 'Secure', 'HttpOnly', 'SameSite' ];
|
|
454
|
+
const cookieAtts = [ 'Expires', 'Max-Age', 'Domain', 'Path', 'Secure', 'HttpOnly', 'SameSite' ];
|
|
390
455
|
const setCookies = (cookies, nameContext = null) => {
|
|
391
456
|
_each(cookies, (name, cookie) => {
|
|
392
457
|
name = nameContext ? `${nameContext}[${name}]` : name;
|
|
@@ -400,9 +465,7 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
400
465
|
var __attr = cookieAtts.reduce((match, _attr) => match || (
|
|
401
466
|
[_attr.toLowerCase(), _attr.replace('-', '').toLowerCase()].includes(attr.toLowerCase()) ? _attr : null
|
|
402
467
|
), null);
|
|
403
|
-
if (!__attr) {
|
|
404
|
-
throw new Error(`Invalid cookie attribute: ${attr}`);
|
|
405
|
-
}
|
|
468
|
+
if (!__attr) throw new Error(`Invalid cookie attribute: ${attr}`);
|
|
406
469
|
expr += cookie[attr] === true ? `; ${__attr}` : `; ${__attr}=${cookie[attr]}`;
|
|
407
470
|
});
|
|
408
471
|
response.setHeader('Set-Cookie', expr);
|
|
@@ -411,51 +474,96 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
411
474
|
}
|
|
412
475
|
});
|
|
413
476
|
};
|
|
414
|
-
|
|
415
|
-
|
|
477
|
+
|
|
478
|
+
// -------------------
|
|
479
|
+
// Route response headers
|
|
480
|
+
// -------------------
|
|
481
|
+
_each($context.response.headers.json(), (name, value) => {
|
|
482
|
+
if ([ 'autoindex', 'filename', 'static' ].includes(name)) return;
|
|
483
|
+
if (name === 'set-cookie') {
|
|
416
484
|
setCookies(value);
|
|
417
485
|
} else {
|
|
418
486
|
if (name.toLowerCase() === 'location' && !$context.response.status) {
|
|
419
|
-
|
|
487
|
+
response.statusCode = 302 /* Temporary */;
|
|
420
488
|
}
|
|
421
489
|
response.setHeader(name, value);
|
|
422
490
|
}
|
|
423
491
|
});
|
|
424
|
-
|
|
425
|
-
// Status code
|
|
426
|
-
// -------------------
|
|
427
|
-
if ($context.response.status) {
|
|
428
|
-
response.statusCode = $context.response.status;
|
|
429
|
-
}
|
|
492
|
+
|
|
430
493
|
// -------------------
|
|
431
494
|
// Send
|
|
432
495
|
// -------------------
|
|
433
|
-
if ($context.response.
|
|
434
|
-
response.end(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
496
|
+
if ($context.response.headers.redirect) {
|
|
497
|
+
response.end();
|
|
498
|
+
} else if ($context.response.original !== undefined && $context.response.original !== null) {
|
|
499
|
+
response.statusCode = $context.response.status;
|
|
500
|
+
response.statusMessage = $context.response.statusText;
|
|
501
|
+
|
|
502
|
+
// ----------------
|
|
503
|
+
// SENDING RESPONSE
|
|
504
|
+
// ----------------
|
|
505
|
+
var body = $context.response.body;
|
|
506
|
+
if ((body instanceof serverNavigationEvent.globals.ReadableStream)
|
|
507
|
+
|| (ArrayBuffer.isView(body) && (body = serverNavigationEvent.globals.ReadableStream.from(body)))) {
|
|
508
|
+
|
|
509
|
+
// We support streaming
|
|
510
|
+
const rangeRequest = serverNavigationEvent.request.headers.range;
|
|
511
|
+
if (rangeRequest) {
|
|
512
|
+
// ...in partials
|
|
513
|
+
const totalLength = $context.response.headers.contentLength;
|
|
514
|
+
// Validate offsets
|
|
515
|
+
if (rangeRequest[0] < 0 || (totalLength && rangeRequest[0] > totalLength)
|
|
516
|
+
|| (rangeRequest[1] > -1 && (rangeRequest[1] <= rangeRequest[0] || (totalLength && rangeRequest[1] >= totalLength)))) {
|
|
517
|
+
response.statusCode = 416;
|
|
518
|
+
response.setHeader('Content-Range', `bytes */${totalLength || '*'}`);
|
|
519
|
+
response.setHeader('Content-Length', 0);
|
|
520
|
+
response.end();
|
|
521
|
+
} else {
|
|
522
|
+
if (totalLength) {
|
|
523
|
+
rangeRequest.clamp(totalLength);
|
|
524
|
+
}
|
|
525
|
+
// Set new headers
|
|
526
|
+
response.writeHead(206, {
|
|
527
|
+
'Content-Range': `bytes ${rangeRequest[0]}-${rangeRequest[1]}/${totalLength || '*'}`,
|
|
528
|
+
'Content-Length': rangeRequest[1] - rangeRequest[0] + 1,
|
|
529
|
+
});
|
|
530
|
+
body
|
|
531
|
+
.pipe(_streamSlice(rangeRequest[0], rangeRequest[1]))
|
|
532
|
+
.pipe(response);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
// ...as a whole
|
|
536
|
+
body.pipe(response);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
// The default
|
|
540
|
+
if ($context.response.headers.contentType === 'application/json') {
|
|
541
|
+
body += '';
|
|
542
|
+
}
|
|
543
|
+
response.end(body);
|
|
544
|
+
}
|
|
545
|
+
|
|
439
546
|
// ----------------
|
|
440
547
|
// PRE-RENDERING
|
|
441
548
|
// ----------------
|
|
442
|
-
if (!$context.response.filename && $context.response.contentType === 'text/html') {
|
|
549
|
+
if (!$context.response.meta.filename && $context.response.headers.contentType === 'text/html') {
|
|
443
550
|
var prerenderMatch = config.prerendering ? await !config.prerendering.match(serverNavigationEvent.url.pathname, flags, hostSetup.layout) : null;
|
|
444
551
|
if (prerenderMatch) {
|
|
445
|
-
router.putPreRendered(serverNavigationEvent.url.pathname, `<!-- PRE-RENDERED -->\r\n` + $context.response.
|
|
552
|
+
router.putPreRendered(serverNavigationEvent.url.pathname, `<!-- PRE-RENDERED -->\r\n` + $context.response.original);
|
|
446
553
|
}
|
|
447
554
|
}
|
|
448
|
-
} else if (!$context.response.redirect) {
|
|
449
|
-
response.statusCode = 404;
|
|
555
|
+
} else if (!$context.response.headers.redirect) {
|
|
556
|
+
response.statusCode = $context.response.status !== 200 ? $context.response.status : 404;
|
|
450
557
|
response.end(`${serverNavigationEvent.request.url} not found!`);
|
|
451
558
|
}
|
|
559
|
+
|
|
452
560
|
}
|
|
453
561
|
|
|
454
562
|
} catch(e) {
|
|
455
563
|
|
|
456
564
|
$context.fatal = e;
|
|
457
|
-
response.statusCode =
|
|
458
|
-
response.end(`Internal server error
|
|
565
|
+
response.statusCode = 500;
|
|
566
|
+
response.end(`Internal server error: ${e.errorCode}`);
|
|
459
567
|
|
|
460
568
|
}
|
|
461
569
|
|
|
@@ -469,12 +577,12 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
469
577
|
Ui.log(''
|
|
470
578
|
+ '[' + (hostSetup.vh ? Ui.style.keyword(hostSetup.vh.host) + '][' : '') + Ui.style.comment((new Date).toUTCString()) + '] '
|
|
471
579
|
+ Ui.style.keyword(protocol.toUpperCase() + ' ' + serverNavigationEvent.request.method) + ' '
|
|
472
|
-
+ Ui.style.url(serverNavigationEvent.request.url) + ($context.response && $context.response.
|
|
473
|
-
+ (' (' + Ui.style.comment($context.response && $context.response.contentType ? $context.response.contentType : 'unknown') + ') ')
|
|
580
|
+
+ Ui.style.url(serverNavigationEvent.request.url) + ($context.response && ($context.response.meta || {}).autoIndex ? Ui.style.comment((!serverNavigationEvent.request.url.endsWith('/') ? '/' : '') + $context.response.meta.autoIndex) : '') + ' '
|
|
581
|
+
+ (' (' + Ui.style.comment($context.response && ($context.response.headers || {}).contentType ? $context.response.headers.contentType : 'unknown') + ') ')
|
|
474
582
|
+ (
|
|
475
|
-
[404, 500].includes(response.statusCode)
|
|
583
|
+
[ 404, 500 ].includes(response.statusCode)
|
|
476
584
|
? Ui.style.err(response.statusCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
|
|
477
|
-
: Ui.style.val(response.statusCode) + ((response.statusCode + '').startsWith('3') ? ' - ' + Ui.style.val(response.getHeader('Location')) : '')
|
|
585
|
+
: Ui.style.val(response.statusCode) + ((response.statusCode + '').startsWith('3') ? ' - ' + Ui.style.val(response.getHeader('Location')) : ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')')
|
|
478
586
|
)
|
|
479
587
|
);
|
|
480
588
|
}
|
package/src/modules/util.js
CHANGED
|
@@ -9,7 +9,7 @@ import _isObject from '@webqit/util/js/isObject.js';
|
|
|
9
9
|
import _beforeLast from '@webqit/util/str/beforeLast.js';
|
|
10
10
|
import _afterLast from '@webqit/util/str/afterLast.js';
|
|
11
11
|
import _arrFrom from '@webqit/util/arr/from.js';
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
/**
|
|
14
14
|
* ---------------
|
|
15
15
|
* @wwwFormPathUnserializeCallback
|
|
@@ -77,23 +77,23 @@ export function wwwFormUnserialize(str, target = {}, delim = '&') {
|
|
|
77
77
|
* ---------------
|
|
78
78
|
*/
|
|
79
79
|
|
|
80
|
-
export function wwwFormPathSerializeCallback(wwwFormPath, value, callback) {
|
|
81
|
-
if (_isObject(value) || _isArray(value)) {
|
|
80
|
+
export function wwwFormPathSerializeCallback(wwwFormPath, value, callback, shouldSerialize = null) {
|
|
81
|
+
if ((_isObject(value) || _isArray(value)) && (!shouldSerialize || shouldSerialize(value, wwwFormPath))) {
|
|
82
82
|
var isArr = _isArray(value);
|
|
83
83
|
Object.keys(value).forEach(key => {
|
|
84
|
-
wwwFormPathSerializeCallback(`${wwwFormPath}[${
|
|
84
|
+
wwwFormPathSerializeCallback(`${wwwFormPath}[${key}]`, value[key], callback, shouldSerialize);
|
|
85
85
|
});
|
|
86
86
|
} else {
|
|
87
87
|
callback(wwwFormPath, !value && value !== 0 ? '' : value);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function wwwFormSerialize(form, delim = '&') {
|
|
91
|
+
export function wwwFormSerialize(form, delim = '&', shouldSerialize = null) {
|
|
92
92
|
var q = [];
|
|
93
93
|
Object.keys(form).forEach(key => {
|
|
94
94
|
wwwFormPathSerializeCallback(key, form[key], (_wwwFormPath, _value) => {
|
|
95
95
|
q.push(`${_wwwFormPath}=${encodeURIComponent(_value)}`);
|
|
96
|
-
});
|
|
96
|
+
}, shouldSerialize);
|
|
97
97
|
});
|
|
98
98
|
return q.join(delim);
|
|
99
99
|
}
|
|
@@ -131,4 +131,4 @@ export const path = {
|
|
|
131
131
|
dirname(path) {
|
|
132
132
|
return this.join(path, "..");
|
|
133
133
|
}
|
|
134
|
-
};
|
|
134
|
+
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @imports
|
|
4
|
-
*/
|
|
5
|
-
import Response from "./Response.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The ClientNavigationEvent class
|
|
9
|
-
*/
|
|
10
|
-
export default class NavigationEvent {
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Initializes a new NavigationEvent instance.
|
|
14
|
-
*
|
|
15
|
-
* @param Request request
|
|
16
|
-
*/
|
|
17
|
-
constructor(request) {
|
|
18
|
-
this._request = request;
|
|
19
|
-
this.Response = Response;
|
|
20
|
-
this.Response._request = request;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// request
|
|
24
|
-
get request() {
|
|
25
|
-
return this._request;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// url
|
|
29
|
-
get url() {
|
|
30
|
-
return this._url;
|
|
31
|
-
}
|
|
32
|
-
}
|