@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.
Files changed (38) hide show
  1. package/docker/Dockerfile +25 -0
  2. package/docker/README.md +69 -0
  3. package/package.json +9 -5
  4. package/src/cmd/client.js +68 -97
  5. package/src/cmd/origins.js +2 -2
  6. package/src/modules/Router.js +130 -0
  7. package/src/modules/_FormData.js +60 -0
  8. package/src/modules/_Headers.js +88 -0
  9. package/src/modules/_MessageStream.js +191 -0
  10. package/src/modules/_NavigationEvent.js +89 -0
  11. package/src/modules/_Request.js +61 -0
  12. package/src/modules/_RequestHeaders.js +72 -0
  13. package/src/modules/_Response.js +56 -0
  14. package/src/modules/_ResponseHeaders.js +81 -0
  15. package/src/modules/_URL.js +111 -0
  16. package/src/modules/client/Cache.js +38 -0
  17. package/src/modules/client/Client.js +51 -22
  18. package/src/modules/client/Http.js +26 -11
  19. package/src/modules/client/NavigationEvent.js +20 -0
  20. package/src/modules/client/Router.js +30 -104
  21. package/src/modules/client/StdRequest.js +34 -33
  22. package/src/modules/client/Storage.js +56 -0
  23. package/src/modules/client/Url.js +1 -1
  24. package/src/modules/client/Worker.js +74 -68
  25. package/src/modules/client/WorkerClient.js +102 -0
  26. package/src/modules/client/WorkerComm.js +183 -0
  27. package/src/modules/client/effects/sounds.js +64 -0
  28. package/src/modules/server/NavigationEvent.js +38 -0
  29. package/src/modules/server/Router.js +53 -124
  30. package/src/modules/server/Server.js +195 -87
  31. package/src/modules/util.js +7 -7
  32. package/src/modules/NavigationEvent.js +0 -32
  33. package/src/modules/Response.js +0 -98
  34. package/src/modules/XURL.js +0 -125
  35. package/src/modules/client/ClientNavigationEvent.js +0 -22
  36. package/src/modules/client/Push.js +0 -84
  37. package/src/modules/server/ServerNavigationEvent.js +0 -23
  38. 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 _isTypeObject from '@webqit/util/js/isTypeObject.js';
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 ServerNavigationEvent from './ServerNavigationEvent.js';
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 && !setup.server.shared) {
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({IncomingMessage: StdIncomingMessage}, (request, response) => {
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({IncomingMessage: StdIncomingMessage}, (request, response) => {
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 serverNavigationEvent = new ServerNavigationEvent(request, protocol);
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[headerName] || '' , headerValue].filter(v => v);
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[name] = value;
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', [serverNavigationEvent], payload, function(_payload) {
326
+ var exitCode = await router.route('deploy', serverNavigationEvent, payload, function(event, _payload) {
263
327
  return defaultPeployFn(_payload);
264
- }, [response]);
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'], [serverNavigationEvent], null, async function() {
279
- var file = await router.fetch(serverNavigationEvent);
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 && !serverNavigationEvent.request.accepts.type(file.contentType)) {
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
- var prerenderMatch = config.prerendering ? await !config.prerendering.match(serverNavigationEvent.url.pathname, flags, hostSetup.layout) : null;
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
- }, [response]);
357
+ });
358
+
359
+ // --------
296
360
  if (!($context.response instanceof serverNavigationEvent.Response)) {
297
- $context.response = new serverNavigationEvent.Response({ body: $context.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.contentType
305
- && !$context.response.redirect
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
- const rendering = await router.route('render', [serverNavigationEvent], $context.response.body, async function(data) {
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 = serverNavigationEvent.url.pathname.split('/');
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: serverNavigationEvent.url.href,
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: serverNavigationEvent.url}, {update: 'merge'});
339
- window.document.body.setAttribute('template', 'page/' + serverNavigationEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
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
- }, [response]);
408
+ });
347
409
  // --------
348
410
  // Serialize rendering?
349
411
  // --------
350
412
  if (_isObject(rendering) && rendering.document) {
351
- $context.response = await _promise(resolve => {
352
- setTimeout(() => {
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
- } else if (serverNavigationEvent.request.accepts.type('application/json')) {
367
- // --------
368
- // JSONfy
369
- // --------
370
- $context.response.contentType = 'application/json';
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 headers
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
- _each($context.response.headers || {}, (name, value) => {
415
- if (name.toLowerCase() === 'set-cookie') {
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
- $context.response.status = 302 /* Temporary */;
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.body !== undefined) {
434
- response.end(
435
- $context.response.contentType === 'application/json' && _isTypeObject($context.response.body)
436
- ? JSON.stringify($context.response.body)
437
- : $context.response.body
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.body);
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 = e.errorCode || 500;
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.body && $context.response.autoIndex ? Ui.style.comment((!serverNavigationEvent.request.url.endsWith('/') ? '/' : '') + $context.response.autoIndex) : '') + ' '
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
  }
@@ -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}[${!isArr ? key : ''}]`, value[key], callback);
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
- }