@webqit/webflo 0.8.71-0 → 0.8.73
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/package.json +1 -1
- package/src/runtime/_MessageStream.js +18 -14
- package/src/runtime/_NavigationEvent.js +6 -4
- package/src/runtime/_ResponseHeaders.js +9 -0
- package/src/runtime/client/Http.js +113 -105
- package/src/runtime/client/NavigationEvent.js +1 -0
- package/src/runtime/client/Navigator.js +250 -0
- package/src/runtime/client/Runtime.js +87 -172
- package/src/runtime/client/Worker.js +4 -5
- package/src/runtime/server/NavigationEvent.js +3 -2
- package/src/runtime/server/Runtime.js +85 -101
package/package.json
CHANGED
|
@@ -104,26 +104,30 @@ const _MessageStream = (NativeMessageStream, Headers, FormData) => {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Payload
|
|
107
|
-
|
|
108
|
-
if (!this._typedDataCache.
|
|
109
|
-
this._typedDataCache.
|
|
110
|
-
var
|
|
107
|
+
data(force = false) {
|
|
108
|
+
if (!this._typedDataCache.data || force) {
|
|
109
|
+
this._typedDataCache.data = new Promise(async (resolve, reject) => {
|
|
110
|
+
var messageInstance = this, data, contentType = messageInstance.headers.get('content-type') || '';
|
|
111
111
|
var type = contentType === 'application/json' || this._typedDataCache.json ? 'json' : (
|
|
112
|
-
contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData
|
|
112
|
+
contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData ? 'formData' : (
|
|
113
113
|
contentType === 'text/plain' ? 'plain' : 'other'
|
|
114
114
|
)
|
|
115
115
|
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type === '
|
|
121
|
-
|
|
116
|
+
try {
|
|
117
|
+
if (type === 'formData') {
|
|
118
|
+
data = (await messageInstance.formData()).json();
|
|
119
|
+
} else {
|
|
120
|
+
data = type === 'json' ? await messageInstance.json() : (
|
|
121
|
+
type === 'plain' ? await messageInstance.text() : messageInstance.body
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
resolve(data);
|
|
125
|
+
} catch(e) {
|
|
126
|
+
reject(e);
|
|
122
127
|
}
|
|
123
|
-
resolve(jsonBuild);
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
|
-
return this._typedDataCache.
|
|
130
|
+
return this._typedDataCache.data;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
};
|
|
@@ -178,7 +182,7 @@ export function encodeBody(body, globals) {
|
|
|
178
182
|
contentLength: Buffer.byteLength(detailsObj.body, 'utf8'), // Buffer.from(string).length
|
|
179
183
|
};
|
|
180
184
|
}
|
|
181
|
-
detailsObj.
|
|
185
|
+
detailsObj.data = body;
|
|
182
186
|
}
|
|
183
187
|
return detailsObj;
|
|
184
188
|
}
|
|
@@ -22,12 +22,14 @@ const _NavigationEvent = globals => {
|
|
|
22
22
|
/**
|
|
23
23
|
* Initializes a new NavigationEvent instance.
|
|
24
24
|
*
|
|
25
|
-
* @param Request
|
|
26
|
-
* @param Object
|
|
25
|
+
* @param Request _request
|
|
26
|
+
* @param Object _session
|
|
27
|
+
* @param Function _sessionFactory
|
|
27
28
|
*/
|
|
28
|
-
constructor(_request, _session = null) {
|
|
29
|
+
constructor(_request, _session = {}, _sessionFactory = null) {
|
|
29
30
|
this._request = _request;
|
|
30
31
|
this._session = _session;
|
|
32
|
+
this.sessionFactory = _sessionFactory;
|
|
31
33
|
// -------
|
|
32
34
|
this.URL = URL;
|
|
33
35
|
// -------
|
|
@@ -73,7 +75,7 @@ const _NavigationEvent = globals => {
|
|
|
73
75
|
init._proxy.referrer = this.request.url;
|
|
74
76
|
request = new NavigationEvent.Request(this._request, init);
|
|
75
77
|
}
|
|
76
|
-
return new NavigationEvent(request, this._session);
|
|
78
|
+
return new NavigationEvent(request, this._session, this.sessionFactory);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
}
|
|
@@ -69,6 +69,14 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
|
|
|
69
69
|
return value;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
get location() {
|
|
73
|
+
return this.get('Location');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set location(value) {
|
|
77
|
+
return this.set('Location', value);
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
get redirect() {
|
|
73
81
|
return this.get('Location');
|
|
74
82
|
}
|
|
@@ -76,6 +84,7 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
|
|
|
76
84
|
set redirect(value) {
|
|
77
85
|
return this.set('Location', value);
|
|
78
86
|
}
|
|
87
|
+
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
export default _ResponseHeaders;
|
|
@@ -39,25 +39,105 @@ export default class Http {
|
|
|
39
39
|
/**
|
|
40
40
|
* Performs a request.
|
|
41
41
|
*
|
|
42
|
-
* @param string href
|
|
43
|
-
* @param object
|
|
44
|
-
* @param object src
|
|
42
|
+
* @param object|string href
|
|
43
|
+
* @param object options
|
|
45
44
|
*
|
|
46
|
-
* @return
|
|
45
|
+
* @return void
|
|
47
46
|
*/
|
|
48
|
-
go(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
47
|
+
async go(url, options = {}) {
|
|
48
|
+
if (this.abortController) {
|
|
49
|
+
this.abortController.abort();
|
|
50
|
+
}
|
|
51
|
+
this.abortController = new AbortController();
|
|
52
|
+
let xRedirectCode = 300;
|
|
53
|
+
// Generates request object
|
|
54
|
+
let generateRequest = (url, options) => {
|
|
55
|
+
return new StdRequest(url, {
|
|
56
|
+
...options,
|
|
57
|
+
headers: {
|
|
58
|
+
'Accept': 'application/json',
|
|
59
|
+
'X-Redirect-Policy': 'manual-when-cross-origin',
|
|
60
|
+
'X-Redirect-Code': xRedirectCode,
|
|
61
|
+
'X-Powered-By': '@webqit/webflo',
|
|
62
|
+
...(options.headers || {}),
|
|
63
|
+
},
|
|
64
|
+
referrer: window.document.location.href,
|
|
65
|
+
signal: this.abortController.signal,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
// Handles response object
|
|
69
|
+
let handleResponse = (response) => {
|
|
70
|
+
if (!response) return;
|
|
71
|
+
if (response.redirected && this.isSameOrigin(response.url)) {
|
|
72
|
+
Observer.set(this.location, { href: response.url }, {
|
|
73
|
+
detail: { isRedirect: true },
|
|
74
|
+
});
|
|
75
|
+
} else if (response.headers.get('Location') && response.status === xRedirectCode) {
|
|
76
|
+
window.location = response.headers.get('Location');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
url = typeof url === 'string' ? { href: url } : url;
|
|
80
|
+
options = { referrer: this.location.href, ...options };
|
|
81
|
+
Observer.set(this.location, url, { detail: options, });
|
|
82
|
+
if (!(_before(url.href, '#') === _before(options.referrer, '#') && (options.method || 'GET').toUpperCase() === 'GET')) {
|
|
83
|
+
handleResponse(await client.call(this, generateRequest(url.href, options)));
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks if an URL is same origin.
|
|
89
|
+
*
|
|
90
|
+
* @param object|string url
|
|
91
|
+
*
|
|
92
|
+
* @return Bool
|
|
93
|
+
*/
|
|
94
|
+
isSameOrigin(url) {
|
|
95
|
+
if (typeof url === 'string') {
|
|
96
|
+
let href = url;
|
|
97
|
+
url = window.document.createElement('a');
|
|
98
|
+
url.href = href
|
|
99
|
+
}
|
|
100
|
+
return !url.origin || url.origin === this.location.origin;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* History object
|
|
105
|
+
*/
|
|
106
|
+
get history() {
|
|
107
|
+
return window.history;
|
|
52
108
|
}
|
|
109
|
+
|
|
53
110
|
};
|
|
54
111
|
|
|
112
|
+
// -----------------------
|
|
113
|
+
// Initialize network
|
|
114
|
+
Observer.set(instance, 'network', {});
|
|
115
|
+
window.addEventListener('online', () => Observer.set(instance.network, 'online', navigator.onLine));
|
|
116
|
+
window.addEventListener('offline', () => Observer.set(instance.network, 'online', navigator.onLine));
|
|
117
|
+
|
|
118
|
+
// -----------------------
|
|
119
|
+
// Initialize location
|
|
120
|
+
Observer.set(instance, 'location', new Url(window.document.location));
|
|
121
|
+
// -----------------------
|
|
122
|
+
// Syndicate changes to the browser;s location bar
|
|
123
|
+
Observer.observe(instance.location, [[ 'href' ]], ([e]) => {
|
|
124
|
+
if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
|
|
125
|
+
// Already from a "popstate" event as above, so don't push again
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
|
|
129
|
+
instance.history.replaceState(instance.history.state, '', instance.location.href);
|
|
130
|
+
} else {
|
|
131
|
+
try { instance.history.pushState(instance.history.state, '', instance.location.href); } catch(e) {}
|
|
132
|
+
}
|
|
133
|
+
}, { diff: true });
|
|
134
|
+
|
|
55
135
|
/**
|
|
56
136
|
* ----------------
|
|
57
|
-
*
|
|
137
|
+
* Navigation Interception
|
|
58
138
|
* ----------------
|
|
59
139
|
*/
|
|
60
|
-
|
|
140
|
+
|
|
61
141
|
// -----------------------
|
|
62
142
|
// This event is triggered by
|
|
63
143
|
// either the browser back button,
|
|
@@ -67,10 +147,9 @@ export default class Http {
|
|
|
67
147
|
window.addEventListener('popstate', e => {
|
|
68
148
|
// Needed to allow window.document.location
|
|
69
149
|
// to update to window.location
|
|
150
|
+
let referrer = window.document.location.href;
|
|
70
151
|
window.setTimeout(() => {
|
|
71
|
-
|
|
72
|
-
detail: { type: 'history', src: window.document.location },
|
|
73
|
-
});
|
|
152
|
+
instance.go(Url.copy(window.document.location), { referrer, src: window.document.location, srcType: 'history', });
|
|
74
153
|
}, 0);
|
|
75
154
|
});
|
|
76
155
|
|
|
@@ -78,24 +157,17 @@ export default class Http {
|
|
|
78
157
|
// Capture all link-clicks
|
|
79
158
|
// and fire to this router.
|
|
80
159
|
window.addEventListener('click', e => {
|
|
81
|
-
var anchor
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Same origin... but...
|
|
86
|
-
&& (!anchor.origin || anchor.origin === instance.location.origin)) {
|
|
87
|
-
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
160
|
+
var anchor = e.target.closest('a');
|
|
161
|
+
if (!anchor || !anchor.href) return;
|
|
162
|
+
if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === instance.location.origin)) {
|
|
163
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
90
164
|
// Publish everything, including hash
|
|
91
|
-
|
|
92
|
-
detail: { type: 'link', src: anchor, },
|
|
93
|
-
});
|
|
165
|
+
instance.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
|
|
94
166
|
// URLs with # will cause a natural navigation
|
|
95
167
|
// even if pointing to a different page, a natural navigation will still happen
|
|
96
168
|
// because with the Observer.set() above, window.document.location.href would have become
|
|
97
169
|
// the destination page, which makes it look like same page navigation
|
|
98
|
-
if (!href.includes('#')) {
|
|
170
|
+
if (!anchor.href.includes('#')) {
|
|
99
171
|
e.preventDefault();
|
|
100
172
|
}
|
|
101
173
|
}
|
|
@@ -105,25 +177,27 @@ export default class Http {
|
|
|
105
177
|
// Capture all form-submit
|
|
106
178
|
// and fire to this router.
|
|
107
179
|
window.addEventListener('submit', e => {
|
|
108
|
-
var
|
|
109
|
-
form = e.target.closest('form'),
|
|
110
|
-
submits = [e.submitter]; //_arrFrom(form.elements).filter(el => el.matches('button,input[type="submit"],input[type="image"]'));
|
|
180
|
+
var form = e.target.closest('form'), submitter = e.submitter;
|
|
111
181
|
var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
|
|
112
|
-
params[prop] =
|
|
182
|
+
params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
|
|
113
183
|
return params;
|
|
114
184
|
}, {});
|
|
115
185
|
// We support method hacking
|
|
186
|
+
submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
|
|
187
|
+
submitParams.submitter = submitter;
|
|
116
188
|
// ---------------
|
|
117
|
-
|
|
189
|
+
var actionEl = window.document.createElement('a');
|
|
190
|
+
actionEl.href = submitParams.action;
|
|
118
191
|
// ---------------
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
192
|
+
// If not targeted and same origin...
|
|
193
|
+
if (!submitParams.target && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
|
|
194
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
195
|
+
// Build data
|
|
122
196
|
var formData = new FormData(form);
|
|
123
|
-
if (
|
|
124
|
-
formData.set(
|
|
197
|
+
if ((submitter || {}).name) {
|
|
198
|
+
formData.set(submitter.name, submitter.value);
|
|
125
199
|
}
|
|
126
|
-
if (submitParams.method === '
|
|
200
|
+
if (submitParams.method.toUpperCase() === 'GET') {
|
|
127
201
|
var query = wwwFormUnserialize(actionEl.search);
|
|
128
202
|
Array.from(formData.entries()).forEach(_entry => {
|
|
129
203
|
wwwFormSet(query, _entry[0], _entry[1], false);
|
|
@@ -131,13 +205,7 @@ export default class Http {
|
|
|
131
205
|
actionEl.search = wwwFormSerialize(query);
|
|
132
206
|
formData = null;
|
|
133
207
|
}
|
|
134
|
-
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// Publish everything, including hash
|
|
138
|
-
Observer.set(instance.location, Url.copy(actionEl), {
|
|
139
|
-
detail: { type: 'form', src: form, submitParams, data: formData },
|
|
140
|
-
});
|
|
208
|
+
instance.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
|
|
141
209
|
// URLs with # will cause a natural navigation
|
|
142
210
|
// even if pointing to a different page, a natural navigation will still happen
|
|
143
211
|
// because with the Observer.set() above, window.document.location.href would have become
|
|
@@ -148,69 +216,9 @@ export default class Http {
|
|
|
148
216
|
}
|
|
149
217
|
});
|
|
150
218
|
|
|
151
|
-
/**
|
|
152
|
-
* ----------------
|
|
153
|
-
* instance.history
|
|
154
|
-
* ----------------
|
|
155
|
-
*/
|
|
156
|
-
|
|
157
|
-
instance.history = window.history;
|
|
158
219
|
// -----------------------
|
|
159
|
-
// Syndicate changes to
|
|
160
|
-
// the browser;s location bar
|
|
161
|
-
Observer.observe(instance.location, [[ 'href' ]], e => {
|
|
162
|
-
e = e[0];
|
|
163
|
-
if ((e.detail || {}).src === window.document.location) {
|
|
164
|
-
// Already from a "popstate" event as above, so don't push again
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (e.value === 'http:') return;
|
|
168
|
-
if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
|
|
169
|
-
instance.history.replaceState(instance.history.state, '', instance.location.href);
|
|
170
|
-
} else {
|
|
171
|
-
try {
|
|
172
|
-
instance.history.pushState(instance.history.state, '', instance.location.href);
|
|
173
|
-
} catch(e) {}
|
|
174
|
-
}
|
|
175
|
-
}, { diff: true });
|
|
176
|
-
|
|
177
|
-
// ----------------------------------
|
|
178
|
-
const createRequest = (url, referrer, e = {}) => {
|
|
179
|
-
var detail = e.detail || {};
|
|
180
|
-
var options = {
|
|
181
|
-
method: (detail.submitParams || detail.src || {}).method || 'get',
|
|
182
|
-
body: detail.data,
|
|
183
|
-
headers: { ...(detail.headers || {}), 'X-Powered-By': '@webqit/webflo', },
|
|
184
|
-
referrer,
|
|
185
|
-
};
|
|
186
|
-
return new StdRequest(url, options);
|
|
187
|
-
};
|
|
188
|
-
const handleResponse = response => {
|
|
189
|
-
if (response && response.redirected) {
|
|
190
|
-
var actionEl = window.document.createElement('a');
|
|
191
|
-
if ((actionEl.href = response.url) && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
|
|
192
|
-
Observer.set(instance.location, { href: response.url }, {
|
|
193
|
-
detail: { follow: false },
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
// ----------------------------------
|
|
199
|
-
|
|
200
|
-
// Observe location and route
|
|
201
|
-
Observer.observe(instance.location, [['href']], async e => {
|
|
202
|
-
e = e[0];
|
|
203
|
-
var detail = e.detail || {};
|
|
204
|
-
if (detail.follow === false) return;
|
|
205
|
-
var method = (detail.submitParams || detail.src || {}).method;
|
|
206
|
-
if ((_before(e.value, '#') !== _before(e.oldValue, '#')) || (method && method.toUpperCase() !== 'GET')) {
|
|
207
|
-
return handleResponse(await client.call(instance, createRequest(e.value, e.oldValue, e), e));
|
|
208
|
-
}
|
|
209
|
-
}, {diff: false /* method might be the difference */});
|
|
210
220
|
// Startup route
|
|
211
|
-
|
|
212
|
-
handleResponse(await client.call(instance, createRequest(window.document.location.href, document.referrer)));
|
|
213
|
-
|
|
221
|
+
instance.go(window.document.location.href, { referrer: document.referrer });
|
|
214
222
|
return instance;
|
|
215
223
|
}
|
|
216
224
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import _before from '@webqit/util/str/before.js';
|
|
6
|
+
import _toTitle from '@webqit/util/str/toTitle.js';
|
|
7
|
+
import { wwwFormUnserialize, wwwFormSet, wwwFormSerialize } from '../util.js';
|
|
8
|
+
import { Observer } from './Runtime.js';
|
|
9
|
+
import Url from './Url.js';
|
|
10
|
+
|
|
11
|
+
export default class Navigator {
|
|
12
|
+
|
|
13
|
+
constructor(client) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ----------------
|
|
18
|
+
* Navigator location
|
|
19
|
+
* ----------------
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// -----------------------
|
|
23
|
+
// Initialize location
|
|
24
|
+
Observer.set(this, 'location', new Url(window.document.location));
|
|
25
|
+
// -----------------------
|
|
26
|
+
// Syndicate changes to the browser;s location bar
|
|
27
|
+
Observer.observe(this.location, [[ 'href' ]], ([e]) => {
|
|
28
|
+
if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
|
|
29
|
+
// Already from a "popstate" event as above, so don't push again
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
|
|
33
|
+
window.history.replaceState(window.history.state, '', this.location.href);
|
|
34
|
+
} else {
|
|
35
|
+
try { window.history.pushState(window.history.state, '', this.location.href); } catch(e) {}
|
|
36
|
+
}
|
|
37
|
+
}, { diff: true });
|
|
38
|
+
|
|
39
|
+
// -----------------------
|
|
40
|
+
// This event is triggered by
|
|
41
|
+
// either the browser back button,
|
|
42
|
+
// the window.history.back(),
|
|
43
|
+
// the window.history.forward(),
|
|
44
|
+
// or the window.history.go() action.
|
|
45
|
+
window.addEventListener('popstate', e => {
|
|
46
|
+
// Needed to allow window.document.location
|
|
47
|
+
// to update to window.location
|
|
48
|
+
window.setTimeout(() => {
|
|
49
|
+
this.go(Url.copy(window.document.location), { src: window.document.location, srcType: 'history', });
|
|
50
|
+
}, 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// -----------------------
|
|
54
|
+
// Capture all link-clicks
|
|
55
|
+
// and fire to this router.
|
|
56
|
+
window.addEventListener('click', e => {
|
|
57
|
+
var anchor = e.target.closest('a');
|
|
58
|
+
if (!anchor || !anchor.href) return;
|
|
59
|
+
if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === this.location.origin)) {
|
|
60
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
61
|
+
// Publish everything, including hash
|
|
62
|
+
this.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
|
|
63
|
+
// URLs with # will cause a natural navigation
|
|
64
|
+
// even if pointing to a different page, a natural navigation will still happen
|
|
65
|
+
// because with the Observer.set() above, window.document.location.href would have become
|
|
66
|
+
// the destination page, which makes it look like same page navigation
|
|
67
|
+
if (!anchor.href.includes('#')) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// -----------------------
|
|
74
|
+
// Capture all form-submit
|
|
75
|
+
// and fire to this router.
|
|
76
|
+
window.addEventListener('submit', e => {
|
|
77
|
+
var form = e.target.closest('form'), submitter = e.submitter;
|
|
78
|
+
var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
|
|
79
|
+
params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
|
|
80
|
+
return params;
|
|
81
|
+
}, {});
|
|
82
|
+
// We support method hacking
|
|
83
|
+
submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
|
|
84
|
+
submitParams.submitter = submitter;
|
|
85
|
+
// ---------------
|
|
86
|
+
var actionEl = window.document.createElement('a');
|
|
87
|
+
actionEl.href = submitParams.action;
|
|
88
|
+
// ---------------
|
|
89
|
+
// If not targeted and same origin...
|
|
90
|
+
if (!submitParams.target && (!actionEl.origin || actionEl.origin === this.location.origin)) {
|
|
91
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
92
|
+
// Build data
|
|
93
|
+
var formData = new FormData(form);
|
|
94
|
+
if ((submitter || {}).name) {
|
|
95
|
+
formData.set(submitter.name, submitter.value);
|
|
96
|
+
}
|
|
97
|
+
if (submitParams.method.toUpperCase() === 'GET') {
|
|
98
|
+
var query = wwwFormUnserialize(actionEl.search);
|
|
99
|
+
Array.from(formData.entries()).forEach(_entry => {
|
|
100
|
+
wwwFormSet(query, _entry[0], _entry[1], false);
|
|
101
|
+
});
|
|
102
|
+
actionEl.search = wwwFormSerialize(query);
|
|
103
|
+
formData = null;
|
|
104
|
+
}
|
|
105
|
+
this.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
|
|
106
|
+
// URLs with # will cause a natural navigation
|
|
107
|
+
// even if pointing to a different page, a natural navigation will still happen
|
|
108
|
+
// because with the Observer.set() above, window.document.location.href would have become
|
|
109
|
+
// the destination page, which makes it look like same page navigation
|
|
110
|
+
if (!actionEl.hash) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ----------------
|
|
118
|
+
* Navigator network
|
|
119
|
+
* ----------------
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
// -----------------------
|
|
123
|
+
// Initialize network
|
|
124
|
+
Observer.set(this, 'network', {});
|
|
125
|
+
window.addEventListener('online', () => Observer.set(this.network, 'online', navigator.onLine));
|
|
126
|
+
window.addEventListener('offline', () => Observer.set(this.network, 'online', navigator.onLine));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* ----------------
|
|
130
|
+
* Initial navigation
|
|
131
|
+
* ----------------
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
this.go(this.location, { srcType: 'init' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* History object
|
|
139
|
+
*/
|
|
140
|
+
get history() {
|
|
141
|
+
return window.history;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Performs a request.
|
|
146
|
+
*
|
|
147
|
+
* @param object|string href
|
|
148
|
+
* @param object params
|
|
149
|
+
*
|
|
150
|
+
* @return void
|
|
151
|
+
*/
|
|
152
|
+
async go(url, params = {}) {
|
|
153
|
+
|
|
154
|
+
// Generates request object
|
|
155
|
+
const generateRequest = (url, params) => {
|
|
156
|
+
return new Request(url, {
|
|
157
|
+
...params,
|
|
158
|
+
headers: {
|
|
159
|
+
'Accept': 'application/json',
|
|
160
|
+
'Cache-Control': 'no-store',
|
|
161
|
+
'X-Redirect-Policy': 'manual-when-cross-origin',
|
|
162
|
+
'X-Redirect-Code': xRedirectCode,
|
|
163
|
+
'X-Powered-By': '@webqit/webflo',
|
|
164
|
+
...(params.headers || {}),
|
|
165
|
+
},
|
|
166
|
+
referrer: window.document.location.href,
|
|
167
|
+
signal: this._abortController.signal,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Initiates remote fetch and sets the status
|
|
172
|
+
const remoteRequest = request => {
|
|
173
|
+
Observer.set(this.network, 'remote', true);
|
|
174
|
+
let _response = fetch(request);
|
|
175
|
+
// This catch() is NOT intended to handle failure of the fetch
|
|
176
|
+
_response.catch(e => Observer.set(this.network, 'error', e.message));
|
|
177
|
+
// Save a reference to this
|
|
178
|
+
return _response.then(async response => {
|
|
179
|
+
// Stop loading status
|
|
180
|
+
Observer.set(this.network, 'remote', false);
|
|
181
|
+
return response;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Handles response object
|
|
186
|
+
const handleResponse = async (response, params) => {
|
|
187
|
+
response = await response;
|
|
188
|
+
Observer.set(this.network, 'remote', false);
|
|
189
|
+
Observer.set(this.network, 'error', null);
|
|
190
|
+
if (['link', 'form'].includes(params.srcType)) {
|
|
191
|
+
Observer.set(params.src, 'active', false);
|
|
192
|
+
Observer.set(params.submitter || {}, 'active', false);
|
|
193
|
+
}
|
|
194
|
+
if (!response) return;
|
|
195
|
+
if (response.redirected && this.isSameOrigin(response.url)) {
|
|
196
|
+
Observer.set(this.location, { href: response.url }, {
|
|
197
|
+
detail: { isRedirect: true },
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let location = response.headers.get('Location');
|
|
202
|
+
if (location && response.status === xRedirectCode) {
|
|
203
|
+
Observer.set(this.network, 'redirecting', location);
|
|
204
|
+
window.location = location;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// ------------
|
|
209
|
+
url = typeof url === 'string' ? { href: url } : url;
|
|
210
|
+
params = { referrer: this.location.href, ...params };
|
|
211
|
+
// ------------
|
|
212
|
+
Observer.set(this.location, url, { detail: params, });
|
|
213
|
+
Observer.set(this.network, 'redirecting', null);
|
|
214
|
+
// ------------
|
|
215
|
+
if (['link', 'form'].includes(params.srcType)) {
|
|
216
|
+
Observer.set(params.src, 'active', true);
|
|
217
|
+
Observer.set(params.submitter || {}, 'active', true);
|
|
218
|
+
}
|
|
219
|
+
// ------------
|
|
220
|
+
|
|
221
|
+
if (this._abortController) {
|
|
222
|
+
this._abortController.abort();
|
|
223
|
+
}
|
|
224
|
+
this._abortController = new AbortController();
|
|
225
|
+
let xRedirectCode = 300;
|
|
226
|
+
|
|
227
|
+
if (params.srcType === 'init' || !(_before(url.href, '#') === _before(params.referrer, '#') && (params.method || 'GET').toUpperCase() === 'GET')) {
|
|
228
|
+
handleResponse(this.client.call(this, generateRequest(url.href, params), params, remoteRequest), params);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this._abortController;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Checks if an URL is same origin.
|
|
236
|
+
*
|
|
237
|
+
* @param object|string url
|
|
238
|
+
*
|
|
239
|
+
* @return Bool
|
|
240
|
+
*/
|
|
241
|
+
isSameOrigin(url) {
|
|
242
|
+
if (typeof url === 'string') {
|
|
243
|
+
let href = url;
|
|
244
|
+
url = window.document.createElement('a');
|
|
245
|
+
url.href = href
|
|
246
|
+
}
|
|
247
|
+
return !url.origin || url.origin === this.location.origin;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
}
|
|
@@ -10,7 +10,7 @@ import NavigationEvent from './NavigationEvent.js';
|
|
|
10
10
|
import WorkerClient from './WorkerClient.js';
|
|
11
11
|
import Storage from './Storage.js';
|
|
12
12
|
import Router from './Router.js';
|
|
13
|
-
import
|
|
13
|
+
import Navigator from './Navigator.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* ---------------------------
|
|
@@ -21,192 +21,107 @@ import Http from './Http.js';
|
|
|
21
21
|
export const { Observer } = window.WebQit;
|
|
22
22
|
export default function(layout, params) {
|
|
23
23
|
|
|
24
|
-
const session = Storage();
|
|
25
|
-
const workerClient = new WorkerClient('/worker.js', { startMessages: true });
|
|
26
|
-
Observer.observe(workerClient, changes => {
|
|
27
|
-
console.log('SERVICE_WORKER_STATE', changes[0].name, changes[0].value);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Copy..
|
|
31
24
|
layout = {...layout};
|
|
32
25
|
params = {...params};
|
|
33
|
-
window.addEventListener('online', () => Observer.set(networkWatch, 'online', navigator.onLine));
|
|
34
|
-
window.addEventListener('offline', () => Observer.set(networkWatch, 'online', navigator.onLine));
|
|
35
|
-
var networkProgressOngoing;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* ----------------
|
|
39
|
-
* Apply routing
|
|
40
|
-
* ----------------
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
Http.createClient(async function(request, event = null) {
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Resolve canonicity
|
|
49
|
-
// -------------------
|
|
50
|
-
|
|
51
|
-
// The srvice object
|
|
52
|
-
const $context = {
|
|
53
|
-
layout,
|
|
54
|
-
onHydration: !event && (await window.WebQit.OOHTML.meta.get('isomorphic')),
|
|
55
|
-
response: null,
|
|
56
|
-
}
|
|
27
|
+
const session = Storage();
|
|
28
|
+
const workerClient = new WorkerClient('/worker.js', { startMessages: true });
|
|
29
|
+
const navigator = new Navigator(async (request, params, remoteFetch) => {
|
|
57
30
|
|
|
58
|
-
// The
|
|
59
|
-
const clientNavigationEvent = new NavigationEvent(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
networkProgressOngoing.setActive(false);
|
|
64
|
-
networkProgressOngoing = null;
|
|
65
|
-
}
|
|
31
|
+
// The navigation event
|
|
32
|
+
const clientNavigationEvent = new NavigationEvent(
|
|
33
|
+
new NavigationEvent.Request(request),
|
|
34
|
+
session,
|
|
35
|
+
);
|
|
66
36
|
|
|
67
|
-
|
|
37
|
+
// The app router
|
|
38
|
+
const router = new Router(clientNavigationEvent.url.pathname, layout, {
|
|
39
|
+
layout,
|
|
40
|
+
onHydration: params.srcType === 'init',
|
|
41
|
+
});
|
|
68
42
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
// -----------------
|
|
86
|
-
// Sync session data to cache to be available to service-worker routers
|
|
87
|
-
const response = fetch(event.request, {}, networkProgress.updateProgress.bind(networkProgress));
|
|
88
|
-
// -----------------
|
|
89
|
-
// -----------------
|
|
90
|
-
response.catch(e => networkProgress.throw(e.message));
|
|
91
|
-
return response.then(async _response => {
|
|
92
|
-
_response = new clientNavigationEvent.Response(_response.body, {
|
|
93
|
-
status: _response.status,
|
|
94
|
-
statusText: _response.statusText,
|
|
95
|
-
headers: _response.headers,
|
|
96
|
-
_proxy: {
|
|
97
|
-
url: _response.url,
|
|
98
|
-
ok: _response.ok,
|
|
99
|
-
redirected: _response.redirected
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
// Save a reference to this
|
|
103
|
-
$context.responseClone = _response;
|
|
104
|
-
// Return a promise that never resolves as a new response is underway
|
|
105
|
-
if (!networkProgress.active) {
|
|
106
|
-
return new Promise(() => {});
|
|
107
|
-
}
|
|
108
|
-
// Stop loading status
|
|
109
|
-
networkProgress.setActive(false);
|
|
110
|
-
return _response;
|
|
43
|
+
// --------
|
|
44
|
+
// ROUTE FOR DATA
|
|
45
|
+
// --------
|
|
46
|
+
const httpMethodName = clientNavigationEvent.request.method.toLowerCase();
|
|
47
|
+
const response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, document.state, async function(event) {
|
|
48
|
+
return remoteFetch(event.request).then(response => {
|
|
49
|
+
return new clientNavigationEvent.Response(response.body, {
|
|
50
|
+
status: response.status,
|
|
51
|
+
statusText: response.statusText,
|
|
52
|
+
headers: response.headers,
|
|
53
|
+
_proxy: {
|
|
54
|
+
url: response.url,
|
|
55
|
+
ok: response.ok,
|
|
56
|
+
redirected: response.redirected
|
|
57
|
+
},
|
|
111
58
|
});
|
|
112
59
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
60
|
+
}).catch(e => {
|
|
61
|
+
window.document.body.setAttribute('template', '');
|
|
62
|
+
throw e;
|
|
63
|
+
});
|
|
116
64
|
|
|
117
|
-
// --------
|
|
118
|
-
// Render
|
|
119
|
-
// --------
|
|
120
|
-
const rendering = await router.route('render', clientNavigationEvent, $context.response, async function(event, data) {
|
|
121
|
-
// --------
|
|
122
|
-
// OOHTML would waiting for DOM-ready in order to be initialized
|
|
123
|
-
await new Promise(res => window.WebQit.DOM.ready(res));
|
|
124
|
-
if (!window.document.state.env) {
|
|
125
|
-
window.document.setState({
|
|
126
|
-
env: 'client',
|
|
127
|
-
onHydration: $context.onHydration,
|
|
128
|
-
network: networkWatch,
|
|
129
|
-
url: httpInstance.location,
|
|
130
|
-
session,
|
|
131
|
-
}, { update: true });
|
|
132
|
-
}
|
|
133
|
-
window.document.setState({ page: data }, { update: 'merge' });
|
|
134
|
-
window.document.body.setAttribute('template', 'page/' + requestPath.split('/').filter(a => a).map(a => a + '+-').join('/'));
|
|
135
|
-
return new Promise(res => {
|
|
136
|
-
window.document.addEventListener('templatesreadystatechange', () => res(window));
|
|
137
|
-
if (window.document.templatesReadyState === 'complete') {
|
|
138
|
-
res(window);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
65
|
|
|
66
|
+
// --------
|
|
67
|
+
// Render
|
|
68
|
+
// --------
|
|
69
|
+
const data = response instanceof clientNavigationEvent.Response ? await response.data() : response;
|
|
70
|
+
console.log('----------------data', data);
|
|
71
|
+
await router.route('render', clientNavigationEvent, data, async function(event, data) {
|
|
143
72
|
// --------
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} else {
|
|
155
|
-
document.documentElement.classList.add('scroll-reset');
|
|
156
|
-
document.body.scrollIntoView();
|
|
157
|
-
setTimeout(() => {
|
|
158
|
-
document.documentElement.classList.remove('scroll-reset');
|
|
159
|
-
}, 600);
|
|
160
|
-
}
|
|
161
|
-
}, 0);
|
|
73
|
+
// OOHTML would waiting for DOM-ready in order to be initialized
|
|
74
|
+
await new Promise(res => window.WebQit.DOM.ready(res));
|
|
75
|
+
if (!window.document.state.env) {
|
|
76
|
+
window.document.setState({
|
|
77
|
+
env: 'client',
|
|
78
|
+
onHydration: params.srcType === 'init',
|
|
79
|
+
network: navigator.network,
|
|
80
|
+
url: navigator.location,
|
|
81
|
+
session,
|
|
82
|
+
}, { update: true });
|
|
162
83
|
}
|
|
84
|
+
window.document.setState({ page: data }, { update: 'merge' });
|
|
85
|
+
window.document.body.setAttribute('template', 'page/' + clientNavigationEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
|
|
86
|
+
return new Promise(res => {
|
|
87
|
+
window.document.addEventListener('templatesreadystatechange', () => res(window));
|
|
88
|
+
if (window.document.templatesReadyState === 'complete') {
|
|
89
|
+
res(window);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
163
93
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
94
|
+
// --------
|
|
95
|
+
// Render...
|
|
96
|
+
// --------
|
|
97
|
+
|
|
98
|
+
if (params.src instanceof Element) {
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
let viewportTop;
|
|
101
|
+
if (clientNavigationEvent.url.hash && (urlTarget = document.querySelector(clientNavigationEvent.url.hash))) {
|
|
102
|
+
urlTarget.scrollIntoView();
|
|
103
|
+
} else if (viewportTop = Array.from(document.querySelectorAll('[data-viewport-top]')).pop()) {
|
|
104
|
+
viewportTop.focus();
|
|
105
|
+
} else {
|
|
106
|
+
document.documentElement.classList.add('scroll-reset');
|
|
107
|
+
document.body.scrollIntoView();
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
document.documentElement.classList.remove('scroll-reset');
|
|
110
|
+
}, 600);
|
|
111
|
+
}
|
|
112
|
+
}, 0);
|
|
169
113
|
}
|
|
170
114
|
|
|
171
|
-
return
|
|
115
|
+
return response;
|
|
172
116
|
});
|
|
173
117
|
|
|
118
|
+
Observer.observe(session, changes => {
|
|
119
|
+
//console.log('SESSION_STATE_CHANGE', changes[0].name, changes[0].value);
|
|
120
|
+
});
|
|
121
|
+
Observer.observe(workerClient, changes => {
|
|
122
|
+
//console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
|
|
123
|
+
});
|
|
124
|
+
Observer.observe(navigator, changes => {
|
|
125
|
+
//console.log('NAVIGATORSTATE_CHANGE', changes[0].name, changes[0].value);
|
|
126
|
+
});
|
|
174
127
|
};
|
|
175
|
-
|
|
176
|
-
const networkWatch = { progress: {}, online: navigator.onLine };
|
|
177
|
-
class RequestHandle {
|
|
178
|
-
setActive(state, method = '') {
|
|
179
|
-
if (this.active === false) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
this.active = state;
|
|
183
|
-
Observer.set(networkWatch, {
|
|
184
|
-
method,
|
|
185
|
-
error: '',
|
|
186
|
-
progress: {
|
|
187
|
-
active: state,
|
|
188
|
-
determinate: false,
|
|
189
|
-
valuenow: 0,
|
|
190
|
-
valuetotal: NaN,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
updateProgress(phase, valuenow, valuetotal) {
|
|
195
|
-
if (this.active === false) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
Observer.set(networkWatch.progress, {
|
|
199
|
-
phase,
|
|
200
|
-
determinate: !isNaN(valuetotal),
|
|
201
|
-
valuenow,
|
|
202
|
-
valuetotal,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
throw(message) {
|
|
206
|
-
if (this.active === false) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
this.error = true;
|
|
210
|
-
Observer.set(networkWatch, 'error', message);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
@@ -117,7 +117,7 @@ export default function(layout, params) {
|
|
|
117
117
|
}
|
|
118
118
|
return _response;
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
return defaultFetch(evt);
|
|
122
122
|
};
|
|
123
123
|
evt.respondWith(handleFetch(evt));
|
|
@@ -144,13 +144,12 @@ export default function(layout, params) {
|
|
|
144
144
|
return network_fetch(evt);
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
//evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
|
148
147
|
//evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
|
149
148
|
|
|
150
149
|
const getCacheName = request => request.headers.get('Accept') === 'application/json'
|
|
151
150
|
? params.cache_name + '_json'
|
|
152
151
|
: params.cache_name;
|
|
153
|
-
|
|
152
|
+
|
|
154
153
|
// Caching strategy: cache_first
|
|
155
154
|
const cache_fetch = (evt, cacheRefresh = false, is_Navigate_ForceCache_Document = false) => {
|
|
156
155
|
|
|
@@ -162,10 +161,10 @@ export default function(layout, params) {
|
|
|
162
161
|
let url = new URL(request.url);
|
|
163
162
|
url.searchParams.set('$force-cache', '1');
|
|
164
163
|
request = new Request(url, {
|
|
165
|
-
method: request.method,
|
|
164
|
+
method: request.method,
|
|
166
165
|
headers: request.headers,
|
|
167
166
|
body: request.body,
|
|
168
|
-
mode: request.mode === 'navigate' ? null : request.mode,
|
|
167
|
+
mode: request.mode === 'navigate'/* throws */ ? null : request.mode,
|
|
169
168
|
credentials: request.credentials,
|
|
170
169
|
cache: request.cache,
|
|
171
170
|
redirect: request.redirect,
|
|
@@ -6,7 +6,7 @@ import { URL } from 'url';
|
|
|
6
6
|
import { Readable } from "stream";
|
|
7
7
|
import { FormData, File, Blob } from 'formdata-node';
|
|
8
8
|
import { FormDataEncoder } from 'form-data-encoder';
|
|
9
|
-
import { Request, Response, Headers } from 'node-fetch';
|
|
9
|
+
import fetch, { Request, Response, Headers } from 'node-fetch';
|
|
10
10
|
import _NavigationEvent from '../_NavigationEvent.js';
|
|
11
11
|
import _FormData from '../_FormData.js';
|
|
12
12
|
|
|
@@ -34,5 +34,6 @@ export default _NavigationEvent({
|
|
|
34
34
|
File,
|
|
35
35
|
Blob,
|
|
36
36
|
ReadableStream: Readable,
|
|
37
|
-
FormDataEncoder
|
|
37
|
+
FormDataEncoder,
|
|
38
|
+
fetch
|
|
38
39
|
});
|
|
@@ -17,7 +17,6 @@ import _isArray from '@webqit/util/js/isArray.js';
|
|
|
17
17
|
import { _isString, _isPlainObject, _isPlainArray } from '@webqit/util/js/index.js';
|
|
18
18
|
import _delay from '@webqit/util/js/delay.js';
|
|
19
19
|
import { slice as _streamSlice } from 'stream-slice';
|
|
20
|
-
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
|
|
21
20
|
import * as config from '../../config/index.js';
|
|
22
21
|
import * as services from '../../services/index.js';
|
|
23
22
|
import NavigationEvent from './NavigationEvent.js';
|
|
@@ -34,33 +33,11 @@ import Router from './Router.js';
|
|
|
34
33
|
export default async function(Ui, flags = {}) {
|
|
35
34
|
|
|
36
35
|
const layout = await config.layout.read(flags, {});
|
|
37
|
-
const setup = {
|
|
36
|
+
const v_setup = {}, setup = {
|
|
38
37
|
layout,
|
|
39
38
|
server: await config.server.read(flags, layout),
|
|
40
39
|
variables: await config.variables.read(flags, layout),
|
|
41
40
|
};
|
|
42
|
-
|
|
43
|
-
if (!setup.server.shared && setup.variables.autoload !== false) {
|
|
44
|
-
Object.keys(setup.variables.entries).forEach(key => {
|
|
45
|
-
if (!(key in process.env) || setup.variables.autoload === 2) {
|
|
46
|
-
process.env[key] = setup.variables.entries[key];
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const getSessionInitializer = (sesskey, hostname = null) => {
|
|
52
|
-
const secret = sesskey || (hostname ? uuidv5(hostname, uuidv4()) : uuidv4());
|
|
53
|
-
return Sessions({
|
|
54
|
-
cookieName: '_session', // cookie name dictates the key name added to the request object
|
|
55
|
-
secret, // should be a large unguessable string
|
|
56
|
-
duration: 24 * 60 * 60 * 1000, // how long the session will stay valid in ms
|
|
57
|
-
activeDuration: 1000 * 60 * 5 // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const instanceSetup = setup;
|
|
62
|
-
|
|
63
|
-
const v_setup = {};
|
|
64
41
|
if (setup.server.shared) {
|
|
65
42
|
await Promise.all(((await config.vhosts.read(flags, setup.layout)).entries || []).map(vh => new Promise(async resolve => {
|
|
66
43
|
const vlayout = await config.layout.read(flags, {ROOT: Path.join(setup.layout.ROOT, vh.path)});
|
|
@@ -70,86 +47,57 @@ export default async function(Ui, flags = {}) {
|
|
|
70
47
|
variables: await config.variables.read(flags, vlayout),
|
|
71
48
|
vh,
|
|
72
49
|
};
|
|
73
|
-
v_setup[vh.host].sessionInit = getSessionInitializer(v_setup[vh.host].variables.entries.sesskey, vh.host),
|
|
74
50
|
resolve();
|
|
75
51
|
})));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
} else if (setup.variables.autoload !== false) {
|
|
53
|
+
Object.keys(setup.variables.entries).forEach(key => {
|
|
54
|
+
if (!(key in process.env) || setup.variables.autoload === 2) {
|
|
55
|
+
process.env[key] = setup.variables.entries[key];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
79
59
|
|
|
80
60
|
// ---------------------------------------------
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
61
|
+
|
|
62
|
+
function handleRequest(protocol, request, response) {
|
|
63
|
+
let _setup = setup, hostname = (request.headers.host || '').split(':')[0];
|
|
64
|
+
if (setup.server.shared) {
|
|
65
|
+
if (!(_setup = v_setup[hostname])
|
|
66
|
+
&& ((hostname.startsWith('www.') && (_setup = v_setup[hostname.substr(4)]) && _setup.server.force_www)
|
|
67
|
+
&& (!hostname.startsWith('www.') && (_setup = v_setup['www.' + hostname]) && _setup.server.force_www))) {
|
|
68
|
+
response.statusCode = 500;
|
|
69
|
+
response.end('Unrecognized host');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
86
72
|
}
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
if (protocol === 'http' && _setup.server.https.force && !flags['http-only'] && /** main server */setup.server.https.port) {
|
|
74
|
+
response.statusCode = 302;
|
|
75
|
+
response.setHeader('Location', 'https://' + request.headers.host + request.url);
|
|
76
|
+
response.end();
|
|
77
|
+
return;
|
|
90
78
|
}
|
|
91
|
-
response.statusCode = 500;
|
|
92
|
-
response.end('Unrecognized host');
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const goOrForceWww = (setup, request, response, protocol) => {
|
|
96
|
-
var hostname = request.headers.host || '';
|
|
97
79
|
if (hostname.startsWith('www.') && setup.server.force_www === 'remove') {
|
|
98
80
|
response.statusCode = 302;
|
|
99
81
|
response.setHeader('Location', protocol + '://' + hostname.substr(4) + request.url);
|
|
100
82
|
response.end();
|
|
101
|
-
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!hostname.startsWith('www.') && setup.server.force_www === 'add') {
|
|
102
86
|
response.statusCode = 302;
|
|
103
87
|
response.setHeader('Location', protocol + '://www.' + hostname + request.url);
|
|
104
88
|
response.end();
|
|
105
|
-
|
|
106
|
-
setup.sessionInit(request, response, () => {});
|
|
107
|
-
run(instanceSetup, setup, request, response, Ui, flags, protocol);
|
|
89
|
+
return;
|
|
108
90
|
}
|
|
91
|
+
run(_setup, request, response, Ui, flags, protocol);
|
|
109
92
|
};
|
|
110
93
|
|
|
111
94
|
// ---------------------------------------------
|
|
112
|
-
|
|
113
|
-
if (!flags['https-only']) {
|
|
114
|
-
|
|
115
|
-
Http.createServer((request, response) => {
|
|
116
|
-
if (setup.server.shared) {
|
|
117
|
-
var _setup;
|
|
118
|
-
if (_setup = getVSetup(request, response)) {
|
|
119
|
-
goOrForceHttps(_setup, request, response);
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
goOrForceHttps(setup, request, response);
|
|
123
|
-
}
|
|
124
|
-
}).listen(process.env.PORT || setup.server.port);
|
|
125
|
-
|
|
126
|
-
const goOrForceHttps = ($setup, $request, $response) => {
|
|
127
|
-
if ($setup.server.https.force && !flags['http-only'] && /** main server */setup.server.https.port) {
|
|
128
|
-
$response.statusCode = 302;
|
|
129
|
-
$response.setHeader('Location', 'https://' + $request.headers.host + $request.url);
|
|
130
|
-
$response.end();
|
|
131
|
-
} else {
|
|
132
|
-
goOrForceWww($setup, $request, $response, 'http');
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
95
|
|
|
96
|
+
if (!flags['https-only']) {
|
|
97
|
+
Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || setup.server.port);
|
|
136
98
|
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------
|
|
139
|
-
|
|
140
99
|
if (!flags['http-only'] && setup.server.https.port) {
|
|
141
|
-
|
|
142
|
-
const httpsServer = Https.createServer((request, response) => {
|
|
143
|
-
if (setup.server.shared) {
|
|
144
|
-
var _setup;
|
|
145
|
-
if (_setup = getVSetup(request, response)) {
|
|
146
|
-
goOrForceWww(_setup, request, response, 'https');
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
goOrForceWww(setup, request, response, 'https');
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
100
|
+
const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
|
|
153
101
|
if (setup.server.shared) {
|
|
154
102
|
_each(v_setup, (host, _setup) => {
|
|
155
103
|
if (Fs.existsSync(_setup.server.https.keyfile)) {
|
|
@@ -185,7 +133,6 @@ export default async function(Ui, flags = {}) {
|
|
|
185
133
|
});
|
|
186
134
|
}
|
|
187
135
|
}
|
|
188
|
-
|
|
189
136
|
httpsServer.listen(process.env.PORT2 || setup.server.https.port);
|
|
190
137
|
}
|
|
191
138
|
};
|
|
@@ -193,7 +140,6 @@ export default async function(Ui, flags = {}) {
|
|
|
193
140
|
/**
|
|
194
141
|
* The Server.
|
|
195
142
|
*
|
|
196
|
-
* @param Object instanceSetup
|
|
197
143
|
* @param Object hostSetup
|
|
198
144
|
* @param Request request
|
|
199
145
|
* @param Response response
|
|
@@ -203,13 +149,12 @@ export default async function(Ui, flags = {}) {
|
|
|
203
149
|
*
|
|
204
150
|
* @return void
|
|
205
151
|
*/
|
|
206
|
-
export async function run(
|
|
152
|
+
export async function run(hostSetup, request, response, Ui, flags = {}, protocol = 'http') {
|
|
207
153
|
|
|
208
154
|
// --------
|
|
209
155
|
// Request parsing
|
|
210
156
|
// --------
|
|
211
157
|
|
|
212
|
-
const fullUrl = protocol + '://' + request.headers.host + request.url;
|
|
213
158
|
const requestInit = { method: request.method, headers: request.headers };
|
|
214
159
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
215
160
|
requestInit.body = await new Promise((resolve, reject) => {
|
|
@@ -257,10 +202,37 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
257
202
|
});
|
|
258
203
|
});
|
|
259
204
|
}
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
205
|
+
|
|
206
|
+
// --------
|
|
207
|
+
// NavigationEvent instance
|
|
208
|
+
// --------
|
|
209
|
+
|
|
210
|
+
const fullUrl = protocol + '://' + request.headers.host + request.url;
|
|
211
|
+
const _sessionFactory = function(id, params = {}, callback = null) {
|
|
212
|
+
let factory, secret = hostSetup.variables.entries.SESSION_KEY;
|
|
213
|
+
Sessions({
|
|
214
|
+
duration: 0, // how long the session will stay valid in ms
|
|
215
|
+
activeDuration: 0, // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
|
|
216
|
+
...params,
|
|
217
|
+
cookieName: id, // cookie name dictates the key name added to the request object
|
|
218
|
+
secret, // should be a large unguessable string
|
|
219
|
+
})(request, response, e => {
|
|
220
|
+
factory = Object.getOwnPropertyDescriptor(request, id);
|
|
221
|
+
if (callback) {
|
|
222
|
+
callback(e, factory);
|
|
223
|
+
} else if (e) {
|
|
224
|
+
// TODO
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// Where theres no error, factory is available Sync
|
|
228
|
+
return !callback ? factory : undefined;
|
|
229
|
+
};
|
|
230
|
+
const serverNavigationEvent = new NavigationEvent(
|
|
231
|
+
new NavigationEvent.Request(fullUrl, requestInit),
|
|
232
|
+
_sessionFactory('_session', { duration: 60 * 60 }).get(),
|
|
233
|
+
_sessionFactory
|
|
234
|
+
);
|
|
235
|
+
|
|
264
236
|
const $context = {
|
|
265
237
|
rdr: null,
|
|
266
238
|
layout: hostSetup.layout,
|
|
@@ -434,12 +406,7 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
434
406
|
// -------------------
|
|
435
407
|
// Chrome needs this for audio elements to play
|
|
436
408
|
response.setHeader('Accept-Ranges', 'bytes');
|
|
437
|
-
/*
|
|
438
|
-
if ($context.response.headers.contentLength && !$context.response.headers.contentRange) {
|
|
439
|
-
$context.response.headers.contentRange = `bytes 0-${$context.response.headers.contentLength}/${$context.response.headers.contentLength}`;
|
|
440
|
-
}
|
|
441
409
|
|
|
442
|
-
*/
|
|
443
410
|
// -------------------
|
|
444
411
|
// Automatic response headers
|
|
445
412
|
// -------------------
|
|
@@ -485,7 +452,7 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
485
452
|
if (name === 'set-cookie') {
|
|
486
453
|
setCookies(value);
|
|
487
454
|
} else {
|
|
488
|
-
if (name.toLowerCase() === 'location' &&
|
|
455
|
+
if (name.toLowerCase() === 'location' && $context.response.status === 200) {
|
|
489
456
|
response.statusCode = 302 /* Temporary */;
|
|
490
457
|
}
|
|
491
458
|
response.setHeader(name, value);
|
|
@@ -496,6 +463,15 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
496
463
|
// Send
|
|
497
464
|
// -------------------
|
|
498
465
|
if ($context.response.headers.redirect) {
|
|
466
|
+
let xRedirectPolicy = serverNavigationEvent.request.headers.get('X-Redirect-Policy');
|
|
467
|
+
let xRedirectCode = serverNavigationEvent.request.headers.get('X-Redirect-Code') || 300;
|
|
468
|
+
let isSameOriginRedirect = (new serverNavigationEvent.globals.URL($context.response.headers.location)).origin === serverNavigationEvent.url.origin;
|
|
469
|
+
if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
|
|
470
|
+
response.statusCode = xRedirectCode;
|
|
471
|
+
response.setHeader('X-Redirect-Code', $context.response.status);
|
|
472
|
+
} else {
|
|
473
|
+
response.statusCode = $context.response.status;
|
|
474
|
+
}
|
|
499
475
|
response.end();
|
|
500
476
|
} else if ($context.response.original !== undefined && $context.response.original !== null) {
|
|
501
477
|
response.statusCode = $context.response.status;
|
|
@@ -576,15 +552,23 @@ export async function run(instanceSetup, hostSetup, request, response, Ui, flags
|
|
|
576
552
|
// --------
|
|
577
553
|
|
|
578
554
|
if (flags.logs !== false) {
|
|
555
|
+
let errorCode = [ 404, 500 ].includes(response.statusCode) ? response.statusCode : 0;
|
|
556
|
+
let xRedirectCode = response.getHeader('X-Redirect-Code');
|
|
557
|
+
let redirectCode = xRedirectCode || ((response.statusCode + '').startsWith('3') ? response.statusCode : 0);
|
|
558
|
+
let statusCode = xRedirectCode || response.statusCode;
|
|
579
559
|
Ui.log(''
|
|
580
560
|
+ '[' + (hostSetup.vh ? Ui.style.keyword(hostSetup.vh.host) + '][' : '') + Ui.style.comment((new Date).toUTCString()) + '] '
|
|
581
561
|
+ Ui.style.keyword(protocol.toUpperCase() + ' ' + serverNavigationEvent.request.method) + ' '
|
|
582
562
|
+ Ui.style.url(serverNavigationEvent.request.url) + ($context.response && ($context.response.meta || {}).autoIndex ? Ui.style.comment((!serverNavigationEvent.request.url.endsWith('/') ? '/' : '') + $context.response.meta.autoIndex) : '') + ' '
|
|
583
563
|
+ (' (' + Ui.style.comment($context.response && ($context.response.headers || {}).contentType ? $context.response.headers.contentType : 'unknown') + ') ')
|
|
584
564
|
+ (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
565
|
+
errorCode
|
|
566
|
+
? Ui.style.err(errorCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
|
|
567
|
+
: Ui.style.val(statusCode) + (
|
|
568
|
+
redirectCode
|
|
569
|
+
? ' - ' + Ui.style.val(response.getHeader('Location'))
|
|
570
|
+
: ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')'
|
|
571
|
+
)
|
|
588
572
|
)
|
|
589
573
|
);
|
|
590
574
|
}
|