@webqit/webflo 0.20.4-next.2 → 0.20.4-next.3

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 (43) hide show
  1. package/package.json +5 -20
  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-livejs-transform.js +35 -0
  6. package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +145 -141
  7. package/src/index.js +3 -1
  8. package/src/init-pi/index.js +6 -3
  9. package/src/init-pi/templates/pwa/package.json +2 -2
  10. package/src/init-pi/templates/web/package.json +2 -2
  11. package/src/runtime-pi/AppBootstrap.js +38 -0
  12. package/src/runtime-pi/WebfloRuntime.js +50 -47
  13. package/src/runtime-pi/apis.js +9 -0
  14. package/src/runtime-pi/index.js +2 -4
  15. package/src/runtime-pi/webflo-client/WebfloClient.js +31 -35
  16. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +16 -14
  17. package/src/runtime-pi/webflo-client/WebfloSubClient.js +13 -13
  18. package/src/runtime-pi/webflo-client/bootstrap.js +37 -0
  19. package/src/runtime-pi/webflo-client/index.js +2 -8
  20. package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
  21. package/src/runtime-pi/webflo-fetch/LiveResponse.js +127 -96
  22. package/src/runtime-pi/webflo-fetch/index.js +435 -5
  23. package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
  24. package/src/runtime-pi/webflo-routing/HttpEvent.js +5 -6
  25. package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
  26. package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
  27. package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
  28. package/src/runtime-pi/webflo-server/WebfloServer.js +98 -195
  29. package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
  30. package/src/runtime-pi/webflo-server/index.js +2 -6
  31. package/src/runtime-pi/webflo-server/webflo-devmode.js +13 -24
  32. package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
  33. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
  34. package/src/runtime-pi/webflo-worker/bootstrap.js +38 -0
  35. package/src/runtime-pi/webflo-worker/index.js +3 -7
  36. package/src/webflo-cli.js +1 -2
  37. package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
  38. package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
  39. package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
  40. package/src/runtime-pi/webflo-fetch/headers.js +0 -151
  41. package/src/runtime-pi/webflo-fetch/message.js +0 -49
  42. package/src/runtime-pi/webflo-fetch/request.js +0 -62
  43. 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,34 +170,23 @@ 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,
195
185
  metafile: true, // This is key
196
- treeShaking: true, // Optional optimization
197
186
  logLevel: 'silent', // Suppress output
198
- minify: false,
199
- sourcemap: false,
200
187
  incremental: true,
201
188
  };
202
- buildResult = await EsBuild.build(bundlingConfig);
189
+ buildResult = await this.#app.buildRoutes(bundlingConfig);
203
190
  }
204
191
  } catch (e) { return false; }
205
192
 
@@ -248,6 +235,7 @@ export class WebfloHMR {
248
235
  entries.js = {};
249
236
  entries.js.client = !!this.#dirtiness.clientRoutesAffected.size;
250
237
  entries.js.worker = this.#dirtiness.serviceWorkerAffected;
238
+ entries.js.server = false;
251
239
  // Clear state
252
240
  this.#dirtiness.clientRoutesAffected.clear();
253
241
  this.#dirtiness.serviceWorkerAffected = false;
@@ -265,11 +253,11 @@ export class WebfloHMR {
265
253
 
266
254
  for (const e in entries) {
267
255
  const buildKey = `build:${e}`;
268
- let buildScript,
269
- buildScriptName = this.#options.buildScripts?.[buildKey];
256
+ let buildScriptName = this.#options.buildScripts?.[buildKey];
270
257
  if (buildScriptName === true) {
271
258
  buildScriptName = buildKey;
272
259
  }
260
+ let buildScript;
273
261
  if (buildScriptName
274
262
  && (buildScript = this.#options.appMeta.scripts?.[buildScriptName])) {
275
263
  await this.#spawnProcess(buildScript, entries[e]);
@@ -278,8 +266,9 @@ export class WebfloHMR {
278
266
  }
279
267
 
280
268
  async #spawnProcess(command, options = {}) {
269
+ const $options = Object.fromEntries(Object.entries(options).map(([k, v]) => [`--${k}`, v]));
281
270
  const commandArr = [...new Set(
282
- command.split(/\s+?/).concat(Object.keys(options)).filter((s) => !(s in options) || options[s])
271
+ command.split(/\s+?/).concat(Object.keys($options)).filter((s) => !(s in $options) || $options[s])
283
272
  )];
284
273
  return await new Promise((resolve, reject) => {
285
274
  const child = spawn(commandArr.shift(), commandArr, {
@@ -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,38 @@
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
+ for (const key in process.env) {
25
+ if (publicEnvPattern.test(key)) {
26
+ config.ENV.data[key] = process.env[key];
27
+ }
28
+ }
29
+ }
30
+ const routes = {};
31
+ const $roots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'manifest.json') : [];
32
+ const $sparoots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
33
+ scanRouteHandlers(config.LAYOUT, 'worker', (file, route) => {
34
+ routes[route] = file;
35
+ }, offset, $roots);
36
+ const outdir = Path.join(config.LAYOUT.PUBLIC_DIR, offset);
37
+ return { $init, config, routes, $roots, $sparoots, outdir, offset };
38
+ }
@@ -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);