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
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* external package is not installed in `node_modules`.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
const { getByPath, setByPath } = require('../utils');
|
|
27
|
+
const { isArray, getByPath, setByPath } = require('../utils');
|
|
28
28
|
|
|
29
29
|
const PluginType = {
|
|
30
30
|
Attribute: 1,
|
|
@@ -507,11 +507,15 @@ function createIsolatedControllerState(
|
|
|
507
507
|
return Reflect.get(obj, prop, receiver);
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
// Allow accessing tasks on root state
|
|
510
|
+
// Allow accessing tasks and busy status on root state
|
|
511
511
|
if (!basePath && prop === 'tasks') {
|
|
512
512
|
const ctx = getContext();
|
|
513
513
|
return ctx && ctx.tasks ? ctx.tasks : undefined;
|
|
514
514
|
}
|
|
515
|
+
if (!basePath && (prop === '$busy' || prop === 'isBusy') && !Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
516
|
+
const ctx = getContext();
|
|
517
|
+
return ctx ? Boolean(ctx.isBusy) : false;
|
|
518
|
+
}
|
|
515
519
|
|
|
516
520
|
// Check if accessing a root-level computed property
|
|
517
521
|
if (!basePath && computedDefs && Object.prototype.hasOwnProperty.call(computedDefs, prop)) {
|
|
@@ -523,7 +527,7 @@ function createIsolatedControllerState(
|
|
|
523
527
|
// Track signal dependency in Datastar signals store
|
|
524
528
|
engine.signals.value(signalKeyFor(path));
|
|
525
529
|
// If accessing array length or methods, also subscribe to the array path itself
|
|
526
|
-
if (
|
|
530
|
+
if (isArray(obj) && basePath) {
|
|
527
531
|
engine.signals.value(signalKeyFor(basePath));
|
|
528
532
|
}
|
|
529
533
|
const val = Reflect.get(obj, prop, receiver);
|
|
@@ -562,21 +566,21 @@ function createIsolatedControllerState(
|
|
|
562
566
|
const prevValue = obj[prop];
|
|
563
567
|
const path = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
564
568
|
|
|
565
|
-
if (Object.is(prevValue, rawNext) && !
|
|
569
|
+
if (Object.is(prevValue, rawNext) && !isArray(obj)) {
|
|
566
570
|
return true;
|
|
567
571
|
}
|
|
568
572
|
|
|
569
573
|
Reflect.set(obj, prop, rawNext, receiver);
|
|
570
574
|
|
|
571
575
|
// Seed nested keys if setting an object
|
|
572
|
-
if (rawNext !== null && typeof rawNext === 'object' && !
|
|
576
|
+
if (rawNext !== null && typeof rawNext === 'object' && !isArray(rawNext)) {
|
|
573
577
|
for (const [k, v] of Object.entries(rawNext)) {
|
|
574
578
|
engine.signals.setValue(signalKeyFor(`${path}.${k}`), v);
|
|
575
579
|
}
|
|
576
580
|
}
|
|
577
581
|
engine.signals.setValue(signalKeyFor(path), rawNext);
|
|
578
582
|
if (basePath) {
|
|
579
|
-
engine.signals.setValue(signalKeyFor(basePath),
|
|
583
|
+
engine.signals.setValue(signalKeyFor(basePath), isArray(obj) ? [...obj] : { ...obj });
|
|
580
584
|
}
|
|
581
585
|
return true;
|
|
582
586
|
},
|
|
@@ -609,13 +613,13 @@ function createIsolatedControllerState(
|
|
|
609
613
|
for (const [key, val] of Object.entries(initialState)) {
|
|
610
614
|
const cloned =
|
|
611
615
|
val !== null && typeof val === 'object'
|
|
612
|
-
?
|
|
616
|
+
? isArray(val)
|
|
613
617
|
? [...val]
|
|
614
618
|
: { ...val }
|
|
615
619
|
: val;
|
|
616
620
|
rawStore[key] = cloned;
|
|
617
621
|
engine.signals.setValue(signalKeyFor(key), cloned);
|
|
618
|
-
if (cloned !== null && typeof cloned === 'object' && !
|
|
622
|
+
if (cloned !== null && typeof cloned === 'object' && !isArray(cloned)) {
|
|
619
623
|
for (const [subKey, subVal] of Object.entries(cloned)) {
|
|
620
624
|
engine.signals.setValue(signalKeyFor(`${key}.${subKey}`), subVal);
|
|
621
625
|
}
|
|
@@ -734,6 +738,8 @@ function getMorphNodeKey(node) {
|
|
|
734
738
|
if (node.id) return `id:${node.id}`;
|
|
735
739
|
const wKey = node.getAttribute('data-w-key');
|
|
736
740
|
if (wKey) return `wkey:${wKey}`;
|
|
741
|
+
const hostKey = node.getAttribute('data-w-host');
|
|
742
|
+
if (hostKey) return `host:${hostKey}`;
|
|
737
743
|
const fragId = node.getAttribute('data-w-fragment-id');
|
|
738
744
|
if (fragId) return `frag:${fragId}`;
|
|
739
745
|
const formName = node.getAttribute('data-w-form');
|
|
@@ -850,6 +856,11 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
850
856
|
}
|
|
851
857
|
}
|
|
852
858
|
|
|
859
|
+
// If `toEl` is a client service UI host (`data-w-host`), the service owns its DOM contents
|
|
860
|
+
if (toEl.hasAttribute && toEl.hasAttribute('data-w-host')) {
|
|
861
|
+
return toEl;
|
|
862
|
+
}
|
|
863
|
+
|
|
853
864
|
// If `toEl` has `data-w-text`, client state owns its textContent; do not overwrite from server HTML
|
|
854
865
|
if (ownership.text) {
|
|
855
866
|
return toEl;
|
|
@@ -951,10 +962,13 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
951
962
|
}
|
|
952
963
|
}
|
|
953
964
|
|
|
954
|
-
// Remove leftover existing children that were not matched
|
|
965
|
+
// Remove leftover existing children that were not matched (preserve client service UI hosts)
|
|
955
966
|
for (let i = 0; i < existingChildren.length; i++) {
|
|
956
967
|
const oldChild = existingChildren[i];
|
|
957
968
|
if (!usedExisting.has(oldChild) && oldChild.parentNode === toEl) {
|
|
969
|
+
if (oldChild.nodeType === 1 && oldChild.hasAttribute && oldChild.hasAttribute('data-w-host')) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
958
972
|
if (oldChild.nodeType === 1 && typeof ctx.onBeforeNodeRemove === 'function') {
|
|
959
973
|
ctx.onBeforeNodeRemove(oldChild);
|
|
960
974
|
}
|
|
@@ -1124,10 +1138,31 @@ function createDatastarAdapter(options = {}) {
|
|
|
1124
1138
|
return bindings;
|
|
1125
1139
|
}
|
|
1126
1140
|
|
|
1141
|
+
function toggleElementVisibility(el, visible, initialDisplay) {
|
|
1142
|
+
if (visible) {
|
|
1143
|
+
el.removeAttribute('hidden');
|
|
1144
|
+
if (el.style && el.style.display === 'none') {
|
|
1145
|
+
el.style.display = initialDisplay;
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
el.setAttribute('hidden', '');
|
|
1149
|
+
if (el.style) {
|
|
1150
|
+
el.style.display = 'none';
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1127
1155
|
/**
|
|
1128
1156
|
* Bind V1 declarative attributes inside `rootEl` for a controller instance.
|
|
1129
1157
|
*/
|
|
1130
|
-
function bindControllerDom({
|
|
1158
|
+
function bindControllerDom({
|
|
1159
|
+
instanceId,
|
|
1160
|
+
rootEl,
|
|
1161
|
+
state,
|
|
1162
|
+
invokeAction,
|
|
1163
|
+
isConnected,
|
|
1164
|
+
isLoadingTargetActive,
|
|
1165
|
+
}) {
|
|
1131
1166
|
if (!boundElementsByInstance.has(instanceId)) {
|
|
1132
1167
|
boundElementsByInstance.set(instanceId, new WeakSet());
|
|
1133
1168
|
}
|
|
@@ -1161,16 +1196,31 @@ function createDatastarAdapter(options = {}) {
|
|
|
1161
1196
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1162
1197
|
const stop = engine.effect(() => {
|
|
1163
1198
|
if (!isConnected()) return;
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1199
|
+
toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
|
|
1200
|
+
});
|
|
1201
|
+
disposers.push(stop);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// 2b. data-w-loading & data-w-loading-hide (declarative action/task busy indicators)
|
|
1205
|
+
if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
|
|
1206
|
+
const isHide = el.hasAttribute('data-w-loading-hide');
|
|
1207
|
+
const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
|
|
1208
|
+
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1209
|
+
const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
|
|
1210
|
+
.trim()
|
|
1211
|
+
.split(/\s+/)
|
|
1212
|
+
.filter(Boolean);
|
|
1213
|
+
const stop = engine.effect(() => {
|
|
1214
|
+
if (!isConnected()) return;
|
|
1215
|
+
const active =
|
|
1216
|
+
typeof isLoadingTargetActive === 'function'
|
|
1217
|
+
? Boolean(isLoadingTargetActive(rawSpec))
|
|
1218
|
+
: Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
|
|
1219
|
+
toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
|
|
1220
|
+
if (loadingClasses.length > 0 && el.classList) {
|
|
1221
|
+
for (let c = 0; c < loadingClasses.length; c++) {
|
|
1222
|
+
if (active) el.classList.add(loadingClasses[c]);
|
|
1223
|
+
else el.classList.remove(loadingClasses[c]);
|
|
1174
1224
|
}
|
|
1175
1225
|
}
|
|
1176
1226
|
});
|
|
@@ -1249,7 +1299,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1249
1299
|
const currentVal = getByPath(state, modelPath);
|
|
1250
1300
|
|
|
1251
1301
|
if (type === 'checkbox') {
|
|
1252
|
-
if (
|
|
1302
|
+
if (isArray(currentVal)) {
|
|
1253
1303
|
el.checked = currentVal.map(String).includes(String(el.value));
|
|
1254
1304
|
} else {
|
|
1255
1305
|
el.checked = Boolean(currentVal);
|
|
@@ -1257,7 +1307,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1257
1307
|
} else if (type === 'radio') {
|
|
1258
1308
|
el.checked = String(currentVal) === String(el.value);
|
|
1259
1309
|
} else if (el.tagName === 'SELECT' && el.multiple) {
|
|
1260
|
-
const arr =
|
|
1310
|
+
const arr = isArray(currentVal) ? currentVal.map(String) : [];
|
|
1261
1311
|
const opts = el.options || [];
|
|
1262
1312
|
for (let j = 0; j < opts.length; j++) {
|
|
1263
1313
|
opts[j].selected = arr.includes(String(opts[j].value));
|
|
@@ -1282,7 +1332,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1282
1332
|
|
|
1283
1333
|
if (type === 'checkbox') {
|
|
1284
1334
|
const cur = getByPath(state, modelPath);
|
|
1285
|
-
if (
|
|
1335
|
+
if (isArray(cur)) {
|
|
1286
1336
|
const next = el.checked
|
|
1287
1337
|
? Array.from(new Set([...cur, el.value]))
|
|
1288
1338
|
: cur.filter((item) => String(item) !== String(el.value));
|
package/core/client/api.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* - Raw & streaming escape hatches (`api.raw`, `api.stream`)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
const { isArray, isNumber } = require('./utils');
|
|
17
|
+
|
|
16
18
|
const VALID_POLICIES = new Set(['latest', 'drop', 'queue', 'parallel']);
|
|
17
19
|
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
18
20
|
const RESERVED_OPTION_KEYS = new Set([
|
|
@@ -68,7 +70,7 @@ class ClientRequestError extends Error {
|
|
|
68
70
|
this.key = details.key || '';
|
|
69
71
|
this.fieldErrors = details.fieldErrors || {};
|
|
70
72
|
this.fieldErrorList = details.fieldErrorList || {};
|
|
71
|
-
this.issues =
|
|
73
|
+
this.issues = isArray(details.issues) ? details.issues : [];
|
|
72
74
|
this.formError = details.formError || null;
|
|
73
75
|
this.data = details.data !== undefined ? details.data : null;
|
|
74
76
|
this.isCanceled = this.kind === 'canceled';
|
|
@@ -176,7 +178,7 @@ function buildRequestUrl(urlTemplate, params, query) {
|
|
|
176
178
|
const searchParams = new URLSearchParams();
|
|
177
179
|
for (const [key, rawVal] of Object.entries(query)) {
|
|
178
180
|
if (rawVal === undefined) continue;
|
|
179
|
-
if (
|
|
181
|
+
if (isArray(rawVal)) {
|
|
180
182
|
for (const item of rawVal) {
|
|
181
183
|
if (item === undefined) continue;
|
|
182
184
|
searchParams.append(key, item instanceof Date ? item.toISOString() : String(item));
|
|
@@ -205,7 +207,7 @@ function containsBinaryValue(value) {
|
|
|
205
207
|
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
|
|
206
208
|
if (typeof File !== 'undefined' && value instanceof File) return true;
|
|
207
209
|
if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
|
|
208
|
-
if (
|
|
210
|
+
if (isArray(value)) {
|
|
209
211
|
return value.some(containsBinaryValue);
|
|
210
212
|
}
|
|
211
213
|
for (const v of Object.values(value)) {
|
|
@@ -221,7 +223,7 @@ function objectToFormData(obj) {
|
|
|
221
223
|
const fd = new FormData();
|
|
222
224
|
for (const [key, val] of Object.entries(obj)) {
|
|
223
225
|
if (val === undefined) continue;
|
|
224
|
-
if (
|
|
226
|
+
if (isArray(val)) {
|
|
225
227
|
for (const item of val) {
|
|
226
228
|
if (item === undefined) continue;
|
|
227
229
|
if (typeof Blob !== 'undefined' && item instanceof Blob) {
|
|
@@ -307,9 +309,9 @@ function extractValidationErrors(payload) {
|
|
|
307
309
|
|
|
308
310
|
// 1. Direct fieldErrors map (e.g. { title: 'Required' } or { title: ['Required'] })
|
|
309
311
|
const rawFieldErrors = errObj.fieldErrors || payload.fieldErrors || errObj.fields || null;
|
|
310
|
-
if (rawFieldErrors && typeof rawFieldErrors === 'object' && !
|
|
312
|
+
if (rawFieldErrors && typeof rawFieldErrors === 'object' && !isArray(rawFieldErrors)) {
|
|
311
313
|
for (const [field, msgOrArr] of Object.entries(rawFieldErrors)) {
|
|
312
|
-
const list =
|
|
314
|
+
const list = isArray(msgOrArr) ? msgOrArr.map(String) : [String(msgOrArr)];
|
|
313
315
|
if (list.length > 0) {
|
|
314
316
|
fieldErrors[field] = list[0];
|
|
315
317
|
fieldErrorList[field] = list;
|
|
@@ -320,17 +322,17 @@ function extractValidationErrors(payload) {
|
|
|
320
322
|
|
|
321
323
|
// 2. Zod / Webspresso `issues` or `details` array
|
|
322
324
|
const rawIssues =
|
|
323
|
-
(
|
|
324
|
-
(
|
|
325
|
-
(
|
|
326
|
-
(
|
|
327
|
-
(
|
|
325
|
+
(isArray(errObj.issues) && errObj.issues) ||
|
|
326
|
+
(isArray(payload.issues) && payload.issues) ||
|
|
327
|
+
(isArray(errObj.details) && errObj.details) ||
|
|
328
|
+
(isArray(payload.details) && payload.details) ||
|
|
329
|
+
(isArray(errObj.errors) && errObj.errors) ||
|
|
328
330
|
[];
|
|
329
331
|
|
|
330
332
|
for (const issue of rawIssues) {
|
|
331
333
|
if (!issue || typeof issue !== 'object') continue;
|
|
332
334
|
let rawPath = '';
|
|
333
|
-
if (
|
|
335
|
+
if (isArray(issue.path)) {
|
|
334
336
|
// Strip leading 'body' / 'query' / 'params' prefix from route validation paths
|
|
335
337
|
const cleanParts =
|
|
336
338
|
issue.path.length > 1 &&
|
|
@@ -393,8 +395,8 @@ function unwrapApiResultAndInvalidate(parsedBody, responseHeaders) {
|
|
|
393
395
|
if (
|
|
394
396
|
parsedBody &&
|
|
395
397
|
typeof parsedBody === 'object' &&
|
|
396
|
-
!
|
|
397
|
-
|
|
398
|
+
!isArray(parsedBody) &&
|
|
399
|
+
isArray(parsedBody.invalidate) &&
|
|
398
400
|
'data' in parsedBody
|
|
399
401
|
) {
|
|
400
402
|
for (const tag of parsedBody.invalidate) {
|
|
@@ -495,18 +497,18 @@ function normalizeRetryConfig(retryOpt, upperMethod, retryMutations = false) {
|
|
|
495
497
|
let statusCodes = new Set([408, 429, 502, 503, 504]);
|
|
496
498
|
let onRetry = null;
|
|
497
499
|
|
|
498
|
-
if (
|
|
500
|
+
if (isNumber(retryOpt)) {
|
|
499
501
|
count = Math.max(0, Math.floor(retryOpt));
|
|
500
502
|
} else if (retryOpt === true) {
|
|
501
503
|
count = 2;
|
|
502
504
|
} else if (typeof retryOpt === 'object' && retryOpt !== null) {
|
|
503
|
-
count =
|
|
504
|
-
if (
|
|
505
|
-
if (
|
|
505
|
+
count = isNumber(retryOpt.count) ? Math.max(0, Math.floor(retryOpt.count)) : 2;
|
|
506
|
+
if (isNumber(retryOpt.delay)) delay = Math.max(0, retryOpt.delay);
|
|
507
|
+
if (isNumber(retryOpt.maxDelay)) maxDelay = Math.max(delay, retryOpt.maxDelay);
|
|
506
508
|
if (retryOpt.backoff === 'linear' || retryOpt.backoff === 'exponential') {
|
|
507
509
|
backoff = retryOpt.backoff;
|
|
508
510
|
}
|
|
509
|
-
if (
|
|
511
|
+
if (isArray(retryOpt.statusCodes)) {
|
|
510
512
|
statusCodes = new Set(retryOpt.statusCodes);
|
|
511
513
|
}
|
|
512
514
|
if (typeof retryOpt.onRetry === 'function') {
|
|
@@ -536,7 +538,7 @@ function isRetryableError(err, statusCodes) {
|
|
|
536
538
|
// Network failures and timeouts are retryable
|
|
537
539
|
if (err.kind === 'network' || err.kind === 'timeout' || err.isTimedOut) return true;
|
|
538
540
|
// Transient HTTP status codes
|
|
539
|
-
if (
|
|
541
|
+
if (isNumber(err.status) && statusCodes && statusCodes.has(err.status)) {
|
|
540
542
|
return true;
|
|
541
543
|
}
|
|
542
544
|
return false;
|
|
@@ -797,7 +799,7 @@ function createApiClient(config = {}) {
|
|
|
797
799
|
let timeoutTimer = null;
|
|
798
800
|
let currentSeq = 1;
|
|
799
801
|
|
|
800
|
-
if (
|
|
802
|
+
if (isNumber(options.timeout) && options.timeout > 0) {
|
|
801
803
|
timeoutTimer = setTimeout(() => {
|
|
802
804
|
timedOut = true;
|
|
803
805
|
abortController.abort();
|