@webqit/webflo 0.11.39 → 0.11.41

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 (47) hide show
  1. package/README.md +1 -1
  2. package/docker/Dockerfile +43 -0
  3. package/docker/README.md +91 -0
  4. package/docker/package.json +3 -0
  5. package/package.json +2 -3
  6. package/src/Context.js +3 -3
  7. package/src/config-pi/deployment/Layout.js +0 -1
  8. package/src/config-pi/deployment/Origins.js +7 -0
  9. package/src/config-pi/deployment/{Virtualization.js → Proxy.js} +3 -3
  10. package/src/config-pi/deployment/index.js +2 -2
  11. package/src/config-pi/runtime/Server.js +2 -2
  12. package/src/deployment-pi/origins/index.js +3 -2
  13. package/src/runtime-pi/{RuntimeClient.js → Application.js} +2 -2
  14. package/src/runtime-pi/HttpEvent.js +106 -0
  15. package/src/runtime-pi/Router.js +2 -3
  16. package/src/runtime-pi/Runtime.js +3 -3
  17. package/src/runtime-pi/client/{RuntimeClient.js → Application.js} +12 -4
  18. package/src/runtime-pi/client/Router.js +4 -3
  19. package/src/runtime-pi/client/Runtime.js +37 -59
  20. package/src/runtime-pi/client/Url.js +3 -3
  21. package/src/runtime-pi/client/Workport.js +1 -1
  22. package/src/runtime-pi/client/{Storage.js → createStorage.js} +3 -3
  23. package/src/runtime-pi/client/generate.js +5 -3
  24. package/src/runtime-pi/client/index.js +4 -4
  25. package/src/runtime-pi/client/worker/{WorkerClient.js → Application.js} +12 -8
  26. package/src/runtime-pi/client/worker/{Worker.js → Runtime.js} +25 -27
  27. package/src/runtime-pi/client/worker/index.js +6 -6
  28. package/src/runtime-pi/server/{RuntimeClient.js → Application.js} +8 -8
  29. package/src/runtime-pi/server/Router.js +3 -2
  30. package/src/runtime-pi/server/Runtime.js +50 -107
  31. package/src/runtime-pi/server/index.js +4 -4
  32. package/src/runtime-pi/util-http.js +70 -0
  33. package/src/runtime-pi/util-url.js +147 -0
  34. package/src/runtime-pi/xFormData.js +10 -46
  35. package/src/runtime-pi/xHeaders.js +2 -11
  36. package/src/runtime-pi/xRequest.js +29 -42
  37. package/src/runtime-pi/xRequestHeaders.js +20 -23
  38. package/src/runtime-pi/xResponse.js +19 -15
  39. package/src/runtime-pi/xResponseHeaders.js +41 -43
  40. package/src/runtime-pi/xURL.js +71 -77
  41. package/src/runtime-pi/xfetch.js +15 -6
  42. package/src/runtime-pi/xxHttpMessage.js +102 -0
  43. package/src/runtime-pi/client/whatwag.js +0 -27
  44. package/src/runtime-pi/server/whatwag.js +0 -35
  45. package/src/runtime-pi/util.js +0 -162
  46. package/src/runtime-pi/xHttpEvent.js +0 -101
  47. package/src/runtime-pi/xHttpMessage.js +0 -171
@@ -6,44 +6,22 @@ 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';
10
9
  import Sessions from 'client-sessions';
11
10
  import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
11
  import { _each } from '@webqit/util/obj/index.js';
13
12
  import { _isEmpty } from '@webqit/util/js/index.js';
14
13
  import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
15
14
  import { slice as _streamSlice } from 'stream-slice';
16
- import { urlPattern } from '../util.js';
17
- import * as whatwag from './whatwag.js';
18
- import xURL from '../xURL.js';
19
- import xFormData from "../xFormData.js";
20
- import xRequestHeaders from "../xRequestHeaders.js";
21
- import xResponseHeaders from "../xResponseHeaders.js";
15
+ import { Readable as _ReadableStream } from 'stream';
16
+ import { pattern } from '../util-url.js';
22
17
  import xRequest from "../xRequest.js";
23
18
  import xResponse from "../xResponse.js";
24
19
  import xfetch from '../xfetch.js';
25
- import xHttpEvent from '../xHttpEvent.js';
20
+ import HttpEvent from '../HttpEvent.js';
26
21
  import _Runtime from '../Runtime.js';
27
22
 
28
- const URL = xURL(whatwag.URL);
29
- const FormData = xFormData(whatwag.FormData);
30
- const ReadableStream = whatwag.ReadableStream;
31
- const RequestHeaders = xRequestHeaders(whatwag.Headers);
32
- const ResponseHeaders = xResponseHeaders(whatwag.Headers);
33
- const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
34
- const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
35
- const fetch = xfetch(whatwag.fetch);
36
- const HttpEvent = xHttpEvent(Request, Response, URL);
37
-
38
23
  export {
39
- URL,
40
- FormData,
41
- ReadableStream,
42
- RequestHeaders,
43
- ResponseHeaders,
44
- Request,
45
- Response,
46
- fetch,
24
+ //fetch,
47
25
  HttpEvent,
48
26
  Observer,
49
27
  }
@@ -54,18 +32,18 @@ export default class Runtime extends _Runtime {
54
32
  * Runtime
55
33
  *
56
34
  * @param Object cx
57
- * @param Function clientCallback
35
+ * @param Function applicationInstance
58
36
  *
59
37
  * @return void
60
38
  */
61
- constructor(cx, clientCallback) {
62
- super(cx, clientCallback);
39
+ constructor(cx, applicationInstance) {
40
+ super(cx, applicationInstance);
63
41
  // ---------------
64
42
  this.ready = (async () => {
65
43
  // ---------------
66
44
  const resolveContextObj = async (cx, force = false) => {
67
- if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
68
45
  if (_isEmpty(cx.layout) || force) { cx.layout = await (new cx.config.deployment.Layout(cx)).read(); }
46
+ if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
69
47
  if (_isEmpty(cx.env) || force) { cx.env = await (new cx.config.deployment.Env(cx)).read(); }
70
48
  };
71
49
  await resolveContextObj(this.cx);
@@ -80,14 +58,15 @@ export default class Runtime extends _Runtime {
80
58
  const parseDomains = domains => _arrFrom(domains).reduce((arr, str) => arr.concat(str.split(',')), []).map(str => str.trim()).filter(str => str);
81
59
  const selectDomains = (serverDefs, matchingPort = null) => serverDefs.reduce((doms, def) => doms.length ? doms : (((!matchingPort || def.port === matchingPort) && parseDomains(def.domains || def.hostnames)) || []), []);
82
60
  // ---------------
83
- this.vhosts = new Map;
84
- if (this.cx.config.deployment.Virtualization) {
85
- const vhosts = await (new this.cx.config.deployment.Virtualization(this.cx)).read();
86
- await Promise.all((vhosts.entries || []).map(async vhost => {
61
+ this.proxied = new Map;
62
+ if (this.cx.config.deployment.Proxy) {
63
+ const proxied = await (new this.cx.config.deployment.Proxy(this.cx)).read();
64
+ await Promise.all((proxied.entries || []).map(async vhost => {
87
65
  let cx, hostnames = parseDomains(vhost.hostnames), port = vhost.port, proto = vhost.proto;
88
66
  if (vhost.path) {
89
67
  cx = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
90
68
  await resolveContextObj(cx, true);
69
+ cx.dict.key = true;
91
70
  // From the server that's most likely to be active
92
71
  port || (port = cx.server.https.port || cx.server.port);
93
72
  // The domain list that corresponds to the specified resolved port
@@ -98,7 +77,7 @@ export default class Runtime extends _Runtime {
98
77
  proto || (proto = port === cx.server.https.port ? 'https' : 'http');
99
78
  }
100
79
  hostnames.length || (hostnames = ['*']);
101
- this.vhosts.set(hostnames.sort().join('|'), { cx, hostnames, port, proto });
80
+ this.proxied.set(hostnames.sort().join('|'), { cx, hostnames, port, proto });
102
81
  }));
103
82
  }
104
83
  // ---------------
@@ -139,12 +118,13 @@ export default class Runtime extends _Runtime {
139
118
  });
140
119
  // -------
141
120
  addSSLContext(this.cx.server, domains);
142
- for (const [ /*id*/, vhost ] of this.vhosts) {
121
+ for (const [ /*id*/, vhost ] of this.proxied) {
143
122
  vhost.cx && addSSLContext(vhost.cx.server, vhost.hostnames);
144
123
  }
145
124
  }
146
125
  // ---------------
147
126
  const handleRequest = async (proto, request, response) => {
127
+ request[Symbol.toStringTag] = 'ReadableStream';
148
128
  const [ fullUrl, requestInit ] = await this.parseNodeRequest(proto, request);
149
129
  let clientResponse = await this.go(fullUrl, requestInit, { request, response });
150
130
  if (response.headersSent) return;
@@ -158,9 +138,12 @@ export default class Runtime extends _Runtime {
158
138
  if (clientResponse.headers.location) {
159
139
  return response.end();
160
140
  }
161
- if ((clientResponse.body instanceof ReadableStream)) {
141
+ if ((clientResponse.body instanceof _ReadableStream)) {
162
142
  return clientResponse.body.pipe(response);
163
143
  }
144
+ if ((clientResponse.body instanceof ReadableStream)) {
145
+ return _ReadableStream.from(clientResponse.body).pipe(response);
146
+ }
164
147
  let body = clientResponse.body;
165
148
  if (clientResponse.headers.contentType === 'application/json') {
166
149
  body += '';
@@ -187,20 +170,20 @@ export default class Runtime extends _Runtime {
187
170
  } else {
188
171
  this.cx.logger.info(`> Server not running! No port specified.`);
189
172
  }
190
- if (this.vhosts.size) {
173
+ if (this.proxied.size) {
191
174
  this.cx.logger.info(`> Reverse proxy active.`);
192
- for (let [ id, def ] of this.vhosts) {
175
+ for (let [ id, def ] of this.proxied) {
193
176
  this.cx.logger.info(`> ${ id } >>> ${ def.port }`);
194
177
  }
195
178
  }
196
179
  this.cx.logger.info(``);
197
180
  }
198
- if (this.client && this.client.init) {
199
- const request = this.generateRequest('/');
181
+ if (this.app && this.app.init) {
182
+ const request = this.generateRequest('http://localhost/');
200
183
  const httpEvent = new HttpEvent(request, { srcType: 'initialization' }, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
201
184
  return this.getSession(this.cx, httpEvent, id, options, callback);
202
185
  });
203
- await this.client.init(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
186
+ await this.app.init(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
204
187
  }
205
188
  })();
206
189
  // ---------------
@@ -220,55 +203,14 @@ export default class Runtime extends _Runtime {
220
203
  * @return Array
221
204
  */
222
205
  async parseNodeRequest(proto, request) {
223
- let url = request.url;
224
206
  // Detected when using manual proxy setting in a browser
225
- if (url.startsWith(`http://${ request.headers.host }`) || url.startsWith(`https://${ request.headers.host }`)) {
226
- url = url.split(request.headers.host)[1];
207
+ if (request.url.startsWith(`http://${ request.headers.host }`) || request.url.startsWith(`https://${ request.headers.host }`)) {
208
+ request.url = request.url.split(request.headers.host)[1];
227
209
  }
228
- const fullUrl = proto + '://' + request.headers.host + url;
210
+ const fullUrl = proto + '://' + request.headers.host + request.url;
229
211
  const requestInit = { method: request.method, headers: request.headers };
230
- if (request.method !== 'GET' && request.method !== 'HEAD') {
231
- requestInit.body = await new Promise((resolve, reject) => {
232
- var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: true, keepExtensions: true });
233
- formidable.parse(request, (error, fields, files) => {
234
- if (error) { return reject(error); }
235
- if (request.headers['content-type'] === 'application/json') {
236
- return resolve(fields);
237
- }
238
- const formData = new FormData;
239
- Object.keys(fields).forEach(name => {
240
- if (Array.isArray(fields[name])) {
241
- const values = Array.isArray(fields[name][0])
242
- ? fields[name][0]/* bugly a nested array when there are actually more than entry */
243
- : fields[name];
244
- values.forEach(value => {
245
- formData.append(!name.endsWith(']') ? name + '[]' : name, value);
246
- });
247
- } else {
248
- formData.append(name, fields[name]);
249
- }
250
- });
251
- Object.keys(files).forEach(name => {
252
- const fileCompat = file => {
253
- // IMPORTANT
254
- // Path up the "formidable" file in a way that "formdata-node"
255
- // to can translate it into its own file instance
256
- file[Symbol.toStringTag] = 'File';
257
- file.stream = () => Fs.createReadStream(file.path);
258
- // Done pathcing
259
- return file;
260
- }
261
- if (Array.isArray(files[name])) {
262
- files[name].forEach(value => {
263
- formData.append(name, fileCompat(value));
264
- });
265
- } else {
266
- formData.append(name, fileCompat(files[name]));
267
- }
268
- });
269
- resolve(formData);
270
- });
271
- });
212
+ if (!['GET', 'HEAD'].includes(request.method)) {
213
+ requestInit.body = request;
272
214
  }
273
215
  return [ fullUrl, requestInit ];
274
216
  }
@@ -286,13 +228,13 @@ export default class Runtime extends _Runtime {
286
228
  await this.ready;
287
229
 
288
230
  // ------------
289
- url = typeof url === 'string' ? new whatwag.URL(url) : url;
231
+ url = typeof url === 'string' ? new URL(url) : url;
290
232
  init = { referrer: this.location.href, ...init };
291
233
  // ------------
292
234
  const hosts = [];
293
235
  this.servers.forEach(server => hosts.push(...server.domains));
294
236
  // ------------
295
- for (const [ /*id*/, vhost ] of this.vhosts) {
237
+ for (const [ /*id*/, vhost ] of this.proxied) {
296
238
  if (vhost.hostnames.includes(url.hostname) || (vhost.hostnames.includes('*') && !hosts.includes('*'))) {
297
239
  return this.proxyGo(vhost, url, init);
298
240
  }
@@ -309,10 +251,10 @@ export default class Runtime extends _Runtime {
309
251
  exit = { status: 302, headers: { Location: ( url.hostname = `www.${ url.hostname }`, url.href ) } };
310
252
  } else if (this.cx.config.runtime.server.Redirects) {
311
253
  exit = ((await (new this.cx.config.runtime.server.Redirects(this.cx)).read()).entries || []).reduce((_rdr, entry) => {
312
- return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
254
+ return _rdr || ((_rdr = pattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
313
255
  }, null);
314
256
  }
315
- if (exit) { return new Response(null, exit); }
257
+ if (exit) { return new xResponse(null, exit); }
316
258
  // ------------
317
259
 
318
260
  // ------------
@@ -323,7 +265,7 @@ export default class Runtime extends _Runtime {
323
265
  // ------------
324
266
  // Automatically-added headers
325
267
  const autoHeaders = this.cx.config.runtime.server.Headers
326
- ? ((await (new this.cx.config.runtime.server.Headers(this.cx)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
268
+ ? ((await (new this.cx.config.runtime.server.Headers(this.cx)).read()).entries || []).filter(entry => pattern(entry.url, url.origin).exec(url.href))
327
269
  : [];
328
270
  // The request object
329
271
  const request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
@@ -334,10 +276,10 @@ export default class Runtime extends _Runtime {
334
276
  // Response
335
277
  let response, finalResponse;
336
278
  try {
337
- response = await this.client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
279
+ response = await this.app.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
338
280
  finalResponse = await this.handleResponse(this.cx, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
339
281
  } catch(e) {
340
- finalResponse = new Response(null, { status: 500, statusText: e.message });
282
+ finalResponse = new xResponse(null, { status: 500, statusText: e.message });
341
283
  console.error(e);
342
284
  }
343
285
  // Logging
@@ -353,19 +295,20 @@ export default class Runtime extends _Runtime {
353
295
  // Fetch from proxied host
354
296
  async proxyGo(vhost, url, init) {
355
297
  // ---------
356
- const url2 = new whatwag.URL(url);
298
+ const url2 = new URL(url);
357
299
  url2.port = vhost.port;
358
300
  if (vhost.proto) { url2.protocol = vhost.proto; }
359
301
  // ---------
360
- const init2 = { ...init, compress: false };
302
+ const init2 = { ...init, decompress: false/* honoured in xfetch() */ };
361
303
  if (!init2.headers) init2.headers = {};
362
304
  init2.headers.host = url2.host;
305
+ delete init2.headers.connection;
363
306
  // ---------
364
307
  let response;
365
308
  try {
366
309
  response = await this.remoteFetch(url2, init2);
367
310
  } catch(e) {
368
- response = new Response(null, { status: 500, statusText: e.message });
311
+ response = new xResponse(null, { status: 500, statusText: e.message });
369
312
  console.error(e);
370
313
  }
371
314
  if (this.cx.logger) {
@@ -378,7 +321,7 @@ export default class Runtime extends _Runtime {
378
321
 
379
322
  // Generates request object
380
323
  generateRequest(href, init = {}, autoHeaders = []) {
381
- const request = new Request(href, init);
324
+ const request = new xRequest(href, init);
382
325
  this._autoHeaders(request.headers, autoHeaders);
383
326
  return request;
384
327
  }
@@ -422,24 +365,24 @@ export default class Runtime extends _Runtime {
422
365
  let href = request;
423
366
  if (request instanceof Request) {
424
367
  href = request.url;
425
- } else if (request instanceof whatwag.URL) {
368
+ } else if (request instanceof URL) {
426
369
  href = request.href;
427
370
  }
428
371
  Observer.set(this.network, 'remote', href);
429
- const _response = fetch(request, ...args);
372
+ const _response = xfetch(request, ...args);
430
373
  // This catch() is NOT intended to handle failure of the fetch
431
374
  _response.catch(e => Observer.set(this.network, 'error', e.message));
432
375
  // Save a reference to this
433
376
  return _response.then(async response => {
434
377
  // Stop loading status
435
378
  Observer.set(this.network, 'remote', false);
436
- return new Response(response);
379
+ return new xResponse(response);
437
380
  });
438
381
  }
439
382
 
440
383
  // Handles response object
441
384
  async handleResponse(cx, e, response, autoHeaders = []) {
442
- if (!(response instanceof Response)) { response = new Response(response); }
385
+ if (!(response instanceof xResponse)) { response = new xResponse(response); }
443
386
  Observer.set(this.network, 'remote', false);
444
387
  Observer.set(this.network, 'error', null);
445
388
 
@@ -461,7 +404,7 @@ export default class Runtime extends _Runtime {
461
404
  if (response.headers.location) {
462
405
  const xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
463
406
  const xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
464
- const destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
407
+ const destinationUrl = new URL(response.headers.location, e.url.origin);
465
408
  const isSameOriginRedirect = destinationUrl.origin === e.url.origin;
466
409
  let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
467
410
  if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
@@ -483,7 +426,7 @@ export default class Runtime extends _Runtime {
483
426
 
484
427
  // ----------------
485
428
  // 404
486
- if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
429
+ if (response.meta.body === undefined || response.meta.body === null) {
487
430
  response.attrs.status = response.status !== 200 ? response.status : 404;
488
431
  response.attrs.statusText = `${e.request.url} not found!`;
489
432
  return response;
@@ -507,7 +450,7 @@ export default class Runtime extends _Runtime {
507
450
  // Body
508
451
  let rangeRequest, body = response.body;
509
452
  if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
510
- && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
453
+ && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = _ReadableStream.from(body))))) {
511
454
  // ...in partials
512
455
  const totalLength = response.headers.contentLength || 0;
513
456
  const ranges = await rangeRequest.reduce(async (_ranges, range) => {
@@ -540,7 +483,7 @@ export default class Runtime extends _Runtime {
540
483
  });
541
484
  } else {
542
485
  // TODO: of ranges.parts is more than one, return multipart/byteranges
543
- response = new Response(ranges.parts[0].body, {
486
+ response = new xResponse(ranges.parts[0].body, {
544
487
  status: 206,
545
488
  statusText: response.statusText,
546
489
  headers: response.headers,
@@ -3,16 +3,16 @@
3
3
  * @imports
4
4
  */
5
5
  import Context from './Context.js';
6
- import RuntimeClient from './RuntimeClient.js';
6
+ import Application from './Application.js';
7
7
  import Runtime from './Runtime.js';
8
8
 
9
9
  /**
10
10
  * @start
11
11
  */
12
- export async function start(clientCallback = null) {
12
+ export async function start(applicationInstance = null) {
13
13
  const cx = this || {};
14
- const defaultClientCallback = _cx => new RuntimeClient(_cx);
15
- return new Runtime(Context.create(cx), clientCallback || defaultClientCallback);
14
+ const defaultApplicationInstance = _cx => new Application(_cx);
15
+ return new Runtime(Context.create(cx), applicationInstance || defaultApplicationInstance);
16
16
  }
17
17
 
18
18
  /**
@@ -0,0 +1,70 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { _isString, _isNumeric, _isObject, _isPlainObject, _isArray, _isPlainArray, _isTypeObject, _isNumber } from '@webqit/util/js/index.js';
6
+ import { _before } from '@webqit/util/str/index.js';
7
+ import { params } from './util-url.js';
8
+
9
+ export function formatMessage(body) {
10
+ let type = dataType(body);
11
+ let headers = {};
12
+ if ([ 'Blob', 'File' ].includes(type)) {
13
+ headers = { 'Content-Type': body.type, 'Content-Length': body.size, };
14
+ } else if ([ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer' ].includes(type)) {
15
+ headers = { 'Content-Length': body.byteLength, };
16
+ } else if (type === 'json' && _isTypeObject(body)) {
17
+ const [ _body, isJsonfiable ] = formData(body);
18
+ if (isJsonfiable) {
19
+ body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
20
+ headers = { 'Content-Type': 'application/json', 'Content-Length': (new Blob([ body ])).size, };
21
+ } else {
22
+ body = _body;
23
+ type = 'FormData';
24
+ }
25
+ } else if (type === 'json') {
26
+ headers = { 'Content-Length': (body + '').length, };
27
+ }
28
+ return [ body, headers, type ];
29
+ }
30
+
31
+ export function formData(data = {}) {
32
+ const formData = this instanceof FormData ? this : new FormData;
33
+ let isJsonfiable = true;
34
+ if (arguments.length) {
35
+ params.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
36
+ if (suggestedKeys) {
37
+ const isJson = dataType(value) === 'json';
38
+ isJsonfiable = isJsonfiable && isJson;
39
+ return isJson && suggestedKeys;
40
+ }
41
+ formData.append(contextPath, value);
42
+ });
43
+ return [ formData, isJsonfiable ];
44
+ }
45
+ let json;
46
+ for (let [ name, value ] of formData.entries()) {
47
+ if (!json) { json = _isNumeric(_before(name, '[')) ? [] : {}; }
48
+ const isJson = dataType(value) === 'json';
49
+ isJsonfiable = isJsonfiable && isJson;
50
+ if (value === 'false') { value = false; }
51
+ if (value === 'true') { value = true; }
52
+ if (value === 'null') { value = null; }
53
+ if (value === 'undefined') { value = undefined; }
54
+ params.set(json, name, value);
55
+ }
56
+ return [ json, isJsonfiable ];
57
+ }
58
+
59
+ export function dataType(value) {
60
+ if (_isString(value) || _isNumber(value) || value === null) return 'json';
61
+ if (!_isTypeObject(value)) return;
62
+ const toStringTag = value[Symbol.toStringTag];
63
+ const type = [
64
+ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer', 'Blob', 'File', 'FormData', 'Stream', 'ReadableStream'
65
+ ].reduce((_toStringTag, type) => _toStringTag || (toStringTag === type ? type : null), null);
66
+ if (type) return type;
67
+ if ((_isObject(value) && _isPlainObject(value)) || (_isArray(value) && _isPlainArray(value)) || 'toString' in value) {
68
+ return 'json';
69
+ }
70
+ }
@@ -0,0 +1,147 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { _isString, _isNumeric, _isArray, _isTypeObject } from '@webqit/util/js/index.js';
6
+ if (typeof URLPattern === 'undefined') {
7
+ await import('urlpattern-polyfill');
8
+ }
9
+
10
+ export const params = {
11
+ // Parse a search params string into an object
12
+ parse(str, delim = '&') {
13
+ str = str || '';
14
+ const target = {};
15
+ (str.startsWith('?') ? str.substr(1) : str)
16
+ .split(delim).filter(q => q).map(q => q.split('=').map(q => q.trim()))
17
+ .forEach(q => this.set(target, q[0], decodeURIComponent(q[1])));
18
+ return target;
19
+ },
20
+ // Stringify an object into a search params string
21
+ stringify(targetObject, delim = '&') {
22
+ const q = [];
23
+ Object.keys(targetObject).forEach(key => {
24
+ this.reduceValue(targetObject[key], key, (_value, _pathNotation, suggestedKeys = undefined) => {
25
+ if (suggestedKeys) return suggestedKeys;
26
+ q.push(`${_pathNotation}=${encodeURIComponent(_value)}`);
27
+ });
28
+ });
29
+ return q.join(delim);
30
+ },
31
+
32
+ // Get value by path notation
33
+ get(targetObject, pathNotation) {
34
+ return this.reducePath(pathNotation, targetObject, (key, _targetObject) => {
35
+ if (!_targetObject && _targetObject !== 0) return;
36
+ return _targetObject[key];
37
+ });
38
+ },
39
+ // Set value by path notation
40
+ set(targetObject, pathNotation, value) {
41
+ this.reducePath(pathNotation, targetObject, function(_key, _targetObject, suggestedBranch = undefined) {
42
+ let _value = value;
43
+ if (suggestedBranch) { _value = suggestedBranch; }
44
+ if (_key === '' && _isArray(_targetObject)) {
45
+ _targetObject.push(_value);
46
+ } else {
47
+ _targetObject[_key] = _value;
48
+ }
49
+ return _value;
50
+ });
51
+ },
52
+
53
+ // Resolve a value to its leaf nodes
54
+ reduceValue(value, contextPath, callback) {
55
+ if (_isTypeObject(value)) {
56
+ let suggestedKeys = Object.keys(value);
57
+ let keys = callback(value, contextPath, suggestedKeys);
58
+ if (_isArray(keys)) {
59
+ return keys.forEach(key => {
60
+ this.reduceValue(value[key], contextPath ? `${contextPath}[${key}]` : key, callback);
61
+ });
62
+ }
63
+ }
64
+ callback(value, contextPath);
65
+ },
66
+ // Resolve a path to its leaf index
67
+ reducePath(pathNotation, contextObject, callback) {
68
+ if (_isString(pathNotation) && pathNotation.endsWith(']') && _isTypeObject(contextObject)) {
69
+ let [ key, ...rest ] = pathNotation.split('[');
70
+ if (_isNumeric(key)) { key = parseInt(key); }
71
+ rest = rest.join('[').replace(']', '');
72
+ let branch;
73
+ if (key in contextObject) {
74
+ branch = contextObject[key];
75
+ } else {
76
+ let suggestedBranch = rest === '' || _isNumeric(rest.split('[')[0]) ? [] : {};
77
+ branch = callback(key, contextObject, suggestedBranch);
78
+ }
79
+ return this.reducePath(rest, branch, callback);
80
+ }
81
+ if (_isNumeric(pathNotation)) { pathNotation = parseInt(pathNotation); }
82
+ return callback(pathNotation, contextObject);
83
+ },
84
+ };
85
+
86
+ export const path = {
87
+ join(/* path segments */) {
88
+ // Split the inputs into a list of path commands.
89
+ var parts = [], backsteps = 0;
90
+ for (var i = 0, l = arguments.length; i < l; i++) {
91
+ parts = parts.concat(arguments[i].split("/"));
92
+ }
93
+ // Interpret the path commands to get the new resolved path.
94
+ var newParts = [];
95
+ for (i = 0, l = parts.length; i < l; i++) {
96
+ var part = parts[i];
97
+ // Remove leading and trailing slashes
98
+ // Also remove "." segments
99
+ if (!part || part === ".") continue;
100
+ // Interpret ".." to pop the last segment
101
+ if (part === "..") {
102
+ if (!newParts.length) backsteps ++;
103
+ else newParts.pop();
104
+ }
105
+ // Push new path segments.
106
+ else newParts.push(part);
107
+ }
108
+ // Preserve the initial slash if there was one.
109
+ if (parts[0] === "") newParts.unshift("");
110
+ // Turn back into a single string path.
111
+ return '../'.repeat(backsteps) + newParts.join("/") || (newParts.length ? "/" : ".");
112
+ },
113
+ // A simple function to get the dirname of a path
114
+ // Trailing slashes are ignored. Leading slash is preserved.
115
+ dirname(path) {
116
+ return this.join(path, "..");
117
+ }
118
+ };
119
+
120
+ export const pattern = (pattern, baseUrl = null) => ({
121
+ pattern: new URLPattern(pattern, baseUrl),
122
+ isPattern() {
123
+ return Object.keys(this.pattern.keys || {}).some(compName => this.pattern.keys[compName].length);
124
+ },
125
+ test(...args) { return this.pattern.test(...args) },
126
+ exec(...args) {
127
+ let components = this.pattern.exec(...args);
128
+ if (!components) return;
129
+ components.vars = Object.keys(this.pattern.keys).reduce(({ named, unnamed }, compName) => {
130
+ this.pattern.keys[compName].forEach(key => {
131
+ let value = components[compName].groups[key.name];
132
+ if (typeof key.name === 'number') {
133
+ unnamed.push(value);
134
+ } else {
135
+ named[key.name] = value;
136
+ }
137
+ });
138
+ return { named, unnamed };
139
+ }, { named: {}, unnamed: [] });
140
+ components.render = str => {
141
+ return str.replace(/\$(\$|[0-9A-Z]+)/gi, (a, b) => {
142
+ return b === '$' ? '$' : (_isNumeric(b) ? components.vars.unnamed[b - 1] : components.vars.named[b]) || '';
143
+ });
144
+ }
145
+ return components;
146
+ }
147
+ });