@webqit/webflo 0.11.33 → 0.11.35

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.
@@ -23,6 +23,7 @@ import xRequest from "../xRequest.js";
23
23
  import xResponse from "../xResponse.js";
24
24
  import xfetch from '../xfetch.js';
25
25
  import xHttpEvent from '../xHttpEvent.js';
26
+ import _Runtime from '../Runtime.js';
26
27
 
27
28
  const URL = xURL(whatwag.URL);
28
29
  const FormData = xFormData(whatwag.FormData);
@@ -47,7 +48,7 @@ export {
47
48
  Observer,
48
49
  }
49
50
 
50
- export default class Runtime {
51
+ export default class Runtime extends _Runtime {
51
52
 
52
53
  /**
53
54
  * Runtime
@@ -58,202 +59,212 @@ export default class Runtime {
58
59
  * @return void
59
60
  */
60
61
  constructor(cx, clientCallback) {
62
+ super(cx, clientCallback);
61
63
  // ---------------
62
- this.cx = cx;
63
- this.clients = new Map;
64
- this.mockSessionStore = {};
65
64
  this.ready = (async () => {
66
-
67
65
  // ---------------
68
-
69
- const execClientCallback = (cx, hostname) => {
70
- let client = clientCallback(cx, hostname);
71
- if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
72
- return client;
66
+ const resolveContextObj = async (cx, force = false) => {
67
+ if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
68
+ if (_isEmpty(cx.layout) || force) { cx.layout = await (new cx.config.deployment.Layout(cx)).read(); }
69
+ if (_isEmpty(cx.env) || force) { cx.env = await (new cx.config.deployment.Env(cx)).read(); }
73
70
  };
74
- const loadContextObj = async cx => {
75
- const meta = {};
76
- if (_isEmpty(cx.server)) { cx.server = await (new this.cx.config.runtime.Server(cx)).read(); }
77
- if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
78
- if (_isEmpty(cx.env)) {
79
- let env = await (new this.cx.config.deployment.Env(cx)).read();
80
- cx.env = env.entries;
81
- meta.envAutoloading = env.autoload;
82
- }
83
- return meta;
84
- };
85
- let loadMeta = await loadContextObj(this.cx);
86
- if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
87
- if (_isEmpty(this.cx.vcontexts)) {
88
- this.cx.vcontexts = {};
89
- const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
90
- await Promise.all((vhosts.entries || []).map(vhost => async () => {
91
- this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
92
- await loadContextObj(this.cx.vcontexts[vhost.host]);
93
- }));
94
- }
95
- _each(this.cx.vcontexts, (host, vcontext) => {
96
- this.clients.set(vhost.host, execClientCallback(vcontext, host));
97
- });
98
- } else {
99
- this.clients.set('*', execClientCallback(this.cx, '*'));
100
- }
101
- // Always populate... regardless whether shared setup
102
- if (loadMeta.envAutoloading !== false) {
103
- Object.keys(this.cx.env).forEach(key => {
71
+ await resolveContextObj(this.cx);
72
+ if (this.cx.env.autoload !== false) {
73
+ Object.keys(this.cx.env.entries).forEach(key => {
104
74
  if (!(key in process.env)) {
105
- process.env[key] = this.cx.env[key];
75
+ process.env[key] = this.cx.env.entries[key];
106
76
  }
107
77
  });
108
78
  }
109
-
110
79
  // ---------------
111
-
112
- if (!this.cx.flags['test-only'] && !this.cx.flags['https-only']) {
113
- Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || this.cx.server.port);
80
+ const parseDomains = domains => _arrFrom(domains).reduce((arr, str) => arr.concat(str.split(',')), []).map(str => str.trim()).filter(str => str);
81
+ const selectDomains = (serverDefs, matchingPort = null) => serverDefs.reduce((doms, def) => doms.length ? doms : (((!matchingPort || def.port === matchingPort) && parseDomains(def.domains || def.hostnames)) || []), []);
82
+ // ---------------
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 => {
87
+ let cx, hostnames = parseDomains(vhost.hostnames), port = parseInt(vhost.port);
88
+ if (vhost.path) {
89
+ cx = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
90
+ await resolveContextObj(cx, true);
91
+ // From the server that's most likely to be active
92
+ port || (port = parseInt(cx.server.port || cx.server.https.port));
93
+ // The domain list that corresponds to the specified resolved port
94
+ hostnames.length || (hostnames = selectDomains([cx.server, cx.server.https], port));
95
+ // Or anyone available... hoping that the remote configs can eventually be in sync
96
+ hostnames.length || (hostnames = selectDomains([cx.server, cx.server.https]));
97
+ }
98
+ hostnames.length || (hostnames = ['*']);
99
+ this.vhosts.set(hostnames.sort().join('|'), { cx, hostnames, port });
100
+ }));
101
+ }
102
+ // ---------------
103
+ this.servers = new Map;
104
+ // ---------------
105
+ if (!this.cx.flags['test-only'] && !this.cx.flags['https-only'] && this.cx.server.port) {
106
+ const httpServer = Http.createServer((request, response) => handleRequest('http', request, response));
107
+ httpServer.listen(this.cx.server.port);
108
+ // -------
109
+ let domains = parseDomains(this.cx.server.domains);
110
+ if (!domains.length) { domains = ['*']; }
111
+ this.servers.set('http', {
112
+ instance: httpServer,
113
+ port: this.cx.server.port,
114
+ domains,
115
+ });
114
116
  }
115
-
116
117
  // ---------------
117
-
118
118
  if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
119
119
  const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
120
- if (this.cx.server.shared) {
121
- _each(this.cx.vcontexts, (host, vcontext) => {
122
- if (Fs.existsSync(vcontext.server.https.keyfile)) {
123
- const cert = {
124
- key: Fs.readFileSync(vcontext.server.https.keyfile),
125
- cert: Fs.readFileSync(vcontext.server.https.certfile),
126
- };
127
- var domains = _arrFrom(vcontext.server.https.certdoms);
128
- if (!domains[0] || domains[0].trim() === '*') {
129
- httpsServer.addContext(host, cert);
130
- if (vcontext.server.force_www) {
131
- httpsServer.addContext(host.startsWith('www.') ? host.substr(4) : 'www.' + host, cert);
132
- }
133
- } else {
134
- domains.forEach(domain => {
135
- httpsServer.addContext(domain, cert);
136
- });
137
- }
138
- }
139
- });
140
- } else {
141
- if (Fs.existsSync(this.cx.server.https.keyfile)) {
142
- var domains = _arrFrom(this.cx.server.https.certdoms);
143
- var cert = {
144
- key: Fs.readFileSync(this.cx.server.https.keyfile),
145
- cert: Fs.readFileSync(this.cx.server.https.certfile),
146
- };
147
- if (!domains[0]) {
148
- domains = ['*'];
149
- }
150
- domains.forEach(domain => {
151
- httpsServer.addContext(domain, cert);
152
- });
153
- }
120
+ httpsServer.listen(this.cx.server.https.port);
121
+ // -------
122
+ const addSSLContext = (serverConfig, domains) => {
123
+ if (!Fs.existsSync(serverConfig.https.keyfile)) return;
124
+ const cert = {
125
+ key: Fs.readFileSync(serverConfig.https.keyfile),
126
+ cert: Fs.readFileSync(serverConfig.https.certfile),
127
+ };
128
+ domains.forEach(domain => { httpsServer.addContext(domain, cert); });
129
+ }
130
+ // -------
131
+ let domains = parseDomains(this.cx.server.https.domains);
132
+ if (!domains.length) { domains = ['*']; }
133
+ this.servers.set('https', {
134
+ instance: httpsServer,
135
+ port: this.cx.server.https.port,
136
+ domains,
137
+ });
138
+ // -------
139
+ addSSLContext(this.cx.server, domains);
140
+ for (const [ /*id*/, vhost ] of this.vhosts) {
141
+ vhost.cx && addSSLContext(vhost.cx.server, vhost.hostnames);
154
142
  }
155
- httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
156
143
  }
157
-
158
144
  // ---------------
159
-
160
- const handleRequest = async (protocol, request, response) => {
161
- // --------
162
- // Parse request
163
- const fullUrl = protocol + '://' + request.headers.host + request.url;
164
- const requestInit = { method: request.method, headers: request.headers };
165
- if (request.method !== 'GET' && request.method !== 'HEAD') {
166
- requestInit.body = await new Promise((resolve, reject) => {
167
- var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: true, keepExtensions: true });
168
- formidable.parse(request, (error, fields, files) => {
169
- if (error) {
170
- reject(error);
171
- return;
172
- }
173
- if (request.headers['content-type'] === 'application/json') {
174
- return resolve(fields);
175
- }
176
- const formData = new FormData;
177
- Object.keys(fields).forEach(name => {
178
- if (Array.isArray(fields[name])) {
179
- const values = Array.isArray(fields[name][0])
180
- ? fields[name][0]/* bugly a nested array when there are actually more than entry */
181
- : fields[name];
182
- values.forEach(value => {
183
- formData.append(!name.endsWith(']') ? name + '[]' : name, value);
184
- });
185
- } else {
186
- formData.append(name, fields[name]);
187
- }
188
- });
189
- Object.keys(files).forEach(name => {
190
- const fileCompat = file => {
191
- // IMPORTANT
192
- // Path up the "formidable" file in a way that "formdata-node"
193
- // to can translate it into its own file instance
194
- file[Symbol.toStringTag] = 'File';
195
- file.stream = () => Fs.createReadStream(file.path);
196
- // Done pathcing
197
- return file;
198
- }
199
- if (Array.isArray(files[name])) {
200
- files[name].forEach(value => {
201
- formData.append(name, fileCompat(value));
202
- });
203
- } else {
204
- formData.append(name, fileCompat(files[name]));
205
- }
206
- });
207
- resolve(formData);
208
- });
209
- });
210
- }
211
-
212
- // --------
213
- // Run Application
145
+ const handleRequest = async (proto, request, response) => {
146
+ const [ fullUrl, requestInit ] = await this.parseNodeRequest(proto, request);
214
147
  let clientResponse = await this.go(fullUrl, requestInit, { request, response });
215
148
  if (response.headersSent) return;
216
-
217
149
  // --------
218
- // Set headers
219
150
  _each(clientResponse.headers.json(), (name, value) => {
220
151
  response.setHeader(name, value);
221
152
  });
222
-
223
153
  // --------
224
- // Send
225
154
  response.statusCode = clientResponse.status;
226
155
  response.statusMessage = clientResponse.statusText;
227
- if (clientResponse.headers.redirect) {
228
- response.end();
229
- } else {
230
- var body = clientResponse.body;
231
- if ((body instanceof ReadableStream)) {
232
- body.pipe(response);
233
- } else {
234
- // The default
235
- if (clientResponse.headers.contentType === 'application/json') {
236
- body += '';
237
- }
238
- response.end(body);
239
- }
156
+ if (clientResponse.headers.location) {
157
+ return response.end();
240
158
  }
159
+ if ((clientResponse.body instanceof ReadableStream)) {
160
+ return clientResponse.body.pipe(response);
161
+ }
162
+ let body = clientResponse.body;
163
+ if (clientResponse.headers.contentType === 'application/json') {
164
+ body += '';
165
+ }
166
+ return response.end(body);
241
167
  };
242
-
243
168
  })();
169
+
244
170
  // ---------------
245
171
  Observer.set(this, 'location', {});
246
172
  Observer.set(this, 'network', {});
247
173
  // ---------------
248
- this.ready.then(() => {
249
- if (!this.cx.logger) return;
250
- if (this.cx.server.shared) {
251
- this.cx.logger.info(`> Server running (shared)`);
252
- } else {
253
- this.cx.logger.info(`> Server running (${this.cx.app.title || ''})::${this.cx.server.port}`);
174
+
175
+ // -------------
176
+ // Initialize
177
+ (async () => {
178
+ await this.ready;
179
+ if (this.cx.logger) {
180
+ if (this.servers.size) {
181
+ this.cx.logger.info(`> Server running! (${this.cx.app.title || ''})`);
182
+ for (let [ proto, def ] of this.servers) {
183
+ this.cx.logger.info(`> ${ proto.toUpperCase() } / ${ def.domains.concat('').join(`:${ def.port } / `)}`);
184
+ }
185
+ } else {
186
+ this.cx.logger.info(`> Server not running! No port specified.`);
187
+ }
188
+ if (this.vhosts.size) {
189
+ this.cx.logger.info(`> Reverse proxy active.`);
190
+ for (let [ id, def ] of this.vhosts) {
191
+ this.cx.logger.info(`> ${ id } >>> ${ def.port }`);
192
+ }
193
+ }
194
+ this.cx.logger.info(``);
254
195
  }
255
- });
256
- }
196
+ if (this.client && this.client.init) {
197
+ const request = this.generateRequest('/');
198
+ const httpEvent = new HttpEvent(request, { srcType: 'initialization' }, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
199
+ return this.getSession(this.cx, httpEvent, id, options, callback);
200
+ });
201
+ await this.client.init(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
202
+ }
203
+ })();
204
+ // ---------------
205
+
206
+ // ---------------
207
+ this.mockSessionStore = {};
208
+ // ---------------
209
+
210
+ }
211
+
212
+ /**
213
+ * Parse Nodejs's IncomingMessage to WHATWAG request params.
214
+ *
215
+ * @param String proto
216
+ * @param Http.IncomingMessage request
217
+ *
218
+ * @return Array
219
+ */
220
+ async parseNodeRequest(proto, request) {
221
+ const fullUrl = proto + '://' + request.headers.host + request.url;
222
+ const requestInit = { method: request.method, headers: request.headers };
223
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
224
+ requestInit.body = await new Promise((resolve, reject) => {
225
+ var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: true, keepExtensions: true });
226
+ formidable.parse(request, (error, fields, files) => {
227
+ if (error) { return reject(error); }
228
+ if (request.headers['content-type'] === 'application/json') {
229
+ return resolve(fields);
230
+ }
231
+ const formData = new FormData;
232
+ Object.keys(fields).forEach(name => {
233
+ if (Array.isArray(fields[name])) {
234
+ const values = Array.isArray(fields[name][0])
235
+ ? fields[name][0]/* bugly a nested array when there are actually more than entry */
236
+ : fields[name];
237
+ values.forEach(value => {
238
+ formData.append(!name.endsWith(']') ? name + '[]' : name, value);
239
+ });
240
+ } else {
241
+ formData.append(name, fields[name]);
242
+ }
243
+ });
244
+ Object.keys(files).forEach(name => {
245
+ const fileCompat = file => {
246
+ // IMPORTANT
247
+ // Path up the "formidable" file in a way that "formdata-node"
248
+ // to can translate it into its own file instance
249
+ file[Symbol.toStringTag] = 'File';
250
+ file.stream = () => Fs.createReadStream(file.path);
251
+ // Done pathcing
252
+ return file;
253
+ }
254
+ if (Array.isArray(files[name])) {
255
+ files[name].forEach(value => {
256
+ formData.append(name, fileCompat(value));
257
+ });
258
+ } else {
259
+ formData.append(name, fileCompat(files[name]));
260
+ }
261
+ });
262
+ resolve(formData);
263
+ });
264
+ });
265
+ }
266
+ return [ fullUrl, requestInit ];
267
+ }
257
268
 
258
269
  /**
259
270
  * Performs a request.
@@ -271,28 +282,30 @@ export default class Runtime {
271
282
  url = typeof url === 'string' ? new whatwag.URL(url) : url;
272
283
  init = { referrer: this.location.href, ...init };
273
284
  // ------------
274
- let _context = this.cx, rdr;
275
- if (this.cx.server.shared && !(_context = this.cx.vcontexts[url.hostname])
276
- && !(url.hostname.startsWith('www.') && (_context = this.cx.vcontexts[url.hostname.substr(4)]) && _context.server.force_www)
277
- && !(!url.hostname.startsWith('www.') && (_context = this.cx.vcontexts['www.' + url.hostname]) && _context.server.force_www)) {
278
- rdr = { status: 500, statusText: 'Unrecognized host' };
279
- } else if (url.protocol === 'http:' && _context.server.https.force && !this.cx.flags['http-only'] && /** main server */this.cx.server.https.port) {
280
- rdr = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
281
- } else if (url.hostname.startsWith('www.') && _context.server.force_www === 'remove') {
282
- rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
283
- } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
284
- rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
285
- } else if (_context.config.runtime.server.Redirects) {
286
- rdr = ((await (new _context.config.runtime.server.Redirects(_context)).read()).entries || []).reduce((_rdr, entry) => {
285
+ const hosts = [];
286
+ this.servers.forEach(server => hosts.push(...server.domains));
287
+ // ------------
288
+ for (const [ /*id*/, vhost ] of this.vhosts) {
289
+ if (vhost.hostnames.includes(url.hostname) || (vhost.hostnames.includes('*') && !hosts.includes('*'))) {
290
+ return this.proxyFetch(vhost, url, init);
291
+ }
292
+ }
293
+ // ------------
294
+ let exit;
295
+ if (!hosts.includes(url.hostname) && !hosts.includes('*')) {
296
+ exit = { status: 500, statusText: 'Unrecognized host' };
297
+ } else if (url.protocol === 'http:' && this.cx.server.https.force) {
298
+ exit = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
299
+ } else if (url.hostname.startsWith('www.') && this.cx.server.force_www === 'remove') {
300
+ exit = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
301
+ } else if (!url.hostname.startsWith('www.') && this.cx.server.force_www === 'add') {
302
+ exit = { status: 302, headers: { Location: ( url.hostname = `www.${ url.hostname }`, url.href ) } };
303
+ } else if (this.cx.config.runtime.server.Redirects) {
304
+ exit = ((await (new this.cx.config.runtime.server.Redirects(this.cx)).read()).entries || []).reduce((_rdr, entry) => {
287
305
  return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
288
306
  }, null);
289
307
  }
290
- if (rdr) {
291
- return new Response(null, rdr);
292
- }
293
- const autoHeaders = _context.config.runtime.server.Headers
294
- ? ((await (new _context.config.runtime.server.Headers(_context)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
295
- : [];
308
+ if (exit) { return new Response(null, exit); }
296
309
  // ------------
297
310
 
298
311
  // ------------
@@ -300,27 +313,29 @@ export default class Runtime {
300
313
  Observer.set(this.network, 'redirecting', null);
301
314
  // ------------
302
315
 
316
+ // ------------
317
+ // Automatically-added headers
318
+ const autoHeaders = this.cx.config.runtime.server.Headers
319
+ ? ((await (new this.cx.config.runtime.server.Headers(this.cx)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
320
+ : [];
303
321
  // The request object
304
- let request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
322
+ const request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
305
323
  // The navigation event
306
- let httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
307
- return this.getSession(_context, httpEvent, id, options, callback);
324
+ const httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
325
+ return this.getSession(this.cx, httpEvent, id, options, callback);
308
326
  });
309
327
  // Response
310
- let client = this.clients.get('*'), response, finalResponse;
311
- if (this.cx.server.shared) {
312
- client = this.clients.get(url.hostname);
313
- }
328
+ let response, finalResponse;
314
329
  try {
315
- response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
316
- finalResponse = await this.handleResponse(_context, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
330
+ response = await this.client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
331
+ finalResponse = await this.handleResponse(this.cx, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
317
332
  } catch(e) {
318
333
  finalResponse = new Response(null, { status: 500, statusText: e.message });
319
334
  console.error(e);
320
335
  }
321
336
  // Logging
322
337
  if (this.cx.logger) {
323
- let log = this.generateLog(httpEvent, finalResponse);
338
+ const log = this.generateLog(httpEvent.request, finalResponse);
324
339
  this.cx.logger.log(log);
325
340
  }
326
341
  // ------------
@@ -328,9 +343,31 @@ export default class Runtime {
328
343
  return finalResponse;
329
344
  }
330
345
 
346
+ // Fetch from proxied host
347
+ async proxyFetch(vhost, url, init) {
348
+ const url2 = new whatwag.URL(url);
349
+ url2.port = vhost.port;
350
+ const init2 = { ...init, compress: false };
351
+ if (!init2.headers) init2.headers = {};
352
+ init2.headers.host = url2.host;
353
+ let response;
354
+ try {
355
+ response = await this.remoteFetch(url2, init2);
356
+ } catch(e) {
357
+ response = new Response(null, { status: 500, statusText: e.message });
358
+ console.error(e);
359
+ }
360
+ if (this.cx.logger) {
361
+ const log = this.generateLog({ url: url2.href, ...init2 }, response);
362
+ this.cx.logger.log(log);
363
+ }
364
+ return response;
365
+
366
+ }
367
+
331
368
  // Generates request object
332
- generateRequest(href, init, autoHeaders = []) {
333
- let request = new Request(href, init);
369
+ generateRequest(href, init = {}, autoHeaders = []) {
370
+ const request = new Request(href, init);
334
371
  this._autoHeaders(request.headers, autoHeaders);
335
372
  return request;
336
373
  }
@@ -374,11 +411,11 @@ export default class Runtime {
374
411
  let href = request;
375
412
  if (request instanceof Request) {
376
413
  href = request.url;
377
- } else if (request instanceof self.URL) {
414
+ } else if (request instanceof whatwag.URL) {
378
415
  href = request.href;
379
416
  }
380
417
  Observer.set(this.network, 'remote', href);
381
- let _response = fetch(request, ...args);
418
+ const _response = fetch(request, ...args);
382
419
  // This catch() is NOT intended to handle failure of the fetch
383
420
  _response.catch(e => Observer.set(this.network, 'error', e.message));
384
421
  // Save a reference to this
@@ -410,16 +447,16 @@ export default class Runtime {
410
447
 
411
448
  // ----------------
412
449
  // Redirects
413
- if (response.headers.redirect) {
414
- let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
415
- let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
416
- let destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
417
- let isSameOriginRedirect = destinationUrl.origin === e.url.origin;
450
+ if (response.headers.location) {
451
+ const xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
452
+ const xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
453
+ const destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
454
+ const isSameOriginRedirect = destinationUrl.origin === e.url.origin;
418
455
  let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
419
456
  if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
420
457
  // Longest-first sorting
421
- let sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
422
- let matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
458
+ const sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
459
+ const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
423
460
  isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(e.url.pathname);
424
461
  }
425
462
  if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
@@ -461,8 +498,8 @@ export default class Runtime {
461
498
  if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
462
499
  && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
463
500
  // ...in partials
464
- let totalLength = response.headers.contentLength || 0;
465
- let ranges = await rangeRequest.reduce(async (_ranges, range) => {
501
+ const totalLength = response.headers.contentLength || 0;
502
+ const ranges = await rangeRequest.reduce(async (_ranges, range) => {
466
503
  _ranges = await _ranges;
467
504
  if (range[0] < 0 || (totalLength && range[0] > totalLength)
468
505
  || (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
@@ -475,7 +512,7 @@ export default class Runtime {
475
512
  }
476
513
  if (_ranges.error) return _ranges;
477
514
  if (totalLength) { range.clamp(totalLength); }
478
- let partLength = range[1] - range[0] + 1;
515
+ const partLength = range[1] - range[0] + 1;
479
516
  _ranges.parts.push({
480
517
  body: body.pipe(_streamSlice(range[0], range[1] + 1)),
481
518
  range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
@@ -508,24 +545,24 @@ export default class Runtime {
508
545
  }
509
546
 
510
547
  // Generates log
511
- generateLog(e, response) {
548
+ generateLog(request, response) {
512
549
  let log = [];
513
550
  // ---------------
514
- let style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
515
- let errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
516
- let xRedirectCode = response.headers.get('X-Redirect-Code');
517
- let redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
518
- let statusCode = xRedirectCode || response.status;
551
+ const style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
552
+ const errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
553
+ const xRedirectCode = response.headers.get('X-Redirect-Code');
554
+ const redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
555
+ const statusCode = xRedirectCode || response.status;
519
556
  // ---------------
520
557
  log.push(`[${style.comment((new Date).toUTCString())}]`);
521
- log.push(style.keyword(e.request.method));
522
- log.push(style.url(e.request.url));
558
+ log.push(style.keyword(request.method));
559
+ log.push(style.url(request.url));
523
560
  if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
524
561
  if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
525
562
  if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
526
563
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
527
564
  else log.push(style.val(`${statusCode} ${response.statusText}`));
528
- if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
565
+ if (redirectCode) log.push(`- ${style.url(response.headers.location)}`);
529
566
 
530
567
  return log.join(' ');
531
568
  }