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/devtool.js
CHANGED
|
@@ -15,16 +15,15 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { defaultStorage } = require('./storage');
|
|
18
|
+
const { isArray, isNumber } = require('./utils');
|
|
18
19
|
|
|
19
20
|
const DEVTOOL_HOST_ID = 'webspresso-client-devtools-host';
|
|
20
21
|
|
|
21
|
-
const PKG_VERSION =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
})();
|
|
22
|
+
const PKG_VERSION =
|
|
23
|
+
(typeof window !== 'undefined' &&
|
|
24
|
+
window.__WEBSPRESSO_DEV_META__ &&
|
|
25
|
+
window.__WEBSPRESSO_DEV_META__.version) ||
|
|
26
|
+
'0.0.110';
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Connect to Redux DevTools browser extension if installed.
|
|
@@ -176,10 +175,61 @@ function mountDevtool(runtime, options = {}) {
|
|
|
176
175
|
const position = options.position || 'bottom-right';
|
|
177
176
|
const isRight = position.includes('right');
|
|
178
177
|
const studioUrl = options.studioUrl || 'http://localhost:4567';
|
|
178
|
+
let metaOverrides =
|
|
179
|
+
options.meta && typeof options.meta === 'object' ? { ...options.meta } : {};
|
|
179
180
|
let isOpen = Boolean(options.open);
|
|
180
181
|
let activeTab = 'controllers'; // 'controllers' | 'fragments' | 'network' | 'storage' | 'studio'
|
|
181
182
|
let selectedControllerId = null;
|
|
182
183
|
|
|
184
|
+
function resolveDevFeatures() {
|
|
185
|
+
const winMeta =
|
|
186
|
+
typeof window !== 'undefined' &&
|
|
187
|
+
window.__WEBSPRESSO_DEV_META__ &&
|
|
188
|
+
typeof window.__WEBSPRESSO_DEV_META__ === 'object'
|
|
189
|
+
? window.__WEBSPRESSO_DEV_META__
|
|
190
|
+
: {};
|
|
191
|
+
const merged = { ...winMeta, ...metaOverrides };
|
|
192
|
+
const pluginsList = isArray(merged.plugins)
|
|
193
|
+
? merged.plugins
|
|
194
|
+
: isArray(options.plugins)
|
|
195
|
+
? options.plugins
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
const hasStudio =
|
|
199
|
+
options.hasStudio !== undefined
|
|
200
|
+
? Boolean(options.hasStudio)
|
|
201
|
+
: merged.hasStudio !== undefined
|
|
202
|
+
? Boolean(merged.hasStudio)
|
|
203
|
+
: merged.hasDb !== undefined
|
|
204
|
+
? Boolean(merged.hasDb)
|
|
205
|
+
: Boolean(
|
|
206
|
+
options.studioUrl ||
|
|
207
|
+
pluginsList.includes('studio') ||
|
|
208
|
+
pluginsList.includes('db-studio')
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const hasAdmin =
|
|
212
|
+
options.hasAdmin !== undefined
|
|
213
|
+
? Boolean(options.hasAdmin)
|
|
214
|
+
: merged.hasAdmin !== undefined
|
|
215
|
+
? Boolean(merged.hasAdmin)
|
|
216
|
+
: Boolean(
|
|
217
|
+
options.adminPath ||
|
|
218
|
+
options.adminUrl ||
|
|
219
|
+
pluginsList.includes('admin-panel') ||
|
|
220
|
+
pluginsList.includes('adminPanel') ||
|
|
221
|
+
pluginsList.includes('admin')
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const adminPath = merged.adminPath || options.adminPath || options.adminUrl || '/_admin';
|
|
225
|
+
return {
|
|
226
|
+
hasStudio,
|
|
227
|
+
hasAdmin,
|
|
228
|
+
adminPath,
|
|
229
|
+
plugins: pluginsList,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
183
233
|
// Initialize Redux DevTools extension bridge if available
|
|
184
234
|
const reduxBridge = setupReduxDevToolsBridge(runtime);
|
|
185
235
|
|
|
@@ -257,6 +307,8 @@ function mountDevtool(runtime, options = {}) {
|
|
|
257
307
|
flex-direction: column;
|
|
258
308
|
overflow: hidden;
|
|
259
309
|
z-index: 999999;
|
|
310
|
+
}
|
|
311
|
+
.ws-drawer.ws-drawer-enter {
|
|
260
312
|
animation: wsFadeIn 0.15s ease-out;
|
|
261
313
|
}
|
|
262
314
|
@keyframes wsFadeIn {
|
|
@@ -753,33 +805,46 @@ function mountDevtool(runtime, options = {}) {
|
|
|
753
805
|
}
|
|
754
806
|
|
|
755
807
|
function renderStudioTab() {
|
|
756
|
-
|
|
757
|
-
<div>
|
|
758
|
-
<div style="margin-bottom:16px;">
|
|
759
|
-
<strong style="font-size:13px;color:#fff;">Developer Tools & Studio Integrations</strong>
|
|
760
|
-
<p style="font-size:10px;color:#71717a;">Direct shortcuts to Webspresso's integrated developer productivity tools.</p>
|
|
761
|
-
</div>
|
|
808
|
+
const features = resolveDevFeatures();
|
|
762
809
|
|
|
810
|
+
const studioCardHtml = features.hasStudio
|
|
811
|
+
? `
|
|
763
812
|
<div class="ws-card" style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;">
|
|
764
813
|
<div>
|
|
765
814
|
<div style="font-weight:700;color:#fbbf24;font-size:13px;">🗄️ Webspresso DB Studio</div>
|
|
766
815
|
<div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Visual database browser, query runner, and table inspector.</div>
|
|
767
816
|
<div style="font-size:9px;color:#71717a;margin-top:4px;">Run in terminal: <code>npx webspresso studio</code> (port 4567)</div>
|
|
768
817
|
</div>
|
|
769
|
-
<a href="${studioUrl}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" style="text-decoration:none;">
|
|
818
|
+
<a href="${studioUrl}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
|
|
770
819
|
Open DB Studio ↗
|
|
771
820
|
</a>
|
|
772
821
|
</div>
|
|
822
|
+
`
|
|
823
|
+
: '';
|
|
773
824
|
|
|
825
|
+
const adminCardHtml = features.hasAdmin
|
|
826
|
+
? `
|
|
774
827
|
<div class="ws-card" style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;">
|
|
775
828
|
<div>
|
|
776
829
|
<div style="font-weight:700;color:#38bdf8;font-size:13px;">⚙️ Admin Panel SPA</div>
|
|
777
830
|
<div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Mithril.js model CRUD and staff administration cockpit.</div>
|
|
778
831
|
</div>
|
|
779
|
-
<a href="
|
|
780
|
-
Open
|
|
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} ↗
|
|
781
834
|
</a>
|
|
782
835
|
</div>
|
|
836
|
+
`
|
|
837
|
+
: '';
|
|
838
|
+
|
|
839
|
+
return `
|
|
840
|
+
<div>
|
|
841
|
+
<div style="margin-bottom:16px;">
|
|
842
|
+
<strong style="font-size:13px;color:#fff;">Developer Tools & Studio Integrations</strong>
|
|
843
|
+
<p style="font-size:10px;color:#71717a;">Direct shortcuts to Webspresso's integrated developer productivity tools.</p>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
${studioCardHtml}
|
|
847
|
+
${adminCardHtml}
|
|
783
848
|
|
|
784
849
|
<div class="ws-card" style="display:flex;align-items:center;justify-content:space-between;">
|
|
785
850
|
<div>
|
|
@@ -859,6 +924,9 @@ function mountDevtool(runtime, options = {}) {
|
|
|
859
924
|
`;
|
|
860
925
|
}
|
|
861
926
|
|
|
927
|
+
let lastRenderedHtml = '';
|
|
928
|
+
let wasOpen = false;
|
|
929
|
+
|
|
862
930
|
function findInShadow(id) {
|
|
863
931
|
if (shadow && typeof shadow.getElementById === 'function') {
|
|
864
932
|
return shadow.getElementById(id);
|
|
@@ -866,8 +934,39 @@ function mountDevtool(runtime, options = {}) {
|
|
|
866
934
|
return shadow && typeof shadow.querySelector === 'function' ? shadow.querySelector(`#${id}`) : null;
|
|
867
935
|
}
|
|
868
936
|
|
|
869
|
-
function updateDom() {
|
|
870
|
-
|
|
937
|
+
function updateDom(force = false) {
|
|
938
|
+
const nextHtml = isOpen ? renderDrawer() : renderPill();
|
|
939
|
+
if (!force && nextHtml === lastRenderedHtml && isOpen === wasOpen) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Preserve scroll positions inside drawer before updating DOM
|
|
944
|
+
let prevContentScrollTop = 0;
|
|
945
|
+
const prevContentEl =
|
|
946
|
+
isOpen && wasOpen && shadow && typeof shadow.querySelector === 'function'
|
|
947
|
+
? shadow.querySelector('.ws-content')
|
|
948
|
+
: null;
|
|
949
|
+
if (prevContentEl) {
|
|
950
|
+
prevContentScrollTop = prevContentEl.scrollTop || 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
lastRenderedHtml = nextHtml;
|
|
954
|
+
const openingTransition = isOpen && !wasOpen;
|
|
955
|
+
wasOpen = isOpen;
|
|
956
|
+
|
|
957
|
+
container.innerHTML = nextHtml;
|
|
958
|
+
if (openingTransition) {
|
|
959
|
+
const drawerEl = findInShadow('ws-devtool-drawer');
|
|
960
|
+
if (drawerEl && drawerEl.classList) {
|
|
961
|
+
drawerEl.classList.add('ws-drawer-enter');
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (prevContentScrollTop > 0 && shadow && typeof shadow.querySelector === 'function') {
|
|
965
|
+
const nextContentEl = shadow.querySelector('.ws-content');
|
|
966
|
+
if (nextContentEl) {
|
|
967
|
+
nextContentEl.scrollTop = prevContentScrollTop;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
871
970
|
bindEvents();
|
|
872
971
|
}
|
|
873
972
|
|
|
@@ -1043,7 +1142,7 @@ function mountDevtool(runtime, options = {}) {
|
|
|
1043
1142
|
document.addEventListener('keydown', onKeyDown);
|
|
1044
1143
|
|
|
1045
1144
|
// Periodic polling to keep snapshots live
|
|
1046
|
-
const pollIntervalMs =
|
|
1145
|
+
const pollIntervalMs = isNumber(options.pollInterval) ? options.pollInterval : 2000;
|
|
1047
1146
|
let pollTimer = null;
|
|
1048
1147
|
if (pollIntervalMs > 0) {
|
|
1049
1148
|
pollTimer = setInterval(() => {
|
|
@@ -1076,6 +1175,12 @@ function mountDevtool(runtime, options = {}) {
|
|
|
1076
1175
|
isOpen() {
|
|
1077
1176
|
return isOpen;
|
|
1078
1177
|
},
|
|
1178
|
+
setMeta(nextMeta = {}) {
|
|
1179
|
+
if (nextMeta && typeof nextMeta === 'object') {
|
|
1180
|
+
metaOverrides = { ...metaOverrides, ...nextMeta };
|
|
1181
|
+
updateDom();
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1079
1184
|
getSnapshot() {
|
|
1080
1185
|
return {
|
|
1081
1186
|
controllers: getControllersSnapshot(),
|
package/core/client/forms.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { defaultStorage } = require('./storage');
|
|
4
|
+
const { isArray, setElementBusyState } = require('./utils');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Webspresso Native HTML Form Layer & Typed Form Handle
|
|
@@ -202,12 +203,12 @@ function parseFormElement(formEl, submitterOrOptions = null) {
|
|
|
202
203
|
function appendValue(normName, val, forceArray) {
|
|
203
204
|
if (val === undefined) return;
|
|
204
205
|
if (forceArray) {
|
|
205
|
-
if (!
|
|
206
|
+
if (!isArray(data[normName])) {
|
|
206
207
|
data[normName] = [];
|
|
207
208
|
}
|
|
208
209
|
data[normName].push(val);
|
|
209
210
|
} else if (normName in data) {
|
|
210
|
-
if (!
|
|
211
|
+
if (!isArray(data[normName])) {
|
|
211
212
|
data[normName] = [data[normName]];
|
|
212
213
|
}
|
|
213
214
|
data[normName].push(val);
|
|
@@ -326,7 +327,7 @@ function parseFormElement(formEl, submitterOrOptions = null) {
|
|
|
326
327
|
formData = new FormData();
|
|
327
328
|
for (const [key, val] of Object.entries(data)) {
|
|
328
329
|
if (val === undefined) continue;
|
|
329
|
-
if (
|
|
330
|
+
if (isArray(val)) {
|
|
330
331
|
for (const item of val) {
|
|
331
332
|
if (item === undefined) continue;
|
|
332
333
|
if (typeof Blob !== 'undefined' && item instanceof Blob) {
|
|
@@ -416,11 +417,15 @@ function computeFormDirty(formEl, initialSnapshot) {
|
|
|
416
417
|
*/
|
|
417
418
|
function createFormHandle(formEl, options = {}) {
|
|
418
419
|
if (formEl && formEl.__wFormHandle) {
|
|
420
|
+
if (options.services && typeof formEl.__wFormHandle.__setServices === 'function') {
|
|
421
|
+
formEl.__wFormHandle.__setServices(options.services);
|
|
422
|
+
}
|
|
419
423
|
return formEl.__wFormHandle;
|
|
420
424
|
}
|
|
421
425
|
|
|
422
426
|
const api = options.api;
|
|
423
427
|
const debug = options.debug || null;
|
|
428
|
+
let services = options.services || null;
|
|
424
429
|
const formName =
|
|
425
430
|
(formEl && formEl.getAttribute && formEl.getAttribute('data-w-form')) ||
|
|
426
431
|
(formEl && formEl.getAttribute && formEl.getAttribute('name')) ||
|
|
@@ -615,32 +620,32 @@ function createFormHandle(formEl, options = {}) {
|
|
|
615
620
|
* Toggle pending state on the form and submit buttons to prevent double submissions.
|
|
616
621
|
*/
|
|
617
622
|
function setPendingState(nextPending, activeSubmitter = null) {
|
|
618
|
-
|
|
623
|
+
const next = Boolean(nextPending);
|
|
624
|
+
if (pending === next) return;
|
|
625
|
+
pending = next;
|
|
619
626
|
if (!formEl) return;
|
|
620
627
|
|
|
621
628
|
if (pending) {
|
|
622
629
|
formEl.setAttribute('aria-busy', 'true');
|
|
623
630
|
formEl.setAttribute('data-w-pending', 'true');
|
|
631
|
+
formEl.setAttribute('data-w-busy', 'true');
|
|
624
632
|
} else {
|
|
625
633
|
formEl.removeAttribute('aria-busy');
|
|
626
634
|
formEl.removeAttribute('data-w-pending');
|
|
635
|
+
formEl.removeAttribute('data-w-busy');
|
|
627
636
|
}
|
|
628
637
|
|
|
629
638
|
const submitButtons = formEl.querySelectorAll
|
|
630
639
|
? formEl.querySelectorAll('button[type="submit"], button:not([type]), input[type="submit"]')
|
|
631
640
|
: [];
|
|
641
|
+
let submitterHandled = false;
|
|
632
642
|
for (let i = 0; i < submitButtons.length; i++) {
|
|
633
643
|
const btn = submitButtons[i];
|
|
634
|
-
if (btn
|
|
635
|
-
|
|
636
|
-
btn.__wWasDisabled = Boolean(btn.disabled);
|
|
637
|
-
btn.disabled = true;
|
|
638
|
-
} else if (!btn.__wWasDisabled) {
|
|
639
|
-
btn.disabled = false;
|
|
640
|
-
}
|
|
644
|
+
if (btn === activeSubmitter) submitterHandled = true;
|
|
645
|
+
setElementBusyState(btn, pending);
|
|
641
646
|
}
|
|
642
|
-
if (activeSubmitter && !
|
|
643
|
-
activeSubmitter
|
|
647
|
+
if (activeSubmitter && !submitterHandled) {
|
|
648
|
+
setElementBusyState(activeSubmitter, pending);
|
|
644
649
|
}
|
|
645
650
|
notifyStateChange();
|
|
646
651
|
}
|
|
@@ -734,7 +739,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
734
739
|
if (type === 'file') continue; // Browser security prevents setting file input value
|
|
735
740
|
|
|
736
741
|
if (type === 'checkbox') {
|
|
737
|
-
if (
|
|
742
|
+
if (isArray(val)) {
|
|
738
743
|
ctrl.checked = val.map(String).includes(String(ctrl.value));
|
|
739
744
|
} else {
|
|
740
745
|
ctrl.checked = Boolean(val);
|
|
@@ -743,7 +748,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
743
748
|
ctrl.checked = String(val) === String(ctrl.value);
|
|
744
749
|
} else if (ctrl.tagName === 'SELECT') {
|
|
745
750
|
if (ctrl.multiple) {
|
|
746
|
-
const arr =
|
|
751
|
+
const arr = isArray(val) ? val.map(String) : [String(val)];
|
|
747
752
|
const opts = ctrl.options || [];
|
|
748
753
|
for (let j = 0; j < opts.length; j++) {
|
|
749
754
|
opts[j].selected = arr.includes(String(opts[j].value));
|
|
@@ -923,9 +928,18 @@ function createFormHandle(formEl, options = {}) {
|
|
|
923
928
|
}
|
|
924
929
|
|
|
925
930
|
// Invoke registered success hooks. Note: Any background fragment refresh failure
|
|
926
|
-
//
|
|
931
|
+
// or side-effect service error in onSuccess NEVER puts a succeeded form into validation error
|
|
932
|
+
// state or makes the completed mutation appear rolled back!
|
|
933
|
+
let hookError = null;
|
|
927
934
|
for (const listener of successListeners) {
|
|
928
|
-
|
|
935
|
+
try {
|
|
936
|
+
await listener(responseData, { values: payloadData, form: handle, services });
|
|
937
|
+
} catch (listenerErr) {
|
|
938
|
+
if (!hookError) hookError = listenerErr;
|
|
939
|
+
if (debug && typeof debug.trackCleanupError === 'function') {
|
|
940
|
+
debug.trackCleanupError(`form_${formName}`, formName, 'form:onSuccess', listenerErr);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
929
943
|
}
|
|
930
944
|
|
|
931
945
|
// Explicit redirect behavior
|
|
@@ -951,7 +965,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
951
965
|
}
|
|
952
966
|
}
|
|
953
967
|
|
|
954
|
-
return { ok: true, submitted: true, data: responseData, refreshOk, refreshError };
|
|
968
|
+
return { ok: true, submitted: true, data: responseData, refreshOk, refreshError, hookError };
|
|
955
969
|
} catch (err) {
|
|
956
970
|
if (err && err.kind === 'validation') {
|
|
957
971
|
fieldErrors = { ...(err.fieldErrors || {}) };
|
|
@@ -966,7 +980,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
966
980
|
|
|
967
981
|
for (const listener of errorListeners) {
|
|
968
982
|
try {
|
|
969
|
-
await listener(err, { values: payloadData, form: handle });
|
|
983
|
+
await listener(err, { values: payloadData, form: handle, services });
|
|
970
984
|
} catch (_e) {
|
|
971
985
|
// Ignore
|
|
972
986
|
}
|
|
@@ -1035,6 +1049,12 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1035
1049
|
handle = {
|
|
1036
1050
|
name: formName,
|
|
1037
1051
|
el: formEl,
|
|
1052
|
+
get services() {
|
|
1053
|
+
return services;
|
|
1054
|
+
},
|
|
1055
|
+
__setServices(nextServices) {
|
|
1056
|
+
if (nextServices) services = nextServices;
|
|
1057
|
+
},
|
|
1038
1058
|
get pending() {
|
|
1039
1059
|
return pending;
|
|
1040
1060
|
},
|