@webqit/webflo 1.0.31 → 1.0.33
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 +2 -1
- package/src/config-pi/deployment/Env.js +6 -19
- package/src/config-pi/runtime/Client.js +1 -13
- package/src/config-pi/runtime/Server.js +1 -32
- package/src/runtime-pi/WebfloStorage.js +5 -2
- package/src/runtime-pi/client/Capabilities.js +10 -8
- package/src/runtime-pi/client/WebfloClient.js +7 -0
- package/src/runtime-pi/client/WebfloRootClient1.js +1 -1
- package/src/runtime-pi/client/generate.js +20 -13
- package/src/runtime-pi/client/worker/WebfloWorker.js +1 -0
- package/src/runtime-pi/server/Router.js +8 -5
- package/src/runtime-pi/server/SessionStorage.js +6 -5
- package/src/runtime-pi/server/WebfloServer.js +44 -31
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"vanila-javascript"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://webqit.io/tooling/webflo",
|
|
15
|
-
"version": "1.0.
|
|
15
|
+
"version": "1.0.33",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@webqit/observer": "^2.0.7",
|
|
42
42
|
"@webqit/oohtml-ssr": "^2.1.1",
|
|
43
43
|
"@webqit/util": "^0.8.11",
|
|
44
|
+
"dotenv": "^16.4.7",
|
|
44
45
|
"esbuild": "^0.14.38",
|
|
45
46
|
"ioredis": "^5.5.0",
|
|
46
47
|
"jsdom": "^21.1.1",
|
|
@@ -13,18 +13,13 @@ export default class Env extends Dotfile {
|
|
|
13
13
|
|
|
14
14
|
// @desc
|
|
15
15
|
static get ['@desc']() {
|
|
16
|
-
return 'Environmental
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// isEnv
|
|
20
|
-
get isEnv() {
|
|
21
|
-
return true;
|
|
16
|
+
return 'Environmental variable mappings.';
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
// Defaults merger
|
|
25
20
|
withDefaults(config) {
|
|
26
21
|
return this.merge({
|
|
27
|
-
|
|
22
|
+
mappings: {}
|
|
28
23
|
}, config, 'patch');
|
|
29
24
|
}
|
|
30
25
|
|
|
@@ -33,13 +28,13 @@ export default class Env extends Dotfile {
|
|
|
33
28
|
// Questions
|
|
34
29
|
return [
|
|
35
30
|
{
|
|
36
|
-
name: '
|
|
31
|
+
name: 'mappings',
|
|
37
32
|
type: 'recursive',
|
|
38
33
|
controls: {
|
|
39
|
-
name: 'variable',
|
|
34
|
+
name: 'variable mappings',
|
|
40
35
|
combomode: true,
|
|
41
36
|
},
|
|
42
|
-
initial: config.
|
|
37
|
+
initial: config.mappings,
|
|
43
38
|
schema: [
|
|
44
39
|
{
|
|
45
40
|
name: 'name',
|
|
@@ -54,15 +49,7 @@ export default class Env extends Dotfile {
|
|
|
54
49
|
validation: ['important'],
|
|
55
50
|
},
|
|
56
51
|
],
|
|
57
|
-
}
|
|
58
|
-
{
|
|
59
|
-
name: 'autoload',
|
|
60
|
-
type: 'toggle',
|
|
61
|
-
message: 'Choose whether to autoload variables into "process.env"',
|
|
62
|
-
active: 'YES',
|
|
63
|
-
inactive: 'NO',
|
|
64
|
-
initial: config.autoload,
|
|
65
|
-
},
|
|
52
|
+
}
|
|
66
53
|
];
|
|
67
54
|
}
|
|
68
55
|
}
|
|
@@ -28,8 +28,6 @@ export default class Client extends Dotfile {
|
|
|
28
28
|
webpush: false,
|
|
29
29
|
custom_install: false,
|
|
30
30
|
exposed: ['display-mode', 'notifications'],
|
|
31
|
-
vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
|
|
32
|
-
generic_public_webhook_url_variable: 'GENERIC_PUBLIC_WEBHOOK_URL',
|
|
33
31
|
},
|
|
34
32
|
}, config, 'patch');
|
|
35
33
|
}
|
|
@@ -102,17 +100,7 @@ export default class Client extends Dotfile {
|
|
|
102
100
|
type: 'list',
|
|
103
101
|
message: 'Specify features exposed on capabilities.exposed',
|
|
104
102
|
initial: (config.exposed || []).join(', '),
|
|
105
|
-
}
|
|
106
|
-
{
|
|
107
|
-
name: 'vapid_public_key_variable',
|
|
108
|
-
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
109
|
-
message: 'Enter the environment variable name for APP_VAPID_PUBLIC_KEY if not as written',
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: 'generic_public_webhook_url_variable',
|
|
113
|
-
type: 'text',
|
|
114
|
-
message: 'Enter the environment variable name for GENERIC_PUBLIC_WEBHOOK_URL if not as written',
|
|
115
|
-
},
|
|
103
|
+
}
|
|
116
104
|
]
|
|
117
105
|
}
|
|
118
106
|
];
|
|
@@ -29,17 +29,12 @@ export default class Server extends Dotfile {
|
|
|
29
29
|
force: false,
|
|
30
30
|
},
|
|
31
31
|
force_www: '',
|
|
32
|
-
session_key_variable: 'SESSION_KEY',
|
|
33
32
|
capabilities: {
|
|
34
33
|
database: false,
|
|
35
34
|
database_dialect: 'postgres',
|
|
36
|
-
database_url_variable: 'DATABASE_URL',
|
|
37
35
|
redis: false,
|
|
38
|
-
redis_url_variable: 'REDIS_URL',
|
|
39
36
|
webpush: false,
|
|
40
37
|
vapid_subject: 'mailto:foo@example.com',
|
|
41
|
-
vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
|
|
42
|
-
vapid_private_key_variable: 'VAPID_PRIVATE_KEY',
|
|
43
38
|
},
|
|
44
39
|
}, config, 'patch');
|
|
45
40
|
}
|
|
@@ -77,12 +72,6 @@ export default class Server extends Dotfile {
|
|
|
77
72
|
choices: CHOICES.force_www,
|
|
78
73
|
initial: this.indexOfInitial(CHOICES.force_www, config.force_www),
|
|
79
74
|
},
|
|
80
|
-
{
|
|
81
|
-
name: 'session_key_variable',
|
|
82
|
-
type: 'text',
|
|
83
|
-
message: 'Enter the environment variable name for SESSION_KEY if not as written',
|
|
84
|
-
initial: config.session_key_variable,
|
|
85
|
-
},
|
|
86
75
|
{
|
|
87
76
|
name: 'https',
|
|
88
77
|
controls: {
|
|
@@ -142,11 +131,6 @@ export default class Server extends Dotfile {
|
|
|
142
131
|
type: (prev, answers) => !answers.database ? null : 'text',
|
|
143
132
|
message: 'Enter the database dialect (postgres for now)',
|
|
144
133
|
},
|
|
145
|
-
{
|
|
146
|
-
name: 'database_url_variable',
|
|
147
|
-
type: (prev, answers) => !answers.database ? null : 'text',
|
|
148
|
-
message: 'Enter the environment variable name for DATABASE_URL if not as written',
|
|
149
|
-
},
|
|
150
134
|
{
|
|
151
135
|
name: 'redis',
|
|
152
136
|
type: 'toggle',
|
|
@@ -154,11 +138,6 @@ export default class Server extends Dotfile {
|
|
|
154
138
|
active: 'YES',
|
|
155
139
|
inactive: 'NO',
|
|
156
140
|
},
|
|
157
|
-
{
|
|
158
|
-
name: 'redis_url_variable',
|
|
159
|
-
type: (prev, answers) => !answers.redis ? null : 'text',
|
|
160
|
-
message: 'Enter the environment variable name for REDIS_URL if not as written',
|
|
161
|
-
},
|
|
162
141
|
{
|
|
163
142
|
name: 'webpush',
|
|
164
143
|
type: 'toggle',
|
|
@@ -170,17 +149,7 @@ export default class Server extends Dotfile {
|
|
|
170
149
|
name: 'vapid_subject',
|
|
171
150
|
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
172
151
|
message: 'Enter the vapid_subject URL',
|
|
173
|
-
}
|
|
174
|
-
{
|
|
175
|
-
name: 'vapid_public_key_variable',
|
|
176
|
-
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
177
|
-
message: 'Enter the environment variable name for VAPID_PUBLIC_KEY if not as written',
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: 'vapid_private_key_variable',
|
|
181
|
-
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
182
|
-
message: 'Enter the environment variable name for VAPID_PRIVATE_KEY if not as written',
|
|
183
|
-
},
|
|
152
|
+
}
|
|
184
153
|
]
|
|
185
154
|
},
|
|
186
155
|
];
|
|
@@ -28,8 +28,10 @@ export class WebfloStorage {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async commit() {
|
|
31
|
-
if (
|
|
32
|
-
|
|
31
|
+
if (this.#store && this.#key && this.#modified) {
|
|
32
|
+
await this.#registry.set(this.#key, this.#store);
|
|
33
|
+
}
|
|
34
|
+
this.#modified = false;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
get size() { return this.store().then((store) => Object.keys(store).length); }
|
|
@@ -78,6 +80,7 @@ export class WebfloStorage {
|
|
|
78
80
|
for (const key of await this.keys()) {
|
|
79
81
|
Reflect.deleteProperty(await this.store(), key);
|
|
80
82
|
}
|
|
83
|
+
this.#modified = true;
|
|
81
84
|
await this.emit();
|
|
82
85
|
return this;
|
|
83
86
|
}
|
|
@@ -2,6 +2,7 @@ const { Observer } = webqit;
|
|
|
2
2
|
|
|
3
3
|
export class Capabilities {
|
|
4
4
|
|
|
5
|
+
#runtime;
|
|
5
6
|
#params;
|
|
6
7
|
|
|
7
8
|
#exposed = {};
|
|
@@ -9,11 +10,10 @@ export class Capabilities {
|
|
|
9
10
|
|
|
10
11
|
#cleanups = [];
|
|
11
12
|
|
|
12
|
-
static async initialize(params) {
|
|
13
|
+
static async initialize(runtime, params) {
|
|
13
14
|
const instance = new this;
|
|
15
|
+
instance.#runtime = runtime;
|
|
14
16
|
instance.#params = params;
|
|
15
|
-
instance.#params.generic_public_webhook_url = instance.#params.generic_public_webhook_url_variable && instance.#params.env[instance.#params.generic_public_webhook_url_variable];
|
|
16
|
-
instance.#params.vapid_public_key = instance.#params.vapid_public_key_variable && instance.#params.env[instance.#params.vapid_public_key_variable];
|
|
17
17
|
// --------
|
|
18
18
|
// Custom install
|
|
19
19
|
const onbeforeinstallprompt = (e) => {
|
|
@@ -26,11 +26,11 @@ export class Capabilities {
|
|
|
26
26
|
instance.#cleanups.push(() => window.removeEventListener('beforeinstallprompt', onbeforeinstallprompt));
|
|
27
27
|
// --------
|
|
28
28
|
// Webhooks
|
|
29
|
-
if (instance.#
|
|
29
|
+
if (instance.#runtime.env('GENERIC_PUBLIC_WEBHOOK_URL')) {
|
|
30
30
|
// --------
|
|
31
31
|
// app.installed
|
|
32
32
|
const onappinstalled = () => {
|
|
33
|
-
fetch(instance.#
|
|
33
|
+
fetch(instance.#runtime.env('GENERIC_PUBLIC_WEBHOOK_URL'), {
|
|
34
34
|
method: 'POST',
|
|
35
35
|
headers: { 'Content-Type': 'application/json' },
|
|
36
36
|
body: JSON.stringify({ type: 'app.installed', data: true })
|
|
@@ -51,7 +51,7 @@ export class Capabilities {
|
|
|
51
51
|
if (eventPayload.type === 'push.subscribe' && !eventPayload.data) {
|
|
52
52
|
return window.queueMicrotask(pushPermissionStatusHandler);
|
|
53
53
|
}
|
|
54
|
-
fetch(instance.#
|
|
54
|
+
fetch(instance.#runtime.env('GENERIC_PUBLIC_WEBHOOK_URL'), {
|
|
55
55
|
method: 'POST',
|
|
56
56
|
headers: { 'Content-Type': 'application/json' },
|
|
57
57
|
body: JSON.stringify(eventPayload)
|
|
@@ -134,6 +134,8 @@ export class Capabilities {
|
|
|
134
134
|
if (this.#exposed.custom_install === 'granted') return;
|
|
135
135
|
if (this.#exposed.custom_install) {
|
|
136
136
|
returnValue = await this.#exposed.custom_install.prompt?.();
|
|
137
|
+
const { outcome } = await this.#exposed.custom_install.userChoice;
|
|
138
|
+
if (outcome === 'dismissed') return;
|
|
137
139
|
}
|
|
138
140
|
Observer.set(this.#exposed, 'custom_install', 'granted');
|
|
139
141
|
return returnValue;
|
|
@@ -184,8 +186,8 @@ export class Capabilities {
|
|
|
184
186
|
if (!params.userVisibleOnly) {
|
|
185
187
|
params = { ...params, userVisibleOnly: true };
|
|
186
188
|
}
|
|
187
|
-
if (!params.applicationServerKey && this.#
|
|
188
|
-
params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#
|
|
189
|
+
if (!params.applicationServerKey && this.#runtime.env('VAPID_PUBLIC_KEY')) {
|
|
190
|
+
params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#runtime.env('VAPID_PUBLIC_KEY')) };
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
return params;
|
|
@@ -48,6 +48,12 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
48
48
|
return document.querySelector('meta[name="webflo-viewtransitions"]')?.value;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
env(key) {
|
|
52
|
+
return key in this.cx.params.mappings
|
|
53
|
+
? this.cx.params.env[this.cx.params.mappings[key]]
|
|
54
|
+
: this.cx.params.env[key];
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
constructor(host) {
|
|
52
58
|
super();
|
|
53
59
|
this.#host = host;
|
|
@@ -317,6 +323,7 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
317
323
|
if (scope.eventLifecyclePromises.dirty && !scope.eventLifecyclePromises.size) {
|
|
318
324
|
throw new Error('Final response already sent');
|
|
319
325
|
}
|
|
326
|
+
await scope.httpEvent.session.commit();
|
|
320
327
|
return await this.execPush(scope.clientMessaging, response);
|
|
321
328
|
},
|
|
322
329
|
};
|
|
@@ -52,7 +52,7 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
52
52
|
this.#workport = await this.constructor.Workport.initialize(null, (this.cx.params.public_base_url || '') + filename, restServiceWorkerParams);
|
|
53
53
|
cleanups.push(() => this.#workport.close());
|
|
54
54
|
}
|
|
55
|
-
this.#capabilities = await this.constructor.Capabilities.initialize(
|
|
55
|
+
this.#capabilities = await this.constructor.Capabilities.initialize(this, this.cx.params.capabilities);
|
|
56
56
|
cleanups.push(() => this.#capabilities.close());
|
|
57
57
|
// --------
|
|
58
58
|
// Bind network status handlers
|
|
@@ -25,14 +25,11 @@ export async function generate() {
|
|
|
25
25
|
if (!cx.config.deployment?.Layout) {
|
|
26
26
|
throw new Error(`The Client configurator "config.deployment.Layout" is required in context.`);
|
|
27
27
|
}
|
|
28
|
-
const env = {};
|
|
29
28
|
const clientConfig = await (new cx.config.runtime.Client(cx)).read();
|
|
30
|
-
clientConfig.env = env;
|
|
31
29
|
if (clientConfig.capabilities?.service_worker && !cx.config.runtime.client?.Worker) {
|
|
32
30
|
throw new Error(`The Service Worker configurator "config.runtime.client.Worker" is required in context.`);
|
|
33
31
|
}
|
|
34
32
|
const workerConfig = await (new cx.config.runtime.client.Worker(cx)).read();
|
|
35
|
-
workerConfig.env = env;
|
|
36
33
|
// -----------
|
|
37
34
|
if (!cx.config.deployment?.Layout) {
|
|
38
35
|
throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
|
|
@@ -43,17 +40,25 @@ export async function generate() {
|
|
|
43
40
|
const dirClient = Path.resolve(cx.CWD || '', layoutConfig.CLIENT_DIR);
|
|
44
41
|
const dirWorker = Path.resolve(cx.CWD || '', layoutConfig.WORKER_DIR);
|
|
45
42
|
const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
|
|
43
|
+
const env = {};
|
|
44
|
+
const envConfig = { mappings: {} };
|
|
45
|
+
// Copy vars
|
|
46
46
|
if (clientConfig.copy_public_variables) {
|
|
47
|
-
|
|
48
|
-
throw new Error(`The Layout configurator "config.deployment.Env" is required in context to bundle public env.`);
|
|
49
|
-
}
|
|
50
|
-
const envConfig = await (new cx.config.deployment.Env(cx)).read();
|
|
51
|
-
const $env = { ...envConfig.entries, ...process.env };
|
|
47
|
+
const $env = { ...process.env };
|
|
52
48
|
for (const key in $env) {
|
|
53
49
|
if (!key.includes('PUBLIC_') && !key.includes('_PUBLIC')) continue;
|
|
54
50
|
env[key] = $env[key];
|
|
55
51
|
}
|
|
52
|
+
if (cx.config.deployment?.Env) {
|
|
53
|
+
const $envConfig = await (new cx.config.deployment.Env(cx)).read();
|
|
54
|
+
envConfig.mappings = $envConfig.mappings;
|
|
55
|
+
}
|
|
56
56
|
}
|
|
57
|
+
// Expose these nodes
|
|
58
|
+
clientConfig.env = env;
|
|
59
|
+
workerConfig.env = env;
|
|
60
|
+
clientConfig.mappings = envConfig.mappings;
|
|
61
|
+
workerConfig.mappings = envConfig.mappings;
|
|
57
62
|
// -----------
|
|
58
63
|
// Scan Subdocuments
|
|
59
64
|
const scanSubroots = (sparoot, rootFileName) => {
|
|
@@ -76,7 +81,7 @@ export async function generate() {
|
|
|
76
81
|
const generateClient = async function(sparoot, spaGraphCallback = null) {
|
|
77
82
|
let [ subsparoots, targets ] = (sparoot && scanSubroots(sparoot, 'index.html')) || [ [], false ];
|
|
78
83
|
if (!sparoot) sparoot = '/';
|
|
79
|
-
let spaRouting = { root: sparoot, subroots: subsparoots, targets };
|
|
84
|
+
let spaRouting = { name: 'client', root: sparoot, subroots: subsparoots, targets };
|
|
80
85
|
let codeSplitting = !!(sparoot !== '/' || subsparoots.length);
|
|
81
86
|
let outfileMain = Path.join(sparoot, clientConfig.bundle_filename),
|
|
82
87
|
outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
|
|
@@ -151,7 +156,7 @@ export async function generate() {
|
|
|
151
156
|
const generateWorker = async function(workerroot, workerGraphCallback = null) {
|
|
152
157
|
let [ subworkerroots, targets ] = workerroot && scanSubroots(workerroot, 'workerroot') || [ [], false ];
|
|
153
158
|
if (!workerroot) workerroot = '/';
|
|
154
|
-
let workerRouting = { root: workerroot, subroots: subworkerroots, targets };
|
|
159
|
+
let workerRouting = { name: 'worker', root: workerroot, subroots: subworkerroots, targets };
|
|
155
160
|
let gen = { imports: {}, code: [], };
|
|
156
161
|
if (cx.logger) {
|
|
157
162
|
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
@@ -273,17 +278,19 @@ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
|
|
|
273
278
|
let _routesDir = Path.join(routesDir, routing.root),
|
|
274
279
|
_targetDir = Path.join(targetDir, routing.root);
|
|
275
280
|
cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
|
|
281
|
+
const routeFileEnding = new RegExp(`(?:\\/(?:${routing.name}|index)\\.js)$`);
|
|
276
282
|
// ----------------
|
|
277
283
|
// Directory walker
|
|
278
284
|
const walk = (dir, callback) => {
|
|
279
285
|
Fs.readdirSync(dir).forEach(f => {
|
|
280
286
|
let resource = Path.join(dir, f);
|
|
281
287
|
let _namespace = '/' + Path.relative(routesDir, resource).replace(/\\/g, '/');
|
|
282
|
-
let namespace = _beforeLast(_namespace, '/index.js') || '/';
|
|
283
288
|
if (Fs.statSync(resource).isDirectory()) {
|
|
284
|
-
if (routing.subroots.includes(
|
|
289
|
+
if (routing.subroots.includes(_namespace)) return;
|
|
285
290
|
walk(resource, callback);
|
|
286
291
|
} else {
|
|
292
|
+
if (f === 'index.js' && Fs.existsSync(Path.join(dir, `${routing.name}.js`))) return;
|
|
293
|
+
let namespace = _namespace.replace(routeFileEnding, '') || '/';
|
|
287
294
|
let relativePath = Path.relative(_targetDir, resource).replace(/\\/g, '/');
|
|
288
295
|
callback(resource, namespace, relativePath);
|
|
289
296
|
}
|
|
@@ -295,7 +302,7 @@ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
|
|
|
295
302
|
let indexCount = 0;
|
|
296
303
|
if (Fs.existsSync(_routesDir)) {
|
|
297
304
|
walk(_routesDir, (file, namespace, relativePath) => {
|
|
298
|
-
if (
|
|
305
|
+
if (routeFileEnding.test(relativePath)) {
|
|
299
306
|
// Import code
|
|
300
307
|
let routeName = 'index' + (++ indexCount);
|
|
301
308
|
// IMPORTANT: we;re taking a step back here so that the parent-child relationship for
|
|
@@ -157,6 +157,7 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
157
157
|
if (scope.eventLifecyclePromises.dirty && !scope.eventLifecyclePromises.size) {
|
|
158
158
|
throw new Error('Final response already sent');
|
|
159
159
|
}
|
|
160
|
+
await scope.httpEvent.session.commit();
|
|
160
161
|
return await this.execPush(scope.clientMessaging, response);
|
|
161
162
|
},
|
|
162
163
|
};
|
|
@@ -13,9 +13,9 @@ export class Router extends WebfloRouter {
|
|
|
13
13
|
if (_segmentOnFile.index) return _segmentOnFile;
|
|
14
14
|
var _currentPath = thisTick.trailOnFile.concat(_seg).join('/'),
|
|
15
15
|
routeHandlerFile;
|
|
16
|
-
return Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath, '
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
return Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath, 'server.js')) || Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath, 'index.js'))
|
|
17
|
+
? { seg: _seg, index: routeHandlerFile }
|
|
18
|
+
: (Fs.existsSync(Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath)) ? { seg: _seg, dirExists: true } : _segmentOnFile);
|
|
19
19
|
}, { seg: null });
|
|
20
20
|
thisTick.trail = thisTick.trail.concat(thisTick.currentSegment);
|
|
21
21
|
thisTick.trailOnFile = thisTick.trailOnFile.concat(thisTick.currentSegmentOnFile.seg);
|
|
@@ -23,8 +23,11 @@ export class Router extends WebfloRouter {
|
|
|
23
23
|
} else {
|
|
24
24
|
thisTick.trail = [];
|
|
25
25
|
thisTick.trailOnFile = [];
|
|
26
|
-
|
|
27
|
-
thisTick.
|
|
26
|
+
let routeHandlerFile;
|
|
27
|
+
thisTick.currentSegmentOnFile = Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, 'server.js')) || Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, 'index.js'))
|
|
28
|
+
? { index: routeHandlerFile }
|
|
29
|
+
: {};
|
|
30
|
+
thisTick.exports = thisTick.currentSegmentOnFile.index
|
|
28
31
|
? await import(Url.pathToFileURL(thisTick.currentSegmentOnFile.index))
|
|
29
32
|
: null;
|
|
30
33
|
}
|
|
@@ -6,7 +6,6 @@ export class SessionStorage extends WebfloStorage {
|
|
|
6
6
|
|
|
7
7
|
static create(request, params = {}) {
|
|
8
8
|
let sessionID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
|
|
9
|
-
console.log({params});
|
|
10
9
|
if (sessionID?.includes('.')) {
|
|
11
10
|
if (params.secret) {
|
|
12
11
|
const [rand, signature] = sessionID.split('.');
|
|
@@ -31,26 +30,28 @@ export class SessionStorage extends WebfloStorage {
|
|
|
31
30
|
sessionID = crypto.randomUUID();
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
return new this(params.registry || inmemSessionRegistry, sessionID, request);
|
|
33
|
+
return new this(params.registry || inmemSessionRegistry, sessionID, request, params.ttl);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
#sessionID;
|
|
38
37
|
get sessionID() { return this.#sessionID; }
|
|
38
|
+
#ttl;
|
|
39
39
|
|
|
40
|
-
constructor(reqistry, sessionID, request) {
|
|
40
|
+
constructor(reqistry, sessionID, request, ttl) {
|
|
41
41
|
super(
|
|
42
42
|
reqistry,
|
|
43
|
-
`session
|
|
43
|
+
`session/${sessionID}`,
|
|
44
44
|
request,
|
|
45
45
|
true
|
|
46
46
|
);
|
|
47
47
|
this.#sessionID = sessionID;
|
|
48
|
+
this.#ttl = ttl;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
async commit(response = null) {
|
|
51
52
|
if (response && !response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
52
53
|
// expires six months
|
|
53
|
-
response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age
|
|
54
|
+
response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=${this.#ttl}`);
|
|
54
55
|
}
|
|
55
56
|
await super.commit();
|
|
56
57
|
}
|
|
@@ -5,6 +5,7 @@ import Http from 'http';
|
|
|
5
5
|
import Https from 'https';
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
7
|
import Mime from 'mime-types';
|
|
8
|
+
import 'dotenv/config';
|
|
8
9
|
import QueryString from 'querystring';
|
|
9
10
|
import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
|
|
10
11
|
import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
|
|
@@ -65,20 +66,25 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
65
66
|
this.#cx = cx;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
env(key) {
|
|
70
|
+
return key in this.#cx.env.mappings
|
|
71
|
+
? process.env[this.#cx.env.mappings[key]]
|
|
72
|
+
: process.env[key];
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
async initialize() {
|
|
76
|
+
process.on('uncaughtException', (err) => {
|
|
77
|
+
console.error('Uncaught- Exception:', err);
|
|
78
|
+
});
|
|
79
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
80
|
+
console.log('Unhandled -Rejection', reason, promise);
|
|
81
|
+
});
|
|
69
82
|
const resolveContextObj = async (cx, force = false) => {
|
|
70
83
|
if (_isEmpty(cx.layout) || force) { cx.layout = await (new cx.config.deployment.Layout(cx)).read(); }
|
|
71
84
|
if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
|
|
72
85
|
if (_isEmpty(cx.env) || force) { cx.env = await (new cx.config.deployment.Env(cx)).read(); }
|
|
73
86
|
};
|
|
74
87
|
await resolveContextObj(this.#cx);
|
|
75
|
-
if (this.#cx.env.autoload !== false) {
|
|
76
|
-
Object.keys(this.#cx.env.entries).forEach(key => {
|
|
77
|
-
if (!(key in process.env)) {
|
|
78
|
-
process.env[key] = this.#cx.env.entries[key];
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
88
|
// ---------------
|
|
83
89
|
if (this.#cx.config.deployment.Proxy) {
|
|
84
90
|
const proxied = await (new this.#cx.config.deployment.Proxy(this.#cx)).read();
|
|
@@ -115,11 +121,13 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
115
121
|
}
|
|
116
122
|
if (this.#proxies.size) {
|
|
117
123
|
this.#cx.logger.info(`> Reverse proxy active.`);
|
|
118
|
-
for (
|
|
124
|
+
for (const [id, def] of this.#proxies) {
|
|
119
125
|
this.#cx.logger.info(`> ${id} >>> ${def.port}`);
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
this.#cx.logger.info(``);
|
|
129
|
+
this.#cx.logger.info(`Capabilities: ${Object.keys(this.#sdk).join(', ')}`);
|
|
130
|
+
this.#cx.logger.info(``);
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -183,43 +191,45 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
183
191
|
if (this.#cx.server.capabilities.database_dialect !== 'postgres') {
|
|
184
192
|
throw new Error(`Only postgres supported for now for database dialect`);
|
|
185
193
|
}
|
|
186
|
-
if (
|
|
187
|
-
console.log('Database capabilities');
|
|
194
|
+
if (this.env('DATABASE_URL')) {
|
|
188
195
|
const { SQLClient } = await import('@linked-db/linked-ql/sql');
|
|
189
196
|
const { default: pg } = await import('pg');
|
|
190
197
|
// Obtain pg client
|
|
191
198
|
const pgClient = new pg.Pool({
|
|
192
|
-
connectionString:
|
|
193
|
-
database: 'postgres',
|
|
194
|
-
idleTimeoutMillis: 30000, // 30 seconds,
|
|
195
|
-
keepAlive: true
|
|
199
|
+
connectionString: this.env('DATABASE_URL')
|
|
196
200
|
});
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
await (async function connect() {
|
|
202
|
+
pgClient.on('error', (e) => {
|
|
203
|
+
console.log('PG Error', e);
|
|
204
|
+
});
|
|
205
|
+
pgClient.on('end', (e) => {
|
|
206
|
+
console.log('PG End', e);
|
|
207
|
+
});
|
|
208
|
+
pgClient.on('notice', (e) => {
|
|
209
|
+
console.log('PG Notice', e);
|
|
210
|
+
});
|
|
211
|
+
await pgClient.connect();
|
|
212
|
+
})();
|
|
199
213
|
this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
|
|
200
214
|
} else {
|
|
201
|
-
console.log('No database capabilities');
|
|
202
215
|
//const { ODBClient } = await import('@linked-db/linked-ql/odb');
|
|
203
216
|
//this.#sdk.db = new ODBClient({ dialect: 'postgres' });
|
|
204
217
|
}
|
|
205
218
|
}
|
|
206
219
|
if (this.#cx.server.capabilities?.redis) {
|
|
207
220
|
const { Redis } = await import('ioredis');
|
|
208
|
-
this.#sdk.redis =
|
|
209
|
-
? new Redis(
|
|
221
|
+
this.#sdk.redis = this.env('REDIS_URL')
|
|
222
|
+
? new Redis(this.env('REDIS_URL'))
|
|
210
223
|
: new Redis;
|
|
211
|
-
console.log('Redis capabilities', process.env[this.#cx.server.capabilities.redis_url_variable] ? 'with url' : '');
|
|
212
224
|
}
|
|
213
225
|
if (this.#cx.server.capabilities?.webpush) {
|
|
214
226
|
const { default: webpush } = await import('web-push');
|
|
215
|
-
console.log('Webpuah capabilities');
|
|
216
227
|
this.#sdk.webpush = webpush;
|
|
217
|
-
if (
|
|
218
|
-
&& process.env[this.#cx.server.capabilities.vapid_private_key_variable]) {
|
|
228
|
+
if (this.env('VAPID_PUBLIC_KEY') && this.env('VAPID_PRIVATE_KEY')) {
|
|
219
229
|
webpush.setVapidDetails(
|
|
220
230
|
this.#cx.server.capabilities.vapid_subject,
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
this.env('VAPID_PUBLIC_KEY'),
|
|
232
|
+
this.env('VAPID_PRIVATE_KEY')
|
|
223
233
|
);
|
|
224
234
|
}
|
|
225
235
|
}
|
|
@@ -262,11 +272,12 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
262
272
|
// and actual processing
|
|
263
273
|
scope.request = this.createRequest(scope.url.href, requestInit);
|
|
264
274
|
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
265
|
-
secret:
|
|
275
|
+
secret: this.env('SESSION_KEY'),
|
|
266
276
|
registry: this.#sdk.redis && {
|
|
267
|
-
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(key) || null) },
|
|
268
|
-
set: async (key, value) => { return await this.#sdk.redis.set(key
|
|
277
|
+
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(`${scope.url.host}/${key}`) || null) },
|
|
278
|
+
set: async (key, value) => { return await this.#sdk.redis.set(`${scope.url.host}/${key}`, JSON.stringify(value), 'EX', this.env('SESSION_TTL') || 2592000/*30days*/) },
|
|
269
279
|
},
|
|
280
|
+
ttl: this.env('SESSION_TTL') || 2592000/*30days*/,
|
|
270
281
|
});
|
|
271
282
|
if (!scope.error) {
|
|
272
283
|
if (!(scope.clientMessagingRegistry = this.#globalMessagingRegistry.get(scope.session.sessionID))) {
|
|
@@ -555,6 +566,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
555
566
|
console.error('Final response already sent');
|
|
556
567
|
return;
|
|
557
568
|
}
|
|
569
|
+
await scope.httpEvent.session.commit();
|
|
558
570
|
return await this.execPush(scope.clientMessaging, response);
|
|
559
571
|
},
|
|
560
572
|
};
|
|
@@ -566,11 +578,12 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
566
578
|
scope.request = this.createRequest(scope.url.href, scope.init, scope.autoHeaders.filter((header) => header.type === 'request'));
|
|
567
579
|
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
568
580
|
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
569
|
-
secret:
|
|
581
|
+
secret: this.env('SESSION_KEY'),
|
|
570
582
|
registry: this.#sdk.redis && {
|
|
571
|
-
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(key) || null) },
|
|
572
|
-
set: async (key, value) => { return await this.#sdk.redis.set(key
|
|
583
|
+
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(`${scope.url.host}/${key}`) || null) },
|
|
584
|
+
set: async (key, value) => { return await this.#sdk.redis.set(`${scope.url.host}/${key}`, JSON.stringify(value), 'EX', this.env('SESSION_TTL') || 2592000/*30days*/); },
|
|
573
585
|
},
|
|
586
|
+
ttl: this.env('SESSION_TTL') || 2592000/*30days*/,
|
|
574
587
|
});
|
|
575
588
|
const sessionID = scope.session.sessionID;
|
|
576
589
|
if (!this.#globalMessagingRegistry.has(sessionID)) {
|