webspresso 0.0.99 → 0.0.101
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/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
package/core/queue/scheduler.js
CHANGED
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
const { CronExpression } = require('./cron');
|
|
10
10
|
|
|
11
|
-
let scheduleCounter = 0;
|
|
12
|
-
|
|
13
11
|
class TaskScheduler {
|
|
14
12
|
/**
|
|
15
13
|
* @param {import('./manager').QueueManager} queue - QueueManager instance
|
|
@@ -23,7 +21,7 @@ class TaskScheduler {
|
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
this.queue = queue;
|
|
26
|
-
this.maxIntervalMs = options.maxIntervalMs || 60000;
|
|
24
|
+
this.maxIntervalMs = Math.max(100, options.maxIntervalMs || 60000);
|
|
27
25
|
this.isRunning = false;
|
|
28
26
|
this._timer = null;
|
|
29
27
|
|
|
@@ -55,8 +53,11 @@ class TaskScheduler {
|
|
|
55
53
|
* @param {Object} [options]
|
|
56
54
|
* @param {string} [options.id] - Optional custom schedule ID
|
|
57
55
|
* @param {boolean} [options.overlap=false] - Allow concurrent executions of the same schedule
|
|
56
|
+
* @param {boolean} [options.preventOverlap] - Inverted alias for overlap: false
|
|
58
57
|
* @param {number} [options.maxRuns] - Maximum number of runs before auto-unscheduling
|
|
59
58
|
* @param {boolean} [options.utc=false] - Evaluate schedule in UTC
|
|
59
|
+
* @param {string} [options.timezone] - IANA Timezone (e.g. 'Europe/Istanbul')
|
|
60
|
+
* @param {string} [options.tz] - Alias for timezone
|
|
60
61
|
* @param {boolean} [options.runImmediately=false] - Run immediately upon registration
|
|
61
62
|
* @returns {Object} Registered schedule object
|
|
62
63
|
*/
|
|
@@ -67,13 +68,18 @@ class TaskScheduler {
|
|
|
67
68
|
|
|
68
69
|
const cron = cronExpression instanceof CronExpression
|
|
69
70
|
? cronExpression
|
|
70
|
-
: new CronExpression(cronExpression, {
|
|
71
|
+
: new CronExpression(cronExpression, {
|
|
72
|
+
utc: !!options.utc,
|
|
73
|
+
timezone: options.timezone || options.tz,
|
|
74
|
+
});
|
|
71
75
|
|
|
72
76
|
const id = options.id || `schedule:${jobName}:${cron.resolvedExpression}`;
|
|
73
77
|
|
|
74
78
|
const now = new Date();
|
|
75
79
|
const nextRunAt = cron.getNextDate(now);
|
|
76
80
|
|
|
81
|
+
const allowOverlap = options.overlap === true || (options.preventOverlap === false);
|
|
82
|
+
|
|
77
83
|
const scheduleObj = {
|
|
78
84
|
id,
|
|
79
85
|
name: jobName,
|
|
@@ -81,7 +87,8 @@ class TaskScheduler {
|
|
|
81
87
|
cron,
|
|
82
88
|
data,
|
|
83
89
|
options: {
|
|
84
|
-
overlap:
|
|
90
|
+
overlap: allowOverlap,
|
|
91
|
+
preventOverlap: !allowOverlap,
|
|
85
92
|
maxRuns: typeof options.maxRuns === 'number' && options.maxRuns > 0 ? options.maxRuns : null,
|
|
86
93
|
runImmediately: !!options.runImmediately,
|
|
87
94
|
...options,
|
|
@@ -91,6 +98,7 @@ class TaskScheduler {
|
|
|
91
98
|
nextRunAt,
|
|
92
99
|
isRunning: false,
|
|
93
100
|
activeJobId: null,
|
|
101
|
+
activeJobIds: new Set(),
|
|
94
102
|
registeredAt: now,
|
|
95
103
|
};
|
|
96
104
|
|
|
@@ -98,7 +106,11 @@ class TaskScheduler {
|
|
|
98
106
|
this.queue.emit('schedule:registered', scheduleObj);
|
|
99
107
|
|
|
100
108
|
if (options.runImmediately) {
|
|
101
|
-
setImmediate(() =>
|
|
109
|
+
setImmediate(() => {
|
|
110
|
+
this.trigger(id).catch((err) => {
|
|
111
|
+
this.queue.emit('schedule:error', scheduleObj, err);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
this._rescheduleTimer();
|
|
@@ -118,24 +130,28 @@ class TaskScheduler {
|
|
|
118
130
|
* @returns {boolean} True if removed, false if not found
|
|
119
131
|
*/
|
|
120
132
|
unschedule(idOrJobName) {
|
|
121
|
-
let targetId = null;
|
|
122
|
-
|
|
123
133
|
if (this.schedules.has(idOrJobName)) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
const scheduleObj = this.schedules.get(idOrJobName);
|
|
135
|
+
this.schedules.delete(idOrJobName);
|
|
136
|
+
this.queue.emit('schedule:unregistered', scheduleObj);
|
|
137
|
+
this._rescheduleTimer();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const toRemove = [];
|
|
142
|
+
for (const [id, s] of this.schedules.entries()) {
|
|
143
|
+
if (s.name === idOrJobName) {
|
|
144
|
+
toRemove.push({ id, s });
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
if (
|
|
148
|
+
if (toRemove.length === 0) return false;
|
|
149
|
+
|
|
150
|
+
for (const { id, s } of toRemove) {
|
|
151
|
+
this.schedules.delete(id);
|
|
152
|
+
this.queue.emit('schedule:unregistered', s);
|
|
153
|
+
}
|
|
135
154
|
|
|
136
|
-
const scheduleObj = this.schedules.get(targetId);
|
|
137
|
-
this.schedules.delete(targetId);
|
|
138
|
-
this.queue.emit('schedule:unregistered', scheduleObj);
|
|
139
155
|
this._rescheduleTimer();
|
|
140
156
|
return true;
|
|
141
157
|
}
|
|
@@ -156,6 +172,7 @@ class TaskScheduler {
|
|
|
156
172
|
nextRunAt: s.nextRunAt,
|
|
157
173
|
isRunning: s.isRunning,
|
|
158
174
|
activeJobId: s.activeJobId,
|
|
175
|
+
activeJobIds: Array.from(s.activeJobIds || []),
|
|
159
176
|
registeredAt: s.registeredAt,
|
|
160
177
|
}));
|
|
161
178
|
}
|
|
@@ -186,7 +203,8 @@ class TaskScheduler {
|
|
|
186
203
|
throw new Error(`Schedule not found: '${idOrJobName}'`);
|
|
187
204
|
}
|
|
188
205
|
|
|
189
|
-
|
|
206
|
+
const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
|
|
207
|
+
if (!allowOverlap && s.isRunning) {
|
|
190
208
|
this.queue.emit('schedule:skipped', s, 'overlap');
|
|
191
209
|
return null;
|
|
192
210
|
}
|
|
@@ -198,6 +216,9 @@ class TaskScheduler {
|
|
|
198
216
|
try {
|
|
199
217
|
const job = await this.queue.dispatch(s.name, s.data, s.options);
|
|
200
218
|
s.activeJobId = job.id;
|
|
219
|
+
if (!s.activeJobIds) s.activeJobIds = new Set();
|
|
220
|
+
s.activeJobIds.add(job.id);
|
|
221
|
+
|
|
201
222
|
this.queue.emit('schedule:triggered', s, job);
|
|
202
223
|
|
|
203
224
|
if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
|
|
@@ -206,8 +227,10 @@ class TaskScheduler {
|
|
|
206
227
|
|
|
207
228
|
return job;
|
|
208
229
|
} catch (err) {
|
|
209
|
-
s.
|
|
210
|
-
|
|
230
|
+
if (!s.activeJobIds || s.activeJobIds.size === 0) {
|
|
231
|
+
s.isRunning = false;
|
|
232
|
+
s.activeJobId = null;
|
|
233
|
+
}
|
|
211
234
|
throw err;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
@@ -236,6 +259,20 @@ class TaskScheduler {
|
|
|
236
259
|
return this;
|
|
237
260
|
}
|
|
238
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Pause alias for stop
|
|
264
|
+
*/
|
|
265
|
+
pause() {
|
|
266
|
+
return this.stop();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resume alias for start
|
|
271
|
+
*/
|
|
272
|
+
resume() {
|
|
273
|
+
return this.start();
|
|
274
|
+
}
|
|
275
|
+
|
|
239
276
|
/**
|
|
240
277
|
* Clear all schedules and stop timer
|
|
241
278
|
*/
|
|
@@ -260,9 +297,16 @@ class TaskScheduler {
|
|
|
260
297
|
*/
|
|
261
298
|
_handleJobSettled(job, err, result) {
|
|
262
299
|
for (const s of this.schedules.values()) {
|
|
263
|
-
if (s.activeJobId === job.id) {
|
|
264
|
-
s.
|
|
265
|
-
|
|
300
|
+
if (s.activeJobIds?.has(job.id) || s.activeJobId === job.id) {
|
|
301
|
+
if (s.activeJobIds) {
|
|
302
|
+
s.activeJobIds.delete(job.id);
|
|
303
|
+
}
|
|
304
|
+
if (!s.activeJobIds || s.activeJobIds.size === 0) {
|
|
305
|
+
s.isRunning = false;
|
|
306
|
+
s.activeJobId = null;
|
|
307
|
+
} else {
|
|
308
|
+
s.activeJobId = s.activeJobIds.values().next().value || null;
|
|
309
|
+
}
|
|
266
310
|
this.queue.emit('schedule:finished', s, job, err ? { error: err } : { result });
|
|
267
311
|
break;
|
|
268
312
|
}
|
|
@@ -297,7 +341,7 @@ class TaskScheduler {
|
|
|
297
341
|
|
|
298
342
|
if (earliest === Infinity) return;
|
|
299
343
|
|
|
300
|
-
const delay = Math.max(
|
|
344
|
+
const delay = Math.max(0, Math.min(earliest - now, this.maxIntervalMs));
|
|
301
345
|
|
|
302
346
|
this._timer = setTimeout(() => this._tick(), delay);
|
|
303
347
|
}
|
|
@@ -309,51 +353,63 @@ class TaskScheduler {
|
|
|
309
353
|
async _tick() {
|
|
310
354
|
if (!this.isRunning) return;
|
|
311
355
|
|
|
312
|
-
|
|
313
|
-
|
|
356
|
+
try {
|
|
357
|
+
const now = new Date();
|
|
358
|
+
const nowTime = now.getTime();
|
|
314
359
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
360
|
+
const dueList = [];
|
|
361
|
+
for (const s of this.schedules.values()) {
|
|
362
|
+
if (s.nextRunAt && s.nextRunAt.getTime() <= nowTime) {
|
|
363
|
+
dueList.push(s);
|
|
364
|
+
}
|
|
319
365
|
}
|
|
320
|
-
}
|
|
321
366
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
s.nextRunAt = s.cron.getNextDate(now);
|
|
367
|
+
for (const s of dueList) {
|
|
368
|
+
if (!this.isRunning) break;
|
|
325
369
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.queue.emit('schedule:skipped', s, 'overlap');
|
|
329
|
-
s.lastRunAt = now;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
370
|
+
// Advance next run date immediately to avoid double tick
|
|
371
|
+
s.nextRunAt = s.cron.getNextDate(now);
|
|
332
372
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
373
|
+
const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
|
|
374
|
+
if (!allowOverlap && s.isRunning) {
|
|
375
|
+
this.queue.emit('schedule:skipped', s, 'overlap');
|
|
376
|
+
s.lastRunAt = now;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
336
379
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
s.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
380
|
+
s.isRunning = true;
|
|
381
|
+
s.lastRunAt = now;
|
|
382
|
+
s.runsCount++;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const job = await this.queue.dispatch(s.name, s.data, s.options);
|
|
386
|
+
s.activeJobId = job.id;
|
|
387
|
+
if (!s.activeJobIds) s.activeJobIds = new Set();
|
|
388
|
+
s.activeJobIds.add(job.id);
|
|
389
|
+
|
|
390
|
+
this.queue.emit('schedule:triggered', s, job);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (!s.activeJobIds || s.activeJobIds.size === 0) {
|
|
393
|
+
s.isRunning = false;
|
|
394
|
+
s.activeJobId = null;
|
|
395
|
+
}
|
|
396
|
+
this.queue.emit('schedule:error', s, err);
|
|
397
|
+
}
|
|
346
398
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
399
|
+
// Check maxRuns
|
|
400
|
+
if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
|
|
401
|
+
this.unschedule(s.id);
|
|
402
|
+
}
|
|
350
403
|
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
this.queue.emit('schedule:error', null, err);
|
|
406
|
+
} finally {
|
|
407
|
+
this._rescheduleTimer();
|
|
351
408
|
}
|
|
352
|
-
|
|
353
|
-
this._rescheduleTimer();
|
|
354
409
|
}
|
|
355
410
|
}
|
|
356
411
|
|
|
357
412
|
module.exports = {
|
|
358
413
|
TaskScheduler,
|
|
359
414
|
};
|
|
415
|
+
|
package/core/ssr/stream.js
CHANGED
|
@@ -17,7 +17,15 @@ const { Readable } = require('stream');
|
|
|
17
17
|
* @returns {Readable}
|
|
18
18
|
*/
|
|
19
19
|
function createHtmlStream(options = {}) {
|
|
20
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
env,
|
|
22
|
+
templatePath,
|
|
23
|
+
context = {},
|
|
24
|
+
defer = {},
|
|
25
|
+
nonce: optionNonce,
|
|
26
|
+
loadingTemplate,
|
|
27
|
+
layouts = [],
|
|
28
|
+
} = options;
|
|
21
29
|
const nonce = optionNonce || context.nonce || context.cspNonce || '';
|
|
22
30
|
const scriptTag = nonce ? `<script nonce="${nonce}">` : '<script>';
|
|
23
31
|
|
|
@@ -35,16 +43,33 @@ function createHtmlStream(options = {}) {
|
|
|
35
43
|
|
|
36
44
|
if (deferredKeys.length === 0) {
|
|
37
45
|
// Standard template streaming (render and stream)
|
|
38
|
-
|
|
46
|
+
let html = env.render(templatePath, context);
|
|
47
|
+
if (layouts && layouts.length > 0) {
|
|
48
|
+
const { applyCascadingLayouts } = require('../../src/routing/cascading');
|
|
49
|
+
html = applyCascadingLayouts(env, layouts, html, context);
|
|
50
|
+
}
|
|
39
51
|
stream.push(html);
|
|
40
52
|
stream.push(null);
|
|
41
53
|
return;
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
// Render loading skeleton if provided
|
|
57
|
+
let loadingHtml = '';
|
|
58
|
+
if (loadingTemplate) {
|
|
59
|
+
try {
|
|
60
|
+
loadingHtml = env.render(loadingTemplate, { ...context, __streaming: true });
|
|
61
|
+
} catch (_) {
|
|
62
|
+
loadingHtml = '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
// Initial shell rendering with deferred slot placeholders
|
|
45
67
|
const shellContext = {
|
|
46
68
|
...context,
|
|
47
69
|
__streaming: true,
|
|
70
|
+
loadingHtml,
|
|
71
|
+
__loadingHtml: loadingHtml,
|
|
72
|
+
loadingSkeleton: loadingHtml,
|
|
48
73
|
};
|
|
49
74
|
|
|
50
75
|
for (const key of deferredKeys) {
|
|
@@ -53,6 +78,35 @@ function createHtmlStream(options = {}) {
|
|
|
53
78
|
|
|
54
79
|
let shellHtml = env.render(templatePath, shellContext);
|
|
55
80
|
|
|
81
|
+
// Check slot-specific loading templates first
|
|
82
|
+
for (const key of deferredKeys) {
|
|
83
|
+
const deferredItem = defer[key];
|
|
84
|
+
if (deferredItem && typeof deferredItem === 'object' && deferredItem.loadingTemplate) {
|
|
85
|
+
try {
|
|
86
|
+
const slotLoadingHtml = env.render(deferredItem.loadingTemplate, { ...context, __streaming: true });
|
|
87
|
+
const slotRegex = new RegExp(
|
|
88
|
+
`(<([a-zA-Z0-9_-]+)\\b[^>]*data-stream-target=["']${key}["'][^>]*>)\\s*(<\\/\\2>)`,
|
|
89
|
+
'g'
|
|
90
|
+
);
|
|
91
|
+
shellHtml = shellHtml.replace(slotRegex, `$1${slotLoadingHtml}$3`);
|
|
92
|
+
} catch (_) {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If empty stream targets exist and default loadingHtml is available, populate them
|
|
97
|
+
if (loadingHtml) {
|
|
98
|
+
shellHtml = shellHtml.replace(
|
|
99
|
+
/(<([a-zA-Z0-9_-]+)\b[^>]*data-stream-target=["'][^"']+["'][^>]*>)\s*(<\/\2>)/g,
|
|
100
|
+
`$1${loadingHtml}$3`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wrap shell with cascading nested layouts if provided
|
|
105
|
+
if (layouts && layouts.length > 0) {
|
|
106
|
+
const { applyCascadingLayouts } = require('../../src/routing/cascading');
|
|
107
|
+
shellHtml = applyCascadingLayouts(env, layouts, shellHtml, shellContext);
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
// Check if template contains streaming slot markers, or split at first slot
|
|
57
111
|
// We push the initial shell chunk immediately to start browser TTFB & CSS download
|
|
58
112
|
stream.push(shellHtml);
|
|
@@ -127,6 +181,8 @@ ${scriptTag}
|
|
|
127
181
|
* @param {Object} [options.defer] - Deferred slot promises
|
|
128
182
|
* @param {number} [options.status=200] - HTTP status code
|
|
129
183
|
* @param {boolean} [options.flushImmediately=true] - Flush headers immediately
|
|
184
|
+
* @param {string} [options.loadingTemplate] - Loading skeleton template
|
|
185
|
+
* @param {Array<Object>} [options.layouts] - Cascading layouts
|
|
130
186
|
* @returns {Promise<void>}
|
|
131
187
|
*/
|
|
132
188
|
function renderStream(res, templatePath, context = {}, options = {}) {
|
|
@@ -137,6 +193,8 @@ function renderStream(res, templatePath, context = {}, options = {}) {
|
|
|
137
193
|
status = 200,
|
|
138
194
|
flushImmediately = true,
|
|
139
195
|
nonce: optionNonce,
|
|
196
|
+
loadingTemplate,
|
|
197
|
+
layouts,
|
|
140
198
|
} = options;
|
|
141
199
|
|
|
142
200
|
const nonce =
|
|
@@ -169,6 +227,8 @@ function renderStream(res, templatePath, context = {}, options = {}) {
|
|
|
169
227
|
context,
|
|
170
228
|
defer,
|
|
171
229
|
nonce,
|
|
230
|
+
loadingTemplate,
|
|
231
|
+
layouts,
|
|
172
232
|
});
|
|
173
233
|
|
|
174
234
|
let finished = false;
|