@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.
- package/README.md +1 -1
- package/docker/Dockerfile +43 -0
- package/docker/README.md +91 -0
- package/docker/package.json +3 -0
- package/package.json +2 -3
- package/src/Context.js +3 -3
- package/src/config-pi/deployment/Layout.js +0 -1
- package/src/config-pi/deployment/Origins.js +7 -0
- package/src/config-pi/deployment/{Virtualization.js → Proxy.js} +3 -3
- package/src/config-pi/deployment/index.js +2 -2
- package/src/config-pi/runtime/Server.js +2 -2
- package/src/deployment-pi/origins/index.js +3 -2
- package/src/runtime-pi/{RuntimeClient.js → Application.js} +2 -2
- package/src/runtime-pi/HttpEvent.js +106 -0
- package/src/runtime-pi/Router.js +2 -3
- package/src/runtime-pi/Runtime.js +3 -3
- package/src/runtime-pi/client/{RuntimeClient.js → Application.js} +12 -4
- package/src/runtime-pi/client/Router.js +4 -3
- package/src/runtime-pi/client/Runtime.js +37 -59
- package/src/runtime-pi/client/Url.js +3 -3
- package/src/runtime-pi/client/Workport.js +1 -1
- package/src/runtime-pi/client/{Storage.js → createStorage.js} +3 -3
- package/src/runtime-pi/client/generate.js +5 -3
- package/src/runtime-pi/client/index.js +4 -4
- package/src/runtime-pi/client/worker/{WorkerClient.js → Application.js} +12 -8
- package/src/runtime-pi/client/worker/{Worker.js → Runtime.js} +25 -27
- package/src/runtime-pi/client/worker/index.js +6 -6
- package/src/runtime-pi/server/{RuntimeClient.js → Application.js} +8 -8
- package/src/runtime-pi/server/Router.js +3 -2
- package/src/runtime-pi/server/Runtime.js +50 -107
- package/src/runtime-pi/server/index.js +4 -4
- package/src/runtime-pi/util-http.js +70 -0
- package/src/runtime-pi/util-url.js +147 -0
- package/src/runtime-pi/xFormData.js +10 -46
- package/src/runtime-pi/xHeaders.js +2 -11
- package/src/runtime-pi/xRequest.js +29 -42
- package/src/runtime-pi/xRequestHeaders.js +20 -23
- package/src/runtime-pi/xResponse.js +19 -15
- package/src/runtime-pi/xResponseHeaders.js +41 -43
- package/src/runtime-pi/xURL.js +71 -77
- package/src/runtime-pi/xfetch.js +15 -6
- package/src/runtime-pi/xxHttpMessage.js +102 -0
- package/src/runtime-pi/client/whatwag.js +0 -27
- package/src/runtime-pi/server/whatwag.js +0 -35
- package/src/runtime-pi/util.js +0 -162
- package/src/runtime-pi/xHttpEvent.js +0 -101
- 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 {
|
|
17
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
|
35
|
+
* @param Function applicationInstance
|
|
58
36
|
*
|
|
59
37
|
* @return void
|
|
60
38
|
*/
|
|
61
|
-
constructor(cx,
|
|
62
|
-
super(cx,
|
|
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.
|
|
84
|
-
if (this.cx.config.deployment.
|
|
85
|
-
const
|
|
86
|
-
await Promise.all((
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
173
|
+
if (this.proxied.size) {
|
|
191
174
|
this.cx.logger.info(`> Reverse proxy active.`);
|
|
192
|
-
for (let [ id, def ] of this.
|
|
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.
|
|
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.
|
|
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 (
|
|
231
|
-
requestInit.body =
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
|
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 =>
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
368
|
+
} else if (request instanceof URL) {
|
|
426
369
|
href = request.href;
|
|
427
370
|
}
|
|
428
371
|
Observer.set(this.network, 'remote', href);
|
|
429
|
-
const _response =
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
12
|
+
export async function start(applicationInstance = null) {
|
|
13
13
|
const cx = this || {};
|
|
14
|
-
const
|
|
15
|
-
return new Runtime(Context.create(cx),
|
|
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
|
+
});
|