@webqit/webflo 1.0.32 → 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 +8 -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 -4
- package/src/runtime-pi/server/WebfloServer.js +35 -40
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)
|
|
@@ -186,8 +186,8 @@ export class Capabilities {
|
|
|
186
186
|
if (!params.userVisibleOnly) {
|
|
187
187
|
params = { ...params, userVisibleOnly: true };
|
|
188
188
|
}
|
|
189
|
-
if (!params.applicationServerKey && this.#
|
|
190
|
-
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')) };
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
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
|
}
|
|
@@ -30,26 +30,28 @@ export class SessionStorage extends WebfloStorage {
|
|
|
30
30
|
sessionID = crypto.randomUUID();
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
return new this(params.registry || inmemSessionRegistry, sessionID, request);
|
|
33
|
+
return new this(params.registry || inmemSessionRegistry, sessionID, request, params.ttl);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
#sessionID;
|
|
37
37
|
get sessionID() { return this.#sessionID; }
|
|
38
|
+
#ttl;
|
|
38
39
|
|
|
39
|
-
constructor(reqistry, sessionID, request) {
|
|
40
|
+
constructor(reqistry, sessionID, request, ttl) {
|
|
40
41
|
super(
|
|
41
42
|
reqistry,
|
|
42
|
-
`session
|
|
43
|
+
`session/${sessionID}`,
|
|
43
44
|
request,
|
|
44
45
|
true
|
|
45
46
|
);
|
|
46
47
|
this.#sessionID = sessionID;
|
|
48
|
+
this.#ttl = ttl;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
async commit(response = null) {
|
|
50
52
|
if (response && !response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
51
53
|
// expires six months
|
|
52
|
-
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}`);
|
|
53
55
|
}
|
|
54
56
|
await super.commit();
|
|
55
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,18 +121,14 @@ 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
|
-
process.on('uncaughtException', (err) => {
|
|
125
|
-
console.error('Uncaught Exception:', err);
|
|
126
|
-
});
|
|
127
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
128
|
-
console.log('Unhandled Rejection', reason, promise);
|
|
129
|
-
});
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
control() {
|
|
@@ -189,55 +191,45 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
189
191
|
if (this.#cx.server.capabilities.database_dialect !== 'postgres') {
|
|
190
192
|
throw new Error(`Only postgres supported for now for database dialect`);
|
|
191
193
|
}
|
|
192
|
-
if (
|
|
193
|
-
console.log('Database capabilities');
|
|
194
|
+
if (this.env('DATABASE_URL')) {
|
|
194
195
|
const { SQLClient } = await import('@linked-db/linked-ql/sql');
|
|
195
196
|
const { default: pg } = await import('pg');
|
|
196
197
|
// Obtain pg client
|
|
197
198
|
const pgClient = new pg.Pool({
|
|
198
|
-
connectionString:
|
|
199
|
-
database: 'postgres',
|
|
200
|
-
idle_in_transaction_session_timeout: 0
|
|
199
|
+
connectionString: this.env('DATABASE_URL')
|
|
201
200
|
});
|
|
202
|
-
// Connect
|
|
203
201
|
await (async function connect() {
|
|
204
|
-
/*
|
|
205
202
|
pgClient.on('error', (e) => {
|
|
206
|
-
console.log('
|
|
203
|
+
console.log('PG Error', e);
|
|
207
204
|
});
|
|
208
205
|
pgClient.on('end', (e) => {
|
|
209
|
-
console.log('
|
|
206
|
+
console.log('PG End', e);
|
|
210
207
|
});
|
|
211
208
|
pgClient.on('notice', (e) => {
|
|
212
|
-
console.log('
|
|
209
|
+
console.log('PG Notice', e);
|
|
213
210
|
});
|
|
214
|
-
*/
|
|
215
211
|
await pgClient.connect();
|
|
216
212
|
})();
|
|
217
213
|
this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
|
|
218
214
|
} else {
|
|
219
|
-
console.log('No database capabilities');
|
|
220
215
|
//const { ODBClient } = await import('@linked-db/linked-ql/odb');
|
|
221
216
|
//this.#sdk.db = new ODBClient({ dialect: 'postgres' });
|
|
222
217
|
}
|
|
223
218
|
}
|
|
224
219
|
if (this.#cx.server.capabilities?.redis) {
|
|
225
220
|
const { Redis } = await import('ioredis');
|
|
226
|
-
this.#sdk.redis =
|
|
227
|
-
? new Redis(
|
|
221
|
+
this.#sdk.redis = this.env('REDIS_URL')
|
|
222
|
+
? new Redis(this.env('REDIS_URL'))
|
|
228
223
|
: new Redis;
|
|
229
|
-
console.log('Redis capabilities', process.env[this.#cx.server.capabilities.redis_url_variable] ? 'with url' : '');
|
|
230
224
|
}
|
|
231
225
|
if (this.#cx.server.capabilities?.webpush) {
|
|
232
226
|
const { default: webpush } = await import('web-push');
|
|
233
|
-
console.log('Webpuah capabilities');
|
|
234
227
|
this.#sdk.webpush = webpush;
|
|
235
|
-
if (
|
|
236
|
-
&& process.env[this.#cx.server.capabilities.vapid_private_key_variable]) {
|
|
228
|
+
if (this.env('VAPID_PUBLIC_KEY') && this.env('VAPID_PRIVATE_KEY')) {
|
|
237
229
|
webpush.setVapidDetails(
|
|
238
230
|
this.#cx.server.capabilities.vapid_subject,
|
|
239
|
-
|
|
240
|
-
|
|
231
|
+
this.env('VAPID_PUBLIC_KEY'),
|
|
232
|
+
this.env('VAPID_PRIVATE_KEY')
|
|
241
233
|
);
|
|
242
234
|
}
|
|
243
235
|
}
|
|
@@ -280,11 +272,12 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
280
272
|
// and actual processing
|
|
281
273
|
scope.request = this.createRequest(scope.url.href, requestInit);
|
|
282
274
|
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
283
|
-
secret:
|
|
275
|
+
secret: this.env('SESSION_KEY'),
|
|
284
276
|
registry: this.#sdk.redis && {
|
|
285
|
-
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(key) || null) },
|
|
286
|
-
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*/) },
|
|
287
279
|
},
|
|
280
|
+
ttl: this.env('SESSION_TTL') || 2592000/*30days*/,
|
|
288
281
|
});
|
|
289
282
|
if (!scope.error) {
|
|
290
283
|
if (!(scope.clientMessagingRegistry = this.#globalMessagingRegistry.get(scope.session.sessionID))) {
|
|
@@ -573,6 +566,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
573
566
|
console.error('Final response already sent');
|
|
574
567
|
return;
|
|
575
568
|
}
|
|
569
|
+
await scope.httpEvent.session.commit();
|
|
576
570
|
return await this.execPush(scope.clientMessaging, response);
|
|
577
571
|
},
|
|
578
572
|
};
|
|
@@ -584,11 +578,12 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
584
578
|
scope.request = this.createRequest(scope.url.href, scope.init, scope.autoHeaders.filter((header) => header.type === 'request'));
|
|
585
579
|
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
586
580
|
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
587
|
-
secret:
|
|
581
|
+
secret: this.env('SESSION_KEY'),
|
|
588
582
|
registry: this.#sdk.redis && {
|
|
589
|
-
get: async (key) => { return JSON.parse(await this.#sdk.redis.get(key) || null) },
|
|
590
|
-
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*/); },
|
|
591
585
|
},
|
|
586
|
+
ttl: this.env('SESSION_TTL') || 2592000/*30days*/,
|
|
592
587
|
});
|
|
593
588
|
const sessionID = scope.session.sessionID;
|
|
594
589
|
if (!this.#globalMessagingRegistry.has(sessionID)) {
|