@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.
Files changed (56) hide show
  1. package/package.json +13 -34
  2. package/site/docs/concepts/realtime.md +45 -44
  3. package/site/docs/getting-started.md +40 -40
  4. package/src/{Context.js → CLIContext.js} +9 -8
  5. package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
  6. package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
  7. package/src/index.js +3 -1
  8. package/src/init-pi/index.js +7 -4
  9. package/src/init-pi/templates/pwa/.gitignore +6 -0
  10. package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
  11. package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
  12. package/src/init-pi/templates/pwa/package.json +2 -2
  13. package/src/init-pi/templates/pwa/public/manifest.json +2 -2
  14. package/src/init-pi/templates/web/.gitignore +6 -0
  15. package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
  16. package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
  17. package/src/init-pi/templates/web/package.json +2 -2
  18. package/src/runtime-pi/AppBootstrap.js +38 -0
  19. package/src/runtime-pi/WebfloRuntime.js +68 -56
  20. package/src/runtime-pi/apis.js +9 -0
  21. package/src/runtime-pi/index.js +2 -4
  22. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
  23. package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
  24. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
  25. package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
  26. package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
  27. package/src/runtime-pi/webflo-client/bootstrap.js +38 -0
  28. package/src/runtime-pi/webflo-client/index.js +2 -8
  29. package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
  30. package/src/runtime-pi/webflo-fetch/LiveResponse.js +154 -116
  31. package/src/runtime-pi/webflo-fetch/index.js +436 -5
  32. package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
  33. package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
  34. package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
  35. package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
  36. package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
  37. package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
  38. package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
  39. package/src/runtime-pi/webflo-server/WebfloServer.js +138 -200
  40. package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
  41. package/src/runtime-pi/webflo-server/index.js +2 -6
  42. package/src/runtime-pi/webflo-server/webflo-devmode.js +24 -31
  43. package/src/runtime-pi/webflo-url/Url.js +1 -1
  44. package/src/runtime-pi/webflo-url/xURL.js +1 -1
  45. package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
  46. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
  47. package/src/runtime-pi/webflo-worker/bootstrap.js +39 -0
  48. package/src/runtime-pi/webflo-worker/index.js +3 -7
  49. package/src/webflo-cli.js +1 -2
  50. package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
  51. package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
  52. package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
  53. package/src/runtime-pi/webflo-fetch/headers.js +0 -151
  54. package/src/runtime-pi/webflo-fetch/message.js +0 -49
  55. package/src/runtime-pi/webflo-fetch/request.js +0 -62
  56. 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.buildJS(this.#jsMeta.mustRevalidate/*fullBuild*/);
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.DEV_DIR, event.affectedHandler)}?_webflohmrhash=${Date.now()}`;
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 === 1) {
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 buildJS(fullBuild = false) {
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
- entryPoints,
190
- outdir: this.#app.config.DEV_DIR,
191
- entryNames,
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 EsBuild.build(bundlingConfig);
190
+ buildResult = await this.#app.buildRoutes(bundlingConfig);
203
191
  }
204
- } catch (e) { return false; }
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 buildScript,
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/quantum-js';
4
+ import { Observer } from '@webqit/use-live';
5
5
 
6
6
  export class Url {
7
7
 
@@ -1,5 +1,5 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
- import { Observer } from '@webqit/quantum-js';
2
+ import { Observer } from '@webqit/use-live';
3
3
  import { DeepURLSearchParams } from './util.js';
4
4
 
5
5
  export class xURL extends URL {
@@ -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
- self.addEventListener('push', webpushHandler, { signal: instanceController.signal });
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.#sdk.storage?.('session'),
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.#sdk.storage?.('user'),
119
+ store: this.createStorage('user'),
124
120
  request: scopeObj.request,
125
- realtime: scopeObj.clientRequestRealtime,
121
+ client: scopeObj.clientRequestRealtime,
126
122
  session: scopeObj.session,
127
123
  });
128
124
  scopeObj.httpEvent = this.createHttpEvent({
129
125
  request: scopeObj.request,
130
- realtime: scopeObj.clientRequestRealtime,
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
- responseRealtime: `br:${scopeObj.httpEvent.realtime.name}`
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
- if (request.method !== 'GET' || !response || response.status !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
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.get('Cookie', true).map((c) => [c.name, c])
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(this || {});
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.Context.create({
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);