@stacksjs/stx 0.2.152 → 0.2.154
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/dist/animation.js +6 -6
- package/dist/app-shell.js +2 -1
- package/dist/browser-composables.d.ts +8 -0
- package/dist/build-id.d.ts +49 -0
- package/dist/build-id.js +20 -0
- package/dist/build-message.d.ts +10 -0
- package/dist/build-message.js +26 -0
- package/dist/builtins/attrs.js +6 -1
- package/dist/cli.js +1743 -1255
- package/dist/client-script-bundler.js +2 -4
- package/dist/client-script.d.ts +29 -0
- package/dist/client-script.js +142 -97
- package/dist/color-mode-boot.d.ts +46 -10
- package/dist/color-mode-boot.js +53 -13
- package/dist/component-client-factories.js +3 -2
- package/dist/component-renderer.js +10 -21
- package/dist/composables/index.d.ts +8 -1
- package/dist/composables/use-clipboard.d.ts +1 -5
- package/dist/composables/use-clipboard.js +57 -46
- package/dist/composables/use-color-mode.d.ts +19 -4
- package/dist/composables/use-color-mode.js +49 -28
- package/dist/composables/use-router.d.ts +28 -11
- package/dist/composables/use-router.js +63 -9
- package/dist/composables/use-storage.d.ts +14 -15
- package/dist/composables/use-storage.js +63 -2
- package/dist/config.js +22 -7
- package/dist/craft.js +1319 -841
- package/dist/dev-server/crosswind.js +8 -6
- package/dist/dev-server/serve-app.js +13 -7
- package/dist/dev-server/serve-markdown.js +4 -1
- package/dist/dev-server/serve-multi.js +32 -20
- package/dist/dev-server/types.d.ts +8 -1
- package/dist/document-shell.d.ts +120 -0
- package/dist/document-shell.js +161 -14
- package/dist/dynamic-components.js +123 -4
- package/dist/events.d.ts +26 -49
- package/dist/events.js +23 -9
- package/dist/expressions.d.ts +1 -1
- package/dist/expressions.js +3 -3
- package/dist/framework-composables.d.ts +41 -0
- package/dist/framework-composables.js +115 -0
- package/dist/head.js +5 -1
- package/dist/hot-reload.js +8 -2
- package/dist/includes.js +4 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/isolated-build.d.ts +12 -0
- package/dist/isolated-build.js +18 -0
- package/dist/plugin-system.d.ts +1 -1
- package/dist/process.js +46 -12
- package/dist/production-builder.js +3 -1
- package/dist/production-server.js +4 -1
- package/dist/prop-sentinels.d.ts +31 -0
- package/dist/prop-sentinels.js +7 -0
- package/dist/pwa.js +1258 -780
- package/dist/reactive.js +1 -1
- package/dist/reactivity.d.ts +0 -15
- package/dist/reactivity.js +15 -6
- package/dist/runtime-globals.d.ts +40 -2
- package/dist/runtime-globals.js +90 -0
- package/dist/runtime-injection.d.ts +51 -0
- package/dist/runtime-injection.js +13 -0
- package/dist/script-emit.d.ts +20 -0
- package/dist/script-emit.js +11 -0
- package/dist/script-validation.js +12 -7
- package/dist/signal-processing.js +25 -40
- package/dist/signals-api.d.ts +30 -0
- package/dist/signals-api.js +18 -0
- package/dist/signals.js +532 -85
- package/dist/site-builder/build.js +21 -7
- package/dist/site-builder/seo.js +19 -10
- package/dist/site-builder/theme.js +5 -4
- package/dist/site-builder/types.d.ts +1 -0
- package/dist/ssg.js +1255 -777
- package/dist/state-management.js +4 -1
- package/dist/store-loader.d.ts +0 -11
- package/dist/store-loader.js +27 -3
- package/dist/strict-lint.d.ts +9 -6
- package/dist/strict-lint.js +2 -21
- package/dist/stx.d.ts +125 -22
- package/dist/types/config-types.d.ts +1 -0
- package/dist/unresolved-identifiers.d.ts +50 -0
- package/dist/unresolved-identifiers.js +146 -0
- package/dist/utils.js +3 -3
- package/dist/variable-extractor.js +17 -1
- package/dist/x-element.js +1 -1
- package/package.json +5 -5
- package/stx.d.ts +125 -22
package/dist/signals.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { state, derived, effect, batch, onMount, onDestroy, isSignal, isDerived, untrack, peek } from "./signals-api.js";
|
|
2
|
+
import { runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
|
|
2
3
|
|
|
3
4
|
export * from "./signals-api.js";
|
|
4
5
|
export function generateSignalsRuntime() {
|
|
@@ -689,6 +690,12 @@ catch (e) { console.warn('[stx] dispose error:', e); } });
|
|
|
689
690
|
return typeof v === 'function' && v._isSignal === true;
|
|
690
691
|
}
|
|
691
692
|
|
|
693
|
+
// The derived brand is set at creation; this predicate was declared and
|
|
694
|
+
// auto-imported for years without ever existing here (#1804).
|
|
695
|
+
function isDerived(v) {
|
|
696
|
+
return typeof v === 'function' && v._isDerived === true;
|
|
697
|
+
}
|
|
698
|
+
|
|
692
699
|
function untrack(v) {
|
|
693
700
|
return isSignal(v) || (typeof v === 'function' && v._isDerived) ? v() : v;
|
|
694
701
|
}
|
|
@@ -873,7 +880,13 @@ finally {
|
|
|
873
880
|
|
|
874
881
|
function useFetch(urlOrFn, options = {}) {
|
|
875
882
|
const data = state(options.initialData ?? null);
|
|
876
|
-
|
|
883
|
+
// Starts false when nothing will be requested. It used to start true
|
|
884
|
+
// unconditionally while the only thing clearing it was the fetch that
|
|
885
|
+
// immediate:false suppresses \u2014 so the documented way to declare a deferred
|
|
886
|
+
// request produced a composable stuck loading forever, which is exactly the
|
|
887
|
+
// case where a template is most likely driving a spinner off it (#1818).
|
|
888
|
+
const immediate = options.immediate !== false;
|
|
889
|
+
const loading = state(immediate);
|
|
877
890
|
const error = state(null);
|
|
878
891
|
if (options.suspense) registerSuspense(loading, error);
|
|
879
892
|
|
|
@@ -927,20 +940,27 @@ finally {
|
|
|
927
940
|
}
|
|
928
941
|
};
|
|
929
942
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// If urlOrFn is a function, watch for changes and refetch
|
|
943
|
+
// A reactive URL drives its own fetching: effect() runs eagerly on creation,
|
|
944
|
+
// so the first run IS the mount fetch. Registering onMount as well issued
|
|
945
|
+
// two requests for every function URL (#1818).
|
|
936
946
|
if (typeof urlOrFn === 'function') {
|
|
947
|
+
var firstUrlRun = true;
|
|
937
948
|
effect(() => {
|
|
938
949
|
const url = urlOrFn();
|
|
939
|
-
if (url
|
|
940
|
-
|
|
950
|
+
if (!url) return;
|
|
951
|
+
// Honour immediate:false for the FIRST evaluation only. A later change
|
|
952
|
+
// to the URL is a new request the caller asked for by changing it, not
|
|
953
|
+
// the initial one they deferred.
|
|
954
|
+
if (firstUrlRun) {
|
|
955
|
+
firstUrlRun = false;
|
|
956
|
+
if (!immediate) return;
|
|
941
957
|
}
|
|
958
|
+
fetchData();
|
|
942
959
|
});
|
|
943
960
|
}
|
|
961
|
+
else if (immediate) {
|
|
962
|
+
onMount(fetchData);
|
|
963
|
+
}
|
|
944
964
|
|
|
945
965
|
return {
|
|
946
966
|
data,
|
|
@@ -973,17 +993,26 @@ finally {
|
|
|
973
993
|
// Navigation API
|
|
974
994
|
// ==========================================================================
|
|
975
995
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
996
|
+
// Second argument is an options object: { replace, reload }.
|
|
997
|
+
//
|
|
998
|
+
// It used to be a bare forceReload boolean while the shipped declaration
|
|
999
|
+
// promised { replace?: boolean } \u2014 so the one call the type system invited,
|
|
1000
|
+
// navigate(url, { replace: true }), took the truthy branch and did a full
|
|
1001
|
+
// document load that PUSHED a history entry: the opposite of replace, at the
|
|
1002
|
+
// cost of the SPA router, with no error (#1807).
|
|
1003
|
+
//
|
|
1004
|
+
// A bare boolean is still accepted and still means "full reload", because
|
|
1005
|
+
// that was the real shipped behaviour and docs and examples contain it.
|
|
1006
|
+
function navigate(url, options) {
|
|
1007
|
+
var opts = (options && typeof options === 'object') ? options : { reload: !!options };
|
|
1008
|
+
var replace = !!opts.replace;
|
|
1009
|
+
var reload = !!(opts.reload || opts.forceReload);
|
|
1010
|
+
if (!reload && window.stxRouter && typeof window.stxRouter.navigate === 'function') {
|
|
1011
|
+
window.stxRouter.navigate(url, { replace: replace });
|
|
979
1012
|
return;
|
|
980
1013
|
}
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
else {
|
|
985
|
-
window.location.href = url;
|
|
986
|
-
}
|
|
1014
|
+
if (replace) window.location.replace(url);
|
|
1015
|
+
else window.location.href = url;
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
1018
|
function goBack() { window.history.back(); }
|
|
@@ -1038,20 +1067,66 @@ else {
|
|
|
1038
1067
|
};
|
|
1039
1068
|
window.addEventListener('popstate', syncFromUrl);
|
|
1040
1069
|
window.addEventListener('stx:navigate', syncFromUrl);
|
|
1070
|
+
// Own-property reads only. The backing object comes from Object.fromEntries,
|
|
1071
|
+
// so it inherits Object.prototype \u2014 an unguarded params()[key] returned a
|
|
1072
|
+
// FUNCTION for 'toString', 'constructor', 'valueOf' and friends, which is
|
|
1073
|
+
// neither a string nor undefined and matches no declared return type.
|
|
1074
|
+
var own = function(key) {
|
|
1075
|
+
var current = params();
|
|
1076
|
+
return Object.prototype.hasOwnProperty.call(current, key);
|
|
1077
|
+
};
|
|
1078
|
+
// pushState by default; replaceState when the caller asks for it (#1825).
|
|
1079
|
+
//
|
|
1080
|
+
// The canonical reason to delete a param is to CONSUME a one-shot value \u2014
|
|
1081
|
+
// an OAuth callback result, ?checkout=success, a flash token \u2014 and the
|
|
1082
|
+
// point of consuming it is that it must not survive a Back press. Under
|
|
1083
|
+
// pushState the pre-delete URL, the one still carrying the param, becomes
|
|
1084
|
+
// the previous entry, so Back replays the callback and whatever consuming
|
|
1085
|
+
// it triggered runs again.
|
|
1086
|
+
//
|
|
1087
|
+
// Default stays pushState so nothing that works today changes, and the
|
|
1088
|
+
// option is spelled { replace } to match navigate(), which already takes it.
|
|
1089
|
+
var commit = function(url, options) {
|
|
1090
|
+
// A plain truthy read of the replace property looks equivalent and is
|
|
1091
|
+
// not: it asks whether the value HAS a replace property, and every string
|
|
1092
|
+
// has String.prototype.replace \u2014 so delete(key, 'push') would have
|
|
1093
|
+
// replaced, the exact opposite of what it reads as. Requiring an object
|
|
1094
|
+
// also keeps this identical to the module implementation in
|
|
1095
|
+
// composables/use-router.ts; with optional chaining alone the two
|
|
1096
|
+
// disagreed on the empty string, which short-circuits in one and hits
|
|
1097
|
+
// String.prototype.replace in the other.
|
|
1098
|
+
//
|
|
1099
|
+
// NB: no backticks anywhere in this block. This runtime is assembled as a
|
|
1100
|
+
// template literal, so a backtick in a COMMENT ends the string and the
|
|
1101
|
+
// rest of the runtime is parsed as code.
|
|
1102
|
+
var replace = !!(options && typeof options === 'object' && options.replace);
|
|
1103
|
+
if (replace)
|
|
1104
|
+
window.history.replaceState({}, '', url);
|
|
1105
|
+
else
|
|
1106
|
+
window.history.pushState({}, '', url);
|
|
1107
|
+
syncFromUrl();
|
|
1108
|
+
};
|
|
1041
1109
|
return {
|
|
1042
1110
|
data: params,
|
|
1043
|
-
get: function(key) { return params()[key]; },
|
|
1044
|
-
|
|
1111
|
+
get: function(key) { return own(key) ? params()[key] : undefined; },
|
|
1112
|
+
has: own,
|
|
1113
|
+
set: function(key, value, options) {
|
|
1045
1114
|
var url = new URL(window.location.href);
|
|
1046
1115
|
url.searchParams.set(key, value);
|
|
1047
|
-
|
|
1048
|
-
|
|
1116
|
+
commit(url, options);
|
|
1117
|
+
},
|
|
1118
|
+
// delete and has were DECLARED but absent, so the compiler endorsed calls
|
|
1119
|
+
// that threw in the browser; setAll and data were present but undeclared
|
|
1120
|
+
// (#1806). Deleting writes history the same way set does, for consistency.
|
|
1121
|
+
'delete': function(key, options) {
|
|
1122
|
+
var url = new URL(window.location.href);
|
|
1123
|
+
url.searchParams['delete'](key);
|
|
1124
|
+
commit(url, options);
|
|
1049
1125
|
},
|
|
1050
|
-
setAll: function(obj) {
|
|
1126
|
+
setAll: function(obj, options) {
|
|
1051
1127
|
var url = new URL(window.location.href);
|
|
1052
1128
|
Object.keys(obj).forEach(function(k) { url.searchParams.set(k, obj[k]); });
|
|
1053
|
-
|
|
1054
|
-
syncFromUrl();
|
|
1129
|
+
commit(url, options);
|
|
1055
1130
|
}
|
|
1056
1131
|
};
|
|
1057
1132
|
}
|
|
@@ -1139,11 +1214,16 @@ finally {
|
|
|
1139
1214
|
onMount(fetchData);
|
|
1140
1215
|
}
|
|
1141
1216
|
|
|
1142
|
-
// refetchOnFocus
|
|
1217
|
+
// refetchOnFocus \u2014 removed on destroy, like refetchInterval right below.
|
|
1218
|
+
// It used to add a listener and never remove it, so every instance leaked
|
|
1219
|
+
// one for the life of the page and each SPA navigation added more; every
|
|
1220
|
+
// stale listener still fired, refetching for components long gone (#1818).
|
|
1143
1221
|
if (options.refetchOnFocus) {
|
|
1144
|
-
|
|
1222
|
+
var onVisibility = function() {
|
|
1145
1223
|
if (!document.hidden) fetchData();
|
|
1146
|
-
}
|
|
1224
|
+
};
|
|
1225
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
1226
|
+
onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
|
|
1147
1227
|
}
|
|
1148
1228
|
|
|
1149
1229
|
// refetchInterval
|
|
@@ -1921,12 +2001,6 @@ catch (e) {
|
|
|
1921
2001
|
}
|
|
1922
2002
|
|
|
1923
2003
|
function processElement(el, scope = componentScope) {
|
|
1924
|
-
// Debug: log every element with x-class or @click
|
|
1925
|
-
if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute) {
|
|
1926
|
-
if (el.hasAttribute('x-class') || el.hasAttribute('@click')) {
|
|
1927
|
-
console.log('[stx] processElement:', el.tagName, 'x-class:', el.hasAttribute('x-class'), '@click:', el.hasAttribute('@click'));
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
2004
|
// Lazy hydration: if this element has stx-hydrate and hasn't been hydrated
|
|
1931
2005
|
// yet, defer its subtree processing until the trigger fires.
|
|
1932
2006
|
if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute && el.hasAttribute('stx-hydrate') && !el.__stx_hydrated) {
|
|
@@ -2137,23 +2211,36 @@ else if (part) {
|
|
|
2137
2211
|
const pipeResult = parsePipeExpression(expr, activeScope);
|
|
2138
2212
|
if (pipeResult) {
|
|
2139
2213
|
const unwrapScope = createExpressionAutoUnwrapProxy(activeScope, expr);
|
|
2140
|
-
|
|
2214
|
+
var piped = maybeUnwrapSignal(executePipeExpression(pipeResult.valueExpr, pipeResult.pipes, unwrapScope));
|
|
2215
|
+
noteExprSuccess(expr);
|
|
2216
|
+
return piped;
|
|
2141
2217
|
}
|
|
2142
2218
|
|
|
2143
2219
|
// Use auto-unwrap proxy (Feature #1)
|
|
2144
2220
|
const unwrapScope = createExpressionAutoUnwrapProxy(activeScope, expr);
|
|
2145
2221
|
const fn = new Function('__scope__', 'with(__scope__) { return (' + expr + ') }');
|
|
2146
|
-
|
|
2222
|
+
var value = maybeUnwrapSignal(fn(unwrapScope));
|
|
2223
|
+
noteExprSuccess(expr);
|
|
2224
|
+
return value;
|
|
2147
2225
|
}
|
|
2148
2226
|
catch (e) {
|
|
2149
2227
|
// Auto-unwrap can break explicit signal calls like errorData().message
|
|
2150
2228
|
// Retry without auto-unwrap so signal functions remain callable
|
|
2151
2229
|
try {
|
|
2152
2230
|
const fn = new Function('__scope__', 'with(__scope__) { return (' + expr + ') }');
|
|
2153
|
-
|
|
2231
|
+
var retried = maybeUnwrapSignal(fn(activeScope));
|
|
2232
|
+
noteExprSuccess(expr);
|
|
2233
|
+
return retried;
|
|
2154
2234
|
}
|
|
2155
2235
|
catch (e2) {
|
|
2156
2236
|
if (!(e2 instanceof ReferenceError) && !(e2 instanceof TypeError)) console.warn('[STX] Attribute expression error:', expr, e2);
|
|
2237
|
+
// ReferenceError/TypeError stay unlogged HERE on purpose: a signal
|
|
2238
|
+
// may not exist yet on an effect's first pass, and warning on every
|
|
2239
|
+
// one of those trains people to ignore the console. Instead the
|
|
2240
|
+
// failure is recorded and cleared the moment the same expression
|
|
2241
|
+
// evaluates successfully, so only expressions that are STILL broken
|
|
2242
|
+
// when hydration finishes get reported \u2014 by the audit (#1773).
|
|
2243
|
+
else noteExprFailure(expr, e2);
|
|
2157
2244
|
return '';
|
|
2158
2245
|
}
|
|
2159
2246
|
}
|
|
@@ -2172,24 +2259,17 @@ catch (e2) {
|
|
|
2172
2259
|
var KEY_MAP = {enter:'Enter', tab:'Tab', escape:'Escape', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', 'delete':'Delete', backspace:'Backspace'};
|
|
2173
2260
|
|
|
2174
2261
|
// Handle attributes
|
|
2175
|
-
if (el.hasAttribute && (el.hasAttribute('x-class') || el.hasAttribute('@click'))) {
|
|
2176
|
-
console.log('[stx] attr loop entry:', el.tagName, 'attrs:', getElementAttributes(el).map(a => a.name).join(', '));
|
|
2177
|
-
}
|
|
2178
2262
|
getElementAttributes(el).forEach(attr => {
|
|
2179
2263
|
const name = attr.name;
|
|
2180
2264
|
const value = attr.value;
|
|
2181
2265
|
if (!name) return;
|
|
2182
2266
|
|
|
2183
|
-
if (el.hasAttribute && (el.hasAttribute('x-class') || el.hasAttribute('@click'))) {
|
|
2184
|
-
console.log('[stx] attr iter:', el.tagName, 'name:', name);
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
2267
|
// Dynamic attribute binding: @bind:attr, x-bind:attr, :attr, OR x-attr
|
|
2188
2268
|
// x-attr (e.g. x-class, x-style, x-href, x-src) is the canonical binding prefix.
|
|
2189
2269
|
// :attr still works for backward compat but is reserved for structural directives.
|
|
2190
2270
|
// x-text, x-html, x-model, x-show, x-if, x-for, x-cloak, x-ref, x-data are
|
|
2191
2271
|
// handled by their own code paths below \u2014 exclude them here.
|
|
2192
|
-
var X_HANDLED = {
|
|
2272
|
+
var X_HANDLED = ${runtimeHandledXAttrsLiteral()};
|
|
2193
2273
|
if (name.startsWith('@bind:') || name.startsWith('x-bind:')
|
|
2194
2274
|
|| (name.startsWith(':') && !name.startsWith('::') && !DIRECTIVE_NAMES[name.slice(1).split('.')[0]] && !EVENT_RE.test(name.slice(1)))
|
|
2195
2275
|
|| (name.startsWith('x-') && !X_HANDLED[name.split('.')[0]] && !X_HANDLED[name])) {
|
|
@@ -2278,7 +2358,6 @@ else {
|
|
|
2278
2358
|
el.removeAttribute(name);
|
|
2279
2359
|
}
|
|
2280
2360
|
else if (name === '@class' || name === ':class' || name === 'x-class') {
|
|
2281
|
-
console.log('[stx] HIT x-class handler:', el.tagName);
|
|
2282
2361
|
bindClass(el, value, scope);
|
|
2283
2362
|
el.removeAttribute(name);
|
|
2284
2363
|
}
|
|
@@ -2336,9 +2415,7 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
|
|
|
2336
2415
|
? { ...globalHelpers, ...el.__stx_parent_scope }
|
|
2337
2416
|
: { ...globalHelpers, ...scope, ...(findElementScope(el) || {}) };
|
|
2338
2417
|
|
|
2339
|
-
console.log('[stx] binding event:', eventName, 'on', el.tagName, 'expr:', value.substring(0, 40));
|
|
2340
2418
|
el.addEventListener(eventName, (event) => {
|
|
2341
|
-
console.log('[stx] event fired:', eventName, 'on', el.tagName);
|
|
2342
2419
|
// Vue-style component listeners fall through to the rendered native
|
|
2343
2420
|
// root unless the child emits the same event. Signal components use
|
|
2344
2421
|
// a scope wrapper, so native events arrive here from a descendant.
|
|
@@ -2459,7 +2536,7 @@ catch (e) {
|
|
|
2459
2536
|
// Skip script elements entirely
|
|
2460
2537
|
if (child.tagName === 'SCRIPT') return;
|
|
2461
2538
|
// Skip elements already processed by stx.mount() \u2014 they have their own scope and effects
|
|
2462
|
-
if (child.__stx_scope)
|
|
2539
|
+
if (child.__stx_scope) return;
|
|
2463
2540
|
// Skip data-stx-scope elements \u2014 they are managed by the reactive (x-data) runtime
|
|
2464
2541
|
if (child.hasAttribute && child.hasAttribute('data-stx-scope')) return;
|
|
2465
2542
|
processElement(child, scope);
|
|
@@ -2660,7 +2737,11 @@ catch (e) {
|
|
|
2660
2737
|
});
|
|
2661
2738
|
}
|
|
2662
2739
|
else if (tag === 'select') {
|
|
2663
|
-
|
|
2740
|
+
// Same equality guard as the text branch. Harmless here \u2014 a select has
|
|
2741
|
+
// no caret \u2014 but an unconditional write is still a needless DOM mutation
|
|
2742
|
+
// on every signal change, and the inconsistency invites the text-branch
|
|
2743
|
+
// bug being reintroduced by copying this one (#1799).
|
|
2744
|
+
var syncSelect = () => { var next = getValue() ?? ''; if (el.value !== next) el.value = next; };
|
|
2664
2745
|
el.__stx_model_sync = syncSelect;
|
|
2665
2746
|
effect(syncSelect);
|
|
2666
2747
|
if (!el.__stx_model_observer && typeof window.MutationObserver !== 'undefined') {
|
|
@@ -2680,7 +2761,41 @@ catch (e) {
|
|
|
2680
2761
|
el.addEventListener('change', () => setValue(coerceValue(el.value)));
|
|
2681
2762
|
}
|
|
2682
2763
|
else {
|
|
2683
|
-
|
|
2764
|
+
// Writing el.value moves the text cursor to the END of the control, and
|
|
2765
|
+
// typing forms a loop: input -> setValue -> the signal changes -> this
|
|
2766
|
+
// effect re-runs -> the write. So every keystroke reset the caret
|
|
2767
|
+
// (stacksjs/stx#1799).
|
|
2768
|
+
//
|
|
2769
|
+
// Invisible while appending at the end of a field, which is why a login
|
|
2770
|
+
// form with x-model felt fine and a textarea people actually edit did
|
|
2771
|
+
// not. Worse inside :for, where key-based reuse updates the item signal
|
|
2772
|
+
// on reuse (#1669) and re-evaluates the reused row's bindings on every
|
|
2773
|
+
// character.
|
|
2774
|
+
//
|
|
2775
|
+
// The equality guard alone fixes typing, because the value this effect
|
|
2776
|
+
// computes IS the value just typed. The offset is preserved as well for
|
|
2777
|
+
// the case the guard cannot cover: a value that legitimately changed
|
|
2778
|
+
// from elsewhere while the field is focused \u2014 a formatter rewriting
|
|
2779
|
+
// input, a remote sync.
|
|
2780
|
+
effect(() => {
|
|
2781
|
+
var next = getValue() ?? '';
|
|
2782
|
+
if (el.value === next) return;
|
|
2783
|
+
var active = typeof document !== 'undefined' && document.activeElement === el;
|
|
2784
|
+
var start = null;
|
|
2785
|
+
var end = null;
|
|
2786
|
+
if (active) {
|
|
2787
|
+
// selectionStart throws on input types that do not support
|
|
2788
|
+
// selection (number, email, date), so it cannot be assumed present.
|
|
2789
|
+
try { start = el.selectionStart; end = el.selectionEnd; }
|
|
2790
|
+
catch (e) { start = null; end = null; }
|
|
2791
|
+
}
|
|
2792
|
+
el.value = next;
|
|
2793
|
+
if (active && start !== null && typeof el.setSelectionRange === 'function') {
|
|
2794
|
+
// Clamp: the incoming value may be shorter than the old offset.
|
|
2795
|
+
try { el.setSelectionRange(Math.min(start, next.length), Math.min(end == null ? start : end, next.length)); }
|
|
2796
|
+
catch (e) { /* type does not support selection */ }
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2684
2799
|
el.addEventListener(modifiers.includes('lazy') ? 'change' : 'input', () => setValue(coerceValue(el.value)));
|
|
2685
2800
|
}
|
|
2686
2801
|
|
|
@@ -3947,9 +4062,6 @@ catch (e2) {
|
|
|
3947
4062
|
const fullScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
3948
4063
|
const directSignal = fullScope[expr];
|
|
3949
4064
|
|
|
3950
|
-
if (directSignal) {
|
|
3951
|
-
console.log('[stx] bindIf direct signal for :if=' + expr, 'signal identity:', directSignal === componentScope[expr] ? 'SAME' : 'DIFFERENT', 'signal():', directSignal());
|
|
3952
|
-
}
|
|
3953
4065
|
effect(() => {
|
|
3954
4066
|
var value;
|
|
3955
4067
|
if (directSignal && (directSignal._isSignal || directSignal._isDerived)) {
|
|
@@ -4029,6 +4141,11 @@ else if (!value && isInserted) {
|
|
|
4029
4141
|
// if) fire too. The if attribute was already removed, so no recursion.
|
|
4030
4142
|
if (value && isInserted && !childrenProcessed) {
|
|
4031
4143
|
childrenProcessed = true;
|
|
4144
|
+
// Until this deferred pass runs, the shown subtree still holds literal
|
|
4145
|
+
// {{ }}. Flag it so the hydration audit doesn't report those as a
|
|
4146
|
+
// binding miss in the gap before the setTimeout fires \u2014 a :for nested
|
|
4147
|
+
// in a :if binds a macrotask after the synchronous stx:load audit (#1773).
|
|
4148
|
+
el.__stx_if_pending = true;
|
|
4032
4149
|
setTimeout(function() {
|
|
4033
4150
|
var childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
4034
4151
|
// A false single-element conditional is detached before the
|
|
@@ -4046,6 +4163,8 @@ else if (!value && isInserted) {
|
|
|
4046
4163
|
// :if children still have x-cloak and stay hidden.
|
|
4047
4164
|
el.removeAttribute('x-cloak');
|
|
4048
4165
|
el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
|
|
4166
|
+
// Deferred bind complete \u2014 the audit may inspect this subtree now.
|
|
4167
|
+
el.__stx_if_pending = false;
|
|
4049
4168
|
}, 0);
|
|
4050
4169
|
}
|
|
4051
4170
|
}
|
|
@@ -4081,10 +4200,25 @@ else if (!value && isInserted) {
|
|
|
4081
4200
|
|
|
4082
4201
|
// provide() \u2014 share values across components (like Vue's provide/inject)
|
|
4083
4202
|
// Usage: provide('Modal', Modal) \u2014 makes Modal accessible in all components
|
|
4203
|
+
//
|
|
4204
|
+
// inject() was auto-imported but implemented nowhere, so half of the pair
|
|
4205
|
+
// worked: provide() succeeded and the failure surfaced only in the consumer
|
|
4206
|
+
// (#1804). Values are recorded in a private registry as well as on window,
|
|
4207
|
+
// because the global write collides with native properties \u2014 provide('name',
|
|
4208
|
+
// x) lands on window.name, which the browser coerces to a string.
|
|
4209
|
+
var __stxProvided = Object.create(null);
|
|
4210
|
+
|
|
4084
4211
|
function provide(name, value) {
|
|
4212
|
+
__stxProvided[name] = value;
|
|
4085
4213
|
window[name] = value;
|
|
4086
4214
|
}
|
|
4087
4215
|
|
|
4216
|
+
function inject(name, defaultValue) {
|
|
4217
|
+
if (name in __stxProvided) return __stxProvided[name];
|
|
4218
|
+
if (name in window) return window[name];
|
|
4219
|
+
return defaultValue;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4088
4222
|
// ==========================================================================
|
|
4089
4223
|
// Timer & Utility Composables
|
|
4090
4224
|
// ==========================================================================
|
|
@@ -4366,6 +4500,33 @@ else if (timer === null) {
|
|
|
4366
4500
|
});
|
|
4367
4501
|
}
|
|
4368
4502
|
|
|
4503
|
+
// Watch several sources at once, calling back with (newValues, oldValues).
|
|
4504
|
+
// Declared and auto-imported but never implemented here (#1804). NOT an alias
|
|
4505
|
+
// of $watch, which spreads the values and passes no oldValues \u2014 callers
|
|
4506
|
+
// written against the declared contract would silently receive the wrong
|
|
4507
|
+
// arguments. Sources may be signals, Vue-style refs, or plain values.
|
|
4508
|
+
function watchMultiple(sources, callback, options) {
|
|
4509
|
+
options = options || {};
|
|
4510
|
+
var read = function() {
|
|
4511
|
+
return sources.map(function(s) {
|
|
4512
|
+
if (typeof s === 'function') return s();
|
|
4513
|
+
if (s && typeof s === 'object' && 'value' in s) return s.value;
|
|
4514
|
+
return s;
|
|
4515
|
+
});
|
|
4516
|
+
};
|
|
4517
|
+
var oldValues = read();
|
|
4518
|
+
var first = true;
|
|
4519
|
+
return effect(function() {
|
|
4520
|
+
var newValues = read();
|
|
4521
|
+
if (first) {
|
|
4522
|
+
first = false;
|
|
4523
|
+
if (!options.immediate) return;
|
|
4524
|
+
}
|
|
4525
|
+
callback(newValues, oldValues);
|
|
4526
|
+
oldValues = newValues;
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4369
4530
|
function useAsync(fn, options) {
|
|
4370
4531
|
options = options || {};
|
|
4371
4532
|
var asyncState = 'idle';
|
|
@@ -4410,16 +4571,49 @@ else if (timer === null) {
|
|
|
4410
4571
|
// flash the boot script exists to prevent.
|
|
4411
4572
|
var boot = null;
|
|
4412
4573
|
try { boot = window.__STX_COLOR_MODE__ || null; } catch (e) {}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4574
|
+
// Explicit-undefined checks rather than ||, so an explicit darkClass:null
|
|
4575
|
+
// or attribute:null opts out instead of falling through to the default.
|
|
4576
|
+
// An explicit null means "do not manage this", from the call site OR from
|
|
4577
|
+
// app.colorMode. The boot payload is generated by stx and always carries
|
|
4578
|
+
// every key, so absent and explicitly-null ARE distinguishable there, and
|
|
4579
|
+
// rejecting null made the boot global the one path that could not say it
|
|
4580
|
+
// (#1813). The asymmetry hid because it only bites where the fallback is
|
|
4581
|
+
// non-null: attribute:null looked fine, darkClass:null silently became
|
|
4582
|
+
// 'dark' \u2014 so opting out of the class required repeating the option at
|
|
4583
|
+
// every call site, defeating the single-source arrangement the global
|
|
4584
|
+
// exists for.
|
|
4585
|
+
function pick(a, b, fallback) {
|
|
4586
|
+
if (a !== undefined) return a;
|
|
4587
|
+
if (b !== undefined) return b;
|
|
4588
|
+
return fallback;
|
|
4589
|
+
}
|
|
4590
|
+
// Read through a helper rather than a short-circuit on boot itself: that
|
|
4591
|
+
// expression yields undefined for an undefined boot but NULL for a null
|
|
4592
|
+
// one, which pick would now honour as an explicit opt-out.
|
|
4593
|
+
function fromBoot(key) {
|
|
4594
|
+
return (boot && typeof boot === 'object') ? boot[key] : undefined;
|
|
4595
|
+
}
|
|
4596
|
+
var storageKey = pick(options.storageKey, fromBoot('storageKey'), 'stx-color-mode');
|
|
4597
|
+
var initialMode = pick(options.initialMode, fromBoot('initialMode'), 'auto');
|
|
4598
|
+
var darkClass = pick(options.darkClass, fromBoot('darkClass'), 'dark');
|
|
4599
|
+
var attribute = pick(options.attribute, fromBoot('attribute'), null);
|
|
4600
|
+
var autoValueOption = pick(options.autoValue, fromBoot('autoValue'), undefined);
|
|
4417
4601
|
var disableTransitions = options.disableTransitions !== false;
|
|
4418
|
-
var preference = initialMode;
|
|
4419
4602
|
var resolved = 'light';
|
|
4420
4603
|
var listeners = [];
|
|
4421
4604
|
var cleanups = [];
|
|
4422
4605
|
|
|
4606
|
+
// Storage tokens meaning "follow the system". 'system' is a common spelling
|
|
4607
|
+
// for the same concept; rejecting it used to send the app down a silent
|
|
4608
|
+
// data-loss path \u2014 read 'system', fail to recognise it, fall back to
|
|
4609
|
+
// 'auto', then OVERWRITE the stored value with 'auto' (#1788).
|
|
4610
|
+
function normalizeMode(v) {
|
|
4611
|
+
if (v === 'light' || v === 'dark') return v;
|
|
4612
|
+
if (v === 'auto' || v === 'system') return 'auto';
|
|
4613
|
+
return null;
|
|
4614
|
+
}
|
|
4615
|
+
var preference = normalizeMode(initialMode) || 'auto';
|
|
4616
|
+
|
|
4423
4617
|
function getSystem() {
|
|
4424
4618
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
4425
4619
|
}
|
|
@@ -4427,27 +4621,42 @@ else if (timer === null) {
|
|
|
4427
4621
|
function applyDOM(mode) {
|
|
4428
4622
|
var el = document.documentElement;
|
|
4429
4623
|
if (disableTransitions) el.style.setProperty('transition', 'none', 'important');
|
|
4430
|
-
|
|
4431
|
-
|
|
4624
|
+
// Both, when both are configured. The class drives the utility
|
|
4625
|
+
// framework's dark: variants, the attribute drives everything else \u2014
|
|
4626
|
+
// complements, not alternatives.
|
|
4627
|
+
if (attribute) el.setAttribute(attribute, mode);
|
|
4628
|
+
// Explicit add/remove, not toggle(cls, force) \u2014 very-happy-dom ignores
|
|
4629
|
+
// the force argument and plain-toggles, inverting every light-mode test.
|
|
4630
|
+
if (darkClass) { if (mode === 'dark') el.classList.add(darkClass); else el.classList.remove(darkClass); }
|
|
4432
4631
|
if (disableTransitions) { el.offsetHeight; el.style.removeProperty('transition'); }
|
|
4433
4632
|
}
|
|
4434
|
-
function
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
try { var v = localStorage.getItem(storageKey); if (v === 'light' || v === 'dark' || v === 'auto') return v; }
|
|
4438
|
-
catch (e) {}
|
|
4439
|
-
return null;
|
|
4633
|
+
function readRaw() {
|
|
4634
|
+
try { return localStorage.getItem(storageKey); }
|
|
4635
|
+
catch (e) { return null; }
|
|
4440
4636
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4637
|
+
// The spelling written back for 'auto': an explicit option wins, else
|
|
4638
|
+
// whatever the app already stored, so its own pre-paint script keeps
|
|
4639
|
+
// reading a value it recognises.
|
|
4640
|
+
var storedRaw = readRaw();
|
|
4641
|
+
var autoValue = autoValueOption !== undefined
|
|
4642
|
+
? autoValueOption
|
|
4643
|
+
: ((storedRaw === 'auto' || storedRaw === 'system') ? storedRaw : 'auto');
|
|
4644
|
+
|
|
4645
|
+
function persist(pref) {
|
|
4646
|
+
try { localStorage.setItem(storageKey, pref === 'auto' ? autoValue : pref); }
|
|
4647
|
+
catch (e) {} }
|
|
4648
|
+
// persistChoice: only an explicit user choice is written back. Persisting
|
|
4649
|
+
// on init would stamp the resolved default over a stored value this
|
|
4650
|
+
// composable didn't recognise, destroying it.
|
|
4651
|
+
function update(pref, persistChoice) {
|
|
4652
|
+
preference = normalizeMode(pref) || 'auto';
|
|
4653
|
+
resolved = resolve(preference);
|
|
4444
4654
|
applyDOM(resolved);
|
|
4445
|
-
persist(
|
|
4655
|
+
if (persistChoice !== false) persist(preference);
|
|
4446
4656
|
listeners.forEach(function(fn) { fn(resolved, preference); });
|
|
4447
4657
|
}
|
|
4448
4658
|
|
|
4449
|
-
|
|
4450
|
-
update(persisted || initialMode);
|
|
4659
|
+
update(normalizeMode(storedRaw) || initialMode, false);
|
|
4451
4660
|
|
|
4452
4661
|
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
4453
4662
|
var onSystemChange = function() {
|
|
@@ -4462,8 +4671,11 @@ catch (e) {}
|
|
|
4462
4671
|
|
|
4463
4672
|
var onStorage = function(e) {
|
|
4464
4673
|
if (e.key !== storageKey) return;
|
|
4465
|
-
var v = e.newValue;
|
|
4466
|
-
if (v
|
|
4674
|
+
var v = normalizeMode(e.newValue);
|
|
4675
|
+
if (v) {
|
|
4676
|
+
// Track the other tab's spelling too, so a write from here doesn't
|
|
4677
|
+
// convert the key out from under it.
|
|
4678
|
+
if (v === 'auto' && (e.newValue === 'auto' || e.newValue === 'system')) autoValue = e.newValue;
|
|
4467
4679
|
preference = v; resolved = resolve(v); applyDOM(resolved);
|
|
4468
4680
|
listeners.forEach(function(fn) { fn(resolved, preference); });
|
|
4469
4681
|
}
|
|
@@ -4887,9 +5099,22 @@ catch (e) {}
|
|
|
4887
5099
|
if (config.bodyAttrs && config.bodyAttrs.class) {
|
|
4888
5100
|
config.bodyAttrs.class.split(' ').forEach(function(cls) { if (cls) document.body.classList.add(cls); });
|
|
4889
5101
|
}
|
|
4890
|
-
// Html lang
|
|
4891
|
-
|
|
4892
|
-
|
|
5102
|
+
// Html attributes. Every key, not just lang \u2014 the module impl
|
|
5103
|
+
// (head.ts updateHead) has always applied all of them, and a root-level
|
|
5104
|
+
// class is the main reason to reach for htmlAttrs at all (#1798).
|
|
5105
|
+
// class merges rather than replaces, matching bodyAttrs.class above:
|
|
5106
|
+
// the color-mode boot script owns a class on this same element.
|
|
5107
|
+
if (config.htmlAttrs) {
|
|
5108
|
+
for (var hk in config.htmlAttrs) {
|
|
5109
|
+
if (!config.htmlAttrs.hasOwnProperty(hk)) continue;
|
|
5110
|
+
var hv = config.htmlAttrs[hk];
|
|
5111
|
+
if (hv == null) continue;
|
|
5112
|
+
if (hk === 'class') {
|
|
5113
|
+
String(hv).split(' ').forEach(function(cls) { if (cls) document.documentElement.classList.add(cls); });
|
|
5114
|
+
} else {
|
|
5115
|
+
document.documentElement.setAttribute(hk, hv);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
4893
5118
|
}
|
|
4894
5119
|
}
|
|
4895
5120
|
// Apply immediately if DOM is ready, otherwise on mount
|
|
@@ -5320,16 +5545,25 @@ catch (e) {}
|
|
|
5320
5545
|
// Component mount system
|
|
5321
5546
|
var mountQueue = [];
|
|
5322
5547
|
|
|
5323
|
-
window.stx
|
|
5548
|
+
// MERGE, never replace (#1804). Three client bundles assign window.stx and
|
|
5549
|
+
// the last one used to erase the others. Object.assign also preserves the
|
|
5550
|
+
// object IDENTITY, which matters because the generated store runtime captures
|
|
5551
|
+
// window.stx in a closure variable \u2014 replacing the object leaves that closure
|
|
5552
|
+
// calling a detached one.
|
|
5553
|
+
window.stx = Object.assign(window.stx || {}, {
|
|
5324
5554
|
hydrate: hydrateSubtree,
|
|
5325
5555
|
state,
|
|
5326
5556
|
derived,
|
|
5327
5557
|
effect,
|
|
5328
5558
|
batch,
|
|
5329
5559
|
isSignal,
|
|
5560
|
+
isDerived,
|
|
5330
5561
|
untrack,
|
|
5331
5562
|
peek,
|
|
5563
|
+
inject,
|
|
5564
|
+
watchMultiple,
|
|
5332
5565
|
onMount,
|
|
5566
|
+
onBeforeMount: onMount,
|
|
5333
5567
|
onDestroy,
|
|
5334
5568
|
onMounted: onMount,
|
|
5335
5569
|
onBeforeUnmount: onDestroy,
|
|
@@ -5446,6 +5680,11 @@ catch (e) {}
|
|
|
5446
5680
|
defineSlots: function() {
|
|
5447
5681
|
return window.__STX_CURRENT_SLOTS__ || {};
|
|
5448
5682
|
},
|
|
5683
|
+
// Vue spells the same lookup useSlots(); it was auto-imported with no
|
|
5684
|
+
// implementation behind it (#1804).
|
|
5685
|
+
useSlots: function() {
|
|
5686
|
+
return window.__STX_CURRENT_SLOTS__ || {};
|
|
5687
|
+
},
|
|
5449
5688
|
|
|
5450
5689
|
_mountCallbacks: mountCallbacks,
|
|
5451
5690
|
_destroyCallbacks: destroyCallbacks,
|
|
@@ -5862,7 +6101,7 @@ else {
|
|
|
5862
6101
|
doMount();
|
|
5863
6102
|
}
|
|
5864
6103
|
}
|
|
5865
|
-
};
|
|
6104
|
+
});
|
|
5866
6105
|
|
|
5867
6106
|
// Also expose globally for convenience
|
|
5868
6107
|
window.state = state;
|
|
@@ -5895,6 +6134,12 @@ else {
|
|
|
5895
6134
|
window.nextTick = nextTick;
|
|
5896
6135
|
window.useAsync = useAsync;
|
|
5897
6136
|
window.useLocalStorage = useLocalStorage;
|
|
6137
|
+
// Paired with the line above. Both are in STX_RUNTIME_GLOBALS, so a
|
|
6138
|
+
// <script client> block gets either via the setup destructure \u2014 but code
|
|
6139
|
+
// that reads the bare window global directly (a plain <script>, or a
|
|
6140
|
+
// bundled module that wasn't rewritten) found useLocalStorage defined and
|
|
6141
|
+
// useSessionStorage undefined.
|
|
6142
|
+
window.useSessionStorage = useSessionStorage;
|
|
5898
6143
|
window.useEventListener = useEventListener;
|
|
5899
6144
|
window.useWebSocket = useWebSocket;
|
|
5900
6145
|
window.useColorMode = useColorMode;
|
|
@@ -6095,10 +6340,20 @@ else {
|
|
|
6095
6340
|
const setupName = el.getAttribute('data-stx');
|
|
6096
6341
|
console.log('[DOMContentLoaded] data-stx:', setupName, 'el:', el.tagName, 'fn exists:', !!(setupName && window[setupName]));
|
|
6097
6342
|
if (setupName && window[setupName]) {
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6343
|
+
// Contained, like the two sibling setup call sites. An uncaught throw
|
|
6344
|
+
// here aborted the whole forEach and everything after it in this
|
|
6345
|
+
// handler, so ONE bad name in ONE page's setup left every root on the
|
|
6346
|
+
// page unhydrated \u2014 the user sees raw {{ }}, and the console names the
|
|
6347
|
+
// identifier without saying it cost them the document (#1805).
|
|
6348
|
+
try {
|
|
6349
|
+
const result = window[setupName]();
|
|
6350
|
+
console.log('[DOMContentLoaded] setup returned:', result ? Object.keys(result).slice(0, 10) : 'null');
|
|
6351
|
+
if (typeof result === 'object' && result !== null) {
|
|
6352
|
+
Object.assign(componentScope, result);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
catch (e) {
|
|
6356
|
+
console.error('[stx] page setup "' + setupName + '" threw; this root will not hydrate:', e);
|
|
6102
6357
|
}
|
|
6103
6358
|
}
|
|
6104
6359
|
});
|
|
@@ -6190,7 +6445,7 @@ else {
|
|
|
6190
6445
|
componentScope = { ...componentScope, ...scopeVars };
|
|
6191
6446
|
}
|
|
6192
6447
|
|
|
6193
|
-
console.log('[stx] calling processElement on scope:', scopeId, 'componentScope
|
|
6448
|
+
console.log('[stx] calling processElement on scope:', scopeId, 'componentScope keys:', Object.keys(componentScope).length, 'scopeVars keys:', Object.keys(scopeVars).length);
|
|
6194
6449
|
var disposeEffects = trackEffects(function() { processElement(el); });
|
|
6195
6450
|
el.__stx_disposers = disposeEffects;
|
|
6196
6451
|
console.log('[stx] scope processed OK:', scopeId);
|
|
@@ -6285,6 +6540,9 @@ catch (e) {
|
|
|
6285
6540
|
}
|
|
6286
6541
|
}
|
|
6287
6542
|
});
|
|
6543
|
+
|
|
6544
|
+
// Everything that should have hydrated has now had its chance (#1773).
|
|
6545
|
+
auditHydration(document.body, 'DOMContentLoaded');
|
|
6288
6546
|
};
|
|
6289
6547
|
window.__stxDomReadyHandler = stxDomReadyHandler;
|
|
6290
6548
|
console.log('[stx] registering DOMContentLoaded handler');
|
|
@@ -6376,6 +6634,192 @@ catch (e) {
|
|
|
6376
6634
|
}
|
|
6377
6635
|
}
|
|
6378
6636
|
|
|
6637
|
+
// ==========================================================================
|
|
6638
|
+
// Post-hydration invariant sweep (stacksjs/stx#1773)
|
|
6639
|
+
// ==========================================================================
|
|
6640
|
+
//
|
|
6641
|
+
// Preserved moustaches are the framework's de-facto error UI: ANY failure in
|
|
6642
|
+
// the detect \u2192 extract \u2192 bundle \u2192 ship \u2192 rebind relay collapses into the same
|
|
6643
|
+
// visible symptom, literal {{ }} on screen. Eight separate fixes each patched
|
|
6644
|
+
// one site's heuristic, and no invariant anywhere asserted the actual
|
|
6645
|
+
// requirement \u2014 zero unhydrated scopes after hydration. So every new upstream
|
|
6646
|
+
// miss reached users silently and had to be diagnosed from a screenshot.
|
|
6647
|
+
//
|
|
6648
|
+
// This is that invariant. It runs at the end of both hydration entry points
|
|
6649
|
+
// and turns a silent miss into one loud, self-healing event:
|
|
6650
|
+
//
|
|
6651
|
+
// - an element carrying data-stx-scope whose id isn't in the registry, or
|
|
6652
|
+
// that hydration reached but never processed, is reported AND processed
|
|
6653
|
+
// with the ambient scope, which is what the walk would have done had the
|
|
6654
|
+
// script arrived. That recovers the D2/D3 shapes (a silent early return
|
|
6655
|
+
// in the walk, a stale __stx_scope guard) rather than merely naming them.
|
|
6656
|
+
// - leftover {{ }} text is reported. Diagnostic only, never auto-fixed:
|
|
6657
|
+
// without a scope there is nothing to interpolate against, and a page may
|
|
6658
|
+
// legitimately print moustaches.
|
|
6659
|
+
//
|
|
6660
|
+
// Deliberately quiet in the normal case: nothing is logged when the page has
|
|
6661
|
+
// no signals at all, and containers that legitimately show template syntax
|
|
6662
|
+
// (<pre>, <code>, <template>, <script>, <style>, <textarea>, and anything
|
|
6663
|
+
// under [data-stx-ignore]) are skipped.
|
|
6664
|
+
|
|
6665
|
+
var AUDIT_SKIP_TAGS = { PRE: 1, CODE: 1, SCRIPT: 1, STYLE: 1, TEXTAREA: 1, TEMPLATE: 1 };
|
|
6666
|
+
|
|
6667
|
+
// Expressions whose evaluation threw a ReferenceError/TypeError and have not
|
|
6668
|
+
// since succeeded. evalAttrExpr swallows those two on purpose \u2014 a signal may
|
|
6669
|
+
// not exist yet on an effect's first pass \u2014 which is why a genuinely broken
|
|
6670
|
+
// binding produces no diagnostic at all today. The classic case is
|
|
6671
|
+
// :if="flag()" where flag is a signal: the auto-unwrap proxy returns the
|
|
6672
|
+
// VALUE, calling it throws TypeError, the directive suppresses it, and the
|
|
6673
|
+
// element silently stays hidden with no warning anywhere (see CLAUDE.md,
|
|
6674
|
+
// "Signals: scripts call, templates don't").
|
|
6675
|
+
//
|
|
6676
|
+
// Recording rather than warning immediately is what keeps this quiet on
|
|
6677
|
+
// working pages: a first-pass failure that later succeeds clears itself, so
|
|
6678
|
+
// only expressions still broken when hydration finishes are reported.
|
|
6679
|
+
var _exprFailures = {};
|
|
6680
|
+
var _exprFailureCount = 0;
|
|
6681
|
+
|
|
6682
|
+
function noteExprFailure(expr, err) {
|
|
6683
|
+
if (_exprFailures[expr] !== undefined) return;
|
|
6684
|
+
// Bounded: one runaway loop must not grow this without limit.
|
|
6685
|
+
if (_exprFailureCount >= 50) return;
|
|
6686
|
+
_exprFailureCount++;
|
|
6687
|
+
_exprFailures[expr] = (err && err.message) ? err.message : String(err);
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
function noteExprSuccess(expr) {
|
|
6691
|
+
if (_exprFailures[expr] === undefined) return;
|
|
6692
|
+
delete _exprFailures[expr];
|
|
6693
|
+
_exprFailureCount--;
|
|
6694
|
+
}
|
|
6695
|
+
|
|
6696
|
+
function auditSkipped(node) {
|
|
6697
|
+
// Not rendered, not audited. Covers <template> content (a detached
|
|
6698
|
+
// DocumentFragment \u2014 which some DOM implementations still walk into) and
|
|
6699
|
+
// :if branches parked off-document, neither of which the user can see.
|
|
6700
|
+
if (node.isConnected === false) return true;
|
|
6701
|
+
for (var p = node.parentNode; p; p = p.parentNode) {
|
|
6702
|
+
if (p.nodeType === Node.DOCUMENT_FRAGMENT_NODE) return true;
|
|
6703
|
+
if (p.nodeType !== Node.ELEMENT_NODE) continue;
|
|
6704
|
+
if (AUDIT_SKIP_TAGS[p.tagName]) return true;
|
|
6705
|
+
if (p.hasAttribute && p.hasAttribute('data-stx-ignore')) return true;
|
|
6706
|
+
// A :if whose deferred child-binding pass hasn't run yet legitimately holds
|
|
6707
|
+
// literal {{ }} that binds a macrotask later \u2014 not a hydration miss (#1773).
|
|
6708
|
+
if (p.__stx_if_pending) return true;
|
|
6709
|
+
// A deferred island (client=visible|idle|\u2026) is unhydrated ON PURPOSE until
|
|
6710
|
+
// its trigger fires, so its {{ }} legitimately stay literal in the meantime
|
|
6711
|
+
// \u2014 not a miss (#1746). The stranded-scope pass already skips stx-hydrate;
|
|
6712
|
+
// the literal sweep must too.
|
|
6713
|
+
if (p.hasAttribute && p.hasAttribute('stx-hydrate') && !p.__stx_hydrated) return true;
|
|
6714
|
+
}
|
|
6715
|
+
return false;
|
|
6716
|
+
}
|
|
6717
|
+
|
|
6718
|
+
function findLiteralMoustaches(root) {
|
|
6719
|
+
var found = [];
|
|
6720
|
+
if (!root || typeof document.createTreeWalker !== 'function') return found;
|
|
6721
|
+
// Read NodeFilter off window, not the bare global: in a real browser they
|
|
6722
|
+
// are the same object, but some DOM implementations used for testing expose
|
|
6723
|
+
// it only on window, and a bare reference then throws inside the audit.
|
|
6724
|
+
var showText = (window.NodeFilter && window.NodeFilter.SHOW_TEXT) || 4;
|
|
6725
|
+
var walker = document.createTreeWalker(root, showText, null);
|
|
6726
|
+
var node;
|
|
6727
|
+
while ((node = walker.nextNode())) {
|
|
6728
|
+
var text = node.nodeValue || '';
|
|
6729
|
+
if (text.indexOf('{{') === -1 || text.indexOf('}}') === -1) continue;
|
|
6730
|
+
if (auditSkipped(node)) continue;
|
|
6731
|
+
found.push(text.trim().slice(0, 80));
|
|
6732
|
+
if (found.length >= 5) break;
|
|
6733
|
+
}
|
|
6734
|
+
return found;
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
// A diagnostic that can break hydration is worse than no diagnostic, so the
|
|
6738
|
+
// whole sweep is wrapped and every DOM capability it uses is feature-checked.
|
|
6739
|
+
// Minimal document stubs (tests, non-browser embeddings) simply skip it.
|
|
6740
|
+
function auditHydration(container, phase) {
|
|
6741
|
+
try { auditHydrationUnsafe(container, phase); }
|
|
6742
|
+
catch (e) { console.warn('[stx] hydration audit skipped:', e && e.message ? e.message : e); }
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6745
|
+
function auditHydrationUnsafe(container, phase) {
|
|
6746
|
+
if (!window.stx || !window.stx._scopes) return;
|
|
6747
|
+
var root = container && typeof container.querySelectorAll === 'function' ? container : document.body;
|
|
6748
|
+
if (!root || typeof root.querySelectorAll !== 'function') return;
|
|
6749
|
+
|
|
6750
|
+
// Unregistered / unprocessed scopes \u2014 the unambiguous failure, and the one
|
|
6751
|
+
// worth repairing.
|
|
6752
|
+
var stranded = [];
|
|
6753
|
+
root.querySelectorAll('[data-stx-scope]').forEach(function(el) {
|
|
6754
|
+
if (el.__stx_audit_recovered) return;
|
|
6755
|
+
// Deferred islands are unhydrated ON PURPOSE until their trigger fires.
|
|
6756
|
+
if (el.hasAttribute('stx-hydrate') || el.__stx_hydration_scheduled) return;
|
|
6757
|
+
var scopeId = el.getAttribute('data-stx-scope');
|
|
6758
|
+
var registered = window.stx._scopes[scopeId];
|
|
6759
|
+
if (registered && el.__stx_disposers) return;
|
|
6760
|
+
stranded.push({ el: el, id: scopeId, registered: !!registered });
|
|
6761
|
+
});
|
|
6762
|
+
|
|
6763
|
+
if (stranded.length) {
|
|
6764
|
+
console.error(
|
|
6765
|
+
'[stx] hydration invariant failed after ' + phase + ': '
|
|
6766
|
+
+ stranded.length + ' scope(s) left unhydrated \u2014 '
|
|
6767
|
+
+ stranded.map(function(s) {
|
|
6768
|
+
return s.id + (s.registered ? ' (registered, never processed)' : ' (not registered)');
|
|
6769
|
+
}).join(', ')
|
|
6770
|
+
+ '. Recovering with the ambient scope; please report with the page that produced it (stacksjs/stx#1773).',
|
|
6771
|
+
);
|
|
6772
|
+
stranded.forEach(function(s) {
|
|
6773
|
+
// Once per element: a repair that itself fails must not loop on the
|
|
6774
|
+
// next navigation.
|
|
6775
|
+
s.el.__stx_audit_recovered = true;
|
|
6776
|
+
try {
|
|
6777
|
+
var vars = s.registered ? window.stx._scopes[s.id] : null;
|
|
6778
|
+
if (vars) Object.assign(componentScope, vars);
|
|
6779
|
+
s.el.__stx_disposers = trackEffects(function() { processElement(s.el, componentScope); });
|
|
6780
|
+
s.el.removeAttribute('x-cloak');
|
|
6781
|
+
s.el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
|
|
6782
|
+
}
|
|
6783
|
+
catch (e) {
|
|
6784
|
+
console.error('[stx] hydration recovery failed for scope ' + s.id + ':', e);
|
|
6785
|
+
}
|
|
6786
|
+
});
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
var literals = findLiteralMoustaches(root);
|
|
6790
|
+
if (literals.length) {
|
|
6791
|
+
console.error(
|
|
6792
|
+
'[stx] hydration invariant failed after ' + phase + ': literal {{ }} left in the DOM \u2014 '
|
|
6793
|
+
+ literals.join(' | ')
|
|
6794
|
+
+ '. An expression did not bind; see stacksjs/stx#1773. Wrap intentional '
|
|
6795
|
+
+ 'template syntax in <pre>/<code> or [data-stx-ignore] to exclude it.',
|
|
6796
|
+
);
|
|
6797
|
+
}
|
|
6798
|
+
|
|
6799
|
+
// Expressions that threw and never recovered. Reported here rather than at
|
|
6800
|
+
// throw time so a first-pass miss that later resolves stays silent.
|
|
6801
|
+
var brokenExprs = [];
|
|
6802
|
+
for (var expr in _exprFailures) {
|
|
6803
|
+
if (Object.prototype.hasOwnProperty.call(_exprFailures, expr))
|
|
6804
|
+
brokenExprs.push(expr + ' \u2192 ' + _exprFailures[expr]);
|
|
6805
|
+
}
|
|
6806
|
+
if (brokenExprs.length) {
|
|
6807
|
+
var hint = brokenExprs.some(function(m) { return m.indexOf('is not a function') !== -1; })
|
|
6808
|
+
? ' If one of these calls a signal, drop the parentheses: templates read '
|
|
6809
|
+
+ 'a signal by bare name (:if="flag"), scripts call it (flag()). Calling '
|
|
6810
|
+
+ 'the unwrapped value is a TypeError, which directives suppress \u2014 the '
|
|
6811
|
+
+ 'element just stays hidden.'
|
|
6812
|
+
: '';
|
|
6813
|
+
console.error(
|
|
6814
|
+
'[stx] hydration invariant failed after ' + phase + ': '
|
|
6815
|
+
+ brokenExprs.length + ' expression(s) never evaluated \u2014 '
|
|
6816
|
+
+ brokenExprs.slice(0, 5).join(' | ') + '.' + hint,
|
|
6817
|
+
);
|
|
6818
|
+
_exprFailures = {};
|
|
6819
|
+
_exprFailureCount = 0;
|
|
6820
|
+
}
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6379
6823
|
// ==========================================================================
|
|
6380
6824
|
// Container Cleanup (for SPA navigation)
|
|
6381
6825
|
// ==========================================================================
|
|
@@ -6617,6 +7061,9 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
6617
7061
|
catch (e) { console.warn('[stx] mount callback error:', e); }
|
|
6618
7062
|
});
|
|
6619
7063
|
mountCallbacks.length = 0;
|
|
7064
|
+
|
|
7065
|
+
// Everything that should have hydrated has now had its chance (#1773).
|
|
7066
|
+
auditHydration(container, 'stx:load');
|
|
6620
7067
|
}
|
|
6621
7068
|
|
|
6622
7069
|
// Helper to process elements while skipping already-processed scoped containers
|