@stacksjs/stx 0.2.152 → 0.2.153
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/app-shell.js +28 -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/cli.js +886 -648
- package/dist/client-script.js +2 -2
- package/dist/color-mode-boot.d.ts +15 -10
- package/dist/color-mode-boot.js +12 -8
- package/dist/component-client-factories.js +3 -2
- package/dist/composables/index.d.ts +6 -1
- package/dist/composables/use-color-mode.d.ts +19 -4
- package/dist/composables/use-color-mode.js +37 -24
- package/dist/composables/use-storage.d.ts +14 -15
- package/dist/composables/use-storage.js +63 -2
- package/dist/craft.js +622 -384
- package/dist/dev-server/serve-app.js +9 -5
- package/dist/dev-server/types.d.ts +8 -1
- package/dist/document-shell.d.ts +55 -0
- package/dist/document-shell.js +103 -3
- package/dist/head.js +5 -1
- package/dist/includes.js +2 -2
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/process.js +7 -3
- package/dist/production-builder.js +3 -1
- package/dist/pwa.js +581 -343
- package/dist/signals.js +280 -44
- package/dist/ssg.js +571 -333
- package/dist/stx.d.ts +14 -6
- package/dist/types/config-types.d.ts +1 -0
- package/package.json +5 -5
- package/stx.d.ts +14 -6
package/dist/signals.js
CHANGED
|
@@ -1921,12 +1921,6 @@ catch (e) {
|
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
1923
1923
|
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
1924
|
// Lazy hydration: if this element has stx-hydrate and hasn't been hydrated
|
|
1931
1925
|
// yet, defer its subtree processing until the trigger fires.
|
|
1932
1926
|
if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute && el.hasAttribute('stx-hydrate') && !el.__stx_hydrated) {
|
|
@@ -2137,23 +2131,36 @@ else if (part) {
|
|
|
2137
2131
|
const pipeResult = parsePipeExpression(expr, activeScope);
|
|
2138
2132
|
if (pipeResult) {
|
|
2139
2133
|
const unwrapScope = createExpressionAutoUnwrapProxy(activeScope, expr);
|
|
2140
|
-
|
|
2134
|
+
var piped = maybeUnwrapSignal(executePipeExpression(pipeResult.valueExpr, pipeResult.pipes, unwrapScope));
|
|
2135
|
+
noteExprSuccess(expr);
|
|
2136
|
+
return piped;
|
|
2141
2137
|
}
|
|
2142
2138
|
|
|
2143
2139
|
// Use auto-unwrap proxy (Feature #1)
|
|
2144
2140
|
const unwrapScope = createExpressionAutoUnwrapProxy(activeScope, expr);
|
|
2145
2141
|
const fn = new Function('__scope__', 'with(__scope__) { return (' + expr + ') }');
|
|
2146
|
-
|
|
2142
|
+
var value = maybeUnwrapSignal(fn(unwrapScope));
|
|
2143
|
+
noteExprSuccess(expr);
|
|
2144
|
+
return value;
|
|
2147
2145
|
}
|
|
2148
2146
|
catch (e) {
|
|
2149
2147
|
// Auto-unwrap can break explicit signal calls like errorData().message
|
|
2150
2148
|
// Retry without auto-unwrap so signal functions remain callable
|
|
2151
2149
|
try {
|
|
2152
2150
|
const fn = new Function('__scope__', 'with(__scope__) { return (' + expr + ') }');
|
|
2153
|
-
|
|
2151
|
+
var retried = maybeUnwrapSignal(fn(activeScope));
|
|
2152
|
+
noteExprSuccess(expr);
|
|
2153
|
+
return retried;
|
|
2154
2154
|
}
|
|
2155
2155
|
catch (e2) {
|
|
2156
2156
|
if (!(e2 instanceof ReferenceError) && !(e2 instanceof TypeError)) console.warn('[STX] Attribute expression error:', expr, e2);
|
|
2157
|
+
// ReferenceError/TypeError stay unlogged HERE on purpose: a signal
|
|
2158
|
+
// may not exist yet on an effect's first pass, and warning on every
|
|
2159
|
+
// one of those trains people to ignore the console. Instead the
|
|
2160
|
+
// failure is recorded and cleared the moment the same expression
|
|
2161
|
+
// evaluates successfully, so only expressions that are STILL broken
|
|
2162
|
+
// when hydration finishes get reported \u2014 by the audit (#1773).
|
|
2163
|
+
else noteExprFailure(expr, e2);
|
|
2157
2164
|
return '';
|
|
2158
2165
|
}
|
|
2159
2166
|
}
|
|
@@ -2172,18 +2179,11 @@ catch (e2) {
|
|
|
2172
2179
|
var KEY_MAP = {enter:'Enter', tab:'Tab', escape:'Escape', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', 'delete':'Delete', backspace:'Backspace'};
|
|
2173
2180
|
|
|
2174
2181
|
// 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
2182
|
getElementAttributes(el).forEach(attr => {
|
|
2179
2183
|
const name = attr.name;
|
|
2180
2184
|
const value = attr.value;
|
|
2181
2185
|
if (!name) return;
|
|
2182
2186
|
|
|
2183
|
-
if (el.hasAttribute && (el.hasAttribute('x-class') || el.hasAttribute('@click'))) {
|
|
2184
|
-
console.log('[stx] attr iter:', el.tagName, 'name:', name);
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
2187
|
// Dynamic attribute binding: @bind:attr, x-bind:attr, :attr, OR x-attr
|
|
2188
2188
|
// x-attr (e.g. x-class, x-style, x-href, x-src) is the canonical binding prefix.
|
|
2189
2189
|
// :attr still works for backward compat but is reserved for structural directives.
|
|
@@ -2278,7 +2278,6 @@ else {
|
|
|
2278
2278
|
el.removeAttribute(name);
|
|
2279
2279
|
}
|
|
2280
2280
|
else if (name === '@class' || name === ':class' || name === 'x-class') {
|
|
2281
|
-
console.log('[stx] HIT x-class handler:', el.tagName);
|
|
2282
2281
|
bindClass(el, value, scope);
|
|
2283
2282
|
el.removeAttribute(name);
|
|
2284
2283
|
}
|
|
@@ -2336,9 +2335,7 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
|
|
|
2336
2335
|
? { ...globalHelpers, ...el.__stx_parent_scope }
|
|
2337
2336
|
: { ...globalHelpers, ...scope, ...(findElementScope(el) || {}) };
|
|
2338
2337
|
|
|
2339
|
-
console.log('[stx] binding event:', eventName, 'on', el.tagName, 'expr:', value.substring(0, 40));
|
|
2340
2338
|
el.addEventListener(eventName, (event) => {
|
|
2341
|
-
console.log('[stx] event fired:', eventName, 'on', el.tagName);
|
|
2342
2339
|
// Vue-style component listeners fall through to the rendered native
|
|
2343
2340
|
// root unless the child emits the same event. Signal components use
|
|
2344
2341
|
// a scope wrapper, so native events arrive here from a descendant.
|
|
@@ -2459,7 +2456,7 @@ catch (e) {
|
|
|
2459
2456
|
// Skip script elements entirely
|
|
2460
2457
|
if (child.tagName === 'SCRIPT') return;
|
|
2461
2458
|
// Skip elements already processed by stx.mount() \u2014 they have their own scope and effects
|
|
2462
|
-
if (child.__stx_scope)
|
|
2459
|
+
if (child.__stx_scope) return;
|
|
2463
2460
|
// Skip data-stx-scope elements \u2014 they are managed by the reactive (x-data) runtime
|
|
2464
2461
|
if (child.hasAttribute && child.hasAttribute('data-stx-scope')) return;
|
|
2465
2462
|
processElement(child, scope);
|
|
@@ -4410,16 +4407,34 @@ else if (timer === null) {
|
|
|
4410
4407
|
// flash the boot script exists to prevent.
|
|
4411
4408
|
var boot = null;
|
|
4412
4409
|
try { boot = window.__STX_COLOR_MODE__ || null; } catch (e) {}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4410
|
+
// Explicit-undefined checks rather than ||, so an explicit darkClass:null
|
|
4411
|
+
// or attribute:null opts out instead of falling through to the default.
|
|
4412
|
+
function pick(a, b, fallback) {
|
|
4413
|
+
if (a !== undefined) return a;
|
|
4414
|
+
if (b !== undefined && b !== null) return b;
|
|
4415
|
+
return fallback;
|
|
4416
|
+
}
|
|
4417
|
+
var storageKey = pick(options.storageKey, boot && boot.storageKey, 'stx-color-mode');
|
|
4418
|
+
var initialMode = pick(options.initialMode, boot && boot.initialMode, 'auto');
|
|
4419
|
+
var darkClass = pick(options.darkClass, boot && boot.darkClass, 'dark');
|
|
4420
|
+
var attribute = pick(options.attribute, boot && boot.attribute, null);
|
|
4421
|
+
var autoValueOption = pick(options.autoValue, boot && boot.autoValue, undefined);
|
|
4417
4422
|
var disableTransitions = options.disableTransitions !== false;
|
|
4418
|
-
var preference = initialMode;
|
|
4419
4423
|
var resolved = 'light';
|
|
4420
4424
|
var listeners = [];
|
|
4421
4425
|
var cleanups = [];
|
|
4422
4426
|
|
|
4427
|
+
// Storage tokens meaning "follow the system". 'system' is a common spelling
|
|
4428
|
+
// for the same concept; rejecting it used to send the app down a silent
|
|
4429
|
+
// data-loss path \u2014 read 'system', fail to recognise it, fall back to
|
|
4430
|
+
// 'auto', then OVERWRITE the stored value with 'auto' (#1788).
|
|
4431
|
+
function normalizeMode(v) {
|
|
4432
|
+
if (v === 'light' || v === 'dark') return v;
|
|
4433
|
+
if (v === 'auto' || v === 'system') return 'auto';
|
|
4434
|
+
return null;
|
|
4435
|
+
}
|
|
4436
|
+
var preference = normalizeMode(initialMode) || 'auto';
|
|
4437
|
+
|
|
4423
4438
|
function getSystem() {
|
|
4424
4439
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
4425
4440
|
}
|
|
@@ -4427,27 +4442,42 @@ else if (timer === null) {
|
|
|
4427
4442
|
function applyDOM(mode) {
|
|
4428
4443
|
var el = document.documentElement;
|
|
4429
4444
|
if (disableTransitions) el.style.setProperty('transition', 'none', 'important');
|
|
4430
|
-
|
|
4431
|
-
|
|
4445
|
+
// Both, when both are configured. The class drives the utility
|
|
4446
|
+
// framework's dark: variants, the attribute drives everything else \u2014
|
|
4447
|
+
// complements, not alternatives.
|
|
4448
|
+
if (attribute) el.setAttribute(attribute, mode);
|
|
4449
|
+
// Explicit add/remove, not toggle(cls, force) \u2014 very-happy-dom ignores
|
|
4450
|
+
// the force argument and plain-toggles, inverting every light-mode test.
|
|
4451
|
+
if (darkClass) { if (mode === 'dark') el.classList.add(darkClass); else el.classList.remove(darkClass); }
|
|
4432
4452
|
if (disableTransitions) { el.offsetHeight; el.style.removeProperty('transition'); }
|
|
4433
4453
|
}
|
|
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;
|
|
4454
|
+
function readRaw() {
|
|
4455
|
+
try { return localStorage.getItem(storageKey); }
|
|
4456
|
+
catch (e) { return null; }
|
|
4440
4457
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4458
|
+
// The spelling written back for 'auto': an explicit option wins, else
|
|
4459
|
+
// whatever the app already stored, so its own pre-paint script keeps
|
|
4460
|
+
// reading a value it recognises.
|
|
4461
|
+
var storedRaw = readRaw();
|
|
4462
|
+
var autoValue = autoValueOption !== undefined
|
|
4463
|
+
? autoValueOption
|
|
4464
|
+
: ((storedRaw === 'auto' || storedRaw === 'system') ? storedRaw : 'auto');
|
|
4465
|
+
|
|
4466
|
+
function persist(pref) {
|
|
4467
|
+
try { localStorage.setItem(storageKey, pref === 'auto' ? autoValue : pref); }
|
|
4468
|
+
catch (e) {} }
|
|
4469
|
+
// persistChoice: only an explicit user choice is written back. Persisting
|
|
4470
|
+
// on init would stamp the resolved default over a stored value this
|
|
4471
|
+
// composable didn't recognise, destroying it.
|
|
4472
|
+
function update(pref, persistChoice) {
|
|
4473
|
+
preference = normalizeMode(pref) || 'auto';
|
|
4474
|
+
resolved = resolve(preference);
|
|
4444
4475
|
applyDOM(resolved);
|
|
4445
|
-
persist(
|
|
4476
|
+
if (persistChoice !== false) persist(preference);
|
|
4446
4477
|
listeners.forEach(function(fn) { fn(resolved, preference); });
|
|
4447
4478
|
}
|
|
4448
4479
|
|
|
4449
|
-
|
|
4450
|
-
update(persisted || initialMode);
|
|
4480
|
+
update(normalizeMode(storedRaw) || initialMode, false);
|
|
4451
4481
|
|
|
4452
4482
|
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
4453
4483
|
var onSystemChange = function() {
|
|
@@ -4462,8 +4492,11 @@ catch (e) {}
|
|
|
4462
4492
|
|
|
4463
4493
|
var onStorage = function(e) {
|
|
4464
4494
|
if (e.key !== storageKey) return;
|
|
4465
|
-
var v = e.newValue;
|
|
4466
|
-
if (v
|
|
4495
|
+
var v = normalizeMode(e.newValue);
|
|
4496
|
+
if (v) {
|
|
4497
|
+
// Track the other tab's spelling too, so a write from here doesn't
|
|
4498
|
+
// convert the key out from under it.
|
|
4499
|
+
if (v === 'auto' && (e.newValue === 'auto' || e.newValue === 'system')) autoValue = e.newValue;
|
|
4467
4500
|
preference = v; resolved = resolve(v); applyDOM(resolved);
|
|
4468
4501
|
listeners.forEach(function(fn) { fn(resolved, preference); });
|
|
4469
4502
|
}
|
|
@@ -4887,9 +4920,22 @@ catch (e) {}
|
|
|
4887
4920
|
if (config.bodyAttrs && config.bodyAttrs.class) {
|
|
4888
4921
|
config.bodyAttrs.class.split(' ').forEach(function(cls) { if (cls) document.body.classList.add(cls); });
|
|
4889
4922
|
}
|
|
4890
|
-
// Html lang
|
|
4891
|
-
|
|
4892
|
-
|
|
4923
|
+
// Html attributes. Every key, not just lang \u2014 the module impl
|
|
4924
|
+
// (head.ts updateHead) has always applied all of them, and a root-level
|
|
4925
|
+
// class is the main reason to reach for htmlAttrs at all (#1798).
|
|
4926
|
+
// class merges rather than replaces, matching bodyAttrs.class above:
|
|
4927
|
+
// the color-mode boot script owns a class on this same element.
|
|
4928
|
+
if (config.htmlAttrs) {
|
|
4929
|
+
for (var hk in config.htmlAttrs) {
|
|
4930
|
+
if (!config.htmlAttrs.hasOwnProperty(hk)) continue;
|
|
4931
|
+
var hv = config.htmlAttrs[hk];
|
|
4932
|
+
if (hv == null) continue;
|
|
4933
|
+
if (hk === 'class') {
|
|
4934
|
+
String(hv).split(' ').forEach(function(cls) { if (cls) document.documentElement.classList.add(cls); });
|
|
4935
|
+
} else {
|
|
4936
|
+
document.documentElement.setAttribute(hk, hv);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4893
4939
|
}
|
|
4894
4940
|
}
|
|
4895
4941
|
// Apply immediately if DOM is ready, otherwise on mount
|
|
@@ -5895,6 +5941,12 @@ else {
|
|
|
5895
5941
|
window.nextTick = nextTick;
|
|
5896
5942
|
window.useAsync = useAsync;
|
|
5897
5943
|
window.useLocalStorage = useLocalStorage;
|
|
5944
|
+
// Paired with the line above. Both are in STX_RUNTIME_GLOBALS, so a
|
|
5945
|
+
// <script client> block gets either via the setup destructure \u2014 but code
|
|
5946
|
+
// that reads the bare window global directly (a plain <script>, or a
|
|
5947
|
+
// bundled module that wasn't rewritten) found useLocalStorage defined and
|
|
5948
|
+
// useSessionStorage undefined.
|
|
5949
|
+
window.useSessionStorage = useSessionStorage;
|
|
5898
5950
|
window.useEventListener = useEventListener;
|
|
5899
5951
|
window.useWebSocket = useWebSocket;
|
|
5900
5952
|
window.useColorMode = useColorMode;
|
|
@@ -6285,6 +6337,9 @@ catch (e) {
|
|
|
6285
6337
|
}
|
|
6286
6338
|
}
|
|
6287
6339
|
});
|
|
6340
|
+
|
|
6341
|
+
// Everything that should have hydrated has now had its chance (#1773).
|
|
6342
|
+
auditHydration(document.body, 'DOMContentLoaded');
|
|
6288
6343
|
};
|
|
6289
6344
|
window.__stxDomReadyHandler = stxDomReadyHandler;
|
|
6290
6345
|
console.log('[stx] registering DOMContentLoaded handler');
|
|
@@ -6376,6 +6431,184 @@ catch (e) {
|
|
|
6376
6431
|
}
|
|
6377
6432
|
}
|
|
6378
6433
|
|
|
6434
|
+
// ==========================================================================
|
|
6435
|
+
// Post-hydration invariant sweep (stacksjs/stx#1773)
|
|
6436
|
+
// ==========================================================================
|
|
6437
|
+
//
|
|
6438
|
+
// Preserved moustaches are the framework's de-facto error UI: ANY failure in
|
|
6439
|
+
// the detect \u2192 extract \u2192 bundle \u2192 ship \u2192 rebind relay collapses into the same
|
|
6440
|
+
// visible symptom, literal {{ }} on screen. Eight separate fixes each patched
|
|
6441
|
+
// one site's heuristic, and no invariant anywhere asserted the actual
|
|
6442
|
+
// requirement \u2014 zero unhydrated scopes after hydration. So every new upstream
|
|
6443
|
+
// miss reached users silently and had to be diagnosed from a screenshot.
|
|
6444
|
+
//
|
|
6445
|
+
// This is that invariant. It runs at the end of both hydration entry points
|
|
6446
|
+
// and turns a silent miss into one loud, self-healing event:
|
|
6447
|
+
//
|
|
6448
|
+
// - an element carrying data-stx-scope whose id isn't in the registry, or
|
|
6449
|
+
// that hydration reached but never processed, is reported AND processed
|
|
6450
|
+
// with the ambient scope, which is what the walk would have done had the
|
|
6451
|
+
// script arrived. That recovers the D2/D3 shapes (a silent early return
|
|
6452
|
+
// in the walk, a stale __stx_scope guard) rather than merely naming them.
|
|
6453
|
+
// - leftover {{ }} text is reported. Diagnostic only, never auto-fixed:
|
|
6454
|
+
// without a scope there is nothing to interpolate against, and a page may
|
|
6455
|
+
// legitimately print moustaches.
|
|
6456
|
+
//
|
|
6457
|
+
// Deliberately quiet in the normal case: nothing is logged when the page has
|
|
6458
|
+
// no signals at all, and containers that legitimately show template syntax
|
|
6459
|
+
// (<pre>, <code>, <template>, <script>, <style>, <textarea>, and anything
|
|
6460
|
+
// under [data-stx-ignore]) are skipped.
|
|
6461
|
+
|
|
6462
|
+
var AUDIT_SKIP_TAGS = { PRE: 1, CODE: 1, SCRIPT: 1, STYLE: 1, TEXTAREA: 1, TEMPLATE: 1 };
|
|
6463
|
+
|
|
6464
|
+
// Expressions whose evaluation threw a ReferenceError/TypeError and have not
|
|
6465
|
+
// since succeeded. evalAttrExpr swallows those two on purpose \u2014 a signal may
|
|
6466
|
+
// not exist yet on an effect's first pass \u2014 which is why a genuinely broken
|
|
6467
|
+
// binding produces no diagnostic at all today. The classic case is
|
|
6468
|
+
// :if="flag()" where flag is a signal: the auto-unwrap proxy returns the
|
|
6469
|
+
// VALUE, calling it throws TypeError, the directive suppresses it, and the
|
|
6470
|
+
// element silently stays hidden with no warning anywhere (see CLAUDE.md,
|
|
6471
|
+
// "Signals: scripts call, templates don't").
|
|
6472
|
+
//
|
|
6473
|
+
// Recording rather than warning immediately is what keeps this quiet on
|
|
6474
|
+
// working pages: a first-pass failure that later succeeds clears itself, so
|
|
6475
|
+
// only expressions still broken when hydration finishes are reported.
|
|
6476
|
+
var _exprFailures = {};
|
|
6477
|
+
var _exprFailureCount = 0;
|
|
6478
|
+
|
|
6479
|
+
function noteExprFailure(expr, err) {
|
|
6480
|
+
if (_exprFailures[expr] !== undefined) return;
|
|
6481
|
+
// Bounded: one runaway loop must not grow this without limit.
|
|
6482
|
+
if (_exprFailureCount >= 50) return;
|
|
6483
|
+
_exprFailureCount++;
|
|
6484
|
+
_exprFailures[expr] = (err && err.message) ? err.message : String(err);
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
function noteExprSuccess(expr) {
|
|
6488
|
+
if (_exprFailures[expr] === undefined) return;
|
|
6489
|
+
delete _exprFailures[expr];
|
|
6490
|
+
_exprFailureCount--;
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
function auditSkipped(node) {
|
|
6494
|
+
// Not rendered, not audited. Covers <template> content (a detached
|
|
6495
|
+
// DocumentFragment \u2014 which some DOM implementations still walk into) and
|
|
6496
|
+
// :if branches parked off-document, neither of which the user can see.
|
|
6497
|
+
if (node.isConnected === false) return true;
|
|
6498
|
+
for (var p = node.parentNode; p; p = p.parentNode) {
|
|
6499
|
+
if (p.nodeType === Node.DOCUMENT_FRAGMENT_NODE) return true;
|
|
6500
|
+
if (p.nodeType !== Node.ELEMENT_NODE) continue;
|
|
6501
|
+
if (AUDIT_SKIP_TAGS[p.tagName]) return true;
|
|
6502
|
+
if (p.hasAttribute && p.hasAttribute('data-stx-ignore')) return true;
|
|
6503
|
+
}
|
|
6504
|
+
return false;
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
function findLiteralMoustaches(root) {
|
|
6508
|
+
var found = [];
|
|
6509
|
+
if (!root || typeof document.createTreeWalker !== 'function') return found;
|
|
6510
|
+
// Read NodeFilter off window, not the bare global: in a real browser they
|
|
6511
|
+
// are the same object, but some DOM implementations used for testing expose
|
|
6512
|
+
// it only on window, and a bare reference then throws inside the audit.
|
|
6513
|
+
var showText = (window.NodeFilter && window.NodeFilter.SHOW_TEXT) || 4;
|
|
6514
|
+
var walker = document.createTreeWalker(root, showText, null);
|
|
6515
|
+
var node;
|
|
6516
|
+
while ((node = walker.nextNode())) {
|
|
6517
|
+
var text = node.nodeValue || '';
|
|
6518
|
+
if (text.indexOf('{{') === -1 || text.indexOf('}}') === -1) continue;
|
|
6519
|
+
if (auditSkipped(node)) continue;
|
|
6520
|
+
found.push(text.trim().slice(0, 80));
|
|
6521
|
+
if (found.length >= 5) break;
|
|
6522
|
+
}
|
|
6523
|
+
return found;
|
|
6524
|
+
}
|
|
6525
|
+
|
|
6526
|
+
// A diagnostic that can break hydration is worse than no diagnostic, so the
|
|
6527
|
+
// whole sweep is wrapped and every DOM capability it uses is feature-checked.
|
|
6528
|
+
// Minimal document stubs (tests, non-browser embeddings) simply skip it.
|
|
6529
|
+
function auditHydration(container, phase) {
|
|
6530
|
+
try { auditHydrationUnsafe(container, phase); }
|
|
6531
|
+
catch (e) { console.warn('[stx] hydration audit skipped:', e && e.message ? e.message : e); }
|
|
6532
|
+
}
|
|
6533
|
+
|
|
6534
|
+
function auditHydrationUnsafe(container, phase) {
|
|
6535
|
+
if (!window.stx || !window.stx._scopes) return;
|
|
6536
|
+
var root = container && typeof container.querySelectorAll === 'function' ? container : document.body;
|
|
6537
|
+
if (!root || typeof root.querySelectorAll !== 'function') return;
|
|
6538
|
+
|
|
6539
|
+
// Unregistered / unprocessed scopes \u2014 the unambiguous failure, and the one
|
|
6540
|
+
// worth repairing.
|
|
6541
|
+
var stranded = [];
|
|
6542
|
+
root.querySelectorAll('[data-stx-scope]').forEach(function(el) {
|
|
6543
|
+
if (el.__stx_audit_recovered) return;
|
|
6544
|
+
// Deferred islands are unhydrated ON PURPOSE until their trigger fires.
|
|
6545
|
+
if (el.hasAttribute('stx-hydrate') || el.__stx_hydration_scheduled) return;
|
|
6546
|
+
var scopeId = el.getAttribute('data-stx-scope');
|
|
6547
|
+
var registered = window.stx._scopes[scopeId];
|
|
6548
|
+
if (registered && el.__stx_disposers) return;
|
|
6549
|
+
stranded.push({ el: el, id: scopeId, registered: !!registered });
|
|
6550
|
+
});
|
|
6551
|
+
|
|
6552
|
+
if (stranded.length) {
|
|
6553
|
+
console.error(
|
|
6554
|
+
'[stx] hydration invariant failed after ' + phase + ': '
|
|
6555
|
+
+ stranded.length + ' scope(s) left unhydrated \u2014 '
|
|
6556
|
+
+ stranded.map(function(s) {
|
|
6557
|
+
return s.id + (s.registered ? ' (registered, never processed)' : ' (not registered)');
|
|
6558
|
+
}).join(', ')
|
|
6559
|
+
+ '. Recovering with the ambient scope; please report with the page that produced it (stacksjs/stx#1773).',
|
|
6560
|
+
);
|
|
6561
|
+
stranded.forEach(function(s) {
|
|
6562
|
+
// Once per element: a repair that itself fails must not loop on the
|
|
6563
|
+
// next navigation.
|
|
6564
|
+
s.el.__stx_audit_recovered = true;
|
|
6565
|
+
try {
|
|
6566
|
+
var vars = s.registered ? window.stx._scopes[s.id] : null;
|
|
6567
|
+
if (vars) Object.assign(componentScope, vars);
|
|
6568
|
+
s.el.__stx_disposers = trackEffects(function() { processElement(s.el, componentScope); });
|
|
6569
|
+
s.el.removeAttribute('x-cloak');
|
|
6570
|
+
s.el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
|
|
6571
|
+
}
|
|
6572
|
+
catch (e) {
|
|
6573
|
+
console.error('[stx] hydration recovery failed for scope ' + s.id + ':', e);
|
|
6574
|
+
}
|
|
6575
|
+
});
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6578
|
+
var literals = findLiteralMoustaches(root);
|
|
6579
|
+
if (literals.length) {
|
|
6580
|
+
console.error(
|
|
6581
|
+
'[stx] hydration invariant failed after ' + phase + ': literal {{ }} left in the DOM \u2014 '
|
|
6582
|
+
+ literals.join(' | ')
|
|
6583
|
+
+ '. An expression did not bind; see stacksjs/stx#1773. Wrap intentional '
|
|
6584
|
+
+ 'template syntax in <pre>/<code> or [data-stx-ignore] to exclude it.',
|
|
6585
|
+
);
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
// Expressions that threw and never recovered. Reported here rather than at
|
|
6589
|
+
// throw time so a first-pass miss that later resolves stays silent.
|
|
6590
|
+
var brokenExprs = [];
|
|
6591
|
+
for (var expr in _exprFailures) {
|
|
6592
|
+
if (Object.prototype.hasOwnProperty.call(_exprFailures, expr))
|
|
6593
|
+
brokenExprs.push(expr + ' \u2192 ' + _exprFailures[expr]);
|
|
6594
|
+
}
|
|
6595
|
+
if (brokenExprs.length) {
|
|
6596
|
+
var hint = brokenExprs.some(function(m) { return m.indexOf('is not a function') !== -1; })
|
|
6597
|
+
? ' If one of these calls a signal, drop the parentheses: templates read '
|
|
6598
|
+
+ 'a signal by bare name (:if="flag"), scripts call it (flag()). Calling '
|
|
6599
|
+
+ 'the unwrapped value is a TypeError, which directives suppress \u2014 the '
|
|
6600
|
+
+ 'element just stays hidden.'
|
|
6601
|
+
: '';
|
|
6602
|
+
console.error(
|
|
6603
|
+
'[stx] hydration invariant failed after ' + phase + ': '
|
|
6604
|
+
+ brokenExprs.length + ' expression(s) never evaluated \u2014 '
|
|
6605
|
+
+ brokenExprs.slice(0, 5).join(' | ') + '.' + hint,
|
|
6606
|
+
);
|
|
6607
|
+
_exprFailures = {};
|
|
6608
|
+
_exprFailureCount = 0;
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6379
6612
|
// ==========================================================================
|
|
6380
6613
|
// Container Cleanup (for SPA navigation)
|
|
6381
6614
|
// ==========================================================================
|
|
@@ -6617,6 +6850,9 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
6617
6850
|
catch (e) { console.warn('[stx] mount callback error:', e); }
|
|
6618
6851
|
});
|
|
6619
6852
|
mountCallbacks.length = 0;
|
|
6853
|
+
|
|
6854
|
+
// Everything that should have hydrated has now had its chance (#1773).
|
|
6855
|
+
auditHydration(container, 'stx:load');
|
|
6620
6856
|
}
|
|
6621
6857
|
|
|
6622
6858
|
// Helper to process elements while skipping already-processed scoped containers
|