@webqit/webflo 0.20.4-next.2 → 0.20.4-next.4
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 +13 -34
- package/site/docs/concepts/realtime.md +45 -44
- package/site/docs/getting-started.md +40 -40
- package/src/{Context.js → CLIContext.js} +9 -8
- package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
- package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
- package/src/index.js +3 -1
- package/src/init-pi/index.js +7 -4
- package/src/init-pi/templates/pwa/.gitignore +6 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/pwa/package.json +2 -2
- package/src/init-pi/templates/pwa/public/manifest.json +2 -2
- package/src/init-pi/templates/web/.gitignore +6 -0
- package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
- package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/web/package.json +2 -2
- package/src/runtime-pi/AppBootstrap.js +38 -0
- package/src/runtime-pi/WebfloRuntime.js +68 -56
- package/src/runtime-pi/apis.js +9 -0
- package/src/runtime-pi/index.js +2 -4
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
- package/src/runtime-pi/webflo-client/bootstrap.js +38 -0
- package/src/runtime-pi/webflo-client/index.js +2 -8
- package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +154 -116
- package/src/runtime-pi/webflo-fetch/index.js +436 -5
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
- package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
- package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
- package/src/runtime-pi/webflo-server/WebfloServer.js +138 -200
- package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
- package/src/runtime-pi/webflo-server/index.js +2 -6
- package/src/runtime-pi/webflo-server/webflo-devmode.js +24 -31
- package/src/runtime-pi/webflo-url/Url.js +1 -1
- package/src/runtime-pi/webflo-url/xURL.js +1 -1
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
- package/src/runtime-pi/webflo-worker/bootstrap.js +39 -0
- package/src/runtime-pi/webflo-worker/index.js +3 -7
- package/src/webflo-cli.js +1 -2
- package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
- package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
- package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
- package/src/runtime-pi/webflo-fetch/headers.js +0 -151
- package/src/runtime-pi/webflo-fetch/message.js +0 -49
- package/src/runtime-pi/webflo-fetch/request.js +0 -62
- package/src/runtime-pi/webflo-fetch/response.js +0 -110
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import Path from 'path';
|
|
2
|
-
import $glob from 'fast-glob';
|
|
3
|
-
import EsBuild from 'esbuild';
|
|
4
2
|
import chokidar from 'chokidar';
|
|
5
3
|
import { exec, spawn } from 'child_process';
|
|
6
4
|
import { platform } from 'os';
|
|
@@ -108,7 +106,7 @@ export class WebfloHMR {
|
|
|
108
106
|
this.#jsMeta.mustRevalidate = true; // Invalidate graph
|
|
109
107
|
}
|
|
110
108
|
if ((!hasJustBeenRebuilt && type !== 'unlink') || !this.#jsMeta.dependencyMap) { // We need graph in place to process affected routes for an unlink event
|
|
111
|
-
await this.
|
|
109
|
+
await this.buildRoutes(this.#jsMeta.mustRevalidate/*fullBuild*/);
|
|
112
110
|
hasJustBeenRebuilt = true;
|
|
113
111
|
}
|
|
114
112
|
const affectedHandlers = this.#jsMeta.dependencyMap[target] || [];
|
|
@@ -145,7 +143,7 @@ export class WebfloHMR {
|
|
|
145
143
|
if (/^unlink/.test(event.actionableEffect)) {
|
|
146
144
|
delete this.#app.routes[event.affectedRoute];
|
|
147
145
|
} else if (event.realm === 'server') {
|
|
148
|
-
this.#app.routes[event.affectedRoute] = `${Path.join(this.#app.config.
|
|
146
|
+
this.#app.routes[event.affectedRoute] = `${Path.join(this.#app.config.RUNTIME_DIR, event.affectedHandler)}?_webflohmrhash=${Date.now()}`;
|
|
149
147
|
}
|
|
150
148
|
} else if (event.fileType === 'css') {
|
|
151
149
|
this.#dirtiness.CSSAffected = true;
|
|
@@ -153,7 +151,7 @@ export class WebfloHMR {
|
|
|
153
151
|
this.#dirtiness.HTMLAffected = true;
|
|
154
152
|
}
|
|
155
153
|
}
|
|
156
|
-
if (this.#options.buildSensitivity ===
|
|
154
|
+
if (this.#options.buildSensitivity === 2) {
|
|
157
155
|
await this.bundleAssetsIfPending();
|
|
158
156
|
}
|
|
159
157
|
// Broadcast to clients
|
|
@@ -172,36 +170,29 @@ export class WebfloHMR {
|
|
|
172
170
|
}
|
|
173
171
|
}
|
|
174
172
|
|
|
175
|
-
async
|
|
173
|
+
async buildRoutes(fullBuild = false) {
|
|
176
174
|
// 0. Generate graph
|
|
177
175
|
let buildResult;
|
|
178
176
|
try {
|
|
179
177
|
if (this.#jsMeta.prevBuildResult) {
|
|
180
178
|
if (fullBuild) await this.#jsMeta.prevBuildResult.rebuild.dispose();
|
|
181
179
|
else buildResult = await buildResult.rebuild();
|
|
182
|
-
}
|
|
180
|
+
}``
|
|
183
181
|
if (!buildResult) {
|
|
184
|
-
const routeDirs = [...this.#routeDirs];
|
|
185
|
-
const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{,.client,.worker,.server}.js`), { absolute: true })
|
|
186
|
-
.then((files) => files.map((file) => file.replace(/\\/g, '/')));
|
|
187
|
-
const entryNames = routeDirs.length === 1 ? `${Path.relative(process.cwd(), routeDirs[0])}/[dir]/[name]` : `[dir]/[name]`;
|
|
188
182
|
const bundlingConfig = {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
bundle: true,
|
|
193
|
-
format: 'esm',
|
|
194
|
-
platform: 'browser', // optional but good for clarity
|
|
183
|
+
client: true,
|
|
184
|
+
worker: true,
|
|
185
|
+
server: true,
|
|
195
186
|
metafile: true, // This is key
|
|
196
|
-
treeShaking: true, // Optional optimization
|
|
197
187
|
logLevel: 'silent', // Suppress output
|
|
198
|
-
minify: false,
|
|
199
|
-
sourcemap: false,
|
|
200
188
|
incremental: true,
|
|
201
189
|
};
|
|
202
|
-
buildResult = await
|
|
190
|
+
buildResult = await this.#app.buildRoutes(bundlingConfig);
|
|
203
191
|
}
|
|
204
|
-
} catch (e) {
|
|
192
|
+
} catch (e) {
|
|
193
|
+
//console.error(e);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
205
196
|
|
|
206
197
|
// 1. Forward dependency graph (file -> [imported files])
|
|
207
198
|
const forward = {};
|
|
@@ -241,35 +232,36 @@ export class WebfloHMR {
|
|
|
241
232
|
return true;
|
|
242
233
|
}
|
|
243
234
|
|
|
244
|
-
async bundleAssetsIfPending() {
|
|
235
|
+
async bundleAssetsIfPending(ohForce = false) {
|
|
245
236
|
const entries = {};
|
|
246
237
|
|
|
247
|
-
if (this.#dirtiness.clientRoutesAffected.size || this.#dirtiness.serviceWorkerAffected) {
|
|
238
|
+
if (this.#dirtiness.clientRoutesAffected.size || this.#dirtiness.serviceWorkerAffected || ohForce) {
|
|
248
239
|
entries.js = {};
|
|
249
|
-
entries.js.client = !!this.#dirtiness.clientRoutesAffected.size;
|
|
250
|
-
entries.js.worker = this.#dirtiness.serviceWorkerAffected;
|
|
240
|
+
entries.js.client = !!this.#dirtiness.clientRoutesAffected.size || ohForce;
|
|
241
|
+
entries.js.worker = this.#dirtiness.serviceWorkerAffected || ohForce;
|
|
242
|
+
entries.js.server = false;
|
|
251
243
|
// Clear state
|
|
252
244
|
this.#dirtiness.clientRoutesAffected.clear();
|
|
253
245
|
this.#dirtiness.serviceWorkerAffected = false;
|
|
254
246
|
}
|
|
255
247
|
|
|
256
|
-
if (this.#dirtiness.HTMLAffected) {
|
|
248
|
+
if (this.#dirtiness.HTMLAffected || ohForce) {
|
|
257
249
|
this.#dirtiness.HTMLAffected = false;
|
|
258
250
|
entries.html = {};
|
|
259
251
|
}
|
|
260
252
|
|
|
261
|
-
if (this.#dirtiness.CSSAffected) {
|
|
253
|
+
if (this.#dirtiness.CSSAffected || ohForce) {
|
|
262
254
|
this.#dirtiness.CSSAffected = false;
|
|
263
255
|
entries.css = {};
|
|
264
256
|
}
|
|
265
257
|
|
|
266
258
|
for (const e in entries) {
|
|
267
259
|
const buildKey = `build:${e}`;
|
|
268
|
-
let
|
|
269
|
-
buildScriptName = this.#options.buildScripts?.[buildKey];
|
|
260
|
+
let buildScriptName = this.#options.buildScripts?.[buildKey];
|
|
270
261
|
if (buildScriptName === true) {
|
|
271
262
|
buildScriptName = buildKey;
|
|
272
263
|
}
|
|
264
|
+
let buildScript;
|
|
273
265
|
if (buildScriptName
|
|
274
266
|
&& (buildScript = this.#options.appMeta.scripts?.[buildScriptName])) {
|
|
275
267
|
await this.#spawnProcess(buildScript, entries[e]);
|
|
@@ -278,8 +270,9 @@ export class WebfloHMR {
|
|
|
278
270
|
}
|
|
279
271
|
|
|
280
272
|
async #spawnProcess(command, options = {}) {
|
|
273
|
+
const $options = Object.fromEntries(Object.entries(options).map(([k, v]) => [`--${k}`, v]));
|
|
281
274
|
const commandArr = [...new Set(
|
|
282
|
-
command.split(/\s+?/).concat(Object.keys(options)).filter((s) => !(s in options) || options[s])
|
|
275
|
+
command.split(/\s+?/).concat(Object.keys($options)).filter((s) => !(s in $options) || $options[s])
|
|
283
276
|
)];
|
|
284
277
|
return await new Promise((resolve, reject) => {
|
|
285
278
|
const child = spawn(commandArr.shift(), commandArr, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _with } from '@webqit/util/obj/index.js';
|
|
2
2
|
import { _isArray, _isObject, _isTypeObject, _isString, _isEmpty } from '@webqit/util/js/index.js';
|
|
3
3
|
import { DeepURLSearchParams } from './util.js';
|
|
4
|
-
import { Observer } from '@webqit/
|
|
4
|
+
import { Observer } from '@webqit/use-live';
|
|
5
5
|
|
|
6
6
|
export class Url {
|
|
7
7
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _any } from '@webqit/util/arr/index.js';
|
|
2
2
|
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
3
|
+
import { response as responseShim } from '../webflo-fetch/index.js';
|
|
3
4
|
import { WQBroadcastChannel } from '../webflo-messaging/WQBroadcastChannel.js';
|
|
4
5
|
import { WorkerSideWorkport } from './WorkerSideWorkport.js';
|
|
5
6
|
import { WorkerSideCookies } from './WorkerSideCookies.js';
|
|
@@ -21,13 +22,6 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
21
22
|
|
|
22
23
|
static get Workport() { return WorkerSideWorkport; }
|
|
23
24
|
|
|
24
|
-
static create(cx) {
|
|
25
|
-
return new this(this.Context.create(cx));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#sdk = {};
|
|
29
|
-
get sdk() { return this.#sdk; }
|
|
30
|
-
|
|
31
25
|
async initialize() {
|
|
32
26
|
const instanceController = super.initialize();
|
|
33
27
|
// ONINSTALL
|
|
@@ -98,7 +92,9 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
98
92
|
self.registration.showNotification(title, params);
|
|
99
93
|
};
|
|
100
94
|
self.addEventListener('fetch', fetchHandler, { signal: instanceController.signal });
|
|
101
|
-
|
|
95
|
+
if (this.config.CLIENT.capabilities.webpush) {
|
|
96
|
+
self.addEventListener('push', webpushHandler, { signal: instanceController.signal });
|
|
97
|
+
}
|
|
102
98
|
return instanceController;
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -114,25 +110,24 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
114
110
|
request: scopeObj.request
|
|
115
111
|
});
|
|
116
112
|
scopeObj.session = this.createHttpSession({
|
|
117
|
-
store: this
|
|
113
|
+
store: this.createStorage('session'),
|
|
118
114
|
request: scopeObj.request
|
|
119
115
|
});
|
|
120
116
|
const requestID = crypto.randomUUID();
|
|
121
117
|
scopeObj.clientRequestRealtime = new WQBroadcastChannel(requestID);
|
|
122
118
|
scopeObj.user = this.createHttpUser({
|
|
123
|
-
store: this
|
|
119
|
+
store: this.createStorage('user'),
|
|
124
120
|
request: scopeObj.request,
|
|
125
|
-
|
|
121
|
+
client: scopeObj.clientRequestRealtime,
|
|
126
122
|
session: scopeObj.session,
|
|
127
123
|
});
|
|
128
124
|
scopeObj.httpEvent = this.createHttpEvent({
|
|
129
125
|
request: scopeObj.request,
|
|
130
|
-
|
|
126
|
+
client: scopeObj.clientRequestRealtime,
|
|
131
127
|
cookies: scopeObj.cookies,
|
|
132
128
|
session: scopeObj.session,
|
|
133
129
|
user: scopeObj.user,
|
|
134
130
|
detail: scopeObj.detail,
|
|
135
|
-
sdk: {}
|
|
136
131
|
});
|
|
137
132
|
// Dispatch for response
|
|
138
133
|
scopeObj.response = await this.dispatchNavigationEvent({
|
|
@@ -144,7 +139,7 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
144
139
|
}
|
|
145
140
|
return await this.remoteFetch(event.request);
|
|
146
141
|
},
|
|
147
|
-
|
|
142
|
+
clientPortB: `br:${scopeObj.httpEvent.client.name}`
|
|
148
143
|
});
|
|
149
144
|
return scopeObj.response;
|
|
150
145
|
}
|
|
@@ -211,7 +206,8 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
211
206
|
|
|
212
207
|
async refreshCache(request, response) {
|
|
213
208
|
// Check if we received a valid response
|
|
214
|
-
|
|
209
|
+
const statusCode = responseShim.prototype.status.get.call(response);
|
|
210
|
+
if (request.method !== 'GET' || !response || statusCode !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
|
|
215
211
|
return response;
|
|
216
212
|
}
|
|
217
213
|
// IMPORTANT: Clone the response. A response is a stream
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { HttpCookies } from '../webflo-routing/HttpCookies.js';
|
|
2
|
+
import { headers as headersShim } from '../webflo-fetch/index.js';
|
|
2
3
|
|
|
3
4
|
export class WorkerSideCookies extends HttpCookies {
|
|
4
5
|
static create({ request }) {
|
|
5
6
|
return new this({
|
|
6
7
|
request,
|
|
7
|
-
entries: request.headers
|
|
8
|
+
entries: headersShim.get.value.call(request.headers, 'Cookie', true).map((c) => [c.name, c])
|
|
8
9
|
});
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Fs from 'fs';
|
|
2
|
+
import Path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
readLayoutConfig,
|
|
5
|
+
readEnvConfig,
|
|
6
|
+
readClientConfig,
|
|
7
|
+
readWorkerConfig,
|
|
8
|
+
scanRoots,
|
|
9
|
+
scanRouteHandlers,
|
|
10
|
+
} from '../../deployment-pi/util.js';
|
|
11
|
+
|
|
12
|
+
export async function bootstrap(cx, offset = '') {
|
|
13
|
+
const $init = Fs.existsSync('./init.worker.js')
|
|
14
|
+
? Path.resolve('./init.worker.js')
|
|
15
|
+
: null;
|
|
16
|
+
const config = {
|
|
17
|
+
LAYOUT: await readLayoutConfig(cx),
|
|
18
|
+
ENV: await readEnvConfig(cx),
|
|
19
|
+
CLIENT: await readClientConfig(cx),
|
|
20
|
+
WORKER: await readWorkerConfig(cx),
|
|
21
|
+
};
|
|
22
|
+
if (config.CLIENT.copy_public_variables) {
|
|
23
|
+
const publicEnvPattern = /(?:^|_)PUBLIC(?:_|$)/;
|
|
24
|
+
config.ENV.data = config.ENV.data || {};
|
|
25
|
+
for (const key in process.env) {
|
|
26
|
+
if (publicEnvPattern.test(key)) {
|
|
27
|
+
config.ENV.data[key] = process.env[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const routes = {};
|
|
32
|
+
const $roots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'manifest.json') : [];
|
|
33
|
+
const $sparoots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
|
|
34
|
+
scanRouteHandlers(config.LAYOUT, 'worker', (file, route) => {
|
|
35
|
+
routes[route] = file;
|
|
36
|
+
}, offset, $roots);
|
|
37
|
+
const outdir = Path.join(config.LAYOUT.PUBLIC_DIR, offset);
|
|
38
|
+
return { $init, config, routes, $roots, $sparoots, outdir, offset };
|
|
39
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { WebfloWorker } from './WebfloWorker.js';
|
|
2
2
|
|
|
3
|
-
export async function start() {
|
|
4
|
-
const instance = WebfloWorker.create(
|
|
3
|
+
export async function start(bootstrap) {
|
|
4
|
+
const instance = WebfloWorker.create(bootstrap);
|
|
5
5
|
await instance.initialize();
|
|
6
6
|
return instance;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
WebfloWorker
|
|
11
|
-
}
|
|
7
|
+
}
|
package/src/webflo-cli.js
CHANGED
|
@@ -16,12 +16,11 @@ const appMeta = jsonFile.read('./package.json');
|
|
|
16
16
|
/**
|
|
17
17
|
* @cx
|
|
18
18
|
*/
|
|
19
|
-
const cx = WebfloPI.
|
|
19
|
+
const cx = WebfloPI.CLIContext.create({
|
|
20
20
|
meta: { title: webfloMeta.title, version: webfloMeta.version },
|
|
21
21
|
appMeta: { ...appMeta },
|
|
22
22
|
logger: Logger,
|
|
23
23
|
config: WebfloPI.config,
|
|
24
|
-
middlewares: [ WebfloPI.deployment.origins.webhook ],
|
|
25
24
|
});
|
|
26
25
|
|
|
27
26
|
/**
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function renderCookieObjToString(cookieObj) {
|
|
2
|
-
const attrsArr = [`${cookieObj.name}=${/*encodeURIComponent*/(cookieObj.value)}`];
|
|
3
|
-
for (const attrName in cookieObj) {
|
|
4
|
-
if (['name', 'value'].includes(attrName)) continue;
|
|
5
|
-
let _attrName = attrName[0].toUpperCase() + attrName.substring(1);
|
|
6
|
-
if (_attrName === 'MaxAge') { _attrName = 'Max-Age' };
|
|
7
|
-
attrsArr.push(cookieObj[attrName] === true ? _attrName : `${_attrName}=${cookieObj[attrName]}`);
|
|
8
|
-
}
|
|
9
|
-
return attrsArr.join(';');
|
|
10
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { renderHttpMessageInit } from './message.js';
|
|
2
|
-
|
|
3
|
-
const nativeFetch = fetch;
|
|
4
|
-
export async function fetch(url, init = {}) {
|
|
5
|
-
return await nativeFetch(url);
|
|
6
|
-
if (init.body) {
|
|
7
|
-
const { body, headers } = renderHttpMessageInit(init);
|
|
8
|
-
init = { ...init, body, headers, };
|
|
9
|
-
}
|
|
10
|
-
let response = await nativeFetch(url, init), encoding;
|
|
11
|
-
if (init.decompress === false && (encoding = response.headers.get('Content-Encoding'))) {
|
|
12
|
-
const recompressedBody = response.body.pipeThrough(new CompressionStream(encoding));
|
|
13
|
-
response = new Response(recompressedBody, response);
|
|
14
|
-
}
|
|
15
|
-
return response;
|
|
16
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { _isNumeric } from '@webqit/util/js/index.js';
|
|
3
|
-
import { _before } from '@webqit/util/str/index.js';
|
|
4
|
-
import { DeepURLSearchParams } from '../webflo-url/util.js';
|
|
5
|
-
import { dataType } from './util.js';
|
|
6
|
-
|
|
7
|
-
export function createFormDataFromJson(data = {}, jsonfy = true, getIsJsonfiable = false) {
|
|
8
|
-
const formData = new FormData;
|
|
9
|
-
let isJsonfiable = true;
|
|
10
|
-
DeepURLSearchParams.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
|
|
11
|
-
if (suggestedKeys) {
|
|
12
|
-
const isJson = dataType(value) === 'json';
|
|
13
|
-
isJsonfiable = isJsonfiable && isJson;
|
|
14
|
-
return isJson && suggestedKeys;
|
|
15
|
-
}
|
|
16
|
-
if (jsonfy && [true, false, null].includes(value)) {
|
|
17
|
-
value = new Blob([value], { type: 'application/json' });
|
|
18
|
-
}
|
|
19
|
-
formData.append(contextPath, value);
|
|
20
|
-
});
|
|
21
|
-
if (getIsJsonfiable) return [formData, isJsonfiable];
|
|
22
|
-
return formData;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function renderFormDataToJson(formData, jsonfy = true, getIsJsonfiable = false) {
|
|
26
|
-
let isJsonfiable = true;
|
|
27
|
-
let json;
|
|
28
|
-
for (let [name, value] of formData.entries()) {
|
|
29
|
-
if (!json) { json = _isNumeric(_before(name, '[')) ? [] : {}; }
|
|
30
|
-
let type = dataType(value);
|
|
31
|
-
if (jsonfy && ['Blob', 'File'].includes(type) && value.type === 'application/json') {
|
|
32
|
-
let _value = await value.text();
|
|
33
|
-
value = JSON.parse(_value);
|
|
34
|
-
type = 'json';
|
|
35
|
-
}
|
|
36
|
-
isJsonfiable = isJsonfiable && type === 'json';
|
|
37
|
-
DeepURLSearchParams.set(json, name, value);
|
|
38
|
-
}
|
|
39
|
-
if (getIsJsonfiable) return [json, isJsonfiable];
|
|
40
|
-
return json;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
Object.defineProperties(FormData, {
|
|
44
|
-
json: { value: createFormDataFromJson }
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
Object.defineProperties(FormData.prototype, {
|
|
48
|
-
json: {
|
|
49
|
-
value: async function (data = {}) {
|
|
50
|
-
const result = await renderFormDataToJson(this, ...arguments);
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { _after } from '@webqit/util/str/index.js';
|
|
2
|
-
import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
|
|
3
|
-
import { _from as _arrFrom } from '@webqit/util/arr/index.js';
|
|
4
|
-
import { renderCookieObjToString } from './cookies.js';
|
|
5
|
-
|
|
6
|
-
const prototypeOriginals = {
|
|
7
|
-
set: Headers.prototype.set,
|
|
8
|
-
get: Headers.prototype.get,
|
|
9
|
-
append: Headers.prototype.append,
|
|
10
|
-
};
|
|
11
|
-
const prototypeExtensions = {
|
|
12
|
-
set: {
|
|
13
|
-
value: function (name, value) {
|
|
14
|
-
// -------------------------
|
|
15
|
-
// Format "Set-Cookie" response header
|
|
16
|
-
if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
|
|
17
|
-
value = renderCookieObjToString(value);
|
|
18
|
-
}
|
|
19
|
-
// -------------------------
|
|
20
|
-
// Format "Cookie" request header
|
|
21
|
-
if (/Cookie/i.test(name) && _isTypeObject(value)) {
|
|
22
|
-
value = [].concat(value).map(renderCookieObjToString).join(';');
|
|
23
|
-
}
|
|
24
|
-
// -------------------------
|
|
25
|
-
// Format "Content-Range" response header?
|
|
26
|
-
if (/^Content-Range$/i.test(name) && Array.isArray(value)) {
|
|
27
|
-
if (value.length < 2 || !value[0].includes('-')) {
|
|
28
|
-
throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
|
|
29
|
-
}
|
|
30
|
-
value = `bytes ${value.join('/')}`;
|
|
31
|
-
}
|
|
32
|
-
// -------------------------
|
|
33
|
-
// Format "Range" request header?
|
|
34
|
-
if (/^Range$/i.test(name)) {
|
|
35
|
-
let rangeArr = [];
|
|
36
|
-
_arrFrom(value).forEach((range, i) => {
|
|
37
|
-
let rangeStr = Array.isArray(range) ? range.join('-') : range + '';
|
|
38
|
-
if (i === 0 && !rangeStr.includes('bytes=')) {
|
|
39
|
-
rangeStr = `bytes=${rangeStr}`;
|
|
40
|
-
}
|
|
41
|
-
rangeArr.push(rangeStr);
|
|
42
|
-
});
|
|
43
|
-
value = rangeArr.join(', ');
|
|
44
|
-
}
|
|
45
|
-
// -------------------------
|
|
46
|
-
// Format "Accept" request header?
|
|
47
|
-
if (/^Accept$/i.test(name) && Array.isArray(value)) {
|
|
48
|
-
value = value.join(',');
|
|
49
|
-
}
|
|
50
|
-
// -------------------------
|
|
51
|
-
return prototypeOriginals.set.call(this, name, value);
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
append: {
|
|
55
|
-
value: function (name, value) {
|
|
56
|
-
// -------------------------
|
|
57
|
-
// Format "Set-Cookie" response header
|
|
58
|
-
if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
|
|
59
|
-
value = renderCookieObjToString(value);
|
|
60
|
-
}
|
|
61
|
-
// -------------------------
|
|
62
|
-
return prototypeOriginals.append.call(this, name, value);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
get: {
|
|
66
|
-
value: function (name, parsed = false) {
|
|
67
|
-
let value = prototypeOriginals.get.call(this, name);
|
|
68
|
-
// -------------------------
|
|
69
|
-
// Parse "Set-Cookie" response header
|
|
70
|
-
if (/^Set-Cookie$/i.test(name) && parsed) {
|
|
71
|
-
value = this.getSetCookie()/*IMPORTANT*/.map((str) => {
|
|
72
|
-
const [cookieDefinition, attrsStr] = str.split(';');
|
|
73
|
-
const [name, value] = cookieDefinition.split('=').map((s) => s.trim());
|
|
74
|
-
const cookieObj = { name, value: /*decodeURIComponent*/(value), };
|
|
75
|
-
attrsStr && attrsStr.split(/\;/g).map(attrStr => attrStr.trim().split('=')).forEach(attrsArr => {
|
|
76
|
-
cookieObj[attrsArr[0][0].toLowerCase() + attrsArr[0].substring(1).replace('-', '')] = attrsArr.length === 1 ? true : attrsArr[1];
|
|
77
|
-
});
|
|
78
|
-
return cookieObj;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
// -------------------------
|
|
82
|
-
// Parse "Cookie" request header
|
|
83
|
-
if (/^Cookie$/i.test(name) && parsed) {
|
|
84
|
-
value = value?.split(';').map((str) => {
|
|
85
|
-
const [name, value] = str.split('=').map((s) => s.trim());
|
|
86
|
-
return { name, value: /*decodeURIComponent*/(value), };
|
|
87
|
-
}) || [];
|
|
88
|
-
}
|
|
89
|
-
// -------------------------
|
|
90
|
-
// Parse "Content-Range" response header?
|
|
91
|
-
if (/^Content-Range$/i.test(name) && value && parsed) {
|
|
92
|
-
value = _after(value, 'bytes ').split('/');
|
|
93
|
-
}
|
|
94
|
-
// -------------------------
|
|
95
|
-
// Parse "Range" request header?
|
|
96
|
-
if (/^Range$/i.test(name) && parsed) {
|
|
97
|
-
value = !value ? [] : _after(value, 'bytes=').split(',').map((rangeStr) => {
|
|
98
|
-
const range = rangeStr.trim().split('-').map((s) => s ? parseInt(s, 10) : null);
|
|
99
|
-
range.render = (totalLength) => {
|
|
100
|
-
if (range[1] === null) {
|
|
101
|
-
range[1] = totalLength - 1;
|
|
102
|
-
}
|
|
103
|
-
if (range[0] === null) {
|
|
104
|
-
range[0] = range[1] ? totalLength - range[1] - 1 : 0;
|
|
105
|
-
}
|
|
106
|
-
return range
|
|
107
|
-
};
|
|
108
|
-
range.isValid = (currentStart, totalLength) => {
|
|
109
|
-
// Start higher than end or vice versa?
|
|
110
|
-
if (range[0] > range[1] || range[1] < range[0]) return false;
|
|
111
|
-
// Stretching beyond valid start/end?
|
|
112
|
-
if (range[0] < currentStart || range[1] > totalLength) return false;
|
|
113
|
-
return true;
|
|
114
|
-
};
|
|
115
|
-
return range;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
// -------------------------
|
|
119
|
-
// Parse "Accept" request header?
|
|
120
|
-
if (/^Accept$/i.test(name) && value && parsed) {
|
|
121
|
-
const parseSpec = (spec) => {
|
|
122
|
-
const [mime, q] = spec.trim().split(';').map((s) => s.trim());
|
|
123
|
-
return [mime, parseFloat((q || 'q=1').replace('q=', ''))];
|
|
124
|
-
};
|
|
125
|
-
const list = value.split(',')
|
|
126
|
-
.map((spec) => parseSpec(spec))
|
|
127
|
-
.sort((a, b) => a[1] > b[1] ? -1 : 1) || [];
|
|
128
|
-
const $value = value;
|
|
129
|
-
value = {
|
|
130
|
-
match(mime) {
|
|
131
|
-
if (!mime) return 0;
|
|
132
|
-
const splitMime = (mime) => mime.split('/').map((s) => s.trim());
|
|
133
|
-
const $mime = splitMime(mime + '');
|
|
134
|
-
return list.reduce((prev, [entry, q]) => {
|
|
135
|
-
if (prev) return prev;
|
|
136
|
-
const $entry = splitMime(entry);
|
|
137
|
-
return [0, 1].every((i) => (($mime[i] === $entry[i]) || $mime[i] === '*' || $entry[i] === '*')) ? q : 0;
|
|
138
|
-
}, 0);
|
|
139
|
-
},
|
|
140
|
-
toString() {
|
|
141
|
-
return $value;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
// -------------------------
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
Object.defineProperties(Headers.prototype, prototypeExtensions);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { _isTypeObject } from '@webqit/util/js/index.js';
|
|
2
|
-
import { createFormDataFromJson } from './formdata.js';
|
|
3
|
-
import { dataType } from './util.js';
|
|
4
|
-
|
|
5
|
-
export function renderHttpMessageInit(httpMessageInit) {
|
|
6
|
-
// JSONfy headers
|
|
7
|
-
const headers = (httpMessageInit.headers instanceof Headers) ? [...httpMessageInit.headers.entries()].reduce((_headers, [name, value]) => {
|
|
8
|
-
return { ..._headers, [name/* lower-cased */]: _headers[name] ? [].concat(_headers[name], value) : value };
|
|
9
|
-
}, {}) : Object.keys(httpMessageInit.headers || {}).reduce((_headers, name) => {
|
|
10
|
-
return { ..._headers, [name.toLowerCase()]: httpMessageInit.headers[name] };
|
|
11
|
-
}, {});
|
|
12
|
-
// Process body
|
|
13
|
-
let body = httpMessageInit.body, type = dataType(httpMessageInit.body);
|
|
14
|
-
if (['Blob', 'File'].includes(type)) {
|
|
15
|
-
!headers['content-type'] && (headers['content-type'] = body.type);
|
|
16
|
-
!headers['content-length'] && (headers['content-length'] = body.size);
|
|
17
|
-
} else if (['Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer'].includes(type)) {
|
|
18
|
-
!headers['content-length'] && (headers['content-length'] = body.byteLength);
|
|
19
|
-
} else if (type === 'json' && _isTypeObject(body)/*JSON object*/) {
|
|
20
|
-
if (!headers['content-type']) {
|
|
21
|
-
const [_body, isJsonfiable] = createFormDataFromJson(body, true/*jsonfy*/, true/*getIsJsonfiable*/);
|
|
22
|
-
if (isJsonfiable) {
|
|
23
|
-
body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
|
|
24
|
-
headers['content-type'] = 'application/json';
|
|
25
|
-
headers['content-length'] = (new Blob([body])).size;
|
|
26
|
-
} else {
|
|
27
|
-
body = _body;
|
|
28
|
-
type = 'FormData';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
} else if (type === 'json'/*JSON string*/ && !headers['content-length']) {
|
|
32
|
-
(headers['content-length'] = (body + '').length);
|
|
33
|
-
}
|
|
34
|
-
return { body, headers, $type: type };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function parseHttpMessage(httpMessage) {
|
|
38
|
-
let result;
|
|
39
|
-
const contentType = httpMessage.headers.get('Content-Type') || '';
|
|
40
|
-
if (contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/form-data')) {
|
|
41
|
-
const formData = await httpMessage.formData();
|
|
42
|
-
result = await formData?.json();
|
|
43
|
-
} else if (contentType.startsWith('application/json')/*can include charset*/) {
|
|
44
|
-
result = await httpMessage.json();
|
|
45
|
-
} else /*if (contentType === 'text/plain')*/ {
|
|
46
|
-
result = httpMessage.body;
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { _wq } from '../../util.js';
|
|
2
|
-
import { renderHttpMessageInit } from './message.js';
|
|
3
|
-
|
|
4
|
-
const prototypeOriginals = { clone: Request.prototype.clone };
|
|
5
|
-
const prototypeExtensions = {
|
|
6
|
-
carries: { get: function () { return new Set(_wq(this, 'meta').get('carries') || []); } },
|
|
7
|
-
clone: {
|
|
8
|
-
value: function (init = {}) {
|
|
9
|
-
const clone = prototypeOriginals.clone.call(this, init);
|
|
10
|
-
const requestMeta = _wq(this, 'meta');
|
|
11
|
-
_wq(clone).set('meta', requestMeta);
|
|
12
|
-
return clone;
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
parse: { value: function () { return parseHttpMessage(this); } },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const staticExtensions = {
|
|
19
|
-
from: {
|
|
20
|
-
value: function (url, init = {}) {
|
|
21
|
-
if (url instanceof Request) return url;
|
|
22
|
-
let $$type, $$body = init.body;
|
|
23
|
-
if ('body' in init) {
|
|
24
|
-
const { body, headers, $type } = renderHttpMessageInit(init);
|
|
25
|
-
init = { ...init, body, headers };
|
|
26
|
-
$$type = $type;
|
|
27
|
-
}
|
|
28
|
-
const instance = new Request(url, init);
|
|
29
|
-
const responseMeta = _wq(instance, 'meta');
|
|
30
|
-
responseMeta.set('body', $$body);
|
|
31
|
-
responseMeta.set('type', $$type);
|
|
32
|
-
return instance;
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
copy: {
|
|
36
|
-
value: async function (request, init = {}) {
|
|
37
|
-
const requestInit = [
|
|
38
|
-
'method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
|
|
39
|
-
].reduce(($init, prop) => (
|
|
40
|
-
{ ...$init, [prop]: prop in init ? init[prop] : (prop === 'headers' ? new Headers(request[prop]) : request[prop]) }
|
|
41
|
-
), {});
|
|
42
|
-
if (!['GET', 'HEAD'].includes(init.method?.toUpperCase() || request.method)) {
|
|
43
|
-
if ('body' in init) {
|
|
44
|
-
requestInit.body = init.body
|
|
45
|
-
if (!('headers' in init)) {
|
|
46
|
-
requestInit.headers.delete('Content-Type');
|
|
47
|
-
requestInit.headers.delete('Content-Length');
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
requestInit.body = await request.clone().arrayBuffer();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (requestInit.mode === 'navigate') {
|
|
54
|
-
requestInit.mode = 'cors';
|
|
55
|
-
}
|
|
56
|
-
return { url: request.url, ...requestInit };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
Object.defineProperties(Request.prototype, prototypeExtensions);
|
|
62
|
-
Object.defineProperties(Request, staticExtensions);
|