@stacksjs/stx 0.2.155 → 0.2.156
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/a11y.d.ts +87 -46
- package/dist/animation.d.ts +96 -42
- package/dist/appearance-bootstrap.d.ts +49 -15
- package/dist/asset-roots.d.ts +32 -0
- package/dist/asset-roots.js +7 -0
- package/dist/assets.d.ts +9 -7
- package/dist/async-components.d.ts +78 -56
- package/dist/auth.d.ts +3 -0
- package/dist/build-externalize.d.ts +40 -0
- package/dist/build-externalize.js +63 -0
- package/dist/build-id.d.ts +22 -22
- package/dist/build-message.d.ts +73 -2
- package/dist/build-message.js +47 -8
- package/dist/builtins/attrs.d.ts +14 -0
- package/dist/builtins/drawer.d.ts +22 -0
- package/dist/builtins/icon.d.ts +16 -2
- package/dist/builtins/index.d.ts +5 -15
- package/dist/builtins/modal.d.ts +24 -0
- package/dist/builtins/safe-image.d.ts +42 -0
- package/dist/builtins/stx-image.d.ts +27 -0
- package/dist/builtins/stx-link.d.ts +9 -0
- package/dist/builtins/stx-loading-indicator.d.ts +11 -0
- package/dist/builtins/suspense.d.ts +28 -0
- package/dist/builtins/toast.d.ts +17 -0
- package/dist/builtins/transition-group.d.ts +25 -0
- package/dist/caching.d.ts +12 -15
- package/dist/cli.js +1798 -908
- package/dist/client/directive.d.ts +7 -0
- package/dist/client/index.d.ts +0 -1
- package/dist/client-script-bundler.d.ts +63 -5
- package/dist/client-script-bundler.js +30 -2
- package/dist/client-script.d.ts +102 -47
- package/dist/client-script.js +15 -3
- package/dist/codemod.d.ts +56 -0
- package/dist/codemod.js +192 -0
- package/dist/color-mode-boot.d.ts +83 -65
- package/dist/component-client-factories.d.ts +1 -6
- package/dist/component-processing.d.ts +58 -49
- package/dist/component-processing.js +1 -1
- package/dist/component-registry.d.ts +94 -31
- package/dist/component-renderer.d.ts +18 -0
- package/dist/composable-loader.d.ts +24 -0
- package/dist/composable-loader.js +12 -9
- package/dist/composables/index.d.ts +7 -0
- package/dist/composables/index.js +1 -0
- package/dist/composables/use-fetch.js +30 -2
- package/dist/composables/use-form.d.ts +91 -0
- package/dist/composables/use-form.js +112 -0
- package/dist/composables.d.ts +64 -60
- package/dist/computed.d.ts +86 -92
- package/dist/conditionals.d.ts +24 -8
- package/dist/conditionals.js +16 -0
- package/dist/config.d.ts +34 -21
- package/dist/config.js +35 -14
- package/dist/craft.js +1476 -768
- package/dist/crosswind-config.d.ts +58 -0
- package/dist/crosswind-config.js +40 -0
- package/dist/csp.d.ts +107 -41
- package/dist/defer.d.ts +24 -0
- package/dist/dev-server/crosswind.d.ts +35 -26
- package/dist/dev-server/crosswind.js +2 -18
- package/dist/dev-server/serve-app.js +4 -3
- package/dist/dev-server/terminal-colors.d.ts +32 -32
- package/dist/directive-api.d.ts +86 -43
- package/dist/document-shell.d.ts +54 -41
- package/dist/dynamic-components.d.ts +9 -0
- package/dist/error-boundaries.d.ts +47 -41
- package/dist/error-handling.d.ts +1 -1
- package/dist/errors/codes.d.ts +51 -43
- package/dist/errors/formatter.d.ts +58 -38
- package/dist/errors/index.d.ts +5 -61
- package/dist/errors/logger.d.ts +114 -40
- package/dist/errors/sanitizer.d.ts +53 -23
- package/dist/errors/types.d.ts +44 -31
- package/dist/events.d.ts +86 -25
- package/dist/expressions.d.ts +50 -42
- package/dist/forms-validation.d.ts +138 -123
- package/dist/forms-validation.js +1 -90
- package/dist/forms.d.ts +120 -66
- package/dist/forms.js +31 -11
- package/dist/fragment-container.d.ts +57 -0
- package/dist/fragment-container.js +109 -0
- package/dist/framework-composables.d.ts +32 -0
- package/dist/head.d.ts +156 -114
- package/dist/heatmap.d.ts +152 -72
- package/dist/html-masking.d.ts +40 -34
- package/dist/hydration-runtime.d.ts +20 -24
- package/dist/i18n.d.ts +93 -32
- package/dist/image-optimization/auto-sibling.d.ts +32 -4
- package/dist/includes.d.ts +69 -7
- package/dist/index.d.ts +66 -0
- package/dist/index.js +23 -0
- package/dist/internal-markdown.d.ts +12 -8
- package/dist/island-chunking.d.ts +30 -13
- package/dist/keep-alive.d.ts +72 -52
- package/dist/layout-types.d.ts +45 -0
- package/dist/layout-types.js +56 -0
- package/dist/lazy-module.d.ts +10 -0
- package/dist/loops.d.ts +66 -0
- package/dist/loops.js +47 -6
- package/dist/markdown.d.ts +0 -1
- package/dist/media/image/component.d.ts +25 -9
- package/dist/media/image/directive.d.ts +15 -1
- package/dist/media/image/placeholder.d.ts +20 -9
- package/dist/media/image/processor/cache.d.ts +40 -18
- package/dist/media/image/processor/index.d.ts +1 -10
- package/dist/media/image/processor/optimizer.d.ts +12 -1
- package/dist/media/image/processor/responsive.d.ts +16 -6
- package/dist/media/image/srcset.d.ts +47 -50
- package/dist/media/shared/cache.d.ts +51 -31
- package/dist/media/shared/hash.d.ts +7 -0
- package/dist/media/types.d.ts +1235 -754
- package/dist/media/video/directive.d.ts +15 -1
- package/dist/media/video/index.d.ts +39 -17
- package/dist/media/video/processor/cache.d.ts +42 -19
- package/dist/media/video/processor/index.d.ts +1 -18
- package/dist/media/video/processor/streaming.d.ts +17 -10
- package/dist/media/video/processor/thumbnail.d.ts +21 -15
- package/dist/media/video/processor/transcoder.d.ts +11 -1
- package/dist/method-spoofing.d.ts +7 -0
- package/dist/middleware.d.ts +60 -0
- package/dist/owned-routes.d.ts +42 -0
- package/dist/owned-routes.js +26 -0
- package/dist/page-meta.d.ts +38 -0
- package/dist/page-meta.js +16 -0
- package/dist/parser/directive-parser.d.ts +57 -45
- package/dist/parser/expression-parser.d.ts +33 -35
- package/dist/parser/index.d.ts +4 -39
- package/dist/parser/tokenizer.d.ts +61 -49
- package/dist/partial-hydration.d.ts +82 -50
- package/dist/performance-utils.d.ts +217 -124
- package/dist/placeholder.d.ts +18 -9
- package/dist/plugin-system.d.ts +264 -115
- package/dist/process.d.ts +11 -6
- package/dist/process.js +21 -4
- package/dist/production-builder.js +7 -0
- package/dist/prop-sentinels.d.ts +11 -11
- package/dist/public-env.d.ts +14 -0
- package/dist/pwa/directives.d.ts +12 -3
- package/dist/pwa/inject.d.ts +10 -0
- package/dist/pwa/precache.d.ts +22 -16
- package/dist/pwa/service-worker.d.ts +12 -0
- package/dist/pwa.js +1318 -610
- package/dist/reactive-bindings.d.ts +34 -11
- package/dist/reactive.d.ts +58 -30
- package/dist/reactive.js +56 -31
- package/dist/render.js +1 -0
- package/dist/route-middleware.d.ts +2 -35
- package/dist/route-types.d.ts +134 -0
- package/dist/route-types.js +24 -0
- package/dist/router.d.ts +5 -7
- package/dist/routes.d.ts +1 -3
- package/dist/runtime-globals.d.ts +147 -24
- package/dist/runtime-globals.js +16 -1
- package/dist/runtime-injection.d.ts +8 -0
- package/dist/runtime-injection.js +33 -24
- package/dist/script-emit.d.ts +30 -0
- package/dist/script-json.d.ts +36 -0
- package/dist/script-json.js +6 -0
- package/dist/script-validation.d.ts +21 -4
- package/dist/seo.d.ts +118 -47
- package/dist/server-bindings.d.ts +21 -0
- package/dist/sfc-template.d.ts +8 -8
- package/dist/signal-processing.d.ts +35 -15
- package/dist/signals-api.d.ts +117 -58
- package/dist/signals-api.js +7 -4
- package/dist/signals.d.ts +24 -17
- package/dist/signals.js +765 -60
- package/dist/site-builder/build.js +8 -0
- package/dist/site-url.d.ts +46 -0
- package/dist/site-url.js +70 -0
- package/dist/slots.d.ts +86 -20
- package/dist/source-signature.d.ts +45 -0
- package/dist/source-signature.js +20 -0
- package/dist/ssg.d.ts +285 -142
- package/dist/ssg.js +1268 -556
- package/dist/state-dir.d.ts +4 -4
- package/dist/state-management.d.ts +0 -12
- package/dist/state-management.js +8 -25
- package/dist/store-imports.d.ts +23 -0
- package/dist/store-imports.js +3 -0
- package/dist/store-loader.d.ts +10 -0
- package/dist/store-loader.js +10 -9
- package/dist/story/analytics.d.ts +138 -60
- package/dist/story/bookmarks.d.ts +87 -34
- package/dist/story/cli.d.ts +123 -69
- package/dist/story/collect/scanner.d.ts +4 -0
- package/dist/story/collect/tree.d.ts +8 -1
- package/dist/story/commands/build.d.ts +11 -6
- package/dist/story/commands/dev.d.ts +15 -8
- package/dist/story/commands/preview.d.ts +13 -7
- package/dist/story/config-watcher.d.ts +20 -13
- package/dist/story/config.d.ts +11 -8
- package/dist/story/context.d.ts +14 -8
- package/dist/story/hmr.d.ts +24 -26
- package/dist/story/hot-swap.d.ts +34 -23
- package/dist/story/keyboard-shortcuts.d.ts +23 -18
- package/dist/story/output.d.ts +46 -44
- package/dist/story/performance.d.ts +107 -45
- package/dist/story/presets.d.ts +107 -41
- package/dist/story/renderer.d.ts +41 -27
- package/dist/story/search.d.ts +23 -12
- package/dist/story/server.d.ts +21 -11
- package/dist/story/setup.d.ts +23 -15
- package/dist/story/testing.d.ts +65 -40
- package/dist/story/types.d.ts +262 -160
- package/dist/streaming.d.ts +119 -57
- package/dist/stx-virtual-ts.d.ts +165 -0
- package/dist/stx-virtual-ts.js +372 -0
- package/dist/stx.d.ts +78 -1
- package/dist/style-scoping.d.ts +17 -4
- package/dist/suspense.d.ts +67 -52
- package/dist/teleport.d.ts +22 -0
- package/dist/template-tag-balance.d.ts +22 -6
- package/dist/transitions.d.ts +66 -55
- package/dist/typecheck.d.ts +77 -0
- package/dist/typecheck.js +156 -0
- package/dist/types/component-types.d.ts +187 -122
- package/dist/types/config-types.d.ts +758 -297
- package/dist/types/context-types.d.ts +46 -24
- package/dist/types/csp-types.d.ts +70 -66
- package/dist/types/directive-types.d.ts +211 -155
- package/dist/types/index.d.ts +6 -109
- package/dist/types/pwa-types.d.ts +273 -148
- package/dist/types.d.ts +0 -1
- package/dist/typescript-templates.d.ts +11 -0
- package/dist/unresolved-identifiers.d.ts +24 -22
- package/dist/unresolved-identifiers.js +2 -0
- package/dist/utils.d.ts +24 -21
- package/dist/validator.d.ts +28 -26
- package/dist/variable-extractor.d.ts +48 -13
- package/dist/variable-extractor.js +27 -4
- package/dist/view-composers.d.ts +80 -81
- package/dist/virtual-scrolling.d.ts +101 -81
- package/dist/vue-template.d.ts +23 -0
- package/dist/web-components/css-scoping.d.ts +21 -11
- package/dist/web-components.d.ts +81 -58
- package/dist/x-element.d.ts +0 -1
- package/package.json +5 -5
- package/stx.d.ts +78 -1
package/dist/signals.js
CHANGED
|
@@ -3,13 +3,13 @@ import { runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
|
|
|
3
3
|
|
|
4
4
|
export * from "./signals-api.js";
|
|
5
5
|
export function generateSignalsRuntime() {
|
|
6
|
+
const stripped = stripConsoleLog(generateSignalsRuntimeDev());
|
|
6
7
|
try {
|
|
7
|
-
const stripped = stripConsoleLog(generateSignalsRuntimeDev());
|
|
8
8
|
let minified = new Bun.Transpiler({ loader: "js", minifyWhitespace: !0 }).transformSync(stripped);
|
|
9
9
|
minified = minified.replace(/\}(var |let |const |function )/g, "};$1");
|
|
10
10
|
return minified;
|
|
11
11
|
} catch {
|
|
12
|
-
return
|
|
12
|
+
return stripped;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
function stripConsoleLog(src) {
|
|
@@ -107,15 +107,118 @@ console.log('[stx] entering IIFE');
|
|
|
107
107
|
} catch (e) { return '[unserializable]'; }
|
|
108
108
|
}
|
|
109
109
|
// Wraps fetch with timing + a timeline record (the data layer's queries).
|
|
110
|
+
// Turn a non-ok Response into an Error, reading the body FIRST.
|
|
111
|
+
//
|
|
112
|
+
// Every data primitive threw a synthesized 'HTTP nnn: statusText' before
|
|
113
|
+
// touching the body, so whatever the server actually said \u2014 a validation
|
|
114
|
+
// map, a rate-limit reason, an auth message \u2014 was unreachable from any stx
|
|
115
|
+
// primitive, with no escape hatch (stacksjs/stx#1848). That is the single
|
|
116
|
+
// most cited reason an app keeps writing raw fetch() calls.
|
|
117
|
+
//
|
|
118
|
+
// The body is attached rather than only interpolated: err.data is what a
|
|
119
|
+
// caller needs to render field errors, and err.status is what it needs to
|
|
120
|
+
// tell 401 from 500 without parsing the message.
|
|
121
|
+
async function __stxHttpError(response) {
|
|
122
|
+
var body = null;
|
|
123
|
+
var text = '';
|
|
124
|
+
try { text = await response.text(); } catch (e) { text = ''; }
|
|
125
|
+
if (text) {
|
|
126
|
+
try { body = JSON.parse(text); } catch (e) { body = text; }
|
|
127
|
+
}
|
|
128
|
+
var detail = '';
|
|
129
|
+
if (body && typeof body === 'object') detail = body.message || body.error || '';
|
|
130
|
+
else if (typeof body === 'string') detail = body.slice(0, 200);
|
|
131
|
+
var message = 'HTTP ' + response.status;
|
|
132
|
+
if (response.statusText) message += ': ' + response.statusText;
|
|
133
|
+
if (detail) message += ' - ' + detail;
|
|
134
|
+
var err = new Error(message);
|
|
135
|
+
err.status = response.status;
|
|
136
|
+
err.statusText = response.statusText;
|
|
137
|
+
err.data = body;
|
|
138
|
+
err.response = response;
|
|
139
|
+
return err;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// One place an app can reach the data layer. Every stx data primitive goes
|
|
143
|
+
// through __stxFetch, the only fetch call in this runtime, so a single pair
|
|
144
|
+
// of hooks covers useFetch, useQuery and useMutation (#1855).
|
|
145
|
+
//
|
|
146
|
+
// Without it there is no way to attach an Authorization header or notice a
|
|
147
|
+
// 401 in one place, which is the reason an app keeps hand-written fetch
|
|
148
|
+
// calls sitting next to the primitives that were meant to replace them.
|
|
149
|
+
//
|
|
150
|
+
// Hooks mutate the context they are handed and their return value is
|
|
151
|
+
// ignored \u2014 the ofetch shape most people will already know. Throwing from a
|
|
152
|
+
// hook fails the request, which is what a token-refresh flow wants.
|
|
153
|
+
var __stxFetchHooks = { onRequest: null, onResponse: null, onResponseError: null };
|
|
154
|
+
|
|
155
|
+
function configureFetch(cfg) {
|
|
156
|
+
cfg = cfg || {};
|
|
157
|
+
// Replace rather than stack: a module re-evaluated by hot reload or by a
|
|
158
|
+
// SPA script re-run must not end up with the same interceptor installed
|
|
159
|
+
// twice, sending two Authorization headers or counting a 401 twice.
|
|
160
|
+
__stxFetchHooks.onRequest = typeof cfg.onRequest === 'function' ? cfg.onRequest : null;
|
|
161
|
+
__stxFetchHooks.onResponse = typeof cfg.onResponse === 'function' ? cfg.onResponse : null;
|
|
162
|
+
// Fires for a non-ok response and for a thrown fetch (network error). If it
|
|
163
|
+
// returns a Response, that Response replaces the original \u2014 this is how a
|
|
164
|
+
// 401 -> refresh -> retry is expressed once instead of per call site. The
|
|
165
|
+
// retry itself belongs to the hook: it should refresh the token and re-issue
|
|
166
|
+
// the request with a plain fetch() (not this data layer) so it does not
|
|
167
|
+
// re-enter the hook and loop. Returning nothing lets the error stand.
|
|
168
|
+
__stxFetchHooks.onResponseError = typeof cfg.onResponseError === 'function' ? cfg.onResponseError : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// headers may be a function, so it can be evaluated per request rather than
|
|
172
|
+
// captured once \u2014 a token that changes between calls. Object spread over a
|
|
173
|
+
// function copies no own enumerable properties, so passing one used to
|
|
174
|
+
// contribute nothing at all, in silence.
|
|
175
|
+
function __stxHeaders(h) {
|
|
176
|
+
return (typeof h === 'function' ? h() : h) || {};
|
|
177
|
+
}
|
|
178
|
+
|
|
110
179
|
async function __stxFetch(source, url, opts) {
|
|
111
180
|
var t0 = __stxDevtoolsNow();
|
|
181
|
+
opts = opts || {};
|
|
182
|
+
if (__stxFetchHooks.onRequest) {
|
|
183
|
+
// Normalised first so a hook can write ctx.options.headers.Authorization
|
|
184
|
+
// without having to create the bag itself.
|
|
185
|
+
opts.headers = __stxHeaders(opts.headers);
|
|
186
|
+
var reqCtx = { source: source, url: String(url), options: opts };
|
|
187
|
+
await __stxFetchHooks.onRequest(reqCtx);
|
|
188
|
+
// Reassignable, so a hook can prefix a baseURL or swap the options
|
|
189
|
+
// wholesale instead of only mutating them.
|
|
190
|
+
if (reqCtx.url) url = reqCtx.url;
|
|
191
|
+
opts = reqCtx.options || opts;
|
|
192
|
+
}
|
|
112
193
|
var method = (opts && opts.method) || 'GET';
|
|
194
|
+
// A hook may hand back a Response to use in place of the original. Anything
|
|
195
|
+
// else (a truthy non-Response, undefined) leaves the outcome unchanged.
|
|
196
|
+
function __stxIsResponse(v) { return !!v && typeof v.ok === 'boolean'; }
|
|
113
197
|
try {
|
|
114
198
|
var r = await fetch(url, opts);
|
|
115
199
|
__stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: r.status, ok: r.ok, ms: __stxDevtoolsNow() - t0 });
|
|
200
|
+
if (__stxFetchHooks.onResponse) {
|
|
201
|
+
await __stxFetchHooks.onResponse({ source: source, url: String(url), options: opts, response: r });
|
|
202
|
+
}
|
|
203
|
+
if (!r.ok && __stxFetchHooks.onResponseError) {
|
|
204
|
+
var recovered = await __stxFetchHooks.onResponseError({ source: source, url: String(url), options: opts, response: r, error: null });
|
|
205
|
+
if (__stxIsResponse(recovered)) {
|
|
206
|
+
__stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: recovered.status, ok: recovered.ok, retried: true, ms: __stxDevtoolsNow() - t0 });
|
|
207
|
+
return recovered;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
116
210
|
return r;
|
|
117
211
|
} catch (e) {
|
|
118
212
|
__stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: 0, ok: false, error: String((e && e.message) || e), ms: __stxDevtoolsNow() - t0 });
|
|
213
|
+
// A network-level failure (no Response) also reaches the hook, so a retry
|
|
214
|
+
// can cover a dropped connection, not only an auth status.
|
|
215
|
+
if (__stxFetchHooks.onResponseError) {
|
|
216
|
+
var recoveredErr = await __stxFetchHooks.onResponseError({ source: source, url: String(url), options: opts, response: null, error: e });
|
|
217
|
+
if (__stxIsResponse(recoveredErr)) {
|
|
218
|
+
__stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: recoveredErr.status, ok: recoveredErr.ok, retried: true, ms: __stxDevtoolsNow() - t0 });
|
|
219
|
+
return recoveredErr;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
119
222
|
throw e;
|
|
120
223
|
}
|
|
121
224
|
}
|
|
@@ -303,6 +406,40 @@ else {
|
|
|
303
406
|
});
|
|
304
407
|
}
|
|
305
408
|
|
|
409
|
+
// A proxy for the WRITE side of x-model, kept separate from the read proxy
|
|
410
|
+
// on purpose.
|
|
411
|
+
//
|
|
412
|
+
// The read proxy returns a plain object as-is, which is right for reading \u2014
|
|
413
|
+
// ordinary data should stay ordinary. But it means an assignment lands on the
|
|
414
|
+
// raw object, so a signal held there gets REPLACED by the typed value rather
|
|
415
|
+
// than set (#1883). Descending through a write proxy instead keeps set-trap
|
|
416
|
+
// semantics all the way down, so the signal at the end of any path is set.
|
|
417
|
+
function createModelWriteProxy(target) {
|
|
418
|
+
return new Proxy(target, {
|
|
419
|
+
get(t, prop) {
|
|
420
|
+
var val = t[prop];
|
|
421
|
+
if (val && typeof val === 'function' && (val._isSignal || val._isDerived)) {
|
|
422
|
+
// Descend INTO the signal's value: x-model="form.email" over
|
|
423
|
+
// state({email}) must reach the object, not stamp a property onto
|
|
424
|
+
// the signal function itself.
|
|
425
|
+
var inner = val();
|
|
426
|
+
return (inner && typeof inner === 'object') ? createModelWriteProxy(inner) : inner;
|
|
427
|
+
}
|
|
428
|
+
if (val && typeof val === 'object') return createModelWriteProxy(val);
|
|
429
|
+
return val;
|
|
430
|
+
},
|
|
431
|
+
set(t, prop, value) {
|
|
432
|
+
var cur = t[prop];
|
|
433
|
+
if (cur && typeof cur === 'function' && cur._isSignal && typeof cur.set === 'function') {
|
|
434
|
+
cur.set(value);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
t[prop] = value;
|
|
438
|
+
return true;
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
306
443
|
// ==========================================================================
|
|
307
444
|
// Pipe Syntax Support (Feature #2)
|
|
308
445
|
// ==========================================================================
|
|
@@ -607,7 +744,16 @@ finally {
|
|
|
607
744
|
activeEffect = runEffect;
|
|
608
745
|
effectStack.push(runEffect);
|
|
609
746
|
try {
|
|
610
|
-
cleanup
|
|
747
|
+
// Only a FUNCTION is a cleanup. An effect body is an expression as
|
|
748
|
+
// often as it is a block, and an expression has a value: an effect
|
|
749
|
+
// written as an arrow assigning to textContent returns the assigned
|
|
750
|
+
// string. Storing that and calling it on the next run threw
|
|
751
|
+
// "cleanup is not a function" from the line above \u2014 outside this try,
|
|
752
|
+
// so it escaped runEffect entirely and propagated out of whatever
|
|
753
|
+
// .set() triggered the notification, taking unrelated subscribers with
|
|
754
|
+
// it. Only ever on the second run, which is why it read as correct.
|
|
755
|
+
var __result = fn();
|
|
756
|
+
cleanup = typeof __result === 'function' ? __result : undefined;
|
|
611
757
|
}
|
|
612
758
|
catch (e) {
|
|
613
759
|
// Auto-dispose effects that fail \u2014 prevents zombie subscriptions
|
|
@@ -745,6 +891,38 @@ finally {
|
|
|
745
891
|
destroyCallbacks.push(fn);
|
|
746
892
|
}
|
|
747
893
|
|
|
894
|
+
// Run mount callbacks and keep whatever teardown they hand back.
|
|
895
|
+
//
|
|
896
|
+
// A mount callback may return its own cleanup \u2014 the shape LifecycleCallback
|
|
897
|
+
// has always declared, and the shape every peer framework uses. Only the two
|
|
898
|
+
// component-factory paths honoured it; the other flush sites called fn() and
|
|
899
|
+
// dropped the result on the floor, so the teardown never ran and nothing said
|
|
900
|
+
// so. A listener or timer registered that way outlived its element and leaked
|
|
901
|
+
// once per re-render (#1857).
|
|
902
|
+
//
|
|
903
|
+
// The sink argument is where a returned cleanup is parked: a scoped flush
|
|
904
|
+
// passes that scope's __destroyCallbacks, a global flush passes the module
|
|
905
|
+
// queue. (No backticks in this comment \u2014 it lives inside a template literal.)
|
|
906
|
+
function runMountCallbacks(list, sink) {
|
|
907
|
+
if (!list || !list.length) return;
|
|
908
|
+
var target = sink || destroyCallbacks;
|
|
909
|
+
for (var i = 0; i < list.length; i++) {
|
|
910
|
+
try {
|
|
911
|
+
var cleanup = list[i]();
|
|
912
|
+
if (typeof cleanup === 'function') target.push(cleanup);
|
|
913
|
+
}
|
|
914
|
+
catch (e) { console.error('[stx] onMount error:', e); }
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// The scope's own teardown queue, created on demand so a scope that never
|
|
919
|
+
// returns a cleanup does not carry an empty array around.
|
|
920
|
+
function scopeDestroySink(scopeVars) {
|
|
921
|
+
if (!scopeVars) return destroyCallbacks;
|
|
922
|
+
scopeVars.__destroyCallbacks = scopeVars.__destroyCallbacks || [];
|
|
923
|
+
return scopeVars.__destroyCallbacks;
|
|
924
|
+
}
|
|
925
|
+
|
|
748
926
|
// Drain any early mount/destroy calls captured by the pre-initialization shim
|
|
749
927
|
if (window.__stx_early_mounts) { window.__stx_early_mounts.forEach(function(fn) { mountCallbacks.push(fn); }); window.__stx_early_mounts = null; }
|
|
750
928
|
if (window.__stx_early_destroys) { window.__stx_early_destroys.forEach(function(fn) { destroyCallbacks.push(fn); }); window.__stx_early_destroys = null; }
|
|
@@ -890,7 +1068,16 @@ finally {
|
|
|
890
1068
|
const error = state(null);
|
|
891
1069
|
if (options.suspense) registerSuspense(loading, error);
|
|
892
1070
|
|
|
1071
|
+
// A superseded or unmounted request must not resolve into a torn-down
|
|
1072
|
+
// scope. Each run gets its own AbortController; a newer run (refetch or a
|
|
1073
|
+
// reactive URL change) aborts the one before it, and onDestroy aborts the
|
|
1074
|
+
// last in-flight one. State writes are guarded on signal.aborted (#1871).
|
|
1075
|
+
let __stxAbort = null;
|
|
1076
|
+
|
|
893
1077
|
const fetchData = async () => {
|
|
1078
|
+
if (__stxAbort) __stxAbort.abort();
|
|
1079
|
+
__stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1080
|
+
const __signal = __stxAbort ? __stxAbort.signal : undefined;
|
|
894
1081
|
loading.set(true);
|
|
895
1082
|
error.set(null);
|
|
896
1083
|
|
|
@@ -905,8 +1092,9 @@ finally {
|
|
|
905
1092
|
method: options.method || 'GET',
|
|
906
1093
|
headers: {
|
|
907
1094
|
'Content-Type': 'application/json',
|
|
908
|
-
...(options.headers
|
|
909
|
-
}
|
|
1095
|
+
...__stxHeaders(options.headers)
|
|
1096
|
+
},
|
|
1097
|
+
signal: __signal
|
|
910
1098
|
};
|
|
911
1099
|
|
|
912
1100
|
if (options.body) {
|
|
@@ -916,18 +1104,21 @@ finally {
|
|
|
916
1104
|
}
|
|
917
1105
|
|
|
918
1106
|
const response = await __stxFetch('useFetch', url, fetchOptions);
|
|
1107
|
+
if (__signal && __signal.aborted) return;
|
|
919
1108
|
|
|
920
|
-
if (!response.ok)
|
|
921
|
-
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
|
|
922
|
-
}
|
|
1109
|
+
if (!response.ok) throw await __stxHttpError(response);
|
|
923
1110
|
|
|
924
1111
|
const result = await response.json();
|
|
1112
|
+
if (__signal && __signal.aborted) return;
|
|
925
1113
|
|
|
926
1114
|
// Apply transform if provided
|
|
927
1115
|
const transformed = options.transform ? options.transform(result) : result;
|
|
928
1116
|
data.set(transformed);
|
|
929
1117
|
}
|
|
930
1118
|
catch (e) {
|
|
1119
|
+
// A superseded/unmounted request rejects with AbortError \u2014 expected,
|
|
1120
|
+
// not a failure to surface. The run that superseded it owns the state.
|
|
1121
|
+
if (e && e.name === 'AbortError') return;
|
|
931
1122
|
console.error('[STX] useFetch error:', e);
|
|
932
1123
|
// Store Error objects (not just messages) so consumers can use
|
|
933
1124
|
// .message, .stack, instanceof Error. Matches the composable's
|
|
@@ -936,7 +1127,8 @@ catch (e) {
|
|
|
936
1127
|
if (options.onError) options.onError(e);
|
|
937
1128
|
}
|
|
938
1129
|
finally {
|
|
939
|
-
loading.
|
|
1130
|
+
// If a newer run (or onDestroy) aborted this one, that run owns loading.
|
|
1131
|
+
if (!__signal || !__signal.aborted) loading.set(false);
|
|
940
1132
|
}
|
|
941
1133
|
};
|
|
942
1134
|
|
|
@@ -962,6 +1154,10 @@ else if (immediate) {
|
|
|
962
1154
|
onMount(fetchData);
|
|
963
1155
|
}
|
|
964
1156
|
|
|
1157
|
+
// Abort an in-flight request when the owning scope is destroyed, so its
|
|
1158
|
+
// resolution can't write into a torn-down component (#1871).
|
|
1159
|
+
if (typeof onDestroy === 'function') onDestroy(function() { if (__stxAbort) __stxAbort.abort(); });
|
|
1160
|
+
|
|
965
1161
|
return {
|
|
966
1162
|
data,
|
|
967
1163
|
loading,
|
|
@@ -1015,6 +1211,33 @@ else if (immediate) {
|
|
|
1015
1211
|
else window.location.href = url;
|
|
1016
1212
|
}
|
|
1017
1213
|
|
|
1214
|
+
// Re-run the CURRENT route against the server and swap the result, keeping
|
|
1215
|
+
// every client signal on the page alive.
|
|
1216
|
+
//
|
|
1217
|
+
// The router grew refresh()/invalidate() for this, but they lived only on
|
|
1218
|
+
// window.stxRouter \u2014 nothing in the authoring surface reached them, so a
|
|
1219
|
+
// <script client> block still had no way to say "the mutation changed what
|
|
1220
|
+
// the server rendered". Apps kept calling location.reload() and threw away
|
|
1221
|
+
// the state the SPA exists to preserve. See #1850.
|
|
1222
|
+
//
|
|
1223
|
+
// Degrades to a real reload when the router is absent (a page that never
|
|
1224
|
+
// booted it, or a mid-boot call), mirroring how navigate() falls through to
|
|
1225
|
+
// window.location.
|
|
1226
|
+
function refresh() {
|
|
1227
|
+
if (window.stxRouter && typeof window.stxRouter.refresh === 'function')
|
|
1228
|
+
return window.stxRouter.refresh();
|
|
1229
|
+
window.location.reload();
|
|
1230
|
+
return Promise.resolve(false);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Expire a cached route so the NEXT visit re-fetches it, without navigating
|
|
1234
|
+
// now. Pairs with refresh(): refresh the page you are on, invalidate the ones
|
|
1235
|
+
// your mutation also changed.
|
|
1236
|
+
function invalidateRoute(url) {
|
|
1237
|
+
if (window.stxRouter && typeof window.stxRouter.invalidate === 'function')
|
|
1238
|
+
window.stxRouter.invalidate(url);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1018
1241
|
function goBack() { window.history.back(); }
|
|
1019
1242
|
function goForward() { window.history.forward(); }
|
|
1020
1243
|
|
|
@@ -1060,6 +1283,20 @@ else if (immediate) {
|
|
|
1060
1283
|
};
|
|
1061
1284
|
}
|
|
1062
1285
|
|
|
1286
|
+
// Exported from src/runtime.ts, whose whole stated purpose is client-facing
|
|
1287
|
+
// APIs that hide window internals \u2014 but never published on window.stx, so a
|
|
1288
|
+
// client script importing them destructured undefined and threw on use. The
|
|
1289
|
+
// capability was always here behind useRoute().params; only the documented
|
|
1290
|
+
// names were unreachable (stacksjs/stx#1843).
|
|
1291
|
+
function useRouteParams() {
|
|
1292
|
+
return _routeParams();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function useRouteParam(name, fallback) {
|
|
1296
|
+
var value = _routeParams()[name];
|
|
1297
|
+
return value === undefined ? fallback : value;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1063
1300
|
function useSearchParams() {
|
|
1064
1301
|
var params = state(Object.fromEntries(new URLSearchParams(window.location.search)));
|
|
1065
1302
|
var syncFromUrl = function() {
|
|
@@ -1136,6 +1373,13 @@ else if (immediate) {
|
|
|
1136
1373
|
// ==========================================================================
|
|
1137
1374
|
|
|
1138
1375
|
var _queryCache = {};
|
|
1376
|
+
// Requests currently in flight, keyed exactly like _queryCache. _queryCache
|
|
1377
|
+
// is written only after the body parses, so the whole in-flight window was a
|
|
1378
|
+
// cache miss and N components mounting together issued N identical requests
|
|
1379
|
+
// (#1869). Refcounted: a caller that loses interest must not cancel a
|
|
1380
|
+
// response another caller is still waiting on.
|
|
1381
|
+
var _queryInflight = {};
|
|
1382
|
+
var __stxQueryInstance = 0;
|
|
1139
1383
|
|
|
1140
1384
|
// Suspense registry (#1742). A query created with { suspense: true } pushes
|
|
1141
1385
|
// { el, loading, error } here at creation time (which runs during partial-scope
|
|
@@ -1165,6 +1409,20 @@ else if (immediate) {
|
|
|
1165
1409
|
var isStale = state(false);
|
|
1166
1410
|
if (options.suspense) registerSuspense(loading, error);
|
|
1167
1411
|
|
|
1412
|
+
// Per-run AbortController: a newer run aborts the previous in-flight one and
|
|
1413
|
+
// onDestroy aborts the last, so a resolved request can't write into a
|
|
1414
|
+
// torn-down scope (#1871).
|
|
1415
|
+
var __stxAbort = null;
|
|
1416
|
+
// Identity, so dedup can tell "another component wants this key" from
|
|
1417
|
+
// "this component asked again". They must behave differently: joining is
|
|
1418
|
+
// right for the first, and wrong for the second \u2014 refetch() after a
|
|
1419
|
+
// mutation has to hit the network, not adopt the pre-mutation request that
|
|
1420
|
+
// happens to still be open (#1869).
|
|
1421
|
+
var __instanceId = ++__stxQueryInstance;
|
|
1422
|
+
// The shared entry this instance is currently attached to, so superseding
|
|
1423
|
+
// can release its claim without cancelling a request someone else joined.
|
|
1424
|
+
var __joined = null;
|
|
1425
|
+
|
|
1168
1426
|
var fetchData = async function() {
|
|
1169
1427
|
var resolvedUrl = typeof url === 'function' ? url() : url;
|
|
1170
1428
|
if (!resolvedUrl) { loading.set(false); return; }
|
|
@@ -1186,13 +1444,69 @@ else if (immediate) {
|
|
|
1186
1444
|
isStale.set(true);
|
|
1187
1445
|
}
|
|
1188
1446
|
|
|
1447
|
+
// Join a request ANOTHER instance already has open for this key. Not one
|
|
1448
|
+
// of our own: this instance calling refetch() means "I want fresh data",
|
|
1449
|
+
// and adopting its own pre-mutation request would silently return stale
|
|
1450
|
+
// data \u2014 so an own-instance run still supersedes, exactly as #1871 says.
|
|
1451
|
+
var shared = _queryInflight[key];
|
|
1452
|
+
if (shared && shared.owner !== __instanceId) {
|
|
1453
|
+
shared.refs++;
|
|
1454
|
+
__joined = shared;
|
|
1455
|
+
loading.set(true);
|
|
1456
|
+
error.set(null);
|
|
1457
|
+
try {
|
|
1458
|
+
var joinedResult = await shared.promise;
|
|
1459
|
+
var joinedTransformed = options.transform ? options.transform(joinedResult) : joinedResult;
|
|
1460
|
+
data.set(joinedTransformed);
|
|
1461
|
+
isStale.set(false);
|
|
1462
|
+
if (options.onSuccess) options.onSuccess(joinedTransformed);
|
|
1463
|
+
}
|
|
1464
|
+
catch (e) {
|
|
1465
|
+
if (e && e.name === 'AbortError') return;
|
|
1466
|
+
error.set(e.message || 'Query failed');
|
|
1467
|
+
if (options.onError) options.onError(e);
|
|
1468
|
+
}
|
|
1469
|
+
finally {
|
|
1470
|
+
shared.refs--;
|
|
1471
|
+
if (__joined === shared) __joined = null;
|
|
1472
|
+
loading.set(false);
|
|
1473
|
+
}
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Superseding this instance's own run. Abort ONLY if nobody joined it \u2014
|
|
1478
|
+
// cancelling a request another component is waiting on would leave it
|
|
1479
|
+
// loading forever, which would turn #1871's fix into a worse bug.
|
|
1480
|
+
if (__stxAbort) {
|
|
1481
|
+
if (__joined && __joined.refs > 1) __joined.refs--;
|
|
1482
|
+
else __stxAbort.abort();
|
|
1483
|
+
}
|
|
1484
|
+
__joined = null;
|
|
1485
|
+
__stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1486
|
+
var __signal = __stxAbort ? __stxAbort.signal : undefined;
|
|
1189
1487
|
loading.set(true);
|
|
1190
1488
|
error.set(null);
|
|
1191
1489
|
try {
|
|
1192
|
-
var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...(options.headers
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1490
|
+
var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) }, signal: __signal };
|
|
1491
|
+
// Published BEFORE it is awaited, so a caller starting in the same tick
|
|
1492
|
+
// finds it. The network part only \u2014 transform, cache write and
|
|
1493
|
+
// onSuccess stay per-caller, since each may transform differently.
|
|
1494
|
+
var netPromise = (async function() {
|
|
1495
|
+
var response = await __stxFetch('useQuery', resolvedUrl, fetchOpts);
|
|
1496
|
+
if (!response.ok) throw await __stxHttpError(response);
|
|
1497
|
+
return await response.json();
|
|
1498
|
+
})();
|
|
1499
|
+
var entry = { owner: __instanceId, refs: 1, controller: __stxAbort, promise: netPromise };
|
|
1500
|
+
_queryInflight[key] = entry;
|
|
1501
|
+
__joined = entry;
|
|
1502
|
+
// Cleared on settle, both ways. A settled entry left behind would turn
|
|
1503
|
+
// dedup into a permanent cache and no request for this key would ever
|
|
1504
|
+
// be issued again.
|
|
1505
|
+
var releaseEntry = function() { if (_queryInflight[key] === entry) delete _queryInflight[key]; };
|
|
1506
|
+
netPromise.then(releaseEntry, releaseEntry);
|
|
1507
|
+
|
|
1508
|
+
var result = await netPromise;
|
|
1509
|
+
if (__signal && __signal.aborted) return;
|
|
1196
1510
|
var transformed = options.transform ? options.transform(result) : result;
|
|
1197
1511
|
data.set(transformed);
|
|
1198
1512
|
isStale.set(false);
|
|
@@ -1202,11 +1516,14 @@ else if (immediate) {
|
|
|
1202
1516
|
setTimeout(function() { delete _queryCache[key]; }, cacheTime);
|
|
1203
1517
|
}
|
|
1204
1518
|
catch (e) {
|
|
1519
|
+
// A superseded/unmounted request rejects with AbortError \u2014 expected;
|
|
1520
|
+
// the run that superseded it owns the state (#1871).
|
|
1521
|
+
if (e && e.name === 'AbortError') return;
|
|
1205
1522
|
error.set(e.message || 'Query failed');
|
|
1206
1523
|
if (options.onError) options.onError(e);
|
|
1207
1524
|
}
|
|
1208
1525
|
finally {
|
|
1209
|
-
loading.set(false);
|
|
1526
|
+
if (!__signal || !__signal.aborted) loading.set(false);
|
|
1210
1527
|
}
|
|
1211
1528
|
};
|
|
1212
1529
|
|
|
@@ -1226,12 +1543,25 @@ finally {
|
|
|
1226
1543
|
onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
|
|
1227
1544
|
}
|
|
1228
1545
|
|
|
1229
|
-
// refetchInterval
|
|
1546
|
+
// refetchInterval: poll only while the tab is visible and (if given)
|
|
1547
|
+
// enabled holds; optionally stop after an error so a broken endpoint is
|
|
1548
|
+
// not hammered every interval in a backgrounded tab (#1870).
|
|
1230
1549
|
if (options.refetchInterval) {
|
|
1231
|
-
var intervalId = setInterval(
|
|
1550
|
+
var intervalId = setInterval(function() {
|
|
1551
|
+
if (typeof document !== 'undefined' && document.hidden) return;
|
|
1552
|
+
var en = options.enabled;
|
|
1553
|
+
if (typeof en === 'function' ? !en() : en === false) return;
|
|
1554
|
+
var p = fetchData();
|
|
1555
|
+
if (options.stopOnError && p && typeof p.then === 'function') {
|
|
1556
|
+
p.then(function() { if (error()) clearInterval(intervalId); });
|
|
1557
|
+
}
|
|
1558
|
+
}, options.refetchInterval);
|
|
1232
1559
|
onDestroy(function() { clearInterval(intervalId); });
|
|
1233
1560
|
}
|
|
1234
1561
|
|
|
1562
|
+
// Abort an in-flight request when the owning scope is destroyed (#1871).
|
|
1563
|
+
onDestroy(function() { if (__stxAbort) __stxAbort.abort(); });
|
|
1564
|
+
|
|
1235
1565
|
return {
|
|
1236
1566
|
data: data,
|
|
1237
1567
|
loading: loading,
|
|
@@ -1267,11 +1597,11 @@ finally {
|
|
|
1267
1597
|
var resolvedUrl = typeof url === 'function' ? url() : url;
|
|
1268
1598
|
var fetchOpts = {
|
|
1269
1599
|
method: options.method || 'POST',
|
|
1270
|
-
headers: { 'Content-Type': 'application/json', ...(options.headers
|
|
1600
|
+
headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) },
|
|
1271
1601
|
body: typeof body === 'string' ? body : JSON.stringify(body)
|
|
1272
1602
|
};
|
|
1273
1603
|
var response = await __stxFetch('useMutation', resolvedUrl, fetchOpts);
|
|
1274
|
-
if (!response.ok) throw
|
|
1604
|
+
if (!response.ok) throw await __stxHttpError(response);
|
|
1275
1605
|
var result = await response.json();
|
|
1276
1606
|
var transformed = options.transform ? options.transform(result) : result;
|
|
1277
1607
|
data.set(transformed);
|
|
@@ -1611,7 +1941,7 @@ catch (e) {
|
|
|
1611
1941
|
var sv = sid && window.stx._scopes ? window.stx._scopes[sid] : null;
|
|
1612
1942
|
if (sv && sv.__mountCallbacks && !sv.__mounted) {
|
|
1613
1943
|
sv.__mounted = true;
|
|
1614
|
-
sv.__mountCallbacks
|
|
1944
|
+
runMountCallbacks(sv.__mountCallbacks, scopeDestroySink(sv));
|
|
1615
1945
|
}
|
|
1616
1946
|
window.dispatchEvent(new CustomEvent('stx:hydrated', { detail: { el: el, trigger: trigger } }));
|
|
1617
1947
|
};
|
|
@@ -2697,8 +3027,22 @@ catch (e) {
|
|
|
2697
3027
|
capturedScope[expr].set(val);
|
|
2698
3028
|
}
|
|
2699
3029
|
else {
|
|
2700
|
-
|
|
2701
|
-
|
|
3030
|
+
// Write through the SAME auto-unwrap proxy the read path uses.
|
|
3031
|
+
//
|
|
3032
|
+
// This used to destructure the scope into plain parameters, so an
|
|
3033
|
+
// lvalue like store.title assigned onto the raw object and REPLACED
|
|
3034
|
+
// the signal with a raw string. Everything else bound to that signal
|
|
3035
|
+
// stopped updating, its .set was gone so the next write threw
|
|
3036
|
+
// TypeError, and nothing warned \u2014 the field the user typed into was
|
|
3037
|
+
// the only thing on the page that still looked right (#1883).
|
|
3038
|
+
//
|
|
3039
|
+
// The proxy's set trap already did the correct thing (call .set on a
|
|
3040
|
+
// signal-valued property) and was simply never reached from here.
|
|
3041
|
+
// Routing through it also drops the old requirement that every scope
|
|
3042
|
+
// key be a valid identifier, since nothing is destructured now.
|
|
3043
|
+
var writeScope = createModelWriteProxy(capturedScope);
|
|
3044
|
+
var writeFn = new Function('__scope__', '__v__', 'with(__scope__) { ' + expr + ' = __v__ }');
|
|
3045
|
+
writeFn(writeScope, val);
|
|
2702
3046
|
}
|
|
2703
3047
|
}
|
|
2704
3048
|
catch (e) {
|
|
@@ -3178,10 +3522,7 @@ else if (typeof value === 'string') {
|
|
|
3178
3522
|
});
|
|
3179
3523
|
if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
|
|
3180
3524
|
scopeVars.__mounted = true;
|
|
3181
|
-
scopeVars.__mountCallbacks
|
|
3182
|
-
try { fn(); }
|
|
3183
|
-
catch (e) { console.error('[stx] onMount error:', e); }
|
|
3184
|
-
});
|
|
3525
|
+
runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
|
|
3185
3526
|
}
|
|
3186
3527
|
});
|
|
3187
3528
|
}
|
|
@@ -3631,6 +3972,27 @@ catch (e) {
|
|
|
3631
3972
|
const newGroups = [];
|
|
3632
3973
|
const usedKeys = new Set();
|
|
3633
3974
|
|
|
3975
|
+
// Minimal-move reconciliation.
|
|
3976
|
+
//
|
|
3977
|
+
// Every reused row used to be round-tripped through a DocumentFragment,
|
|
3978
|
+
// and appendChild into a fragment DETACHES the node from the document
|
|
3979
|
+
// first. Removing a focused node blurs it and resets activeElement, so a
|
|
3980
|
+
// list could not contain a field the user was typing in: any write to the
|
|
3981
|
+
// backing array kicked them out of it, losing caret, selection and undo
|
|
3982
|
+
// history. It happened for every row unconditionally, including rows that
|
|
3983
|
+
// had not moved, and unkeyed loops took the same path because getItemKey
|
|
3984
|
+
// falls back to the index (#1882).
|
|
3985
|
+
//
|
|
3986
|
+
// The cursor walks the existing rendering in order. A node already at the
|
|
3987
|
+
// cursor is left ENTIRELY alone \u2014 an unchanged list now performs zero DOM
|
|
3988
|
+
// operations \u2014 and a node that genuinely has to move is moved with
|
|
3989
|
+
// insertBefore, which relocates it without a detach/reattach cycle.
|
|
3990
|
+
let cursor = null;
|
|
3991
|
+
for (let ci = 0; ci < currentGroups.length && !cursor; ci++) {
|
|
3992
|
+
const seed = liveGroupNodes(currentGroups[ci] || []);
|
|
3993
|
+
if (seed.length) cursor = seed[0];
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3634
3996
|
for (let i = 0; i < list.length; i++) {
|
|
3635
3997
|
const key = newKeys[i];
|
|
3636
3998
|
const existing = oldKeyMap.get(key);
|
|
@@ -3643,9 +4005,20 @@ catch (e) {
|
|
|
3643
4005
|
// must not resurrect that hidden node.
|
|
3644
4006
|
const group = existing.shift();
|
|
3645
4007
|
const liveNodes = liveGroupNodes(group);
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4008
|
+
for (let li = 0; li < liveNodes.length; li++) {
|
|
4009
|
+
const groupNode = liveNodes[li];
|
|
4010
|
+
if (groupNode === cursor) {
|
|
4011
|
+
// Already exactly where it belongs. Touching it here is what
|
|
4012
|
+
// used to blur it, so the correct action is to do nothing.
|
|
4013
|
+
cursor = cursor.nextSibling;
|
|
4014
|
+
}
|
|
4015
|
+
else {
|
|
4016
|
+
// Moves without detaching. The cursor does not advance: this node
|
|
4017
|
+
// is now behind it, and the cursor still marks the next position
|
|
4018
|
+
// to fill.
|
|
4019
|
+
parent.insertBefore(groupNode, cursor || placeholder);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
3649
4022
|
newElements.push(...group);
|
|
3650
4023
|
newGroups.push(group);
|
|
3651
4024
|
usedKeys.add(key);
|
|
@@ -3661,7 +4034,10 @@ catch (e) {
|
|
|
3661
4034
|
// New item \u2014 create DOM elements with a fresh item signal
|
|
3662
4035
|
const elements = createItemElements(list[i], i, key);
|
|
3663
4036
|
elements.forEach(el => {
|
|
3664
|
-
|
|
4037
|
+
// Before the cursor, not before the placeholder: a new row inserted
|
|
4038
|
+
// in the MIDDLE of the list must land at its own position, without
|
|
4039
|
+
// forcing every row after it to be moved to get back in order.
|
|
4040
|
+
parent.insertBefore(el, cursor || placeholder);
|
|
3665
4041
|
if (tgReady && el.isConnected) tgEnter(el, tgName);
|
|
3666
4042
|
});
|
|
3667
4043
|
hydrateComponentScopes(elements, elements.find(function(node) {
|
|
@@ -4307,15 +4683,23 @@ else if (timer === null) {
|
|
|
4307
4683
|
return throttled;
|
|
4308
4684
|
}
|
|
4309
4685
|
|
|
4310
|
-
function useInterval(interval, options) {
|
|
4686
|
+
function useInterval(interval, options, thirdOptions) {
|
|
4311
4687
|
// Accept the callback-first form too \u2014 useInterval(fn, 1000) is what most
|
|
4312
4688
|
// callers reach for, and silently ignoring the callback (the previous
|
|
4313
|
-
// behaviour) produced a timer that ticked into the void.
|
|
4689
|
+
// behaviour) produced a timer that ticked into the void. An options object
|
|
4690
|
+
// may follow the interval \u2014 useInterval(fn, 1000, { enabled, whileVisible })
|
|
4691
|
+
// \u2014 or replace it \u2014 useInterval(fn, { enabled }) (#1870).
|
|
4314
4692
|
var callback = null;
|
|
4315
4693
|
if (typeof interval === 'function') {
|
|
4316
4694
|
callback = interval;
|
|
4317
|
-
|
|
4318
|
-
|
|
4695
|
+
if (typeof options === 'number') {
|
|
4696
|
+
interval = options;
|
|
4697
|
+
options = thirdOptions || {};
|
|
4698
|
+
}
|
|
4699
|
+
else {
|
|
4700
|
+
interval = 1000;
|
|
4701
|
+
options = (options && typeof options === 'object') ? options : {};
|
|
4702
|
+
}
|
|
4319
4703
|
}
|
|
4320
4704
|
interval = interval || 1000;
|
|
4321
4705
|
options = options || {};
|
|
@@ -4323,7 +4707,18 @@ else if (timer === null) {
|
|
|
4323
4707
|
var id = null;
|
|
4324
4708
|
var running = false;
|
|
4325
4709
|
var listeners = [];
|
|
4710
|
+
// A tick is skipped (not counted, listeners not called) while the timer is
|
|
4711
|
+
// gated off: enabled is false / returns false, or whileVisible is set
|
|
4712
|
+
// and the document is hidden. This makes "run only while authorized and
|
|
4713
|
+
// visible" expressible without the caller hand-pausing the timer (#1870).
|
|
4714
|
+
function shouldTick() {
|
|
4715
|
+
if (options.whileVisible && typeof document !== 'undefined' && document.hidden) return false;
|
|
4716
|
+
var en = options.enabled;
|
|
4717
|
+
if (typeof en === 'function') return !!en();
|
|
4718
|
+
return en !== false;
|
|
4719
|
+
}
|
|
4326
4720
|
function tick() {
|
|
4721
|
+
if (!shouldTick()) return;
|
|
4327
4722
|
count++;
|
|
4328
4723
|
listeners.forEach(function(fn) { fn(count); });
|
|
4329
4724
|
}
|
|
@@ -4786,7 +5181,68 @@ catch (e) {} }
|
|
|
4786
5181
|
|
|
4787
5182
|
// Vue-compat aliases
|
|
4788
5183
|
var ref = state;
|
|
4789
|
-
|
|
5184
|
+
|
|
5185
|
+
// reactive() used to be a bare alias of state(), which made it the most
|
|
5186
|
+
// dangerous name in the runtime. state({n:0}) returns a SIGNAL, so the Vue
|
|
5187
|
+
// shape everyone reaches for silently did nothing:
|
|
5188
|
+
//
|
|
5189
|
+
// const s = reactive({ count: 0 })
|
|
5190
|
+
// s.count // undefined \u2014 a signal has no such property
|
|
5191
|
+
// s.count++ // writes an expando onto a function object
|
|
5192
|
+
// {{ s.count }} // renders empty, forever, with no warning
|
|
5193
|
+
//
|
|
5194
|
+
// Backed by one signal PER PROPERTY, created on first touch. That is what
|
|
5195
|
+
// makes a read inside an effect track only the field it read, so writing
|
|
5196
|
+
// b does not re-run an effect that only read a (#1885).
|
|
5197
|
+
var __stxReactiveProxies = new WeakMap();
|
|
5198
|
+
|
|
5199
|
+
function reactive(target) {
|
|
5200
|
+
// Primitives have no properties to track. Returning them unchanged keeps
|
|
5201
|
+
// reactive(0) from silently becoming an object.
|
|
5202
|
+
if (!target || typeof target !== 'object') return target;
|
|
5203
|
+
var existing = __stxReactiveProxies.get(target);
|
|
5204
|
+
if (existing) return existing;
|
|
5205
|
+
|
|
5206
|
+
var propSignals = Object.create(null);
|
|
5207
|
+
function signalFor(prop) {
|
|
5208
|
+
if (!propSignals[prop]) propSignals[prop] = state(target[prop]);
|
|
5209
|
+
return propSignals[prop];
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
var proxy = new Proxy(target, {
|
|
5213
|
+
get: function(t, prop, receiver) {
|
|
5214
|
+
// Symbols and inherited members are plumbing \u2014 toJSON, Symbol.iterator,
|
|
5215
|
+
// array methods. Tracking them would create a signal per method lookup
|
|
5216
|
+
// and make every effect depend on the whole object.
|
|
5217
|
+
if (typeof prop === 'symbol' || !Object.prototype.hasOwnProperty.call(t, prop))
|
|
5218
|
+
return Reflect.get(t, prop, receiver);
|
|
5219
|
+
var value = signalFor(prop)();
|
|
5220
|
+
// Nested objects are wrapped too, and cached in the WeakMap, so
|
|
5221
|
+
// identity is stable across reads \u2014 a fresh proxy each time would
|
|
5222
|
+
// break === comparisons and defeat keyed list reuse.
|
|
5223
|
+
return (value && typeof value === 'object') ? reactive(value) : value;
|
|
5224
|
+
},
|
|
5225
|
+
set: function(t, prop, value) {
|
|
5226
|
+
if (typeof prop === 'symbol') return Reflect.set(t, prop, value);
|
|
5227
|
+
var had = Object.prototype.hasOwnProperty.call(t, prop);
|
|
5228
|
+
t[prop] = value;
|
|
5229
|
+
// Write the plain target FIRST so a signal created later reads the
|
|
5230
|
+
// current value, then notify anyone already watching this property.
|
|
5231
|
+
if (propSignals[prop]) propSignals[prop].set(value);
|
|
5232
|
+
else if (had) signalFor(prop);
|
|
5233
|
+
return true;
|
|
5234
|
+
},
|
|
5235
|
+
deleteProperty: function(t, prop) {
|
|
5236
|
+
var had = Object.prototype.hasOwnProperty.call(t, prop);
|
|
5237
|
+
delete t[prop];
|
|
5238
|
+
if (had && propSignals[prop]) propSignals[prop].set(undefined);
|
|
5239
|
+
return true;
|
|
5240
|
+
},
|
|
5241
|
+
});
|
|
5242
|
+
|
|
5243
|
+
__stxReactiveProxies.set(target, proxy);
|
|
5244
|
+
return proxy;
|
|
5245
|
+
}
|
|
4790
5246
|
var computed = derived;
|
|
4791
5247
|
var watch = $watch;
|
|
4792
5248
|
var watchEffect = function(fn) { return effect(fn); };
|
|
@@ -5321,7 +5777,22 @@ catch (e) {} }
|
|
|
5321
5777
|
var confirmText = opts.confirmText || 'OK';
|
|
5322
5778
|
var cancelText = opts.cancelText || 'Cancel';
|
|
5323
5779
|
var id = 'stx-dialog-' + (++_dialogId);
|
|
5324
|
-
|
|
5780
|
+
// Resolve dark from what the APP decided, not only from the OS. stx's own
|
|
5781
|
+
// color-mode boot writes a class (default 'dark') and/or an attribute
|
|
5782
|
+
// (e.g. data-theme) onto <html>, so an app pinned to light on a dark OS used
|
|
5783
|
+
// to get a dark dialog over a light page. An explicit opts.dark wins over
|
|
5784
|
+
// both, and the media query stays as the last resort. See #1875.
|
|
5785
|
+
var _root = document.documentElement;
|
|
5786
|
+
var _attrTheme = _root.getAttribute('data-theme') || _root.getAttribute('data-color-mode');
|
|
5787
|
+
var isDark = opts.dark != null
|
|
5788
|
+
? !!opts.dark
|
|
5789
|
+
: _root.classList.contains('dark')
|
|
5790
|
+
? true
|
|
5791
|
+
: _attrTheme === 'dark'
|
|
5792
|
+
? true
|
|
5793
|
+
: _attrTheme === 'light'
|
|
5794
|
+
? false
|
|
5795
|
+
: !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
5325
5796
|
var bg = isDark ? '#1f2937' : '#ffffff';
|
|
5326
5797
|
var textColor = isDark ? '#f3f4f6' : '#1f2937';
|
|
5327
5798
|
var subColor = isDark ? '#9ca3af' : '#6b7280';
|
|
@@ -5344,23 +5815,45 @@ catch (e) {} }
|
|
|
5344
5815
|
|
|
5345
5816
|
if (title) {
|
|
5346
5817
|
var titleEl = document.createElement('h3');
|
|
5818
|
+
titleEl.id = id + '-title';
|
|
5347
5819
|
titleEl.style.cssText = 'margin:0 0 0.5rem;font-size:1.125rem;font-weight:600';
|
|
5348
5820
|
titleEl.textContent = title;
|
|
5349
5821
|
panel.appendChild(titleEl);
|
|
5822
|
+
// role=alertdialog was announced with no accessible name at all.
|
|
5823
|
+
backdrop.setAttribute('aria-labelledby', titleEl.id);
|
|
5350
5824
|
}
|
|
5351
5825
|
|
|
5352
5826
|
var msgEl = document.createElement('p');
|
|
5827
|
+
msgEl.id = id + '-message';
|
|
5353
5828
|
msgEl.style.cssText = 'margin:0 0 1.25rem;font-size:0.875rem;line-height:1.5;color:' + subColor;
|
|
5354
5829
|
msgEl.textContent = message;
|
|
5355
5830
|
panel.appendChild(msgEl);
|
|
5831
|
+
backdrop.setAttribute('aria-describedby', msgEl.id);
|
|
5832
|
+
if (!title)
|
|
5833
|
+
backdrop.setAttribute('aria-label', message);
|
|
5356
5834
|
|
|
5357
5835
|
var btnRow = document.createElement('div');
|
|
5358
5836
|
btnRow.style.cssText = 'display:flex;gap:0.75rem;justify-content:center';
|
|
5359
5837
|
|
|
5838
|
+
// Captured before the dialog steals focus, so it can be handed back on
|
|
5839
|
+
// close. Without this a keyboard or screen-reader user is dropped at the
|
|
5840
|
+
// top of the document every time a dialog closes.
|
|
5841
|
+
var previouslyFocused = document.activeElement;
|
|
5842
|
+
var closed = false;
|
|
5843
|
+
|
|
5360
5844
|
function cleanup(result) {
|
|
5845
|
+
// Guarded: the escape handler used to be able to fire after a button had
|
|
5846
|
+
// already closed the dialog, because the listener outlived it.
|
|
5847
|
+
if (closed) return;
|
|
5848
|
+
closed = true;
|
|
5849
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
5361
5850
|
backdrop.style.opacity = '0';
|
|
5362
5851
|
panel.style.transform = 'scale(0.95)';
|
|
5363
5852
|
setTimeout(function() { backdrop.remove(); }, 200);
|
|
5853
|
+
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
|
5854
|
+
try { previouslyFocused.focus(); }
|
|
5855
|
+
catch (_e) { /* element may be gone; not worth failing the dialog over */ }
|
|
5856
|
+
}
|
|
5364
5857
|
resolve(result);
|
|
5365
5858
|
}
|
|
5366
5859
|
|
|
@@ -5392,11 +5885,45 @@ catch (e) {} }
|
|
|
5392
5885
|
backdrop.style.opacity = '1';
|
|
5393
5886
|
panel.style.transform = 'scale(1)';
|
|
5394
5887
|
|
|
5395
|
-
// Escape
|
|
5396
|
-
|
|
5397
|
-
|
|
5888
|
+
// Escape, plus a real focus trap.
|
|
5889
|
+
//
|
|
5890
|
+
// The old handler removed itself ONLY inside the Escape branch, so every
|
|
5891
|
+
// dialog dismissed with a button left a permanent keydown listener on
|
|
5892
|
+
// document holding this whole closure \u2014 backdrop, panel and resolve \u2014 alive.
|
|
5893
|
+
// Removal now lives in cleanup(), which every exit path goes through.
|
|
5894
|
+
//
|
|
5895
|
+
// aria-modal="true" was already being claimed on the backdrop while Tab
|
|
5896
|
+
// walked straight out into the page behind it. Capture phase so the trap
|
|
5897
|
+
// wins over anything the page has bound.
|
|
5898
|
+
var keyHandler = function(e) {
|
|
5899
|
+
if (e.key === 'Escape') {
|
|
5900
|
+
e.preventDefault();
|
|
5901
|
+
cleanup(isConfirm ? false : undefined);
|
|
5902
|
+
return;
|
|
5903
|
+
}
|
|
5904
|
+
if (e.key !== 'Tab') return;
|
|
5905
|
+
var focusable = panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
5906
|
+
if (!focusable.length) return;
|
|
5907
|
+
var first = focusable[0];
|
|
5908
|
+
var last = focusable[focusable.length - 1];
|
|
5909
|
+
// Focus may sit outside the panel entirely (the page had it when the
|
|
5910
|
+
// dialog opened), in which case Tab must pull it back in rather than
|
|
5911
|
+
// continue through the document.
|
|
5912
|
+
if (!panel.contains(document.activeElement)) {
|
|
5913
|
+
e.preventDefault();
|
|
5914
|
+
(e.shiftKey ? last : first).focus();
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5917
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
5918
|
+
e.preventDefault();
|
|
5919
|
+
last.focus();
|
|
5920
|
+
}
|
|
5921
|
+
else if (!e.shiftKey && document.activeElement === last) {
|
|
5922
|
+
e.preventDefault();
|
|
5923
|
+
first.focus();
|
|
5924
|
+
}
|
|
5398
5925
|
};
|
|
5399
|
-
document.addEventListener('keydown',
|
|
5926
|
+
document.addEventListener('keydown', keyHandler, true);
|
|
5400
5927
|
|
|
5401
5928
|
// Focus the primary button
|
|
5402
5929
|
okBtn.focus();
|
|
@@ -5526,7 +6053,7 @@ catch (e) {} }
|
|
|
5526
6053
|
el.__stx_disposers = trackEffects(function() { processElement(el, componentScope); });
|
|
5527
6054
|
if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
|
|
5528
6055
|
scopeVars.__mounted = true;
|
|
5529
|
-
scopeVars.__mountCallbacks
|
|
6056
|
+
runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
|
|
5530
6057
|
}
|
|
5531
6058
|
});
|
|
5532
6059
|
// 3. Bind directives across the subtree.
|
|
@@ -5537,7 +6064,7 @@ catch (e) {} }
|
|
|
5537
6064
|
if (mountCallbacks.length) {
|
|
5538
6065
|
var cbs = mountCallbacks.slice();
|
|
5539
6066
|
mountCallbacks.length = 0;
|
|
5540
|
-
cbs
|
|
6067
|
+
runMountCallbacks(cbs, destroyCallbacks);
|
|
5541
6068
|
}
|
|
5542
6069
|
window.dispatchEvent(new CustomEvent('stx:hydrated', { detail: { el: container, trigger: 'stream' } }));
|
|
5543
6070
|
}
|
|
@@ -5571,13 +6098,18 @@ catch (e) {} }
|
|
|
5571
6098
|
useFetch,
|
|
5572
6099
|
useRef,
|
|
5573
6100
|
navigate,
|
|
6101
|
+
refresh,
|
|
6102
|
+
invalidateRoute,
|
|
5574
6103
|
goBack,
|
|
5575
6104
|
goForward,
|
|
5576
6105
|
useRoute,
|
|
6106
|
+
useRouteParams,
|
|
6107
|
+
useRouteParam,
|
|
5577
6108
|
setRouteParams,
|
|
5578
6109
|
useSearchParams,
|
|
5579
6110
|
useQuery,
|
|
5580
6111
|
useMutation,
|
|
6112
|
+
configureFetch,
|
|
5581
6113
|
useOptimistic,
|
|
5582
6114
|
provide,
|
|
5583
6115
|
$computed,
|
|
@@ -5701,8 +6233,11 @@ catch (e) {} }
|
|
|
5701
6233
|
|
|
5702
6234
|
defineStore: function(id, setupOrOptions, storeOptions) {
|
|
5703
6235
|
console.log('[stx:store] defineStore called:', id, 'type:', typeof setupOrOptions === 'function' ? 'setup' : 'options', 'persist:', !!(storeOptions && storeOptions.persist));
|
|
5704
|
-
// Return existing store if already defined
|
|
5705
|
-
|
|
6236
|
+
// Return existing store if already defined.
|
|
6237
|
+
//
|
|
6238
|
+
// During a store HMR pass the definition is deliberately re-run, so the
|
|
6239
|
+
// cached instance must NOT short-circuit it (#1877 ask 4).
|
|
6240
|
+
if (window.stx._stores.has(id) && !window.stx.__hmrStoreReplacing) {
|
|
5706
6241
|
console.log('[stx:store] returning existing store:', id);
|
|
5707
6242
|
return window.stx._stores.get(id);
|
|
5708
6243
|
}
|
|
@@ -5842,18 +6377,72 @@ catch (e) {} }
|
|
|
5842
6377
|
if (persistCfg) {
|
|
5843
6378
|
var pOpts = persistCfg === true ? {} : persistCfg;
|
|
5844
6379
|
var storageKey = pOpts.key || ('stx-store-' + id);
|
|
5845
|
-
var storageType = pOpts.storage === 'sessionStorage' ? sessionStorage : localStorage;
|
|
5846
6380
|
var pick = pOpts.pick || null;
|
|
5847
6381
|
|
|
6382
|
+
// Three additions, all of which apps previously had to hand-roll outside
|
|
6383
|
+
// the store entirely (#1872):
|
|
6384
|
+
//
|
|
6385
|
+
// keys map a field to its OWN storage key, and optionally its own
|
|
6386
|
+
// backend, instead of folding everything into one JSON blob
|
|
6387
|
+
// serialize / deserialize \u2014 the codec was hardcoded to JSON, so a
|
|
6388
|
+
// persisted string came back quoted. A signed-out token
|
|
6389
|
+
// stored as '""' is two characters and TRUTHY, which is a
|
|
6390
|
+
// real class of auth bug, and a legacy raw value could not
|
|
6391
|
+
// be read at all
|
|
6392
|
+
// cookie a backend the SERVER can read. Server-rendered,
|
|
6393
|
+
// owner-scoped pages authenticate from a cookie and cannot
|
|
6394
|
+
// see localStorage, so this previously meant a second
|
|
6395
|
+
// persisted signal plus a hand-written mirroring effect
|
|
6396
|
+
var fieldMap = pOpts.keys || null;
|
|
6397
|
+
var defaultSerialize = pOpts.serialize || function(value) { return JSON.stringify(value); };
|
|
6398
|
+
var defaultDeserialize = pOpts.deserialize || function(raw) { return JSON.parse(raw); };
|
|
6399
|
+
|
|
6400
|
+
// 'local' and 'localStorage' both accepted; the old code only recognised
|
|
6401
|
+
// 'sessionStorage', so 'session' silently fell through to localStorage.
|
|
6402
|
+
// Resolved through window when the bare global is absent, so persist also
|
|
6403
|
+
// works where the runtime is evaluated outside a page's global scope
|
|
6404
|
+
// (component test harnesses, embedded runtimes).
|
|
6405
|
+
function _backendFor(kind) {
|
|
6406
|
+
if (kind === 'cookie') return 'cookie';
|
|
6407
|
+
var scope = typeof window !== 'undefined' ? window : null;
|
|
6408
|
+
if (kind === 'session' || kind === 'sessionStorage')
|
|
6409
|
+
return typeof sessionStorage !== 'undefined' ? sessionStorage : (scope && scope.sessionStorage);
|
|
6410
|
+
return typeof localStorage !== 'undefined' ? localStorage : (scope && scope.localStorage);
|
|
6411
|
+
}
|
|
6412
|
+
var storageType = _backendFor(pOpts.storage);
|
|
6413
|
+
|
|
6414
|
+
// Resolved config for a field that has its own key, or null if it belongs
|
|
6415
|
+
// in the shared blob.
|
|
6416
|
+
function _mappedConfig(field) {
|
|
6417
|
+
if (!fieldMap || !Object.prototype.hasOwnProperty.call(fieldMap, field)) return null;
|
|
6418
|
+
var cfg = fieldMap[field];
|
|
6419
|
+
if (typeof cfg === 'string') cfg = { key: cfg };
|
|
6420
|
+
cfg = cfg || {};
|
|
6421
|
+
return {
|
|
6422
|
+
key: cfg.key || field,
|
|
6423
|
+
backend: _backendFor(cfg.storage === undefined ? pOpts.storage : cfg.storage),
|
|
6424
|
+
cookieOpts: cfg,
|
|
6425
|
+
serialize: cfg.serialize || defaultSerialize,
|
|
6426
|
+
deserialize: cfg.deserialize || defaultDeserialize
|
|
6427
|
+
};
|
|
6428
|
+
}
|
|
6429
|
+
|
|
6430
|
+
// A mapped field owns its key, so it must NOT also be written into the
|
|
6431
|
+
// blob \u2014 otherwise it round-trips through two places that can disagree.
|
|
6432
|
+
function _inBlob(field) {
|
|
6433
|
+
if (pick && pick.indexOf(field) === -1) return false;
|
|
6434
|
+
return !_mappedConfig(field);
|
|
6435
|
+
}
|
|
6436
|
+
|
|
5848
6437
|
// Read persisted state (overrides hydration)
|
|
5849
6438
|
try {
|
|
5850
|
-
var saved = storageType.getItem(storageKey);
|
|
6439
|
+
var saved = storageType === 'cookie' ? null : storageType.getItem(storageKey);
|
|
5851
6440
|
if (saved) {
|
|
5852
|
-
var parsed =
|
|
6441
|
+
var parsed = defaultDeserialize(saved);
|
|
5853
6442
|
console.log('[stx:store] restoring persisted state:', id, 'key:', storageKey, Object.keys(parsed));
|
|
5854
6443
|
batch(function() {
|
|
5855
6444
|
for (var pk in parsed) {
|
|
5856
|
-
if (result[pk] && result[pk]._isSignal && (
|
|
6445
|
+
if (result[pk] && result[pk]._isSignal && _inBlob(pk)) {
|
|
5857
6446
|
result[pk].set(parsed[pk]);
|
|
5858
6447
|
}
|
|
5859
6448
|
}
|
|
@@ -5863,21 +6452,73 @@ catch (e) {} }
|
|
|
5863
6452
|
}
|
|
5864
6453
|
} catch(e) { console.warn('[stx:store] persistence read error:', id, e); }
|
|
5865
6454
|
|
|
6455
|
+
// Per-field persistence: own key, own backend, own codec.
|
|
6456
|
+
//
|
|
6457
|
+
// The disposer swap is the same trick the blob effect below uses \u2014 these
|
|
6458
|
+
// effects must outlive the component that happened to instantiate the
|
|
6459
|
+
// store, or persistence would stop the first time that component
|
|
6460
|
+
// unmounted.
|
|
6461
|
+
var prevFieldDisposers = activeDisposers;
|
|
6462
|
+
activeDisposers = null;
|
|
6463
|
+
if (fieldMap) {
|
|
6464
|
+
for (var fk in fieldMap) {
|
|
6465
|
+
if (!Object.prototype.hasOwnProperty.call(fieldMap, fk)) continue;
|
|
6466
|
+
if (!result[fk] || !result[fk]._isSignal) {
|
|
6467
|
+
console.warn('[stx:store] persist.keys names a field that is not a signal:', id, fk);
|
|
6468
|
+
continue;
|
|
6469
|
+
}
|
|
6470
|
+
(function(field, cfg, signal) {
|
|
6471
|
+
if (cfg.backend === 'cookie') {
|
|
6472
|
+
// useCookie owns path / max-age / SameSite / Secure, so cookie
|
|
6473
|
+
// attributes do not have to be reimplemented here. It is a
|
|
6474
|
+
// string-valued signal, hence serialize on the way out.
|
|
6475
|
+
var cookieSignal = useCookie(cfg.key, cfg.cookieOpts);
|
|
6476
|
+
var initialCookie = cookieSignal();
|
|
6477
|
+
if (initialCookie !== '' && initialCookie != null) {
|
|
6478
|
+
try { signal.set(cfg.deserialize(initialCookie)); }
|
|
6479
|
+
catch(e) { console.warn('[stx:store] persist read error:', id, field, e); }
|
|
6480
|
+
}
|
|
6481
|
+
effect(function() {
|
|
6482
|
+
var next = signal();
|
|
6483
|
+
try { cookieSignal.set(next == null ? '' : String(cfg.serialize(next))); }
|
|
6484
|
+
catch(e) { console.warn('[stx:store] persist write error:', id, field, e); }
|
|
6485
|
+
});
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
try {
|
|
6489
|
+
var rawValue = cfg.backend.getItem(cfg.key);
|
|
6490
|
+
if (rawValue !== null) signal.set(cfg.deserialize(rawValue));
|
|
6491
|
+
} catch(e) { console.warn('[stx:store] persist read error:', id, field, e); }
|
|
6492
|
+
effect(function() {
|
|
6493
|
+
var value = signal();
|
|
6494
|
+
try { cfg.backend.setItem(cfg.key, String(cfg.serialize(value))); }
|
|
6495
|
+
catch(e) { console.warn('[stx:store] persist write error:', id, field, e); }
|
|
6496
|
+
});
|
|
6497
|
+
})(fk, _mappedConfig(fk), result[fk]);
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
activeDisposers = prevFieldDisposers;
|
|
6501
|
+
|
|
5866
6502
|
// Write on change (debounced)
|
|
5867
6503
|
var writeTimer = null;
|
|
5868
6504
|
var prevPersistDisposers = activeDisposers;
|
|
5869
6505
|
activeDisposers = null;
|
|
5870
6506
|
effect(function() {
|
|
5871
6507
|
var snapshot = {};
|
|
6508
|
+
var wrote = false;
|
|
5872
6509
|
for (var wk in result) {
|
|
5873
|
-
if (result[wk] && result[wk]._isSignal && (
|
|
6510
|
+
if (result[wk] && result[wk]._isSignal && _inBlob(wk)) {
|
|
5874
6511
|
snapshot[wk] = result[wk]();
|
|
6512
|
+
wrote = true;
|
|
5875
6513
|
}
|
|
5876
6514
|
}
|
|
6515
|
+
// With every field mapped to its own key there is no blob left, and
|
|
6516
|
+
// writing an empty object would clobber whatever else owns that key.
|
|
6517
|
+
if (!wrote || storageType === 'cookie') return;
|
|
5877
6518
|
if (writeTimer) clearTimeout(writeTimer);
|
|
5878
6519
|
writeTimer = setTimeout(function() {
|
|
5879
6520
|
console.log('[stx:store] persisting:', id, 'key:', storageKey, Object.keys(snapshot));
|
|
5880
|
-
try { storageType.setItem(storageKey,
|
|
6521
|
+
try { storageType.setItem(storageKey, String(defaultSerialize(snapshot))); } catch(e) { console.warn('[stx:store] persist write error:', id, e); }
|
|
5881
6522
|
}, 100);
|
|
5882
6523
|
});
|
|
5883
6524
|
activeDisposers = prevPersistDisposers;
|
|
@@ -5889,6 +6530,26 @@ catch (e) {} }
|
|
|
5889
6530
|
// keep working); only template expression evaluation auto-unwraps.
|
|
5890
6531
|
result._isStxStore = true;
|
|
5891
6532
|
|
|
6533
|
+
// Carry state across a hot replacement.
|
|
6534
|
+
//
|
|
6535
|
+
// The NEW store object is kept rather than patching the old one: a
|
|
6536
|
+
// setup-style store's actions close over the signals created in that
|
|
6537
|
+
// same run, so keeping the old signals would leave the new actions
|
|
6538
|
+
// writing to signals nothing is watching \u2014 an edit that appears to do
|
|
6539
|
+
// nothing, which is worse than the reload this replaces. Seeding the new
|
|
6540
|
+
// signals with the old values keeps state and behaviour consistent, and
|
|
6541
|
+
// the caller re-renders afterwards so components rebind to this object.
|
|
6542
|
+
var seed = window.stx.__hmrStoreSeed && window.stx.__hmrStoreSeed[id];
|
|
6543
|
+
if (seed) {
|
|
6544
|
+
for (var seedKey in seed) {
|
|
6545
|
+
var target = result[seedKey];
|
|
6546
|
+
if (target && target._isSignal && typeof target.set === 'function' && !target._isDerived) {
|
|
6547
|
+
try { target.set(seed[seedKey]); }
|
|
6548
|
+
catch (e) { /* shape changed across the edit \u2014 keep the new default */ }
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
|
|
5892
6553
|
// Register globally
|
|
5893
6554
|
window.stx._stores.set(id, result);
|
|
5894
6555
|
window.__STX_STORES__ = window.__STX_STORES__ || {};
|
|
@@ -5898,6 +6559,55 @@ catch (e) {} }
|
|
|
5898
6559
|
return result;
|
|
5899
6560
|
},
|
|
5900
6561
|
|
|
6562
|
+
// Replace the store definitions in place, keeping their state \u2014 the
|
|
6563
|
+
// acceptHMRUpdate contract (#1877 ask 4). Called by the dev server's HMR
|
|
6564
|
+
// client when a file under storesDir changes, instead of reloading the
|
|
6565
|
+
// document and throwing away everything the SPA exists to preserve.
|
|
6566
|
+
__hmrReplaceStores: function(code) {
|
|
6567
|
+
var seed = {};
|
|
6568
|
+
window.stx._stores.forEach(function(store, id) {
|
|
6569
|
+
var values = {};
|
|
6570
|
+
for (var key in store) {
|
|
6571
|
+
var value = store[key];
|
|
6572
|
+
// Derived values recompute from state, so seeding them would fight
|
|
6573
|
+
// their own recomputation.
|
|
6574
|
+
if (value && value._isSignal && !value._isDerived) {
|
|
6575
|
+
try { values[key] = value.peek ? value.peek() : value(); }
|
|
6576
|
+
catch (e) { /* unreadable \u2014 let the new default stand */ }
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
seed[id] = values;
|
|
6580
|
+
});
|
|
6581
|
+
|
|
6582
|
+
window.stx.__hmrStoreSeed = seed;
|
|
6583
|
+
window.stx.__hmrStoreReplacing = true;
|
|
6584
|
+
var ok = true;
|
|
6585
|
+
try {
|
|
6586
|
+
// Indirect eval, so the bundle evaluates at global scope the way it
|
|
6587
|
+
// does when inlined into the page.
|
|
6588
|
+
(0, eval)(code);
|
|
6589
|
+
}
|
|
6590
|
+
catch (e) {
|
|
6591
|
+
ok = false;
|
|
6592
|
+
console.error('[stx:hmr] store bundle failed to evaluate:', e);
|
|
6593
|
+
}
|
|
6594
|
+
finally {
|
|
6595
|
+
window.stx.__hmrStoreReplacing = false;
|
|
6596
|
+
window.stx.__hmrStoreSeed = null;
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
if (!ok) return false;
|
|
6600
|
+
|
|
6601
|
+
// Components captured the PREVIOUS store object from useStore(), so
|
|
6602
|
+
// re-render the route to rebind them. Still not a document reload: the
|
|
6603
|
+
// swap keeps every non-store signal on the page alive.
|
|
6604
|
+
if (window.stxRouter && typeof window.stxRouter.refresh === 'function') {
|
|
6605
|
+
try { window.stxRouter.refresh(); }
|
|
6606
|
+
catch (e) { /* a refresh that cannot run is not worth a reload here */ }
|
|
6607
|
+
}
|
|
6608
|
+
return true;
|
|
6609
|
+
},
|
|
6610
|
+
|
|
5901
6611
|
registerStoresClient: function(stores) {
|
|
5902
6612
|
window.__STX_STORES__ = window.__STX_STORES__ || {};
|
|
5903
6613
|
var names = Object.keys(stores || {});
|
|
@@ -6154,7 +6864,7 @@ else {
|
|
|
6154
6864
|
window.defineStore = window.stx.defineStore;
|
|
6155
6865
|
window.useStore = window.stx.useStore;
|
|
6156
6866
|
window.ref = state;
|
|
6157
|
-
window.reactive =
|
|
6867
|
+
window.reactive = reactive;
|
|
6158
6868
|
window.computed = derived;
|
|
6159
6869
|
window.watch = $watch;
|
|
6160
6870
|
window.watchEffect = function(fn) { return effect(fn); };
|
|
@@ -6374,7 +7084,7 @@ else {
|
|
|
6374
7084
|
el.removeAttribute('x-cloak');
|
|
6375
7085
|
el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
|
|
6376
7086
|
|
|
6377
|
-
mountCallbacks
|
|
7087
|
+
runMountCallbacks(mountCallbacks, destroyCallbacks);
|
|
6378
7088
|
});
|
|
6379
7089
|
|
|
6380
7090
|
// Multi-root fragment fix: a no-body> setup page tags only the FIRST
|
|
@@ -6463,16 +7173,14 @@ else {
|
|
|
6463
7173
|
var scopeDeferred = el.hasAttribute && el.hasAttribute('stx-hydrate') && !el.__stx_hydrated;
|
|
6464
7174
|
if (scopeVars && scopeVars.__mountCallbacks && !scopeVars.__mounted && !scopeDeferred) {
|
|
6465
7175
|
scopeVars.__mounted = true;
|
|
6466
|
-
scopeVars.__mountCallbacks
|
|
7176
|
+
runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
|
|
6467
7177
|
}
|
|
6468
7178
|
});
|
|
6469
7179
|
|
|
6470
7180
|
// Run global mount callbacks (from partial <script client> blocks that call onMount)
|
|
6471
7181
|
// These are pushed during script execution, before DOMContentLoaded
|
|
6472
7182
|
console.log('[stx] running global mountCallbacks:', mountCallbacks.length);
|
|
6473
|
-
mountCallbacks
|
|
6474
|
-
try { fn(); } catch(e) { console.error('[stx] onMount error:', e); }
|
|
6475
|
-
});
|
|
7183
|
+
runMountCallbacks(mountCallbacks, destroyCallbacks);
|
|
6476
7184
|
mountCallbacks.length = 0;
|
|
6477
7185
|
|
|
6478
7186
|
// Auto-process elements with data-stx-auto (skip already processed scoped elements)
|
|
@@ -7051,15 +7759,12 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
7051
7759
|
var scopeVars = window.stx._scopes && window.stx._scopes[scopeId];
|
|
7052
7760
|
if (scopeVars && scopeVars.__mountCallbacks && !scopeVars.__mounted) {
|
|
7053
7761
|
scopeVars.__mounted = true;
|
|
7054
|
-
scopeVars.__mountCallbacks
|
|
7762
|
+
runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
|
|
7055
7763
|
}
|
|
7056
7764
|
});
|
|
7057
7765
|
|
|
7058
7766
|
// Flush global mountCallbacks (from scripts re-executed after SPA content swap)
|
|
7059
|
-
mountCallbacks
|
|
7060
|
-
try { fn(); }
|
|
7061
|
-
catch (e) { console.warn('[stx] mount callback error:', e); }
|
|
7062
|
-
});
|
|
7767
|
+
runMountCallbacks(mountCallbacks, destroyCallbacks);
|
|
7063
7768
|
mountCallbacks.length = 0;
|
|
7064
7769
|
|
|
7065
7770
|
// Everything that should have hydrated has now had its chance (#1773).
|