webspresso 0.0.112 → 0.0.114
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/README.md +26 -0
- package/bin/commands/admin-password.js +1 -7
- package/bin/commands/api.js +1 -7
- package/bin/commands/new.js +1 -7
- package/bin/commands/page.js +1 -7
- package/bin/commands/seed.js +1 -7
- package/bin/commands/service.js +1 -7
- package/bin/utils/prompt.js +17 -0
- package/core/cache/tagged-lru.js +9 -3
- package/core/client/adapter/datastar.js +602 -26
- package/core/client/api.js +151 -7
- package/core/client/controller.js +172 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +479 -29
- package/core/client/fragments.js +456 -27
- package/core/client/index.js +18 -1
- package/core/client/server-fragments.js +125 -40
- package/core/client/utils.js +72 -17
- package/core/console/commands.js +26 -5
- package/core/console/repl.js +7 -1
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/plugins/plugin-manager.js +155 -1
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +113 -1
- package/package.json +12 -11
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +140 -6
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/dev-reload/index.js +44 -2
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
- package/src/server.js +59 -3
package/core/client/api.js
CHANGED
|
@@ -84,9 +84,28 @@ class ClientRequestError extends Error {
|
|
|
84
84
|
this.status === 408;
|
|
85
85
|
this.isDropped = Boolean(details.isDropped);
|
|
86
86
|
this.isSuperseded = Boolean(details.isSuperseded);
|
|
87
|
+
this.dispatched =
|
|
88
|
+
details.dispatched !== undefined
|
|
89
|
+
? Boolean(details.dispatched)
|
|
90
|
+
: !this.isDropped && (this.kind !== 'canceled' || Boolean(details.cause));
|
|
91
|
+
this.outcomeUnknown =
|
|
92
|
+
details.outcomeUnknown !== undefined
|
|
93
|
+
? Boolean(details.outcomeUnknown)
|
|
94
|
+
: MUTATION_METHODS.has(this.method) &&
|
|
95
|
+
this.dispatched &&
|
|
96
|
+
!this.isDropped &&
|
|
97
|
+
(this.kind === 'timeout' || this.kind === 'network' || this.kind === 'canceled');
|
|
98
|
+
this.outcome = this.outcomeUnknown
|
|
99
|
+
? 'unknown'
|
|
100
|
+
: !this.dispatched || this.isDropped
|
|
101
|
+
? 'not_sent'
|
|
102
|
+
: 'failed';
|
|
87
103
|
this.serverRollbackGuaranteed = false;
|
|
88
104
|
if (this.isCanceled) {
|
|
89
105
|
this.note = 'Client request cancellation/abort does not roll back server-side execution.';
|
|
106
|
+
} else if (this.outcomeUnknown) {
|
|
107
|
+
this.note =
|
|
108
|
+
'Mutation request was dispatched before response delivery failed; the server may have committed the operation.';
|
|
90
109
|
}
|
|
91
110
|
if (details.cause) {
|
|
92
111
|
this.cause = details.cause;
|
|
@@ -599,6 +618,114 @@ function abortableSleep(ms, signals = []) {
|
|
|
599
618
|
});
|
|
600
619
|
}
|
|
601
620
|
|
|
621
|
+
function parseXhrHeaders(rawHeaders) {
|
|
622
|
+
const map = new Map();
|
|
623
|
+
if (typeof rawHeaders === 'string' && rawHeaders) {
|
|
624
|
+
const lines = rawHeaders.trim().split(/[\r\n]+/);
|
|
625
|
+
for (let i = 0; i < lines.length; i++) {
|
|
626
|
+
const idx = lines[i].indexOf(':');
|
|
627
|
+
if (idx > 0) {
|
|
628
|
+
const k = lines[i].slice(0, idx).trim().toLowerCase();
|
|
629
|
+
const v = lines[i].slice(idx + 1).trim();
|
|
630
|
+
map.set(k, v);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
get(name) {
|
|
636
|
+
return map.get(String(name || '').toLowerCase()) ?? null;
|
|
637
|
+
},
|
|
638
|
+
has(name) {
|
|
639
|
+
return map.has(String(name || '').toLowerCase());
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function executeXhrUpload({ url, method, headers, body, credentials, signal, onUploadProgress }) {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
const xhr = new XMLHttpRequest();
|
|
647
|
+
xhr.open(method, url, true);
|
|
648
|
+
if (credentials === 'include' || credentials === 'same-origin') {
|
|
649
|
+
xhr.withCredentials = credentials === 'include';
|
|
650
|
+
}
|
|
651
|
+
if (headers && typeof headers === 'object') {
|
|
652
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
653
|
+
if (v !== undefined && v !== null) {
|
|
654
|
+
xhr.setRequestHeader(k, String(v));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (xhr.upload && typeof onUploadProgress === 'function') {
|
|
660
|
+
xhr.upload.onprogress = (evt) => {
|
|
661
|
+
const lengthComputable = Boolean(evt && evt.lengthComputable);
|
|
662
|
+
const loaded = evt && typeof evt.loaded === 'number' ? evt.loaded : 0;
|
|
663
|
+
const total = lengthComputable && evt && typeof evt.total === 'number' ? evt.total : 0;
|
|
664
|
+
const percent = lengthComputable && total > 0 ? Math.min(100, Math.round((loaded / total) * 100)) : null;
|
|
665
|
+
try {
|
|
666
|
+
onUploadProgress({ loaded, total, percent, lengthComputable });
|
|
667
|
+
} catch (_e) {}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const onAbort = () => {
|
|
672
|
+
try {
|
|
673
|
+
xhr.abort();
|
|
674
|
+
} catch (_e) {}
|
|
675
|
+
};
|
|
676
|
+
if (signal) {
|
|
677
|
+
if (signal.aborted) {
|
|
678
|
+
const abortErr = new Error('The operation was aborted.');
|
|
679
|
+
abortErr.name = 'AbortError';
|
|
680
|
+
reject(abortErr);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const cleanup = () => {
|
|
687
|
+
if (signal) {
|
|
688
|
+
try {
|
|
689
|
+
signal.removeEventListener('abort', onAbort);
|
|
690
|
+
} catch (_e) {}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
xhr.onload = () => {
|
|
695
|
+
cleanup();
|
|
696
|
+
if (xhr.status === 0) {
|
|
697
|
+
reject(new TypeError('Network request failed'));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const status = xhr.status === 1223 ? 204 : xhr.status;
|
|
701
|
+
const rawText = typeof xhr.responseText === 'string' ? xhr.responseText : '';
|
|
702
|
+
const headerObj = parseXhrHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : '');
|
|
703
|
+
resolve({
|
|
704
|
+
ok: status >= 200 && status < 300,
|
|
705
|
+
status,
|
|
706
|
+
statusText: xhr.statusText || '',
|
|
707
|
+
headers: headerObj,
|
|
708
|
+
text: async () => rawText,
|
|
709
|
+
json: async () => (rawText ? JSON.parse(rawText) : null),
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
xhr.onerror = () => {
|
|
714
|
+
cleanup();
|
|
715
|
+
reject(new TypeError('Network request failed'));
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
xhr.onabort = () => {
|
|
719
|
+
cleanup();
|
|
720
|
+
const abortErr = new Error('The operation was aborted.');
|
|
721
|
+
abortErr.name = 'AbortError';
|
|
722
|
+
reject(abortErr);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
xhr.send(body === undefined ? null : body);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
602
729
|
/**
|
|
603
730
|
* Create a Typed API Client instance.
|
|
604
731
|
*
|
|
@@ -675,6 +802,8 @@ function createApiClient(config = {}) {
|
|
|
675
802
|
method: upperMethod,
|
|
676
803
|
url: requestUrl,
|
|
677
804
|
key: requestKey,
|
|
805
|
+
dispatched: false,
|
|
806
|
+
outcomeUnknown: false,
|
|
678
807
|
});
|
|
679
808
|
}
|
|
680
809
|
|
|
@@ -883,13 +1012,25 @@ function createApiClient(config = {}) {
|
|
|
883
1012
|
try {
|
|
884
1013
|
let response;
|
|
885
1014
|
try {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1015
|
+
if (typeof options.onUploadProgress === 'function' && typeof XMLHttpRequest !== 'undefined') {
|
|
1016
|
+
response = await executeXhrUpload({
|
|
1017
|
+
url: requestUrl,
|
|
1018
|
+
method: upperMethod,
|
|
1019
|
+
headers,
|
|
1020
|
+
body: serializedBody,
|
|
1021
|
+
credentials: options.credentials || 'same-origin',
|
|
1022
|
+
signal: abortController.signal,
|
|
1023
|
+
onUploadProgress: options.onUploadProgress,
|
|
1024
|
+
});
|
|
1025
|
+
} else {
|
|
1026
|
+
response = await fetchImpl(requestUrl, {
|
|
1027
|
+
method: upperMethod,
|
|
1028
|
+
headers,
|
|
1029
|
+
body: serializedBody,
|
|
1030
|
+
credentials: options.credentials || 'same-origin',
|
|
1031
|
+
signal: abortController.signal,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
893
1034
|
} catch (fetchErr) {
|
|
894
1035
|
if (timeoutTimer) {
|
|
895
1036
|
clearTimeout(timeoutTimer);
|
|
@@ -914,6 +1055,7 @@ function createApiClient(config = {}) {
|
|
|
914
1055
|
url: requestUrl,
|
|
915
1056
|
key: requestKey,
|
|
916
1057
|
isTimedOut: true,
|
|
1058
|
+
dispatched: true,
|
|
917
1059
|
cause: fetchErr,
|
|
918
1060
|
}
|
|
919
1061
|
);
|
|
@@ -947,6 +1089,7 @@ function createApiClient(config = {}) {
|
|
|
947
1089
|
url: requestUrl,
|
|
948
1090
|
key: requestKey,
|
|
949
1091
|
isSuperseded: superseded,
|
|
1092
|
+
dispatched: true,
|
|
950
1093
|
cause: fetchErr,
|
|
951
1094
|
}
|
|
952
1095
|
);
|
|
@@ -962,6 +1105,7 @@ function createApiClient(config = {}) {
|
|
|
962
1105
|
method: upperMethod,
|
|
963
1106
|
url: requestUrl,
|
|
964
1107
|
key: requestKey,
|
|
1108
|
+
dispatched: true,
|
|
965
1109
|
cause: fetchErr,
|
|
966
1110
|
}
|
|
967
1111
|
);
|
|
@@ -367,6 +367,8 @@ function defineController(definition = {}) {
|
|
|
367
367
|
tasks: definition.tasks && typeof definition.tasks === 'object' ? definition.tasks : {},
|
|
368
368
|
connect: typeof definition.connect === 'function' ? definition.connect : null,
|
|
369
369
|
disconnect: typeof definition.disconnect === 'function' ? definition.disconnect : null,
|
|
370
|
+
beforePatch: typeof definition.beforePatch === 'function' ? definition.beforePatch : null,
|
|
371
|
+
afterPatch: typeof definition.afterPatch === 'function' ? definition.afterPatch : null,
|
|
370
372
|
onError: typeof definition.onError === 'function' ? definition.onError : null,
|
|
371
373
|
persist: definition.persist || null,
|
|
372
374
|
watch: definition.watch && typeof definition.watch === 'object' ? definition.watch : null,
|
|
@@ -738,19 +740,26 @@ function createControllerRuntime(options = {}) {
|
|
|
738
740
|
const signalPrefix = `__w_inst_${instanceId}.task_${taskKey}`;
|
|
739
741
|
|
|
740
742
|
// React Query-style automatic revalidation options for tasks
|
|
743
|
+
const isMutationTask = taskOpts.mutation === true || taskOpts.kind === 'mutation';
|
|
741
744
|
const staleTimeMs = parseDurationMs(taskOpts.staleTime, 0);
|
|
742
|
-
const refetchOnMountMode =
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
745
|
+
const refetchOnMountMode = isMutationTask
|
|
746
|
+
? false
|
|
747
|
+
: parseRefetchMode(
|
|
748
|
+
taskOpts.refetchOnMount !== undefined ? taskOpts.refetchOnMount : taskOpts.refetchOnVisit,
|
|
749
|
+
false
|
|
750
|
+
);
|
|
751
|
+
const refetchOnFocusMode = isMutationTask
|
|
752
|
+
? false
|
|
753
|
+
: parseRefetchMode(
|
|
754
|
+
taskOpts.refetchOnWindowFocus !== undefined ? taskOpts.refetchOnWindowFocus : taskOpts.refetchOnFocus,
|
|
755
|
+
false
|
|
756
|
+
);
|
|
757
|
+
const refetchOnReconnectMode = isMutationTask
|
|
758
|
+
? false
|
|
759
|
+
: parseRefetchMode(taskOpts.refetchOnReconnect, false);
|
|
760
|
+
const refetchIntervalMs = isMutationTask ? 0 : parseDurationMs(taskOpts.refetchInterval, 0);
|
|
752
761
|
const refetchIntervalInBg =
|
|
753
|
-
parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
762
|
+
!isMutationTask && parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
754
763
|
|
|
755
764
|
let currentRunVersion = 0;
|
|
756
765
|
let activeRuns = 0;
|
|
@@ -1507,6 +1516,15 @@ function createControllerRuntime(options = {}) {
|
|
|
1507
1516
|
const inst = fragments && fragments.getInstance ? fragments.getInstance(idOrName) : null;
|
|
1508
1517
|
return inst && inst.input ? { ...inst.input } : {};
|
|
1509
1518
|
},
|
|
1519
|
+
get inputs() {
|
|
1520
|
+
if (!fragments || typeof fragments.getInstances !== 'function') return {};
|
|
1521
|
+
const list = fragments.getInstances(idOrName);
|
|
1522
|
+
const map = {};
|
|
1523
|
+
for (const inst of list) {
|
|
1524
|
+
map[inst.instanceId] = inst.input ? { ...inst.input } : {};
|
|
1525
|
+
}
|
|
1526
|
+
return map;
|
|
1527
|
+
},
|
|
1510
1528
|
refresh: (input, opts) => {
|
|
1511
1529
|
checkFragmentPersistenceConflict(idOrName, input);
|
|
1512
1530
|
if (!fragments) {
|
|
@@ -1797,6 +1815,33 @@ function createControllerRuntime(options = {}) {
|
|
|
1797
1815
|
get isConnected() {
|
|
1798
1816
|
return isConnected();
|
|
1799
1817
|
},
|
|
1818
|
+
invokeBeforePatch(meta = {}) {
|
|
1819
|
+
if (!isConnected() || !reg.definition.beforePatch) return;
|
|
1820
|
+
try {
|
|
1821
|
+
const result = reg.definition.beforePatch(ctx, meta);
|
|
1822
|
+
if (result && typeof result.then === 'function') {
|
|
1823
|
+
// beforePatch must be synchronous; warn in dev and do not block morph
|
|
1824
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') {
|
|
1825
|
+
console.warn(`[webspresso] Controller "${controllerName}" beforePatch() returned a Promise; beforePatch hooks are synchronous and are not awaited.`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
handleControllerError(err, { phase: 'beforePatch' });
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
invokeAfterPatch(meta = {}) {
|
|
1833
|
+
if (!isConnected() || !reg.definition.afterPatch) return;
|
|
1834
|
+
try {
|
|
1835
|
+
const res = reg.definition.afterPatch(ctx, meta);
|
|
1836
|
+
if (res && typeof res.then === 'function') {
|
|
1837
|
+
res.catch((err) => {
|
|
1838
|
+
handleControllerError(err, { phase: 'afterPatch' });
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
handleControllerError(err, { phase: 'afterPatch' });
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1800
1845
|
destroy() {
|
|
1801
1846
|
if (!connected) return;
|
|
1802
1847
|
connected = false;
|
|
@@ -2021,6 +2066,117 @@ function createControllerRuntime(options = {}) {
|
|
|
2021
2066
|
return mounted;
|
|
2022
2067
|
}
|
|
2023
2068
|
|
|
2069
|
+
let activeHookContext = null;
|
|
2070
|
+
|
|
2071
|
+
function isInsideIgnoredSubtree(el, rootEl) {
|
|
2072
|
+
let cur = el && el.parentElement ? el.parentElement : null;
|
|
2073
|
+
while (cur) {
|
|
2074
|
+
if (typeof cur.hasAttribute === 'function' && cur.hasAttribute('data-w-ignore')) {
|
|
2075
|
+
const mode = (cur.getAttribute('data-w-ignore') || '').trim().toLowerCase();
|
|
2076
|
+
if (mode !== 'self') {
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (cur === rootEl) break;
|
|
2081
|
+
cur = cur.parentElement || null;
|
|
2082
|
+
}
|
|
2083
|
+
if (el === rootEl && el && typeof el.hasAttribute === 'function' && el.hasAttribute('data-w-ignore')) {
|
|
2084
|
+
const mode = (el.getAttribute('data-w-ignore') || '').trim().toLowerCase();
|
|
2085
|
+
if (mode === 'all' || mode === 'both') {
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function collectAffectedInstances(fragmentEl) {
|
|
2093
|
+
if (!fragmentEl) return [];
|
|
2094
|
+
const affected = [];
|
|
2095
|
+
for (const inst of activeInstances.values()) {
|
|
2096
|
+
if (!inst || !inst.el || !inst.isConnected) continue;
|
|
2097
|
+
if (
|
|
2098
|
+
inst.el === fragmentEl ||
|
|
2099
|
+
(typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el)) ||
|
|
2100
|
+
(typeof inst.el.contains === 'function' && inst.el.contains(fragmentEl))
|
|
2101
|
+
) {
|
|
2102
|
+
if (isInsideIgnoredSubtree(inst.el, fragmentEl)) continue;
|
|
2103
|
+
affected.push(inst);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
// Sort deterministically: outer ancestors first, inner descendants second
|
|
2107
|
+
affected.sort((a, b) => {
|
|
2108
|
+
if (a.el === b.el) return 0;
|
|
2109
|
+
if (typeof a.el.contains === 'function' && a.el.contains(b.el)) return -1;
|
|
2110
|
+
if (typeof b.el.contains === 'function' && b.el.contains(a.el)) return 1;
|
|
2111
|
+
if (typeof a.el.compareDocumentPosition === 'function') {
|
|
2112
|
+
const pos = a.el.compareDocumentPosition(b.el);
|
|
2113
|
+
if (pos & 4) return -1; // DOCUMENT_POSITION_FOLLOWING
|
|
2114
|
+
if (pos & 2) return 1; // DOCUMENT_POSITION_PRECEDING
|
|
2115
|
+
}
|
|
2116
|
+
return 0;
|
|
2117
|
+
});
|
|
2118
|
+
return affected;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function notifyBeforePatch(fragmentEl, meta = {}) {
|
|
2122
|
+
const affected = collectAffectedInstances(fragmentEl);
|
|
2123
|
+
const prevHookCtx = activeHookContext;
|
|
2124
|
+
activeHookContext = {
|
|
2125
|
+
fragmentEl,
|
|
2126
|
+
depth: meta && typeof meta.hookChainDepth === 'number' ? meta.hookChainDepth : 0,
|
|
2127
|
+
};
|
|
2128
|
+
try {
|
|
2129
|
+
for (let i = 0; i < affected.length; i++) {
|
|
2130
|
+
const inst = affected[i];
|
|
2131
|
+
inst.invokeBeforePatch({
|
|
2132
|
+
...meta,
|
|
2133
|
+
isFragmentRoot: inst.el === fragmentEl,
|
|
2134
|
+
isDescendant: inst.el !== fragmentEl && typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el),
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
} finally {
|
|
2138
|
+
activeHookContext = prevHookCtx;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function notifyAfterPatch(fragmentEl, meta = {}) {
|
|
2143
|
+
const chainDepth = meta && typeof meta.hookChainDepth === 'number' ? meta.hookChainDepth : 0;
|
|
2144
|
+
if (chainDepth > 3) {
|
|
2145
|
+
const msg = `Possible patch hook refresh loop detected on fragment "${meta.fragmentName || meta.fragmentId || 'unknown'}" (causal chain depth ${chainDepth}).`;
|
|
2146
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
2147
|
+
debug.recordEvent('patch:loop-warning', {
|
|
2148
|
+
code: 'PATCH_HOOK_LOOP_DETECTED',
|
|
2149
|
+
fragmentId: meta.fragmentId || null,
|
|
2150
|
+
fragmentName: meta.fragmentName || null,
|
|
2151
|
+
count: chainDepth,
|
|
2152
|
+
message: msg,
|
|
2153
|
+
});
|
|
2154
|
+
} else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') {
|
|
2155
|
+
console.warn(`[webspresso] ${msg}`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
const affected = collectAffectedInstances(fragmentEl);
|
|
2160
|
+
const prevHookCtx = activeHookContext;
|
|
2161
|
+
activeHookContext = {
|
|
2162
|
+
fragmentEl,
|
|
2163
|
+
depth: chainDepth,
|
|
2164
|
+
};
|
|
2165
|
+
try {
|
|
2166
|
+
// Reverse order for afterPatch: inner descendants first, outer ancestors second
|
|
2167
|
+
for (let i = affected.length - 1; i >= 0; i--) {
|
|
2168
|
+
const inst = affected[i];
|
|
2169
|
+
inst.invokeAfterPatch({
|
|
2170
|
+
...meta,
|
|
2171
|
+
isFragmentRoot: inst.el === fragmentEl,
|
|
2172
|
+
isDescendant: inst.el !== fragmentEl && typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el),
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
} finally {
|
|
2176
|
+
activeHookContext = prevHookCtx;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2024
2180
|
return {
|
|
2025
2181
|
register,
|
|
2026
2182
|
services,
|
|
@@ -2034,6 +2190,11 @@ function createControllerRuntime(options = {}) {
|
|
|
2034
2190
|
mountSubtree,
|
|
2035
2191
|
syncTree: mountSubtree,
|
|
2036
2192
|
unmountSubtree,
|
|
2193
|
+
notifyBeforePatch,
|
|
2194
|
+
notifyAfterPatch,
|
|
2195
|
+
getActivePatchHookContext() {
|
|
2196
|
+
return activeHookContext;
|
|
2197
|
+
},
|
|
2037
2198
|
mountControllerOnElement,
|
|
2038
2199
|
getInstanceForElement(el, name) {
|
|
2039
2200
|
const map = elementInstances.get(el);
|
package/core/client/devtool.js
CHANGED
|
@@ -75,6 +75,33 @@ function setupReduxDevToolsBridge(runtime) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function escapeHtml(val) {
|
|
79
|
+
return String(val ?? '')
|
|
80
|
+
.replace(/&/g, '&')
|
|
81
|
+
.replace(/</g, '<')
|
|
82
|
+
.replace(/>/g, '>')
|
|
83
|
+
.replace(/"/g, '"')
|
|
84
|
+
.replace(/'/g, ''');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sanitizeHref(url, fallback = '#') {
|
|
88
|
+
if (!url || typeof url !== 'string') return fallback;
|
|
89
|
+
const trimmed = url.trim();
|
|
90
|
+
if (!trimmed || /[\u0000-\u001F\u007F-\u009F]/.test(trimmed)) return fallback;
|
|
91
|
+
if (trimmed.startsWith('/') && !trimmed.startsWith('//') && !trimmed.startsWith('/\\')) {
|
|
92
|
+
return escapeHtml(trimmed);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(trimmed);
|
|
96
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
97
|
+
return escapeHtml(parsed.href);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/**
|
|
79
106
|
* Format JSON with syntax-colored HTML or safe text.
|
|
80
107
|
*/
|
|
@@ -83,13 +110,11 @@ function formatJsonHtml(data) {
|
|
|
83
110
|
if (data === null) return '<span style="color:#f43f5e">null</span>';
|
|
84
111
|
try {
|
|
85
112
|
const jsonStr = JSON.stringify(data, null, 2);
|
|
86
|
-
return jsonStr
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.replace(/>/g, '>')
|
|
90
|
-
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
|
|
113
|
+
return escapeHtml(jsonStr).replace(
|
|
114
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\&])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
115
|
+
(match) => {
|
|
91
116
|
let cls = 'color:#38bdf8;'; // number
|
|
92
|
-
if (
|
|
117
|
+
if (/^"/.test(match)) {
|
|
93
118
|
if (/:$/.test(match)) {
|
|
94
119
|
cls = 'color:#a78bfa;font-weight:600;'; // key
|
|
95
120
|
} else {
|
|
@@ -101,9 +126,10 @@ function formatJsonHtml(data) {
|
|
|
101
126
|
cls = 'color:#f43f5e;'; // null
|
|
102
127
|
}
|
|
103
128
|
return `<span style="${cls}">${match}</span>`;
|
|
104
|
-
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
105
131
|
} catch (_) {
|
|
106
|
-
return
|
|
132
|
+
return escapeHtml(data);
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
135
|
|
|
@@ -549,12 +575,12 @@ function mountDevtool(runtime, options = {}) {
|
|
|
549
575
|
(c) => `
|
|
550
576
|
<div
|
|
551
577
|
class="ws-card ws-controller-item"
|
|
552
|
-
data-inst-id="${c.instanceId}"
|
|
578
|
+
data-inst-id="${escapeHtml(c.instanceId)}"
|
|
553
579
|
style="cursor:pointer;padding:8px 10px;margin-bottom:6px;${
|
|
554
580
|
c.instanceId === current.instanceId ? 'border-color:#f59e0b;background:#27272a;' : ''
|
|
555
581
|
}">
|
|
556
|
-
<div style="font-weight:700;color:#f4f4f5;">${c.name || 'anonymous'}</div>
|
|
557
|
-
<div style="font-size:10px;color:#a1a1aa;"><${c.tagName}> • ${c.instanceId}</div>
|
|
582
|
+
<div style="font-weight:700;color:#f4f4f5;">${escapeHtml(c.name || 'anonymous')}</div>
|
|
583
|
+
<div style="font-size:10px;color:#a1a1aa;"><${escapeHtml(c.tagName)}> • ${escapeHtml(c.instanceId)}</div>
|
|
558
584
|
</div>
|
|
559
585
|
`
|
|
560
586
|
)
|
|
@@ -565,10 +591,10 @@ function mountDevtool(runtime, options = {}) {
|
|
|
565
591
|
<div style="overflow-y:auto;padding-right:4px;">
|
|
566
592
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
|
567
593
|
<div>
|
|
568
|
-
<h3 style="font-size:14px;font-weight:700;color:#fff;">${current.name || 'anonymous'}</h3>
|
|
569
|
-
<p style="font-size:10px;color:#71717a;">Instance: ${current.instanceId} • Connected: ${current.isConnected ? '✅' : '❌'}</p>
|
|
594
|
+
<h3 style="font-size:14px;font-weight:700;color:#fff;">${escapeHtml(current.name || 'anonymous')}</h3>
|
|
595
|
+
<p style="font-size:10px;color:#71717a;">Instance: ${escapeHtml(current.instanceId)} • Connected: ${current.isConnected ? '✅' : '❌'}</p>
|
|
570
596
|
</div>
|
|
571
|
-
<button class="ws-btn ws-btn-sm" id="ws-btn-highlight-ctrl" data-inst-id="${current.instanceId}">
|
|
597
|
+
<button class="ws-btn ws-btn-sm" id="ws-btn-highlight-ctrl" data-inst-id="${escapeHtml(current.instanceId)}">
|
|
572
598
|
🎯 Highlight in DOM
|
|
573
599
|
</button>
|
|
574
600
|
</div>
|
|
@@ -577,7 +603,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
577
603
|
<div class="ws-card">
|
|
578
604
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
|
579
605
|
<strong style="color:#fbbf24;font-size:11px;">REACTIVE STATE</strong>
|
|
580
|
-
<button class="ws-btn ws-btn-sm" id="ws-btn-copy-state" data-inst-id="${current.instanceId}">📋 Copy</button>
|
|
606
|
+
<button class="ws-btn ws-btn-sm" id="ws-btn-copy-state" data-inst-id="${escapeHtml(current.instanceId)}">📋 Copy</button>
|
|
581
607
|
</div>
|
|
582
608
|
<div class="ws-code-block">${formatJsonHtml(current.state)}</div>
|
|
583
609
|
</div>
|
|
@@ -614,14 +640,14 @@ function mountDevtool(runtime, options = {}) {
|
|
|
614
640
|
<div class="ws-card" style="margin-bottom:10px;">
|
|
615
641
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
616
642
|
<div>
|
|
617
|
-
<strong style="color:#38bdf8;font-size:12px;">${f.name}</strong>
|
|
618
|
-
<span style="font-size:10px;color:#71717a;margin-left:6px;">#${f.instanceId}</span>
|
|
643
|
+
<strong style="color:#38bdf8;font-size:12px;">${escapeHtml(f.name)}</strong>
|
|
644
|
+
<span style="font-size:10px;color:#71717a;margin-left:6px;">#${escapeHtml(f.instanceId)}</span>
|
|
619
645
|
</div>
|
|
620
646
|
<div style="display:flex;gap:6px;">
|
|
621
|
-
<button class="ws-btn ws-btn-sm" id="ws-btn-highlight-frag" data-inst-id="${f.instanceId}">
|
|
647
|
+
<button class="ws-btn ws-btn-sm" id="ws-btn-highlight-frag" data-inst-id="${escapeHtml(f.instanceId)}">
|
|
622
648
|
🎯 Highlight
|
|
623
649
|
</button>
|
|
624
|
-
<button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-frag" data-frag-name="${f.name}">
|
|
650
|
+
<button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-frag" data-frag-name="${escapeHtml(f.name)}">
|
|
625
651
|
🔄 Refresh
|
|
626
652
|
</button>
|
|
627
653
|
</div>
|
|
@@ -631,13 +657,13 @@ function mountDevtool(runtime, options = {}) {
|
|
|
631
657
|
<span style="color:#a1a1aa;">Depends On Tags:</span>
|
|
632
658
|
${
|
|
633
659
|
f.dependsOn.length > 0
|
|
634
|
-
? f.dependsOn.map((t) => `<span class="ws-badge ws-badge-info" style="margin-left:4px;">${t}</span>`).join('')
|
|
660
|
+
? f.dependsOn.map((t) => `<span class="ws-badge ws-badge-info" style="margin-left:4px;">${escapeHtml(t)}</span>`).join('')
|
|
635
661
|
: '<span style="color:#71717a;margin-left:4px;">(none)</span>'
|
|
636
662
|
}
|
|
637
663
|
</div>
|
|
638
664
|
<div>
|
|
639
665
|
<span style="color:#a1a1aa;">Version:</span>
|
|
640
|
-
<span style="color:#f4f4f5;margin-left:4px;">${f.version}</span>
|
|
666
|
+
<span style="color:#f4f4f5;margin-left:4px;">${escapeHtml(f.version)}</span>
|
|
641
667
|
</div>
|
|
642
668
|
</div>
|
|
643
669
|
${
|
|
@@ -674,11 +700,11 @@ function mountDevtool(runtime, options = {}) {
|
|
|
674
700
|
(r) => `
|
|
675
701
|
<div class="ws-card" style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;margin-bottom:6px;">
|
|
676
702
|
<div>
|
|
677
|
-
<span class="ws-badge ws-badge-${r.method === 'GET' ? 'info' : 'warning'}">${r.method}</span>
|
|
678
|
-
<span style="font-weight:600;margin-left:6px;">${r.url}</span>
|
|
703
|
+
<span class="ws-badge ws-badge-${r.method === 'GET' ? 'info' : 'warning'}">${escapeHtml(r.method)}</span>
|
|
704
|
+
<span style="font-weight:600;margin-left:6px;">${escapeHtml(r.url)}</span>
|
|
679
705
|
</div>
|
|
680
706
|
<div style="font-size:10px;color:#a1a1aa;">
|
|
681
|
-
Policy: <span style="color:#38bdf8;">${r.policy || 'none'}</span> • Status: <span style="color:#34d399;">${r.status}</span>
|
|
707
|
+
Policy: <span style="color:#38bdf8;">${escapeHtml(r.policy || 'none')}</span> • Status: <span style="color:#34d399;">${escapeHtml(r.status)}</span>
|
|
682
708
|
</div>
|
|
683
709
|
</div>
|
|
684
710
|
`
|
|
@@ -696,7 +722,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
696
722
|
.map(
|
|
697
723
|
(c) => `
|
|
698
724
|
<div class="ws-card" style="padding:10px;margin-bottom:6px;">
|
|
699
|
-
<div style="font-weight:600;color:#f4f4f5;font-size:11px;">Chain: ${c.chainId}</div>
|
|
725
|
+
<div style="font-weight:600;color:#f4f4f5;font-size:11px;">Chain: ${escapeHtml(c.chainId)}</div>
|
|
700
726
|
<div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(c)}</div>
|
|
701
727
|
</div>
|
|
702
728
|
`
|
|
@@ -718,8 +744,8 @@ function mountDevtool(runtime, options = {}) {
|
|
|
718
744
|
.map(
|
|
719
745
|
(t) => `
|
|
720
746
|
<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #27272a;font-size:11px;">
|
|
721
|
-
<span>${t.fragmentName || t.instanceId}</span>
|
|
722
|
-
<span style="color:#fbbf24;">patch: ${t.patchMs}ms ${t.loadMs ? `(load: ${t.loadMs}ms, render: ${t.renderMs}ms)` : ''}</span>
|
|
747
|
+
<span>${escapeHtml(t.fragmentName || t.instanceId)}</span>
|
|
748
|
+
<span style="color:#fbbf24;">patch: ${escapeHtml(t.patchMs)}ms ${t.loadMs ? `(load: ${escapeHtml(t.loadMs)}ms, render: ${escapeHtml(t.renderMs)}ms)` : ''}</span>
|
|
723
749
|
</div>
|
|
724
750
|
`
|
|
725
751
|
)
|
|
@@ -758,7 +784,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
758
784
|
.map(
|
|
759
785
|
(e) => `
|
|
760
786
|
<div class="ws-card" style="margin-bottom:6px;">
|
|
761
|
-
<div style="font-weight:600;color:#38bdf8;font-size:11px;">${e.key}</div>
|
|
787
|
+
<div style="font-weight:600;color:#38bdf8;font-size:11px;">${escapeHtml(e.key)}</div>
|
|
762
788
|
<div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
|
|
763
789
|
(() => {
|
|
764
790
|
try {
|
|
@@ -784,7 +810,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
784
810
|
.map(
|
|
785
811
|
(e) => `
|
|
786
812
|
<div class="ws-card" style="margin-bottom:6px;">
|
|
787
|
-
<div style="font-weight:600;color:#a78bfa;font-size:11px;">${e.key}</div>
|
|
813
|
+
<div style="font-weight:600;color:#a78bfa;font-size:11px;">${escapeHtml(e.key)}</div>
|
|
788
814
|
<div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
|
|
789
815
|
(() => {
|
|
790
816
|
try {
|
|
@@ -815,7 +841,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
815
841
|
<div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Visual database browser, query runner, and table inspector.</div>
|
|
816
842
|
<div style="font-size:9px;color:#71717a;margin-top:4px;">Run in terminal: <code>npx webspresso studio</code> (port 4567)</div>
|
|
817
843
|
</div>
|
|
818
|
-
<a href="${studioUrl}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
|
|
844
|
+
<a href="${sanitizeHref(studioUrl)}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
|
|
819
845
|
Open DB Studio ↗
|
|
820
846
|
</a>
|
|
821
847
|
</div>
|
|
@@ -829,8 +855,8 @@ function mountDevtool(runtime, options = {}) {
|
|
|
829
855
|
<div style="font-weight:700;color:#38bdf8;font-size:13px;">⚙️ Admin Panel SPA</div>
|
|
830
856
|
<div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Mithril.js model CRUD and staff administration cockpit.</div>
|
|
831
857
|
</div>
|
|
832
|
-
<a href="${features.adminPath}" target="_blank" rel="noopener noreferrer" class="ws-btn" id="ws-btn-open-admin" style="text-decoration:none;">
|
|
833
|
-
Open ${features.adminPath} ↗
|
|
858
|
+
<a href="${sanitizeHref(features.adminPath)}" target="_blank" rel="noopener noreferrer" class="ws-btn" id="ws-btn-open-admin" style="text-decoration:none;">
|
|
859
|
+
Open ${escapeHtml(features.adminPath)} ↗
|
|
834
860
|
</a>
|
|
835
861
|
</div>
|
|
836
862
|
`
|
|
@@ -918,7 +944,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
918
944
|
<!-- Footer -->
|
|
919
945
|
<div class="ws-footer">
|
|
920
946
|
<span>Shortcuts: <strong>Alt + W</strong> to toggle • Console: <code>window.__WEBSPRESSO_DEVTOOLS__</code></span>
|
|
921
|
-
<span style="color:#a1a1aa;">Webspresso v${PKG_VERSION}</span>
|
|
947
|
+
<span style="color:#a1a1aa;">Webspresso v${escapeHtml(PKG_VERSION)}</span>
|
|
922
948
|
</div>
|
|
923
949
|
</div>
|
|
924
950
|
`;
|