@webqit/webflo 0.9.7-0 → 0.10.1

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-0",
15
+ "version": "0.10.1",
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
  });
@@ -76,12 +73,12 @@ export default class Worker {
76
73
  // -------------
77
74
  // ONFETCH
78
75
  self.addEventListener('fetch', async evt => {
79
- return;
80
76
  // URL schemes that might arrive here but not supported; e.g.: chrome-extension://
81
- if (!evt.request.url.startsWith('http') || evt.request.mode === 'navigate') return;
82
- const requestInit = [
77
+ if (!evt.request.url.startsWith('http')) return;
78
+ const deriveInit = req => [
83
79
  'method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
84
- ].reduce((init, prop) => ({ [prop]: evt.request[prop], ...init }), {});
80
+ ].reduce((init, prop) => ({ [prop]: req[prop], ...init }), {});
81
+ const requestInit = deriveInit(evt.request);
85
82
  evt.respondWith(this.go(evt.request.url, requestInit, { event: evt }));
86
83
  });
87
84
 
@@ -92,6 +89,7 @@ export default class Worker {
92
89
  Observer.observe(this.network, es => {
93
90
  //console.log('//////////', ...es.map(e => `${e.name}: ${e.value}`))
94
91
  });
92
+
95
93
  }
96
94
 
97
95
  /**
@@ -112,7 +110,7 @@ export default class Worker {
112
110
  let request = this.generateRequest(url.href, init);
113
111
  if (detail.event instanceof self.Request) {
114
112
  request = detail.event.request;
115
- //Object.defineProperty(detail.event, 'request', { value: request });
113
+ Object.defineProperty(detail.event, 'request', { value: request });
116
114
  }
117
115
  // The navigation event
118
116
  let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
@@ -161,18 +159,19 @@ export default class Worker {
161
159
  if (arguments.length > 1) {
162
160
  request = this.generateRequest(request, ...args);
163
161
  }
162
+ const matchUrl = (patterns, url) => _any((patterns || []).map(p => p.trim()).filter(p => p), p => urlPattern(p, self.origin).test(url));
164
163
  const execFetch = () => {
165
- if (_any((this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
164
+ if (matchUrl(this.cx.params.cache_only_urls, request.url)) {
166
165
  Observer.set(this.network, 'strategy', 'cache-only');
167
166
  return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
168
167
  }
169
168
  // network_only_urls
170
- if (_any((this.cx.params.network_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
169
+ if (matchUrl(this.cx.params.network_only_urls, request.url)) {
171
170
  Observer.set(this.network, 'strategy', 'network-only');
172
171
  return this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
173
172
  }
174
173
  // cache_first_urls
175
- if (_any((this.cx.params.cache_first_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
174
+ if (matchUrl(this.cx.params.cache_first_urls, request.url)) {
176
175
  Observer.set(this.network, 'strategy', 'cache-first');
177
176
  return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
178
177
  }
@@ -183,7 +182,7 @@ export default class Worker {
183
182
  // This catch() is NOT intended to handle failure of the fetch
184
183
  response.catch(e => Observer.set(this.network, 'error', e.message));
185
184
  // Return xResponse
186
- return response.then(_response => new Response(_response));
185
+ return response.then(_response => Response.compat(_response));
187
186
  }
188
187
 
189
188
  // Caching strategy: cache_first
@@ -200,11 +199,6 @@ export default class Worker {
200
199
 
201
200
  // Caching strategy: network_first
202
201
  networkFetch(request, params = {}) {
203
- if (params.forceNetwork) {
204
- let url = new URL(request.url);
205
- url.searchParams.set('$force-cache', '1');
206
- request.attr.url = url.toString();
207
- }
208
202
  if (!params.cacheFallback) {
209
203
  Observer.set(this.network, 'remote', true);
210
204
  return self.fetch(request);
@@ -247,7 +241,7 @@ export default class Worker {
247
241
 
248
242
  // Handles response object
249
243
  handleResponse(e, response) {
250
- if (!(response instanceof Response)) { response = new Response(response); }
244
+ if (!(response instanceof Response)) { response = Response.compat(response); }
251
245
  return response;
252
246
  }
253
247
 
@@ -35,7 +35,7 @@ export default class WorkerClient {
35
35
  return remoteFetch(event.request);
36
36
  }, remoteFetch);
37
37
  if (!(response instanceof httpEvent.Response)) {
38
- response = new httpEvent.Response(response);
38
+ response = httpEvent.Response.compat(response);
39
39
  }
40
40
  return response;
41
41
  };
@@ -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,10 +89,10 @@ 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
97
  contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this.bodyAttrs.formData ? 'formData' : (
98
98
  contentType === 'text/plain' ? 'plain' : 'other'
@@ -100,19 +100,19 @@ const xHttpMessage = (whatwagHttpMessage, Headers, FormData) => {
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
  }
@@ -57,6 +57,13 @@ const xResponse = (whatwagResponse, Headers, FormData, Blob) => class extends xH
57
57
  return 'redirected' in this.attrs ? this.attrs.redirected : super.redirected;
58
58
  }
59
59
 
60
+ static compat(response) {
61
+ if (response instanceof whatwagResponse) {
62
+ return Object.setPrototypeOf(response, new this);
63
+ }
64
+ return new this(response);
65
+ }
66
+
60
67
  };
61
68
 
62
69
  export default xResponse;
@@ -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' } } );
package/vm DELETED
@@ -1,58 +0,0 @@
1
- -------------------
2
- sudo app-get install git
3
- sudo app-get install node
4
- -------------------
5
- git clone ...
6
- cd
7
- node i
8
- sudo npm i -g @webqit/webflo
9
- sudo npm i -g @webqit/oohtml-cli
10
- webflo config origins
11
- webflo config server
12
- sudo webflo start
13
- -------------------
14
- sudo iptables -I INPUT -p tcp -m tcp --dport 80 -j ACCEPT
15
- sudo iptables -I INPUT -p tcp -m tcp --dport 443 -j ACCEPT
16
- List port-forwarding
17
- sudo iptables -t nat -L
18
- Add port-forwarding
19
- sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
20
- sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 4200
21
- Delete port-forwarding
22
- sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
23
- sudo sh -c "iptables-save > /etc/iptables.rules"
24
- sudo apt-get install iptables-persistent
25
- -------------------
26
- sudo apt-get update &&
27
- sudo apt-get install software-properties-common &&
28
- sudo add-apt-repository universe &&
29
- sudo add-apt-repository ppa:certbot/certbot &&
30
- sudo apt-get update
31
-
32
- sudo apt-get install certbot
33
- https://dev.to/omergulen/step-by-step-node-express-ssl-certificate-run-https-server-from-scratch-in-5-steps-5b87
34
- -------------------
35
- sudo certbot certonly --standalone
36
- sudo certbot certonly --webroot
37
- /etc/letsencrypt/live/webqit.io/fullchain.pem
38
- /etc/letsencrypt/live/webqit.io/privkey.pem
39
-
40
-
41
-
42
- sudo certbot certonly --webroot -w /home/webqit_io/www/neatly.fit/public -d neatly.fit
43
- sudo certbot certonly --webroot -w /home/webqit_io/www/neatly.fit/public -d neatly.fit --debug-challenges
44
- sudo certbot-auto certonly --manual --preferred-challenges dns -d neatly.fit --debug-challenges
45
-
46
- sudo certbot run -a webroot -i apache -w /home/webqit_io/www/neatly.fit/public -d neatly.fit --debug-challenges
47
-
48
-
49
-
50
-
51
- /.well-known/acme-challenge
52
- /www/neatly.fit/public/.well-known/acme-challenge
53
- certbot certonly --manual --preferred-challenges http -d neatly.fit --manual-auth-hook certbot-http-auth-hook --manual-cleanup-hook certbot-http-cleanup-hook --debug-challenges
54
-
55
- An unexpected error occurred:
56
- There were too many requests of a given type :: Error creating new order :: too many failed authorizations recently: see https://letsencrypt.org/docs/rate-limits/
57
-
58
- sudo launchctl list | grep postgres