webspresso 0.0.110 → 0.0.111
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/core/client/adapter/datastar.js +73 -23
- package/core/client/api.js +23 -21
- package/core/client/controller.js +288 -30
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +39 -19
- package/core/client/fragments.js +433 -95
- package/core/client/host.js +365 -0
- package/core/client/index.d.ts +18 -0
- package/core/client/index.js +154 -23
- package/core/client/lite.js +101 -4
- package/core/client/server-fragments.js +99 -21
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +322 -28
- package/index.js +3 -0
- package/package.json +1 -1
- package/src/bundle/index.js +175 -14
- package/src/dev/banner.js +4 -1
- package/src/dev-reload/index.js +29 -1
- package/src/server.js +28 -0
package/core/client/lite.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Webspresso Lightweight Client Runtime (`core/client/lite.js`)
|
|
5
5
|
*
|
|
6
|
-
* Micro-footprint (~
|
|
6
|
+
* Micro-footprint (~16.1 KB Brotli) client runtime for server-driven applications.
|
|
7
7
|
* Includes:
|
|
8
|
-
* - SSR Fragment coordinator (batch refresh, invalidation, DOM morphing, focus preservation)
|
|
9
|
-
* - Native HTML Form enhancement (`<form data-w-form>`, validation errors,
|
|
8
|
+
* - SSR Fragment coordinator (batch refresh, invalidation, auto-revalidation, DOM morphing, focus preservation)
|
|
9
|
+
* - Native HTML Form enhancement (`<form data-w-form>`, validation errors, lifecycle hooks, draft persistence)
|
|
10
|
+
* - Shared Client Services & optional lazy UI hosts (`defineClientService`, `services.get`)
|
|
10
11
|
* - Typed API client with auto-invalidation
|
|
11
12
|
* Excludes:
|
|
12
13
|
* - Reactive signals engine, controller proxies, and complex task coordinators.
|
|
@@ -45,6 +46,46 @@ const {
|
|
|
45
46
|
|
|
46
47
|
const { NOOP_DEBUGGER } = require('./utils');
|
|
47
48
|
|
|
49
|
+
const NOOP_SERVICES = Object.freeze({
|
|
50
|
+
get(name) {
|
|
51
|
+
const err = new Error(`Unknown client service "${name}".`);
|
|
52
|
+
err.code = 'CLIENT_SERVICE_NOT_FOUND';
|
|
53
|
+
return Promise.reject(err);
|
|
54
|
+
},
|
|
55
|
+
has() {
|
|
56
|
+
return false;
|
|
57
|
+
},
|
|
58
|
+
isInitialized() {
|
|
59
|
+
return false;
|
|
60
|
+
},
|
|
61
|
+
peek() {
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
register() {},
|
|
65
|
+
registerMany() {},
|
|
66
|
+
dispose() {},
|
|
67
|
+
destroy() {},
|
|
68
|
+
isDisposed: false,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let _servicesMod = null;
|
|
72
|
+
function getServicesModule() {
|
|
73
|
+
if (!_servicesMod) {
|
|
74
|
+
const reqFn = typeof module !== 'undefined' && module.require ? module.require.bind(module) : require;
|
|
75
|
+
_servicesMod = reqFn('./services');
|
|
76
|
+
}
|
|
77
|
+
return _servicesMod;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let _hostMod = null;
|
|
81
|
+
function getHostModule() {
|
|
82
|
+
if (!_hostMod) {
|
|
83
|
+
const reqFn = typeof module !== 'undefined' && module.require ? module.require.bind(module) : require;
|
|
84
|
+
_hostMod = reqFn('./host');
|
|
85
|
+
}
|
|
86
|
+
return _hostMod;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
let _serverFragments = null;
|
|
49
90
|
function getServerFragments() {
|
|
50
91
|
if (!_serverFragments) {
|
|
@@ -66,6 +107,14 @@ function createLiteClientRuntime(options = {}) {
|
|
|
66
107
|
fetch: options.fetch,
|
|
67
108
|
endpoint: options.fragmentEndpoint || DEFAULT_FRAGMENT_ENDPOINT,
|
|
68
109
|
debug,
|
|
110
|
+
staleTime: options.staleTime,
|
|
111
|
+
refetchOnWindowFocus: options.refetchOnWindowFocus,
|
|
112
|
+
refetchOnFocus: options.refetchOnFocus,
|
|
113
|
+
refetchOnReconnect: options.refetchOnReconnect,
|
|
114
|
+
refetchOnMount: options.refetchOnMount,
|
|
115
|
+
refetchOnVisit: options.refetchOnVisit,
|
|
116
|
+
refetchInterval: options.refetchInterval,
|
|
117
|
+
refetchIntervalInBackground: options.refetchIntervalInBackground,
|
|
69
118
|
getCsrfToken: options.getCsrfToken || (() => discoverCsrfToken(rootOption)),
|
|
70
119
|
});
|
|
71
120
|
|
|
@@ -80,6 +129,21 @@ function createLiteClientRuntime(options = {}) {
|
|
|
80
129
|
|
|
81
130
|
const api = createApiClient(apiConfig);
|
|
82
131
|
|
|
132
|
+
let runtime = null;
|
|
133
|
+
|
|
134
|
+
const services =
|
|
135
|
+
options.services && typeof options.services.get === 'function' && typeof options.services.register === 'function'
|
|
136
|
+
? options.services
|
|
137
|
+
: options.services && typeof options.services === 'object'
|
|
138
|
+
? getServicesModule().createClientServiceRegistry({
|
|
139
|
+
root: rootOption,
|
|
140
|
+
api,
|
|
141
|
+
debug,
|
|
142
|
+
getRuntime: () => runtime,
|
|
143
|
+
services: options.services,
|
|
144
|
+
})
|
|
145
|
+
: NOOP_SERVICES;
|
|
146
|
+
|
|
83
147
|
const formsRegistry = new Map();
|
|
84
148
|
function getFormHandle(nameOrEl) {
|
|
85
149
|
if (!nameOrEl) return null;
|
|
@@ -93,6 +157,7 @@ function createLiteClientRuntime(options = {}) {
|
|
|
93
157
|
el,
|
|
94
158
|
createFormHandle(el, {
|
|
95
159
|
api,
|
|
160
|
+
services,
|
|
96
161
|
onInvalidate: (tags) => fragments.invalidate(tags),
|
|
97
162
|
})
|
|
98
163
|
);
|
|
@@ -105,6 +170,7 @@ function createLiteClientRuntime(options = {}) {
|
|
|
105
170
|
nameOrEl,
|
|
106
171
|
createFormHandle(nameOrEl, {
|
|
107
172
|
api,
|
|
173
|
+
services,
|
|
108
174
|
onInvalidate: (tags) => fragments.invalidate(tags),
|
|
109
175
|
})
|
|
110
176
|
);
|
|
@@ -126,16 +192,35 @@ function createLiteClientRuntime(options = {}) {
|
|
|
126
192
|
}
|
|
127
193
|
}
|
|
128
194
|
|
|
129
|
-
|
|
195
|
+
function destroy() {
|
|
196
|
+
for (const handle of formsRegistry.values()) {
|
|
197
|
+
try {
|
|
198
|
+
handle.destroy();
|
|
199
|
+
} catch (_e) {}
|
|
200
|
+
}
|
|
201
|
+
formsRegistry.clear();
|
|
202
|
+
if (fragments && typeof fragments.dispose === 'function') {
|
|
203
|
+
fragments.dispose();
|
|
204
|
+
}
|
|
205
|
+
services.dispose();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
runtime = {
|
|
130
209
|
fragments,
|
|
131
210
|
api,
|
|
211
|
+
services,
|
|
212
|
+
registerService: (name, def) => services.register(name, def),
|
|
132
213
|
forms: {
|
|
133
214
|
get: getFormHandle,
|
|
134
215
|
},
|
|
135
216
|
invalidate: (tags, meta) => fragments.invalidate(tags, meta),
|
|
136
217
|
refresh: (idOrName, input, opts) => fragments.refresh(idOrName, input, opts),
|
|
137
218
|
boot,
|
|
219
|
+
destroy,
|
|
220
|
+
dispose: destroy,
|
|
138
221
|
};
|
|
222
|
+
|
|
223
|
+
return runtime;
|
|
139
224
|
}
|
|
140
225
|
|
|
141
226
|
/**
|
|
@@ -171,5 +256,17 @@ module.exports = {
|
|
|
171
256
|
defaultStorage,
|
|
172
257
|
StorageAdapter,
|
|
173
258
|
MemoryStorage,
|
|
259
|
+
get defineClientService() {
|
|
260
|
+
return getServicesModule().defineClientService;
|
|
261
|
+
},
|
|
262
|
+
get createClientServiceRegistry() {
|
|
263
|
+
return getServicesModule().createClientServiceRegistry;
|
|
264
|
+
},
|
|
265
|
+
get createServiceError() {
|
|
266
|
+
return getServicesModule().createServiceError;
|
|
267
|
+
},
|
|
268
|
+
get createClientHostManager() {
|
|
269
|
+
return getHostModule().createClientHostManager;
|
|
270
|
+
},
|
|
174
271
|
};
|
|
175
272
|
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
const { z } = require('../validation');
|
|
16
16
|
const { ValidationError, NotFoundError, BadRequestError, UnauthorizedError, ForbiddenError } = require('../errors');
|
|
17
17
|
const { onTransactionCommit, getAmbientTransaction } = require('../orm/transaction');
|
|
18
|
+
const { isArray, isNumber } = require('./utils');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Execute an array of Express-style middlewares sequentially.
|
|
21
22
|
*/
|
|
22
23
|
async function runMiddlewares(middlewares, req, res) {
|
|
23
|
-
if (!
|
|
24
|
+
if (!isArray(middlewares) || middlewares.length === 0) return;
|
|
24
25
|
for (const mw of middlewares) {
|
|
25
26
|
if (typeof mw !== 'function') continue;
|
|
26
27
|
await new Promise((resolve, reject) => {
|
|
@@ -89,11 +90,28 @@ function defineFragment(config = {}) {
|
|
|
89
90
|
}
|
|
90
91
|
return {
|
|
91
92
|
template: config.template.trim(),
|
|
92
|
-
dependsOn:
|
|
93
|
+
dependsOn: isArray(config.dependsOn) ? config.dependsOn.map(String) : [],
|
|
93
94
|
schema: config.schema || null,
|
|
94
95
|
auth: config.auth !== undefined ? config.auth : null,
|
|
95
|
-
middleware:
|
|
96
|
+
middleware: isArray(config.middleware) ? config.middleware : [],
|
|
96
97
|
load: typeof config.load === 'function' ? config.load : null,
|
|
98
|
+
staleTime: config.staleTime !== undefined ? config.staleTime : undefined,
|
|
99
|
+
refetchOnWindowFocus:
|
|
100
|
+
config.refetchOnWindowFocus !== undefined
|
|
101
|
+
? config.refetchOnWindowFocus
|
|
102
|
+
: config.refetchOnFocus !== undefined
|
|
103
|
+
? config.refetchOnFocus
|
|
104
|
+
: undefined,
|
|
105
|
+
refetchOnReconnect: config.refetchOnReconnect !== undefined ? config.refetchOnReconnect : undefined,
|
|
106
|
+
refetchOnMount:
|
|
107
|
+
config.refetchOnMount !== undefined
|
|
108
|
+
? config.refetchOnMount
|
|
109
|
+
: config.refetchOnVisit !== undefined
|
|
110
|
+
? config.refetchOnVisit
|
|
111
|
+
: undefined,
|
|
112
|
+
refetchInterval: config.refetchInterval !== undefined ? config.refetchInterval : undefined,
|
|
113
|
+
refetchIntervalInBackground:
|
|
114
|
+
config.refetchIntervalInBackground !== undefined ? config.refetchIntervalInBackground : undefined,
|
|
97
115
|
};
|
|
98
116
|
}
|
|
99
117
|
|
|
@@ -114,7 +132,7 @@ function defineFragment(config = {}) {
|
|
|
114
132
|
* @returns {{ __isWebspressoApiResult: true, data: T, invalidate: string[], status?: number, headers?: Record<string, string> }}
|
|
115
133
|
*/
|
|
116
134
|
function apiResult(data, meta = {}) {
|
|
117
|
-
const rawInvalidate = meta &&
|
|
135
|
+
const rawInvalidate = meta && isArray(meta.invalidate) ? meta.invalidate : [];
|
|
118
136
|
const invalidate = Array.from(
|
|
119
137
|
new Set(rawInvalidate.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
|
|
120
138
|
);
|
|
@@ -122,7 +140,7 @@ function apiResult(data, meta = {}) {
|
|
|
122
140
|
__isWebspressoApiResult: true,
|
|
123
141
|
data,
|
|
124
142
|
invalidate,
|
|
125
|
-
status: meta &&
|
|
143
|
+
status: meta && isNumber(meta.status) ? meta.status : undefined,
|
|
126
144
|
headers: meta && meta.headers && typeof meta.headers === 'object' ? { ...meta.headers } : undefined,
|
|
127
145
|
};
|
|
128
146
|
}
|
|
@@ -131,11 +149,11 @@ function apiResult(data, meta = {}) {
|
|
|
131
149
|
* Check whether a value returned by an API handler is an `apiResult` or `{ data, invalidate }` envelope.
|
|
132
150
|
*/
|
|
133
151
|
function isApiResultEnvelope(val) {
|
|
134
|
-
if (!val || typeof val !== 'object' ||
|
|
152
|
+
if (!val || typeof val !== 'object' || isArray(val)) return false;
|
|
135
153
|
if (val.__isWebspressoApiResult === true) return true;
|
|
136
154
|
if (
|
|
137
155
|
'data' in val &&
|
|
138
|
-
|
|
156
|
+
isArray(val.invalidate) &&
|
|
139
157
|
val.invalidate.every((item) => typeof item === 'string')
|
|
140
158
|
) {
|
|
141
159
|
return true;
|
|
@@ -153,7 +171,7 @@ function isApiResultEnvelope(val) {
|
|
|
153
171
|
*/
|
|
154
172
|
function sendApiResult(req, res, rawResult) {
|
|
155
173
|
const status = rawResult.status || (res.statusCode && res.statusCode !== 200 ? res.statusCode : 200);
|
|
156
|
-
const rawTags =
|
|
174
|
+
const rawTags = isArray(rawResult.invalidate) ? rawResult.invalidate : [];
|
|
157
175
|
const tags = Array.from(
|
|
158
176
|
new Set(rawTags.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
|
|
159
177
|
);
|
|
@@ -265,7 +283,7 @@ function ensureRequestMemo(req, ctx) {
|
|
|
265
283
|
* @returns {Record<string, unknown>}
|
|
266
284
|
*/
|
|
267
285
|
function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
|
|
268
|
-
const inputObj = rawInput && typeof rawInput === 'object' && !
|
|
286
|
+
const inputObj = rawInput && typeof rawInput === 'object' && !isArray(rawInput) ? rawInput : {};
|
|
269
287
|
if (!fragmentDef || !fragmentDef.schema) {
|
|
270
288
|
return { ...inputObj };
|
|
271
289
|
}
|
|
@@ -295,7 +313,7 @@ function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
|
|
|
295
313
|
const parsed = resolvedSchema.safeParse(inputObj);
|
|
296
314
|
if (!parsed.success) {
|
|
297
315
|
const issues = (parsed.error && parsed.error.issues ? parsed.error.issues : []).map((iss) => ({
|
|
298
|
-
path:
|
|
316
|
+
path: isArray(iss.path) ? iss.path.join('.') : String(iss.path || ''),
|
|
299
317
|
message: iss.message,
|
|
300
318
|
code: iss.code,
|
|
301
319
|
}));
|
|
@@ -329,18 +347,18 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
|
|
|
329
347
|
if (!user) {
|
|
330
348
|
throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
|
|
331
349
|
}
|
|
332
|
-
const roles =
|
|
350
|
+
const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
|
|
333
351
|
if (!roles.includes(authRule)) {
|
|
334
352
|
throw new ForbiddenError(`Role "${authRule}" required to access fragment "${fragmentName}".`);
|
|
335
353
|
}
|
|
336
354
|
return;
|
|
337
355
|
}
|
|
338
356
|
|
|
339
|
-
if (
|
|
357
|
+
if (isArray(authRule)) {
|
|
340
358
|
if (!user) {
|
|
341
359
|
throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
|
|
342
360
|
}
|
|
343
|
-
const roles =
|
|
361
|
+
const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
|
|
344
362
|
if (!authRule.some((r) => roles.includes(r))) {
|
|
345
363
|
throw new ForbiddenError(`Insufficient role to access fragment "${fragmentName}".`);
|
|
346
364
|
}
|
|
@@ -363,7 +381,22 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
|
|
|
363
381
|
* Notice: ONLY validated filter/pagination `input` is serialized in `data-w-fragment-input`.
|
|
364
382
|
* Server loader data (`fragmentData`) is NEVER serialized as props!
|
|
365
383
|
*/
|
|
366
|
-
function wrapFragmentRootHtml({
|
|
384
|
+
function wrapFragmentRootHtml({
|
|
385
|
+
name,
|
|
386
|
+
instanceId,
|
|
387
|
+
pagePath,
|
|
388
|
+
dependsOn,
|
|
389
|
+
validatedInput,
|
|
390
|
+
innerHtml,
|
|
391
|
+
tag = 'div',
|
|
392
|
+
className = '',
|
|
393
|
+
staleTime,
|
|
394
|
+
refetchOnWindowFocus,
|
|
395
|
+
refetchOnReconnect,
|
|
396
|
+
refetchOnMount,
|
|
397
|
+
refetchInterval,
|
|
398
|
+
refetchIntervalInBackground,
|
|
399
|
+
}) {
|
|
367
400
|
const safeTag = /^[a-zA-Z][a-zA-Z0-9-]*$/.test(tag || 'div') ? tag : 'div';
|
|
368
401
|
const attrs = [
|
|
369
402
|
`data-w-fragment="${escapeHtmlAttr(name)}"`,
|
|
@@ -371,7 +404,7 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
|
|
|
371
404
|
`data-w-fragment-page="${escapeHtmlAttr(pagePath || '/')}"`,
|
|
372
405
|
];
|
|
373
406
|
|
|
374
|
-
if (
|
|
407
|
+
if (isArray(dependsOn) && dependsOn.length > 0) {
|
|
375
408
|
attrs.push(`data-w-depends-on="${escapeHtmlAttr(dependsOn.join(','))}"`);
|
|
376
409
|
}
|
|
377
410
|
|
|
@@ -379,6 +412,30 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
|
|
|
379
412
|
attrs.push(`data-w-fragment-input="${escapeHtmlAttr(JSON.stringify(validatedInput))}"`);
|
|
380
413
|
}
|
|
381
414
|
|
|
415
|
+
if (staleTime !== undefined && staleTime !== null && staleTime !== false) {
|
|
416
|
+
attrs.push(`data-w-stale-time="${escapeHtmlAttr(String(staleTime))}"`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (refetchOnWindowFocus !== undefined && refetchOnWindowFocus !== null) {
|
|
420
|
+
attrs.push(`data-w-refetch-on-focus="${escapeHtmlAttr(String(refetchOnWindowFocus))}"`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (refetchOnReconnect !== undefined && refetchOnReconnect !== null) {
|
|
424
|
+
attrs.push(`data-w-refetch-on-reconnect="${escapeHtmlAttr(String(refetchOnReconnect))}"`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (refetchOnMount !== undefined && refetchOnMount !== null) {
|
|
428
|
+
attrs.push(`data-w-refetch-on-mount="${escapeHtmlAttr(String(refetchOnMount))}"`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (refetchInterval !== undefined && refetchInterval !== null && refetchInterval !== false) {
|
|
432
|
+
attrs.push(`data-w-refetch-interval="${escapeHtmlAttr(String(refetchInterval))}"`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (refetchIntervalInBackground !== undefined && refetchIntervalInBackground !== null) {
|
|
436
|
+
attrs.push(`data-w-refetch-in-background="${escapeHtmlAttr(String(refetchIntervalInBackground))}"`);
|
|
437
|
+
}
|
|
438
|
+
|
|
382
439
|
if (className) {
|
|
383
440
|
attrs.push(`class="${escapeHtmlAttr(className)}"`);
|
|
384
441
|
}
|
|
@@ -432,7 +489,7 @@ async function executeAndRenderFragment({
|
|
|
432
489
|
ensureRequestMemo(req, ctx);
|
|
433
490
|
|
|
434
491
|
// 1. Run fragment-specific middlewares if any
|
|
435
|
-
if (
|
|
492
|
+
if (isArray(fragmentDef.middleware) && fragmentDef.middleware.length > 0) {
|
|
436
493
|
await runMiddlewares(fragmentDef.middleware, req, res);
|
|
437
494
|
}
|
|
438
495
|
|
|
@@ -494,6 +551,27 @@ async function executeAndRenderFragment({
|
|
|
494
551
|
innerHtml,
|
|
495
552
|
tag: options.tag || 'div',
|
|
496
553
|
className: options.class || options.className || '',
|
|
554
|
+
staleTime: options.staleTime !== undefined ? options.staleTime : fragmentDef.staleTime,
|
|
555
|
+
refetchOnWindowFocus:
|
|
556
|
+
options.refetchOnWindowFocus !== undefined
|
|
557
|
+
? options.refetchOnWindowFocus
|
|
558
|
+
: options.refetchOnFocus !== undefined
|
|
559
|
+
? options.refetchOnFocus
|
|
560
|
+
: fragmentDef.refetchOnWindowFocus,
|
|
561
|
+
refetchOnReconnect:
|
|
562
|
+
options.refetchOnReconnect !== undefined ? options.refetchOnReconnect : fragmentDef.refetchOnReconnect,
|
|
563
|
+
refetchOnMount:
|
|
564
|
+
options.refetchOnMount !== undefined
|
|
565
|
+
? options.refetchOnMount
|
|
566
|
+
: options.refetchOnVisit !== undefined
|
|
567
|
+
? options.refetchOnVisit
|
|
568
|
+
: fragmentDef.refetchOnMount,
|
|
569
|
+
refetchInterval:
|
|
570
|
+
options.refetchInterval !== undefined ? options.refetchInterval : fragmentDef.refetchInterval,
|
|
571
|
+
refetchIntervalInBackground:
|
|
572
|
+
options.refetchIntervalInBackground !== undefined
|
|
573
|
+
? options.refetchIntervalInBackground
|
|
574
|
+
: fragmentDef.refetchIntervalInBackground,
|
|
497
575
|
});
|
|
498
576
|
|
|
499
577
|
return {
|
|
@@ -588,7 +666,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
588
666
|
* @returns {Promise<string>}
|
|
589
667
|
*/
|
|
590
668
|
async function resolvePendingSsrFragments(html, req) {
|
|
591
|
-
if (typeof html !== 'string' || !req || !
|
|
669
|
+
if (typeof html !== 'string' || !req || !isArray(req.__pendingSsrFragments)) {
|
|
592
670
|
return html;
|
|
593
671
|
}
|
|
594
672
|
|
|
@@ -627,7 +705,7 @@ function trimTrailingSlashes(str) {
|
|
|
627
705
|
* @returns {{ entry: object, params: Record<string, string> }|null}
|
|
628
706
|
*/
|
|
629
707
|
function matchPageEntryByPath(pagePath, pageEntries) {
|
|
630
|
-
if (!pagePath || !
|
|
708
|
+
if (!pagePath || !isArray(pageEntries)) return null;
|
|
631
709
|
const cleanPath = trimTrailingSlashes(String(pagePath).split('?')[0]) || '/';
|
|
632
710
|
|
|
633
711
|
// 1. Exact static match
|
|
@@ -722,7 +800,7 @@ function createFragmentRefreshHandler(config) {
|
|
|
722
800
|
},
|
|
723
801
|
];
|
|
724
802
|
} else {
|
|
725
|
-
if (!
|
|
803
|
+
if (!isArray(payload.fragments) || payload.fragments.length === 0) {
|
|
726
804
|
throw new BadRequestError('Fragment refresh request requires a non-empty `fragments` array.');
|
|
727
805
|
}
|
|
728
806
|
requestedFragments = payload.fragments;
|
|
@@ -755,12 +833,12 @@ function createFragmentRefreshHandler(config) {
|
|
|
755
833
|
}
|
|
756
834
|
|
|
757
835
|
// 1. Execute page-level cascading middlewares (`_hooks.js`) and page route `middleware`
|
|
758
|
-
if (
|
|
836
|
+
if (isArray(entry.cascadingMiddlewares) && entry.cascadingMiddlewares.length > 0) {
|
|
759
837
|
await runMiddlewares(entry.cascadingMiddlewares, req, res);
|
|
760
838
|
if (res.headersSent) return;
|
|
761
839
|
}
|
|
762
840
|
|
|
763
|
-
if (pageMod &&
|
|
841
|
+
if (pageMod && isArray(pageMod.middleware) && pageMod.middleware.length > 0) {
|
|
764
842
|
const resolvedPageMws =
|
|
765
843
|
typeof resolveMiddlewareList === 'function'
|
|
766
844
|
? resolveMiddlewareList(pageMod.middleware)
|