webspresso 0.0.110 → 0.0.112
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 +190 -51
- package/core/client/api.js +23 -21
- package/core/client/controller.js +407 -40
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +163 -29
- package/core/client/fragments.js +616 -118
- package/core/client/host.js +369 -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 +142 -22
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +334 -29
- 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.9 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,34 @@ 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,
|
|
115
|
+
persistInput:
|
|
116
|
+
config.persistInput !== undefined
|
|
117
|
+
? config.persistInput
|
|
118
|
+
: config.persist !== undefined
|
|
119
|
+
? config.persist
|
|
120
|
+
: undefined,
|
|
97
121
|
};
|
|
98
122
|
}
|
|
99
123
|
|
|
@@ -114,7 +138,7 @@ function defineFragment(config = {}) {
|
|
|
114
138
|
* @returns {{ __isWebspressoApiResult: true, data: T, invalidate: string[], status?: number, headers?: Record<string, string> }}
|
|
115
139
|
*/
|
|
116
140
|
function apiResult(data, meta = {}) {
|
|
117
|
-
const rawInvalidate = meta &&
|
|
141
|
+
const rawInvalidate = meta && isArray(meta.invalidate) ? meta.invalidate : [];
|
|
118
142
|
const invalidate = Array.from(
|
|
119
143
|
new Set(rawInvalidate.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
|
|
120
144
|
);
|
|
@@ -122,7 +146,7 @@ function apiResult(data, meta = {}) {
|
|
|
122
146
|
__isWebspressoApiResult: true,
|
|
123
147
|
data,
|
|
124
148
|
invalidate,
|
|
125
|
-
status: meta &&
|
|
149
|
+
status: meta && isNumber(meta.status) ? meta.status : undefined,
|
|
126
150
|
headers: meta && meta.headers && typeof meta.headers === 'object' ? { ...meta.headers } : undefined,
|
|
127
151
|
};
|
|
128
152
|
}
|
|
@@ -131,11 +155,11 @@ function apiResult(data, meta = {}) {
|
|
|
131
155
|
* Check whether a value returned by an API handler is an `apiResult` or `{ data, invalidate }` envelope.
|
|
132
156
|
*/
|
|
133
157
|
function isApiResultEnvelope(val) {
|
|
134
|
-
if (!val || typeof val !== 'object' ||
|
|
158
|
+
if (!val || typeof val !== 'object' || isArray(val)) return false;
|
|
135
159
|
if (val.__isWebspressoApiResult === true) return true;
|
|
136
160
|
if (
|
|
137
161
|
'data' in val &&
|
|
138
|
-
|
|
162
|
+
isArray(val.invalidate) &&
|
|
139
163
|
val.invalidate.every((item) => typeof item === 'string')
|
|
140
164
|
) {
|
|
141
165
|
return true;
|
|
@@ -153,7 +177,7 @@ function isApiResultEnvelope(val) {
|
|
|
153
177
|
*/
|
|
154
178
|
function sendApiResult(req, res, rawResult) {
|
|
155
179
|
const status = rawResult.status || (res.statusCode && res.statusCode !== 200 ? res.statusCode : 200);
|
|
156
|
-
const rawTags =
|
|
180
|
+
const rawTags = isArray(rawResult.invalidate) ? rawResult.invalidate : [];
|
|
157
181
|
const tags = Array.from(
|
|
158
182
|
new Set(rawTags.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
|
|
159
183
|
);
|
|
@@ -265,7 +289,7 @@ function ensureRequestMemo(req, ctx) {
|
|
|
265
289
|
* @returns {Record<string, unknown>}
|
|
266
290
|
*/
|
|
267
291
|
function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
|
|
268
|
-
const inputObj = rawInput && typeof rawInput === 'object' && !
|
|
292
|
+
const inputObj = rawInput && typeof rawInput === 'object' && !isArray(rawInput) ? rawInput : {};
|
|
269
293
|
if (!fragmentDef || !fragmentDef.schema) {
|
|
270
294
|
return { ...inputObj };
|
|
271
295
|
}
|
|
@@ -295,7 +319,7 @@ function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
|
|
|
295
319
|
const parsed = resolvedSchema.safeParse(inputObj);
|
|
296
320
|
if (!parsed.success) {
|
|
297
321
|
const issues = (parsed.error && parsed.error.issues ? parsed.error.issues : []).map((iss) => ({
|
|
298
|
-
path:
|
|
322
|
+
path: isArray(iss.path) ? iss.path.join('.') : String(iss.path || ''),
|
|
299
323
|
message: iss.message,
|
|
300
324
|
code: iss.code,
|
|
301
325
|
}));
|
|
@@ -329,18 +353,18 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
|
|
|
329
353
|
if (!user) {
|
|
330
354
|
throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
|
|
331
355
|
}
|
|
332
|
-
const roles =
|
|
356
|
+
const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
|
|
333
357
|
if (!roles.includes(authRule)) {
|
|
334
358
|
throw new ForbiddenError(`Role "${authRule}" required to access fragment "${fragmentName}".`);
|
|
335
359
|
}
|
|
336
360
|
return;
|
|
337
361
|
}
|
|
338
362
|
|
|
339
|
-
if (
|
|
363
|
+
if (isArray(authRule)) {
|
|
340
364
|
if (!user) {
|
|
341
365
|
throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
|
|
342
366
|
}
|
|
343
|
-
const roles =
|
|
367
|
+
const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
|
|
344
368
|
if (!authRule.some((r) => roles.includes(r))) {
|
|
345
369
|
throw new ForbiddenError(`Insufficient role to access fragment "${fragmentName}".`);
|
|
346
370
|
}
|
|
@@ -363,7 +387,23 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
|
|
|
363
387
|
* Notice: ONLY validated filter/pagination `input` is serialized in `data-w-fragment-input`.
|
|
364
388
|
* Server loader data (`fragmentData`) is NEVER serialized as props!
|
|
365
389
|
*/
|
|
366
|
-
function wrapFragmentRootHtml({
|
|
390
|
+
function wrapFragmentRootHtml({
|
|
391
|
+
name,
|
|
392
|
+
instanceId,
|
|
393
|
+
pagePath,
|
|
394
|
+
dependsOn,
|
|
395
|
+
validatedInput,
|
|
396
|
+
innerHtml,
|
|
397
|
+
tag = 'div',
|
|
398
|
+
className = '',
|
|
399
|
+
staleTime,
|
|
400
|
+
refetchOnWindowFocus,
|
|
401
|
+
refetchOnReconnect,
|
|
402
|
+
refetchOnMount,
|
|
403
|
+
refetchInterval,
|
|
404
|
+
refetchIntervalInBackground,
|
|
405
|
+
persistInput,
|
|
406
|
+
}) {
|
|
367
407
|
const safeTag = /^[a-zA-Z][a-zA-Z0-9-]*$/.test(tag || 'div') ? tag : 'div';
|
|
368
408
|
const attrs = [
|
|
369
409
|
`data-w-fragment="${escapeHtmlAttr(name)}"`,
|
|
@@ -371,7 +411,7 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
|
|
|
371
411
|
`data-w-fragment-page="${escapeHtmlAttr(pagePath || '/')}"`,
|
|
372
412
|
];
|
|
373
413
|
|
|
374
|
-
if (
|
|
414
|
+
if (isArray(dependsOn) && dependsOn.length > 0) {
|
|
375
415
|
attrs.push(`data-w-depends-on="${escapeHtmlAttr(dependsOn.join(','))}"`);
|
|
376
416
|
}
|
|
377
417
|
|
|
@@ -379,6 +419,36 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
|
|
|
379
419
|
attrs.push(`data-w-fragment-input="${escapeHtmlAttr(JSON.stringify(validatedInput))}"`);
|
|
380
420
|
}
|
|
381
421
|
|
|
422
|
+
if (staleTime !== undefined && staleTime !== null && staleTime !== false) {
|
|
423
|
+
attrs.push(`data-w-stale-time="${escapeHtmlAttr(String(staleTime))}"`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (refetchOnWindowFocus !== undefined && refetchOnWindowFocus !== null) {
|
|
427
|
+
attrs.push(`data-w-refetch-on-focus="${escapeHtmlAttr(String(refetchOnWindowFocus))}"`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (refetchOnReconnect !== undefined && refetchOnReconnect !== null) {
|
|
431
|
+
attrs.push(`data-w-refetch-on-reconnect="${escapeHtmlAttr(String(refetchOnReconnect))}"`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (refetchOnMount !== undefined && refetchOnMount !== null) {
|
|
435
|
+
attrs.push(`data-w-refetch-on-mount="${escapeHtmlAttr(String(refetchOnMount))}"`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (refetchInterval !== undefined && refetchInterval !== null && refetchInterval !== false) {
|
|
439
|
+
attrs.push(`data-w-refetch-interval="${escapeHtmlAttr(String(refetchInterval))}"`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (refetchIntervalInBackground !== undefined && refetchIntervalInBackground !== null) {
|
|
443
|
+
attrs.push(`data-w-refetch-in-background="${escapeHtmlAttr(String(refetchIntervalInBackground))}"`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (persistInput !== undefined && persistInput !== null && persistInput !== false) {
|
|
447
|
+
const persistAttrVal =
|
|
448
|
+
persistInput === true || persistInput === 'input' ? 'session' : String(persistInput);
|
|
449
|
+
attrs.push(`data-w-persist-input="${escapeHtmlAttr(persistAttrVal)}"`);
|
|
450
|
+
}
|
|
451
|
+
|
|
382
452
|
if (className) {
|
|
383
453
|
attrs.push(`class="${escapeHtmlAttr(className)}"`);
|
|
384
454
|
}
|
|
@@ -432,7 +502,7 @@ async function executeAndRenderFragment({
|
|
|
432
502
|
ensureRequestMemo(req, ctx);
|
|
433
503
|
|
|
434
504
|
// 1. Run fragment-specific middlewares if any
|
|
435
|
-
if (
|
|
505
|
+
if (isArray(fragmentDef.middleware) && fragmentDef.middleware.length > 0) {
|
|
436
506
|
await runMiddlewares(fragmentDef.middleware, req, res);
|
|
437
507
|
}
|
|
438
508
|
|
|
@@ -494,6 +564,33 @@ async function executeAndRenderFragment({
|
|
|
494
564
|
innerHtml,
|
|
495
565
|
tag: options.tag || 'div',
|
|
496
566
|
className: options.class || options.className || '',
|
|
567
|
+
staleTime: options.staleTime !== undefined ? options.staleTime : fragmentDef.staleTime,
|
|
568
|
+
refetchOnWindowFocus:
|
|
569
|
+
options.refetchOnWindowFocus !== undefined
|
|
570
|
+
? options.refetchOnWindowFocus
|
|
571
|
+
: options.refetchOnFocus !== undefined
|
|
572
|
+
? options.refetchOnFocus
|
|
573
|
+
: fragmentDef.refetchOnWindowFocus,
|
|
574
|
+
refetchOnReconnect:
|
|
575
|
+
options.refetchOnReconnect !== undefined ? options.refetchOnReconnect : fragmentDef.refetchOnReconnect,
|
|
576
|
+
refetchOnMount:
|
|
577
|
+
options.refetchOnMount !== undefined
|
|
578
|
+
? options.refetchOnMount
|
|
579
|
+
: options.refetchOnVisit !== undefined
|
|
580
|
+
? options.refetchOnVisit
|
|
581
|
+
: fragmentDef.refetchOnMount,
|
|
582
|
+
refetchInterval:
|
|
583
|
+
options.refetchInterval !== undefined ? options.refetchInterval : fragmentDef.refetchInterval,
|
|
584
|
+
refetchIntervalInBackground:
|
|
585
|
+
options.refetchIntervalInBackground !== undefined
|
|
586
|
+
? options.refetchIntervalInBackground
|
|
587
|
+
: fragmentDef.refetchIntervalInBackground,
|
|
588
|
+
persistInput:
|
|
589
|
+
options.persistInput !== undefined
|
|
590
|
+
? options.persistInput
|
|
591
|
+
: options.persist !== undefined
|
|
592
|
+
? options.persist
|
|
593
|
+
: fragmentDef.persistInput,
|
|
497
594
|
});
|
|
498
595
|
|
|
499
596
|
return {
|
|
@@ -542,6 +639,24 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
542
639
|
|
|
543
640
|
const currentRenderCtx = typeof getRenderCtx === 'function' ? getRenderCtx() : { fsy };
|
|
544
641
|
|
|
642
|
+
// If refreshing a parent fragment that contains this nested child fragment,
|
|
643
|
+
// merge the active client-side child input (`req.__fragmentChildInputs`) over static template defaults
|
|
644
|
+
const childInputs = req && req.__fragmentChildInputs;
|
|
645
|
+
let effectiveRawInput = rawInput || {};
|
|
646
|
+
if (childInputs && typeof childInputs === 'object') {
|
|
647
|
+
const override = Object.prototype.hasOwnProperty.call(childInputs, instanceId)
|
|
648
|
+
? childInputs[instanceId]
|
|
649
|
+
: instanceId === `${cleanName}:default` && Object.prototype.hasOwnProperty.call(childInputs, cleanName)
|
|
650
|
+
? childInputs[cleanName]
|
|
651
|
+
: undefined;
|
|
652
|
+
if (override && typeof override === 'object' && !isArray(override)) {
|
|
653
|
+
effectiveRawInput =
|
|
654
|
+
effectiveRawInput && typeof effectiveRawInput === 'object' && !isArray(effectiveRawInput)
|
|
655
|
+
? { ...effectiveRawInput, ...override }
|
|
656
|
+
: { ...override };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
545
660
|
// Queue execution on `req.__pendingSsrFragments` and return placeholder token
|
|
546
661
|
const placeholderIndex = req.__pendingSsrFragments.length;
|
|
547
662
|
const token = `<!--__WEBSPRESSO_SSR_FRAGMENT_${placeholderIndex}__-->`;
|
|
@@ -551,7 +666,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
551
666
|
fragmentDef,
|
|
552
667
|
instanceId,
|
|
553
668
|
pagePath,
|
|
554
|
-
rawInput:
|
|
669
|
+
rawInput: effectiveRawInput,
|
|
555
670
|
req,
|
|
556
671
|
res,
|
|
557
672
|
ctx,
|
|
@@ -588,7 +703,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
588
703
|
* @returns {Promise<string>}
|
|
589
704
|
*/
|
|
590
705
|
async function resolvePendingSsrFragments(html, req) {
|
|
591
|
-
if (typeof html !== 'string' || !req || !
|
|
706
|
+
if (typeof html !== 'string' || !req || !isArray(req.__pendingSsrFragments)) {
|
|
592
707
|
return html;
|
|
593
708
|
}
|
|
594
709
|
|
|
@@ -627,7 +742,7 @@ function trimTrailingSlashes(str) {
|
|
|
627
742
|
* @returns {{ entry: object, params: Record<string, string> }|null}
|
|
628
743
|
*/
|
|
629
744
|
function matchPageEntryByPath(pagePath, pageEntries) {
|
|
630
|
-
if (!pagePath || !
|
|
745
|
+
if (!pagePath || !isArray(pageEntries)) return null;
|
|
631
746
|
const cleanPath = trimTrailingSlashes(String(pagePath).split('?')[0]) || '/';
|
|
632
747
|
|
|
633
748
|
// 1. Exact static match
|
|
@@ -722,7 +837,7 @@ function createFragmentRefreshHandler(config) {
|
|
|
722
837
|
},
|
|
723
838
|
];
|
|
724
839
|
} else {
|
|
725
|
-
if (!
|
|
840
|
+
if (!isArray(payload.fragments) || payload.fragments.length === 0) {
|
|
726
841
|
throw new BadRequestError('Fragment refresh request requires a non-empty `fragments` array.');
|
|
727
842
|
}
|
|
728
843
|
requestedFragments = payload.fragments;
|
|
@@ -755,12 +870,12 @@ function createFragmentRefreshHandler(config) {
|
|
|
755
870
|
}
|
|
756
871
|
|
|
757
872
|
// 1. Execute page-level cascading middlewares (`_hooks.js`) and page route `middleware`
|
|
758
|
-
if (
|
|
873
|
+
if (isArray(entry.cascadingMiddlewares) && entry.cascadingMiddlewares.length > 0) {
|
|
759
874
|
await runMiddlewares(entry.cascadingMiddlewares, req, res);
|
|
760
875
|
if (res.headersSent) return;
|
|
761
876
|
}
|
|
762
877
|
|
|
763
|
-
if (pageMod &&
|
|
878
|
+
if (pageMod && isArray(pageMod.middleware) && pageMod.middleware.length > 0) {
|
|
764
879
|
const resolvedPageMws =
|
|
765
880
|
typeof resolveMiddlewareList === 'function'
|
|
766
881
|
? resolveMiddlewareList(pageMod.middleware)
|
|
@@ -779,6 +894,11 @@ function createFragmentRefreshHandler(config) {
|
|
|
779
894
|
const ctx = typeof buildContext === 'function' ? buildContext(req, res, { fsy }) : { req, res, fsy };
|
|
780
895
|
ensureRequestMemo(req, ctx);
|
|
781
896
|
|
|
897
|
+
req.__fragmentChildInputs =
|
|
898
|
+
payload.childInputs && typeof payload.childInputs === 'object' && !isArray(payload.childInputs)
|
|
899
|
+
? payload.childInputs
|
|
900
|
+
: null;
|
|
901
|
+
|
|
782
902
|
const renderedInstanceIds = new Set();
|
|
783
903
|
attachSsrFragmentHelper({
|
|
784
904
|
req,
|