@webqit/webflo 0.9.7 → 0.10.2

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 CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.9.7",
15
+ "version": "0.10.2",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -40,17 +40,21 @@
40
40
  "@webqit/oohtml-ssr": "^1.0.3",
41
41
  "@webqit/util": "^0.8.9",
42
42
  "client-sessions": "^0.8.0",
43
+ "esbuild": "^0.14.38",
43
44
  "form-data-encoder": "^1.6.0",
44
45
  "formdata-node": "^4.3.0",
45
46
  "formidable": "^2.0.0-dev.20200131.2",
46
- "is-glob": "^4.0.1",
47
- "micromatch": "^3.1.10",
48
47
  "mime-types": "^2.1.33",
49
- "minimatch": "^3.0.4",
50
48
  "node-fetch": "^2.6.1",
51
49
  "simple-git": "^2.20.1",
52
50
  "stream-slice": "^0.1.2",
53
- "webpack": "^5.50.0"
51
+ "urlpattern-polyfill": "^4.0.3"
52
+ },
53
+ "devDependencies": {
54
+ "chai": "^4.3.6",
55
+ "coveralls": "^3.1.1",
56
+ "mocha": "^10.0.0",
57
+ "mocha-lcov-reporter": "^1.3.0"
54
58
  },
55
59
  "author": "Oxford Harrison <oxharris.dev@gmail.com>",
56
60
  "maintainers": [
@@ -22,9 +22,11 @@ export default class Client extends Configurator {
22
22
  // Defaults merger
23
23
  withDefaults(config) {
24
24
  return _merge(true, {
25
+ bundle_filename: 'bundle.js',
25
26
  support_oohtml: true,
26
27
  support_service_worker: true,
27
28
  worker_scope: '/',
29
+ worker_filename: 'worker.js',
28
30
  }, config);
29
31
  }
30
32
 
@@ -32,6 +34,12 @@ export default class Client extends Configurator {
32
34
  questions(config, choices = {}) {
33
35
  // Questions
34
36
  return [
37
+ {
38
+ name: 'bundle_filename',
39
+ type: 'text',
40
+ message: 'Specify the bundle filename',
41
+ initial: config.bundle_filename,
42
+ },
35
43
  {
36
44
  name: 'support_oohtml',
37
45
  type: 'toggle',
@@ -54,6 +62,12 @@ export default class Client extends Configurator {
54
62
  message: 'Specify the Service Worker scope',
55
63
  initial: config.worker_scope,
56
64
  },
65
+ {
66
+ name: 'worker_filename',
67
+ type: (prev, answers) => answers.support_service_worker ? 'text' : null,
68
+ message: 'Specify the Service Worker filename',
69
+ initial: config.worker_filename,
70
+ },
57
71
  ];
58
72
  }
59
73
  }
@@ -2,7 +2,6 @@
2
2
  /**
3
3
  * imports
4
4
  */
5
- import Path from 'path';
6
5
  import { _merge } from '@webqit/util/obj/index.js';
7
6
  import Configurator from '../../Configurator.js';
8
7
 
@@ -29,14 +28,6 @@ export default class Server extends Configurator {
29
28
  certdoms: ['*'],
30
29
  force: false,
31
30
  },
32
- process: {
33
- name: Path.basename(process.cwd()),
34
- errfile: '',
35
- outfile: '',
36
- exec_mode: 'fork',
37
- autorestart: true,
38
- merge_logs: false,
39
- },
40
31
  force_www: '',
41
32
  shared: false,
42
33
  }, config);
@@ -46,10 +37,6 @@ export default class Server extends Configurator {
46
37
  questions(config, choices = {}) {
47
38
  // Choices
48
39
  const CHOICES = _merge({
49
- exec_mode: [
50
- {value: 'fork',},
51
- {value: 'cluster',},
52
- ],
53
40
  force_www: [
54
41
  {value: '', title: 'do nothing'},
55
42
  {value: 'add',},
@@ -67,10 +54,10 @@ export default class Server extends Configurator {
67
54
  },
68
55
  {
69
56
  name: 'https',
70
- initial: config.https,
71
57
  controls: {
72
58
  name: 'https',
73
59
  },
60
+ initial: config.https,
74
61
  questions: [
75
62
  {
76
63
  name: 'port',
@@ -105,55 +92,6 @@ export default class Server extends Configurator {
105
92
  },
106
93
  ],
107
94
  },
108
- {
109
- name: 'process',
110
- initial: config.process,
111
- controls: {
112
- name: 'background process',
113
- },
114
- questions: [
115
- {
116
- name: 'name',
117
- type: 'text',
118
- message: 'Enter a name for process',
119
- validation: ['important'],
120
- },
121
- {
122
- name: 'errfile',
123
- type: 'text',
124
- message: 'Enter path to error file',
125
- validation: ['important'],
126
- },
127
- {
128
- name: 'outfile',
129
- type: 'text',
130
- message: 'Enter path to output file',
131
- validation: ['important'],
132
- },
133
- {
134
- name: 'exec_mode',
135
- type: 'select',
136
- message: 'Select exec mode',
137
- choices: CHOICES.exec_mode,
138
- validation: ['important'],
139
- },
140
- {
141
- name: 'autorestart',
142
- type: 'toggle',
143
- message: 'Server autorestart on crash?',
144
- active: 'YES',
145
- inactive: 'NO',
146
- initial: config.autorestart,
147
- },
148
- {
149
- name: 'merge_logs',
150
- type: 'toggle',
151
- message: 'Server merge logs?',
152
- active: 'YES',
153
- inactive: 'NO',
154
- },
155
- ],
156
- },
157
95
  {
158
96
  name: 'force_www',
159
97
  type: 'select',
@@ -3,7 +3,6 @@
3
3
  * imports
4
4
  */
5
5
  import Url from 'url';
6
- import Micromatch from 'micromatch';
7
6
  import { _merge } from '@webqit/util/obj/index.js';
8
7
  import { _isObject } from '@webqit/util/js/index.js';
9
8
  import Configurator from '../../../Configurator.js';
@@ -27,18 +26,6 @@ export default class Headers extends Configurator {
27
26
  }, config);
28
27
  }
29
28
 
30
- // Match
31
- async match(url) {
32
- if (!_isObject(url)) {
33
- url = Url.parse(url);
34
- }
35
- return ((await this.read()).entries || []).filter(header => {
36
- var regex = Micromatch.makeRe(header.url, {dot: true});
37
- var rootMatch = url.pathname.split('/').filter(seg => seg).map(seg => seg.trim()).reduce((str, seg) => str.endsWith(' ') ? str : ((str = str + '/' + seg) && str.match(regex) ? str + ' ' : str), '');
38
- return rootMatch.endsWith(' ');
39
- });
40
- }
41
-
42
29
  // Questions generator
43
30
  questions(config, choices = {}) {
44
31
  const CHOICES = _merge({
@@ -5,8 +5,7 @@
5
5
  import Url from 'url';
6
6
  import { _merge } from '@webqit/util/obj/index.js';
7
7
  import { _after } from '@webqit/util/str/index.js';
8
- import { _isObject } from '@webqit/util/js/index.js';
9
- import Micromatch from 'micromatch';
8
+ import { _isObject, _isNumeric } from '@webqit/util/js/index.js';
10
9
  import Configurator from '../../../Configurator.js';
11
10
 
12
11
  export default class Redirects extends Configurator {
@@ -28,33 +27,6 @@ export default class Redirects extends Configurator {
28
27
  }, config);
29
28
  }
30
29
 
31
- // Match
32
- async match(url) {
33
- if (!_isObject(url)) {
34
- url = Url.parse(url);
35
- }
36
- return ((await this.read()).entries || []).reduce((match, rdr) => {
37
- if (match) {
38
- return match;
39
- }
40
- var regex = Micromatch.makeRe(rdr.from, {dot: true});
41
- var rootMatch = url.pathname.split('/').filter(seg => seg).map(seg => seg.trim()).reduce((str, seg) => str.endsWith(' ') ? str : ((str = str + '/' + seg) && str.match(regex) ? str + ' ' : str), '');
42
- if (rootMatch.endsWith(' ')) {
43
- var leaf = _after(url.pathname, rootMatch.trim());
44
- var [ target, targetQuery ] = rdr.to.split('?');
45
- if (rdr.reuseQuery) {
46
- targetQuery = [(url.search || '').substr(1), targetQuery].filter(str => str).join('&');
47
- }
48
- // ---------------
49
- return {
50
- target: target + leaf + (targetQuery ? (leaf.endsWith('?') || leaf.endsWith('&') ? '' : (leaf.includes('?') ? '&' : '?')) + targetQuery : ''),
51
- query: targetQuery,
52
- code: rdr.code,
53
- };
54
- }
55
- }, null);
56
- }
57
-
58
30
  // Questions generator
59
31
  questions(config, choices = {}) {
60
32
  // Choices
@@ -86,19 +58,12 @@ export default class Redirects extends Configurator {
86
58
  message: 'Enter "to" URL',
87
59
  validation: ['important'],
88
60
  },
89
- {
90
- name: 'reuseQuery',
91
- type: 'toggle',
92
- message: 'Reuse query parameters from matched URL in destination URL?',
93
- active: 'YES',
94
- inactive: 'NO',
95
- },
96
61
  {
97
62
  name: 'code',
98
63
  type: 'select',
99
64
  choices: CHOICES.code,
100
65
  message: 'Enter redirect code',
101
- validation: ['number', 'important'],
66
+ validation: ['number'],
102
67
  },
103
68
  ],
104
69
  },
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import { _merge } from '@webqit/util/obj/index.js';
6
6
  import { _isObject } from '@webqit/util/js/index.js';
7
- import Micromatch from 'micromatch';
8
7
  import Configurator from '../../Configurator.js';
9
8
 
10
9
  export default class Ssg extends Configurator {
@@ -26,26 +25,6 @@ export default class Ssg extends Configurator {
26
25
  }, config);
27
26
  }
28
27
 
29
- // Match
30
- async match(url) {
31
- var pathname = url;
32
- if (_isObject(url)) {
33
- pathname = url.pathname;
34
- }
35
- return ((await this.read()).entries || []).reduce((match, prerend) => {
36
- if (match) {
37
- return match;
38
- }
39
- var regex = Micromatch.makeRe(prerend.page, {dot: true});
40
- var rootMatch = pathname.split('/').filter(seg => seg).map(seg => seg.trim()).reduce((str, seg) => str.endsWith(' ') ? str : ((str = str + '/' + seg) && str.match(regex) ? str + ' ' : str), '');
41
- if (rootMatch.endsWith(' ')) {
42
- return {
43
- url: prerend.page,
44
- };
45
- }
46
- }, null);
47
- }
48
-
49
28
  // Questions generator
50
29
  questions(config, choices = {}) {
51
30
  // Questions
@@ -59,7 +38,7 @@ export default class Ssg extends Configurator {
59
38
  initial: config.entries,
60
39
  questions: [
61
40
  {
62
- name: 'page',
41
+ name: 'url',
63
42
  type: 'text',
64
43
  message: 'Page URL',
65
44
  validation: ['important'],
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ import * as config from './config-pi/index.js';
6
6
  import * as deployment from './deployment-pi/index.js';
7
7
  import * as runtime from './runtime-pi/index.js';
8
8
  import * as services from './services-pi/index.js';
9
+ import Context from './Context.js';
9
10
 
10
11
  /**
11
12
  * @exports
@@ -15,4 +16,5 @@ export {
15
16
  deployment,
16
17
  runtime,
17
18
  services,
19
+ Context,
18
20
  }
@@ -186,19 +186,21 @@ export default class Runtime {
186
186
  * @return Response
187
187
  */
188
188
  async go(url, init = {}, detail = {}) {
189
- if (this._abortController) {
190
- this._abortController.abort();
191
- }
192
- this._abortController = new AbortController();
193
- this._xRedirectCode = 200;
194
- // ------------
195
189
  url = typeof url === 'string' ? new whatwag.URL(url) : url;
196
190
  init = { referrer: this.location.href, ...init };
197
191
  // ------------
192
+ // Put his forward before instantiating a request and aborting previous
193
+ // Same-page hash-links clicks on chrome recurse here from histroy popstate
198
194
  if (detail.srcType !== 'init' && (_before(url.href, '#') === _before(init.referrer, '#') && (init.method || 'GET').toUpperCase() === 'GET')) {
199
195
  return;
200
196
  }
201
197
  // ------------
198
+ if (this._abortController) {
199
+ this._abortController.abort();
200
+ }
201
+ this._abortController = new AbortController();
202
+ this._xRedirectCode = 200;
203
+ // ------------
202
204
  if (['link', 'form'].includes(detail.srcType)) {
203
205
  Observer.set(detail.src, 'active', true);
204
206
  Observer.set(detail.submitter || {}, 'active', true);
@@ -15,10 +15,12 @@ export default class RuntimeClient {
15
15
  */
16
16
  constructor(cx) {
17
17
  this.cx = cx;
18
- const workerComm = new WorkerComm('/worker.js', { startMessages: true });
19
- Observer.observe(workerComm, changes => {
20
- //console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
21
- });
18
+ if (this.cx.support_service_worker) {
19
+ const workerComm = new WorkerComm(this.cx.worker_filename, { scope: this.cx.worker_scope, startMessages: true });
20
+ Observer.observe(workerComm, changes => {
21
+ //console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
22
+ });
23
+ }
22
24
  }
23
25
 
24
26
  /**
@@ -5,10 +5,11 @@
5
5
  import Fs from 'fs';
6
6
  import Url from 'url';
7
7
  import Path from 'path';
8
- import Webpack from 'webpack';
9
8
  import { _beforeLast } from '@webqit/util/str/index.js';
10
9
  import { _isObject, _isArray } from '@webqit/util/js/index.js';
11
10
  import * as DotJs from '@webqit/backpack/src/dotfiles/DotJs.js';
11
+ import { gzipSync, brotliCompressSync } from 'zlib';
12
+ import EsBuild from 'esbuild';
12
13
 
13
14
  /**
14
15
  * @generate
@@ -29,7 +30,7 @@ export async function generate() {
29
30
  throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
30
31
  }
31
32
  const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
32
- const bundleOutput = { path: Path.resolve(cx.CWD || '', layoutConfig.PUBLIC_DIR), };
33
+ const dirPublic = Path.resolve(cx.CWD || '', layoutConfig.PUBLIC_DIR);
33
34
  const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
34
35
  // -----------
35
36
  // Generate client build
@@ -37,13 +38,13 @@ export async function generate() {
37
38
  if (clientConfig.support_oohtml) {
38
39
  genClient.imports = { [`${dirSelf}/generate.oohtml.js`]: null, ...genClient.imports };
39
40
  }
40
- await bundle.call(cx, genClient, { ...bundleOutput, filename: 'bundle.js', }, true/* asModule */);
41
+ await bundle.call(cx, genClient, `${dirPublic}/${clientConfig.bundle_filename}`, true/* asModule */);
41
42
  cx.logger && cx.logger.log('');
42
43
  // -----------
43
44
  // Generate worker build
44
45
  if (clientConfig.support_service_worker) {
45
46
  let genWorker = getGen.call(cx, `${dirSelf}/worker`, layoutConfig.WORKER_DIR, workerConfig, `The Worker Build.`);
46
- await bundle.call(cx, genWorker, { ...bundleOutput, filename: 'worker.js', });
47
+ await bundle.call(cx, genWorker, `${dirPublic}/${clientConfig.worker_filename}`);
47
48
  cx.logger && cx.logger.log('');
48
49
  }
49
50
  }
@@ -181,14 +182,16 @@ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
181
182
  * Bundle generated file
182
183
  *
183
184
  * @param object gen
184
- * @param object output
185
+ * @param String outfile
185
186
  * @param boolean asModule
186
187
  *
187
188
  * @return Promise
188
189
  */
189
- function bundle(gen, output, asModule = false) {
190
+ async function bundle(gen, outfile, asModule = false) {
190
191
  const cx = this || {};
191
- const moduleFile = Path.join(output.path, `${_beforeLast(output.filename, '.')}.esm.js`);
192
+ const compression = cx.flags.compress;
193
+ const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
194
+
192
195
  // ------------------
193
196
  // >> Show waiting...
194
197
  if (cx.logger) {
@@ -200,43 +203,58 @@ function bundle(gen, output, asModule = false) {
200
203
  } else {
201
204
  DotJs.write(gen, moduleFile, 'ES Module file');
202
205
  }
206
+
203
207
  // ----------------
204
208
  // >> Webpack config
205
- const bundlingConfig = { entry: moduleFile, output };
206
- if (asModule) {
207
- bundlingConfig.experiments = { outputModule: true, };
208
- bundlingConfig.output.environment = bundlingConfig.output.environment || {};
209
- if (!('module' in bundlingConfig.output)) {
210
- bundlingConfig.output.module = true;
211
- bundlingConfig.output.environment.module = true;
212
- }
209
+ const bundlingConfig = {
210
+ entryPoints: [ moduleFile ],
211
+ outfile,
212
+ bundle: true,
213
+ minify: true,
214
+ banner: { js: '/** @webqit/webflo */', },
215
+ footer: { js: '', },
216
+ format: 'esm',
217
+ };
218
+ if (!asModule) {
219
+ // Support top-level await
220
+ // See: https://github.com/evanw/esbuild/issues/253#issuecomment-826147115
221
+ bundlingConfig.banner.js += '(async () => {';
222
+ bundlingConfig.footer.js += '})();';
213
223
  }
224
+
214
225
  // ----------------
215
226
  // The bundling process
216
- return new Promise(resolve => {
217
- let waiting;
227
+ let waiting;
228
+ if (cx.logger) {
229
+ waiting = cx.logger.waiting(`Bundling...`);
230
+ cx.logger.log('');
231
+ cx.logger.log('> Bundling...');
232
+ cx.logger.info(cx.logger.f`FROM: ${bundlingConfig.entryPoints[0]}`);
233
+ cx.logger.info(cx.logger.f`TO: ${bundlingConfig.outfile}`);
234
+ waiting.start();
235
+ }
236
+ // Run
237
+ await EsBuild.build(bundlingConfig);
238
+ if (waiting) waiting.stop();
239
+ // Remove moduleFile build
240
+ Fs.unlinkSync(bundlingConfig.entryPoints[0]);
241
+
242
+ // ----------------
243
+ // Compress...
244
+ if (compression) {
218
245
  if (cx.logger) {
219
- waiting = cx.logger.waiting(`Bundling...`);
220
- cx.logger.log('');
221
- cx.logger.log('> Bundling...');
222
- cx.logger.info(cx.logger.f`FROM: ${bundlingConfig.entry}`);
223
- cx.logger.info(cx.logger.f`TO: ${bundlingConfig.output.path + '/' + bundlingConfig.output.filename}`);
224
- cx.logger.log('');
246
+ waiting = cx.logger.waiting(`Compressing...`);
225
247
  waiting.start();
226
248
  }
227
- // Run
228
- let compiler = Webpack(bundlingConfig);
229
- compiler.run((err, stats) => {
249
+ const contents = Fs.readFileSync(bundlingConfig.outfile);
250
+ const gzip = gzipSync(contents, {});
251
+ const brotli = brotliCompressSync(contents, {});
252
+ Fs.writeFileSync(`${bundlingConfig.outfile}.gz`, gzip);
253
+ Fs.writeFileSync(`${bundlingConfig.outfile}.br`, brotli);
254
+ if (waiting) {
230
255
  waiting.stop();
231
- if (err) {
232
- cx.logger.title(`Errors!`);
233
- cx.logger.error(err);
234
- }
235
- let log = stats.toString({ colors: true, });
236
- cx.logger && cx.logger.log(log);
237
- // Remove moduleFile build
238
- Fs.unlinkSync(bundlingConfig.entry);
239
- resolve(log);
240
- });
241
- });
256
+ cx.logger.log('');
257
+ cx.logger.log('> Compression: .gz, .br');
258
+ }
259
+ }
242
260
  }
@@ -2,12 +2,9 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import _isGlobe from 'is-glob';
6
- import Minimatch from 'minimatch';
7
5
  import { _any } from '@webqit/util/arr/index.js';
8
- import { _after, _afterLast } from '@webqit/util/str/index.js';
9
- import { HttpEvent, Request, Response } from '../Runtime.js';
10
- import { Observer } from '../Runtime.js';
6
+ import { HttpEvent, Request, Response, Observer } from '../Runtime.js';
7
+ import { urlPattern } from '../../util.js';
11
8
 
12
9
  /**
13
10
  * ---------------------------
@@ -46,8 +43,8 @@ export default class Worker {
46
43
  // Add files to cache
47
44
  evt.waitUntil( self.caches.open(this.cx.params.cache_name).then(cache => {
48
45
  if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Pre-caching resources.'); }
49
- const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c);
50
- return cache.addAll(cache_only_urls.filter(url => !_isGlobe(url) && !_afterLast(url, '.').includes('/')));
46
+ const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c && !c.endsWith('/') && !urlPattern(c, self.origin).isPattern());
47
+ return cache.addAll(cache_only_urls);
51
48
  }) );
52
49
  }
53
50
  });
@@ -80,8 +77,8 @@ export default class Worker {
80
77
  if (!evt.request.url.startsWith('http')) return;
81
78
  const deriveInit = req => [
82
79
  'method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
83
- ].reduce((init, prop) => ({ [prop]: req[prop], ...init }), {});
84
- const requestInit = deriveInit(evt.request);
80
+ ].reduce((init, prop) => ({ [prop]: prop === 'body' && !req.body ? req : req[prop], ...init }), {});
81
+ const requestInit = deriveInit(evt.request.clone());
85
82
  evt.respondWith(this.go(evt.request.url, requestInit, { event: evt }));
86
83
  });
87
84
 
@@ -110,9 +107,8 @@ export default class Worker {
110
107
  init = { referrer: this.location.href, ...init };
111
108
  // ------------
112
109
  // The request object
113
- let request = this.generateRequest(url.href, init);
114
- if (detail.event instanceof self.Request) {
115
- request = detail.event.request;
110
+ let request = await this.generateRequest(url.href, init);
111
+ if (detail.event) {
116
112
  Object.defineProperty(detail.event, 'request', { value: request });
117
113
  }
118
114
  // The navigation event
@@ -130,14 +126,13 @@ export default class Worker {
130
126
  } else {
131
127
  response = await this.remoteFetch(httpEvent.request);
132
128
  }
133
- return response;
134
129
  let finalResponse = this.handleResponse(httpEvent, response);
135
130
  // Return value
136
131
  return finalResponse;
137
132
  }
138
133
 
139
134
  // Generates request object
140
- generateRequest(href, init) {
135
+ async generateRequest(href, init) {
141
136
  // Now, the following is key:
142
137
  // The browser likes to use "force-cache" for "navigate" requests
143
138
  // when, for example, the back button was used.
@@ -146,7 +141,12 @@ export default class Worker {
146
141
  if (init.mode === 'navigate' && init.cache === 'force-cache') {
147
142
  init = { ...init, cache: 'default' };
148
143
  }
149
- let request = new Request(href, init);
144
+ if (init.method === 'POST' && init.body instanceof self.Request) {
145
+ init = { ...init, body: await init.body.text(), };
146
+ } else if (['GET', 'HEAD'].includes(init.method.toUpperCase()) && init.body) {
147
+ init = { ...init, body: null };
148
+ }
149
+ let request = new Request(href, init);
150
150
  return request;
151
151
  }
152
152
 
@@ -163,18 +163,19 @@ export default class Worker {
163
163
  if (arguments.length > 1) {
164
164
  request = this.generateRequest(request, ...args);
165
165
  }
166
+ const matchUrl = (patterns, url) => _any((patterns || []).map(p => p.trim()).filter(p => p), p => urlPattern(p, self.origin).test(url));
166
167
  const execFetch = () => {
167
- if (_any((this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
168
+ if (matchUrl(this.cx.params.cache_only_urls, request.url)) {
168
169
  Observer.set(this.network, 'strategy', 'cache-only');
169
170
  return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
170
171
  }
171
172
  // network_only_urls
172
- if (_any((this.cx.params.network_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
173
+ if (matchUrl(this.cx.params.network_only_urls, request.url)) {
173
174
  Observer.set(this.network, 'strategy', 'network-only');
174
175
  return this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
175
176
  }
176
177
  // cache_first_urls
177
- if (_any((this.cx.params.cache_first_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
178
+ if (matchUrl(this.cx.params.cache_first_urls, request.url)) {
178
179
  Observer.set(this.network, 'strategy', 'cache-first');
179
180
  return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
180
181
  }
@@ -54,56 +54,55 @@ export default class Router extends _Router {
54
54
  /**
55
55
  * Reads a static file from the public directory.
56
56
  *
57
- * @param ServerNavigationEvent event
57
+ * @param ServerNavigationEvent httpEvent
58
58
  *
59
59
  * @return Promise
60
60
  */
61
- file(event) {
62
- var filename = event.url.pathname;
63
- var _filename = Path.join(this.cx.CWD, this.cx.layout.PUBLIC_DIR, decodeURIComponent(filename));
64
- var autoIndex;
65
- if (Fs.existsSync(_filename)) {
66
- // based on the URL path, extract the file extention. e.g. .js, .doc, ...
67
- var ext = Path.parse(filename).ext;
68
- // read file from file system
69
- return new Promise((resolve, reject) => {
70
- // if is a directory search for index file matching the extention
71
- if (!ext && Fs.lstatSync(_filename).isDirectory()) {
72
- ext = '.html';
73
- _filename += '/index' + ext;
74
- autoIndex = 'index.html';
75
- if (!Fs.existsSync(_filename)) {
76
- resolve();
77
- return;
61
+ file(httpEvent) {
62
+ let filename = Path.join(this.cx.CWD, this.cx.layout.PUBLIC_DIR, decodeURIComponent(httpEvent.url.pathname));
63
+ let index, ext = Path.parse(httpEvent.url.pathname).ext;
64
+ // if is a directory search for index file matching the extention
65
+ if (!ext && Fs.existsSync(filename) && Fs.lstatSync(filename).isDirectory()) {
66
+ ext = '.html';
67
+ index = `index${ext}`;
68
+ filename = Path.join(filename, index);
69
+ }
70
+ let enc, acceptEncs = [], supportedEncs = { gzip: '.gz', br: '.br' };
71
+ // based on the URL path, extract the file extention. e.g. .js, .doc, ...
72
+ // and process encoding
73
+ if ((acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map(e => e.trim())).length
74
+ && (enc = acceptEncs.reduce((prev, _enc) => prev || (Fs.existsSync(filename + supportedEncs[_enc]) && _enc), null))) {
75
+ filename = filename + supportedEncs[enc];
76
+ } else {
77
+ if (!Fs.existsSync(filename)) return;
78
+ if (Object.values(supportedEncs).includes(ext)) {
79
+ enc = Object.keys(supportedEncs).reduce((prev, _enc) => prev || (supportedEncs[_enc] === ext && _enc), null);
80
+ ext = Path.parse(filename.substring(0, filename.length - ext.length)).ext;
81
+ }
82
+ }
83
+ // read file from file system
84
+ return new Promise(resolve => {
85
+ Fs.readFile(filename, function(err, data) {
86
+ let response;
87
+ if (err) {
88
+ response = new httpEvent.Response(null, { status: 500, statusText: `Error reading static file: ${filename}` } );
89
+ } else {
90
+ // if the file is found, set Content-type and send data
91
+ const type = Mime.lookup(ext);
92
+ response = new httpEvent.Response(data, { headers: {
93
+ contentType: type === 'application/javascript' ? 'text/javascript' : type,
94
+ contentLength: Buffer.byteLength(data),
95
+ } });
96
+ if (enc) {
97
+ response.headers.set('Content-Encoding', enc);
78
98
  }
79
99
  }
80
- Fs.readFile(_filename, function(err, data){
81
- if (err) {
82
- // To be thrown by caller
83
- reject({
84
- errorCode: 500,
85
- error: 'Error reading static file: ' + filename + '.',
86
- });
87
- } else {
88
-
89
- // if the file is found, set Content-type and send data
90
- const type = Mime.lookup(ext);
91
- resolve( new event.Response(data, {
92
- headers: {
93
- contentType: type === 'application/javascript' ? 'text/javascript' : type,
94
- contentLength: Buffer.byteLength(data),
95
- },
96
- meta: {
97
- filename: _filename,
98
- static: true,
99
- autoIndex,
100
- }
101
- } ) );
102
-
103
- }
104
- });
100
+ response.attrs.filename = filename;
101
+ response.attrs.static = true;
102
+ response.attrs.index = index;
103
+ resolve(response);
105
104
  });
106
- }
105
+ });
107
106
  }
108
107
 
109
108
  /**
@@ -11,8 +11,9 @@ import Sessions from 'client-sessions';
11
11
  import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
12
  import { _each } from '@webqit/util/obj/index.js';
13
13
  import { _isEmpty } from '@webqit/util/js/index.js';
14
- import { _from as _arrFrom } from '@webqit/util/arr/index.js';
14
+ import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
15
15
  import { slice as _streamSlice } from 'stream-slice';
16
+ import { urlPattern } from '../util.js';
16
17
  import * as whatwag from './whatwag.js';
17
18
  import xURL from '../xURL.js';
18
19
  import xFormData from "../xFormData.js";
@@ -276,14 +277,16 @@ export default class Runtime {
276
277
  rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
277
278
  } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
278
279
  rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
279
- } else if (_context.config.Redirects && (rdr = await (new _context.config.Redirects(_context)).match(url.href))) {
280
- rdr = { status: rdr.code || 301 /* Permanent */, headers: { Location: rdr.target } };
280
+ } else if (_context.config.runtime.server.Redirects) {
281
+ rdr = ((await (new _context.config.runtime.server.Redirects(_context)).read()).entries || []).reduce((_rdr, entry) => {
282
+ return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
283
+ }, null);
281
284
  }
282
285
  if (rdr) {
283
- return Response(null, rdr);
286
+ return new Response(null, rdr);
284
287
  }
285
- const autoHeaders = _context.config.Headers
286
- ? await (new _context.config.Headers(_context)).match(url.href)
288
+ const autoHeaders = _context.config.runtime.server.Headers
289
+ ? ((await (new _context.config.runtime.server.Headers(_context)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
287
290
  : [];
288
291
  // ------------
289
292
 
@@ -377,7 +380,6 @@ export default class Runtime {
377
380
 
378
381
  // ----------------
379
382
  // Mock-Cookies?
380
- // ----------------
381
383
  if (!(e.detail.request && e.detail.response)) {
382
384
  for (let cookieName of Object.getOwnPropertyNames(this.mockSessionStore)) {
383
385
  response.headers.set('Set-Cookie', `${cookieName}=1`); // We just want to know availability... not validity, as this is understood to be for testing purposes only
@@ -386,13 +388,11 @@ export default class Runtime {
386
388
 
387
389
  // ----------------
388
390
  // Auto-Headers
389
- // ----------------
390
391
  response.headers.set('Accept-Ranges', 'bytes');
391
392
  this._autoHeaders(response.headers, autoHeaders);
392
393
 
393
394
  // ----------------
394
395
  // Redirects
395
- // ----------------
396
396
  if (response.headers.redirect) {
397
397
  let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
398
398
  let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
@@ -410,7 +410,6 @@ export default class Runtime {
410
410
 
411
411
  // ----------------
412
412
  // 404
413
- // ----------------
414
413
  if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
415
414
  response.attrs.status = response.status !== 200 ? response.status : 404;
416
415
  response.attrs.statusText = `${e.request.url} not found!`;
@@ -419,7 +418,6 @@ export default class Runtime {
419
418
 
420
419
  // ----------------
421
420
  // Not acceptable
422
- // ----------------
423
421
  if (e.request.headers.get('Accept') && !e.request.headers.accept.match(response.headers.contentType)) {
424
422
  response.attrs.status = 406;
425
423
  return response;
@@ -428,14 +426,12 @@ export default class Runtime {
428
426
  // ----------------
429
427
  // Important no-caching
430
428
  // for non-"get" requests
431
- // ----------------
432
429
  if (e.request.method !== 'GET' && !response.headers.get('Cache-Control')) {
433
430
  response.headers.set('Cache-Control', 'no-store');
434
431
  }
435
432
 
436
433
  // ----------------
437
434
  // Body
438
- // ----------------
439
435
  let rangeRequest, body = response.body;
440
436
  if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
441
437
  && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
@@ -501,6 +497,7 @@ export default class Runtime {
501
497
  log.push(style.url(e.request.url));
502
498
  if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
503
499
  if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
500
+ if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
504
501
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
505
502
  else log.push(style.val(`${statusCode} ${response.statusText}`));
506
503
  if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
@@ -2,14 +2,13 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import _isArray from '@webqit/util/js/isArray.js';
6
- import _isNumeric from '@webqit/util/js/isNumeric.js';
7
- import _isString from '@webqit/util/js/isString.js';
8
- import _isObject from '@webqit/util/js/isObject.js';
9
- import _beforeLast from '@webqit/util/str/beforeLast.js';
10
- import _afterLast from '@webqit/util/str/afterLast.js';
11
- import _arrFrom from '@webqit/util/arr/from.js';
12
-
5
+ import { _isString, _isObject, _isNumeric, _isArray } from '@webqit/util/js/index.js';
6
+ import { _beforeLast, _afterLast } from '@webqit/util/str/index.js';
7
+ import { _from as _arrFrom } from '@webqit/util/arr/index.js';
8
+ if (typeof URLPattern === 'undefined') {
9
+ await import('urlpattern-polyfill');
10
+ }
11
+
13
12
  /**
14
13
  * ---------------
15
14
  * @wwwFormPathUnserializeCallback
@@ -132,3 +131,32 @@ export const path = {
132
131
  return this.join(path, "..");
133
132
  }
134
133
  };
134
+
135
+ export const urlPattern = (pattern, baseUrl = null) => ({
136
+ pattern: new URLPattern(pattern, baseUrl),
137
+ isPattern() {
138
+ return Object.keys(this.pattern.keys).some(compName => this.pattern.keys[compName].length);
139
+ },
140
+ test(...args) { this.pattern.test(...args) },
141
+ exec(...args) {
142
+ let components = this.pattern.exec(...args);
143
+ if (!components) return;
144
+ components.vars = Object.keys(this.pattern.keys).reduce(({ named, unnamed }, compName) => {
145
+ this.pattern.keys[compName].forEach(key => {
146
+ let value = components[compName].groups[key.name];
147
+ if (typeof key.name === 'number') {
148
+ unnamed.push(value);
149
+ } else {
150
+ named[key.name] = value;
151
+ }
152
+ });
153
+ return { named, unnamed };
154
+ }, { named: {}, unnamed: [] });
155
+ components.render = str => {
156
+ return str.replace(/\$(\$|[0-9A-Z]+)/gi, (a, b) => {
157
+ return b === '$' ? '$' : (_isNumeric(b) ? components.vars.unnamed[b - 1] : components.vars.named[b]) || '';
158
+ });
159
+ }
160
+ return components;
161
+ }
162
+ });
@@ -89,30 +89,30 @@ const xHttpMessage = (whatwagHttpMessage, Headers, FormData) => {
89
89
  }
90
90
 
91
91
  // Resolve
92
- resolve(force = false) {
93
- if (!this.bodyAttrs.resolved || force) {
94
- this.bodyAttrs.resolved = new Promise(async (resolve, reject) => {
95
- var messageInstance = this, resolved, contentType = messageInstance.headers.get('content-type') || '';
92
+ jsonfy(force = false) {
93
+ if (!this.bodyAttrs.jsonfied || force) {
94
+ this.bodyAttrs.jsonfied = new Promise(async (resolve, reject) => {
95
+ var messageInstance = this, jsonfied, contentType = messageInstance.headers.get('content-type') || '';
96
96
  var type = contentType === 'application/json' || this.bodyAttrs.json ? 'json' : (
97
- contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this.bodyAttrs.formData ? 'formData' : (
97
+ contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/form-data') || this.bodyAttrs.formData ? 'formData' : (
98
98
  contentType === 'text/plain' ? 'plain' : 'other'
99
99
  )
100
100
  );
101
101
  try {
102
102
  if (type === 'formData') {
103
- resolved = (await messageInstance.formData()).json();
103
+ jsonfied = (await messageInstance.formData()).json();
104
104
  } else {
105
- resolved = type === 'json' ? await messageInstance.json() : (
105
+ jsonfied = type === 'json' ? await messageInstance.json() : (
106
106
  type === 'plain' ? await messageInstance.text() : messageInstance.body
107
107
  );
108
108
  }
109
- resolve(resolved);
109
+ resolve(jsonfied);
110
110
  } catch(e) {
111
111
  reject(e);
112
112
  }
113
113
  });
114
114
  }
115
- return this.bodyAttrs.resolved;
115
+ return this.bodyAttrs.jsonfied;
116
116
  }
117
117
 
118
118
  };
@@ -173,7 +173,7 @@ export function encodeBody(body, FormData, Blob) {
173
173
  contentLength: (new Blob([ detailsObj.body ])).size, // Buffer.byteLength(detailsObj.body, 'utf8') isn't cross-environment
174
174
  };
175
175
  }
176
- detailsObj.resolved = body;
176
+ detailsObj.jsonfied = body;
177
177
  }
178
178
  return detailsObj;
179
179
  }
@@ -62,6 +62,13 @@ const xRequest = (whatwagRequest, Headers, FormData, Blob) => class extends xHtt
62
62
  return 'referrer' in this.attrs ? this.attrs.referrer : super.referrer;
63
63
  }
64
64
 
65
+ static compat(request, url = null) {
66
+ if (request instanceof whatwagRequest) {
67
+ return Object.setPrototypeOf(request, new this);
68
+ }
69
+ return new this(url, request);
70
+ }
71
+
65
72
  };
66
73
 
67
74
  export default xRequest;
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * @imports
3
3
  */
4
- import Context from "../src/Context.js";
5
- import * as WebfloPI from '../src/index.js';
4
+ import { config, runtime, Context } from '../src/index.js';
6
5
 
7
6
  let client = {
8
7
  handle: function(httpEvent) {
@@ -19,8 +18,8 @@ let client = {
19
18
  },
20
19
  };
21
20
 
22
- const cx = Context.create({ config: WebfloPI.config, });
21
+ const cx = Context.create({ config: config, });
23
22
  const clientCallback = (_cx, hostName, defaultClientCallback) => client;
24
- const app = await WebfloPI.runtime.server.start.call(cx, clientCallback);
23
+ const app = await runtime.server.start.call(cx, clientCallback);
25
24
 
26
25
  const response = await app.go('http://localhost/', { headers: { range: 'bytes=0-5, 6' } } );