@stacksjs/stx 0.2.176 → 0.2.178
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/builtins/attrs.d.ts +19 -0
- package/dist/builtins/attrs.js +18 -0
- package/dist/builtins/safe-image.js +2 -1
- package/dist/builtins/stx-image.js +2 -1
- package/dist/builtins/stx-link.js +2 -1
- package/dist/cli-flags.d.ts +34 -0
- package/dist/cli-flags.js +20 -0
- package/dist/cli.js +899 -778
- package/dist/client-script.js +2 -1
- package/dist/component-registry.d.ts +9 -0
- package/dist/component-renderer.js +20 -1
- package/dist/composables/use-fetch.d.ts +6 -1
- package/dist/composables/use-fetch.js +40 -18
- package/dist/composables/use-query.d.ts +7 -2
- package/dist/cookie-serialize.d.ts +40 -0
- package/dist/cookie-serialize.js +18 -0
- package/dist/craft.js +500 -381
- package/dist/crosswind-config.d.ts +13 -0
- package/dist/crosswind-config.js +6 -2
- package/dist/dev-server/crosswind.js +2 -1
- package/dist/edge-runtime.d.ts +2 -9
- package/dist/edge-runtime.js +2 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/page-action.d.ts +10 -20
- package/dist/page-action.js +33 -9
- package/dist/production-server.js +8 -7
- package/dist/pwa.js +452 -333
- package/dist/reactive.js +2 -3
- package/dist/signal-processing.js +37 -6
- package/dist/signals.js +150 -32
- package/dist/ssg.js +465 -346
- package/dist/ssr.js +2 -16
- package/dist/stx-virtual-ts.d.ts +45 -1
- package/dist/stx-virtual-ts.js +75 -5
- package/dist/stx.d.ts +66 -7
- package/dist/template-hydrator.d.ts +1 -1
- package/dist/template-hydrator.js +6 -6
- package/dist/theme-tokens.d.ts +88 -0
- package/dist/theme-tokens.js +71 -0
- package/dist/typecheck.d.ts +6 -1
- package/dist/typecheck.js +7 -6
- package/package.json +5 -5
- package/stx.d.ts +66 -7
package/dist/reactive.js
CHANGED
|
@@ -529,7 +529,6 @@ function removeReactiveAttributes(template) {
|
|
|
529
529
|
return output;
|
|
530
530
|
}
|
|
531
531
|
export function processReactiveDirectives(template, context, _filePath) {
|
|
532
|
-
scopeCounter = 0;
|
|
533
532
|
if (!hasReactiveDirectives(template))
|
|
534
533
|
return template;
|
|
535
534
|
const scopes = findReactiveScopes(template);
|
|
@@ -585,8 +584,8 @@ function finalizeTemplate(template, scopes) {
|
|
|
585
584
|
});
|
|
586
585
|
output = output.replace(/\s*x-data\s*=\s*"([^"]*)"/g, ' data-stx-xdata="$1"');
|
|
587
586
|
output = output.replace(/\s*x-data\s*=\s*'([^']*)'/g, " data-stx-xdata='$1'");
|
|
588
|
-
output = output.replace(/\s*x-init\s*=\s*"[^"]*"/g, "");
|
|
589
|
-
output = output.replace(/\s*x-init\s*=\s*'[^']*'/g, "");
|
|
587
|
+
output = output.replace(/\s*x-init\s*=\s*"([^"]*)"/g, ' data-stx-xinit="$1"');
|
|
588
|
+
output = output.replace(/\s*x-init\s*=\s*'([^']*)'/g, " data-stx-xinit='$1'");
|
|
590
589
|
return output;
|
|
591
590
|
}
|
|
592
591
|
export function hasReactiveDirectives(template) {
|
|
@@ -90,6 +90,30 @@ function extractClientSignalNames(template) {
|
|
|
90
90
|
}
|
|
91
91
|
return names;
|
|
92
92
|
}
|
|
93
|
+
function collectClientLoopScopes(template) {
|
|
94
|
+
if (!/[:@]for\s*=/.test(template) && !/\bx-for\s*=/.test(template))
|
|
95
|
+
return [];
|
|
96
|
+
const openingTag = new RegExp(`<([a-zA-Z][\\w-]*)\\b(${TAG_ATTR_RUN})>`, "iy"), loopAttr = /(?:^|\s)(?::for|@for|x-for)\s*=\s*(?:"([^"]*)"|'([^']*)')/i, starts = scanAtElementPosition(template, (html, index) => {
|
|
97
|
+
openingTag.lastIndex = index;
|
|
98
|
+
const match = openingTag.exec(html);
|
|
99
|
+
if (!match || match.index !== index || !loopAttr.test(match[2]))
|
|
100
|
+
return -1;
|
|
101
|
+
return openingTag.lastIndex;
|
|
102
|
+
}), scopes = [];
|
|
103
|
+
for (const start of starts) {
|
|
104
|
+
openingTag.lastIndex = 0;
|
|
105
|
+
const match = openingTag.exec(start.token);
|
|
106
|
+
if (!match)
|
|
107
|
+
continue;
|
|
108
|
+
const attrMatch = match[2].match(loopAttr), parsed = parseLoopExpression(attrMatch?.[1] ?? attrMatch?.[2] ?? ""), vars = [parsed.itemVar, parsed.indexVar].filter(Boolean);
|
|
109
|
+
if (vars.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
const tag = match[1], end = /\/\s*>$/.test(start.token) || VOID_ELEMENT_TAGS.has(tag.toLowerCase()) ? start.end : findElementEnd(template, start.start, tag, start.end);
|
|
112
|
+
if (end !== -1)
|
|
113
|
+
scopes.push({ start: start.start, end, vars: new Set(vars) });
|
|
114
|
+
}
|
|
115
|
+
return scopes;
|
|
116
|
+
}
|
|
93
117
|
const JS_NON_VARS = new Set([
|
|
94
118
|
"true",
|
|
95
119
|
"false",
|
|
@@ -132,7 +156,7 @@ const JS_NON_VARS = new Set([
|
|
|
132
156
|
function stripExprStrings(expr) {
|
|
133
157
|
return expr.replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
|
|
134
158
|
}
|
|
135
|
-
function conditionIsClientReactive(condition, signalNames, context) {
|
|
159
|
+
function conditionIsClientReactive(condition, signalNames, context, clientLoopVars) {
|
|
136
160
|
if (!condition)
|
|
137
161
|
return !1;
|
|
138
162
|
const expr = stripExprStrings(condition);
|
|
@@ -145,7 +169,7 @@ function conditionIsClientReactive(condition, signalNames, context) {
|
|
|
145
169
|
continue;
|
|
146
170
|
if (JS_NON_VARS.has(id))
|
|
147
171
|
continue;
|
|
148
|
-
if (signalNames.has(id))
|
|
172
|
+
if (signalNames.has(id) || clientLoopVars?.has(id))
|
|
149
173
|
refsSignal = !0;
|
|
150
174
|
else if (context && id in context)
|
|
151
175
|
refsServerVar = !0;
|
|
@@ -186,7 +210,14 @@ export function convertSignalDirectivesToAttributes(template, context) {
|
|
|
186
210
|
const signalNames = extractClientSignalNames(template);
|
|
187
211
|
function convert(input) {
|
|
188
212
|
let output = input;
|
|
189
|
-
const
|
|
213
|
+
const clientLoopScopes = collectClientLoopScopes(input), loopVarsAt = (position) => {
|
|
214
|
+
const vars = new Set;
|
|
215
|
+
for (const scope of clientLoopScopes)
|
|
216
|
+
if (position > scope.start && position < scope.end)
|
|
217
|
+
for (const name of scope.vars)
|
|
218
|
+
vars.add(name);
|
|
219
|
+
return vars;
|
|
220
|
+
}, ifDirectiveStart = /@if\s*\(/g;
|
|
190
221
|
let match;
|
|
191
222
|
const replacements = [];
|
|
192
223
|
while ((match = ifDirectiveStart.exec(output)) !== null) {
|
|
@@ -224,8 +255,8 @@ export function convertSignalDirectivesToAttributes(template, context) {
|
|
|
224
255
|
const parsed = findIfBlocks(output.substring(startIdx, endIdx));
|
|
225
256
|
if (parsed.length === 0)
|
|
226
257
|
continue;
|
|
227
|
-
const valueBranches = parsed[0].branches.filter((b) => b.type !== "else");
|
|
228
|
-
if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context))))
|
|
258
|
+
const valueBranches = parsed[0].branches.filter((b) => b.type !== "else"), clientLoopVars = loopVarsAt(startIdx);
|
|
259
|
+
if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context, clientLoopVars))))
|
|
229
260
|
continue;
|
|
230
261
|
replacement = parsed[0].branches.map((branch) => {
|
|
231
262
|
const branchContent = convert(branch.content);
|
|
@@ -237,7 +268,7 @@ export function convertSignalDirectivesToAttributes(template, context) {
|
|
|
237
268
|
}).join(`
|
|
238
269
|
`);
|
|
239
270
|
} else {
|
|
240
|
-
if (!conditionIsClientReactive(condition, signalNames, context))
|
|
271
|
+
if (!conditionIsClientReactive(condition, signalNames, context, loopVarsAt(startIdx)))
|
|
241
272
|
continue;
|
|
242
273
|
replacement = branchToAttrElement(convert(content), "@if", condition);
|
|
243
274
|
}
|
package/dist/signals.js
CHANGED
|
@@ -1065,6 +1065,10 @@ finally {
|
|
|
1065
1065
|
// case where a template is most likely driving a spinner off it (#1818).
|
|
1066
1066
|
const immediate = options.immediate !== false;
|
|
1067
1067
|
const loading = state(immediate);
|
|
1068
|
+
// Any request in flight, background ones included. loading answers "is
|
|
1069
|
+
// there nothing to show yet"; isFetching answers "is a request open" \u2014 and
|
|
1070
|
+
// a background refresh is the case where those two stop agreeing (#1929).
|
|
1071
|
+
const isFetching = state(immediate);
|
|
1068
1072
|
const error = state(null);
|
|
1069
1073
|
if (options.suspense) registerSuspense(loading, error);
|
|
1070
1074
|
|
|
@@ -1074,17 +1078,24 @@ finally {
|
|
|
1074
1078
|
// last in-flight one. State writes are guarded on signal.aborted (#1871).
|
|
1075
1079
|
let __stxAbort = null;
|
|
1076
1080
|
|
|
1077
|
-
|
|
1081
|
+
// runOptions: { background: true } refreshes data WITHOUT touching loading
|
|
1082
|
+
// or clearing the current error, so a poll cannot flash a spinner or blank
|
|
1083
|
+
// an error message that is still true. Everything already on screen stays
|
|
1084
|
+
// there until the new response actually lands (#1929).
|
|
1085
|
+
const fetchData = async (runOptions) => {
|
|
1086
|
+
const background = !!(runOptions && runOptions.background);
|
|
1078
1087
|
if (__stxAbort) __stxAbort.abort();
|
|
1079
1088
|
__stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1080
1089
|
const __signal = __stxAbort ? __stxAbort.signal : undefined;
|
|
1081
|
-
loading.set(true);
|
|
1082
|
-
|
|
1090
|
+
if (!background) loading.set(true);
|
|
1091
|
+
isFetching.set(true);
|
|
1092
|
+
if (!background) error.set(null);
|
|
1083
1093
|
|
|
1084
1094
|
try {
|
|
1085
1095
|
const url = typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn;
|
|
1086
1096
|
if (!url) {
|
|
1087
|
-
loading.set(false);
|
|
1097
|
+
if (!background) loading.set(false);
|
|
1098
|
+
isFetching.set(false);
|
|
1088
1099
|
return;
|
|
1089
1100
|
}
|
|
1090
1101
|
|
|
@@ -1114,6 +1125,10 @@ finally {
|
|
|
1114
1125
|
// Apply transform if provided
|
|
1115
1126
|
const transformed = options.transform ? options.transform(result) : result;
|
|
1116
1127
|
data.set(transformed);
|
|
1128
|
+
// A background run did not clear the error up front, so a recovery has
|
|
1129
|
+
// to clear it here \u2014 otherwise the first failed poll would leave the
|
|
1130
|
+
// message on screen for the life of the page.
|
|
1131
|
+
if (background) error.set(null);
|
|
1117
1132
|
}
|
|
1118
1133
|
catch (e) {
|
|
1119
1134
|
// A superseded/unmounted request rejects with AbortError \u2014 expected,
|
|
@@ -1128,7 +1143,10 @@ catch (e) {
|
|
|
1128
1143
|
}
|
|
1129
1144
|
finally {
|
|
1130
1145
|
// If a newer run (or onDestroy) aborted this one, that run owns loading.
|
|
1131
|
-
if (!__signal || !__signal.aborted)
|
|
1146
|
+
if (!__signal || !__signal.aborted) {
|
|
1147
|
+
if (!background) loading.set(false);
|
|
1148
|
+
isFetching.set(false);
|
|
1149
|
+
}
|
|
1132
1150
|
}
|
|
1133
1151
|
};
|
|
1134
1152
|
|
|
@@ -1161,6 +1179,7 @@ else if (immediate) {
|
|
|
1161
1179
|
return {
|
|
1162
1180
|
data,
|
|
1163
1181
|
loading,
|
|
1182
|
+
isFetching,
|
|
1164
1183
|
error,
|
|
1165
1184
|
refetch: fetchData,
|
|
1166
1185
|
// Convenience getters
|
|
@@ -1411,6 +1430,10 @@ else if (immediate) {
|
|
|
1411
1430
|
var cacheKey = options.cacheKey || (typeof url === 'function' ? null : url);
|
|
1412
1431
|
var data = state(options.initialData || null);
|
|
1413
1432
|
var loading = state(true);
|
|
1433
|
+
// See useFetch: loading means "nothing to show yet", isFetching means "a
|
|
1434
|
+
// request is open" \u2014 including a background refresh, which is the case that
|
|
1435
|
+
// makes the distinction necessary (#1929).
|
|
1436
|
+
var isFetching = state(true);
|
|
1414
1437
|
var error = state(null);
|
|
1415
1438
|
var isStale = state(false);
|
|
1416
1439
|
if (options.suspense) registerSuspense(loading, error);
|
|
@@ -1429,16 +1452,21 @@ else if (immediate) {
|
|
|
1429
1452
|
// can release its claim without cancelling a request someone else joined.
|
|
1430
1453
|
var __joined = null;
|
|
1431
1454
|
|
|
1432
|
-
|
|
1455
|
+
// runOptions: { background: true } refreshes without touching loading or
|
|
1456
|
+
// clearing the current error. A poll must not flash a spinner over data
|
|
1457
|
+
// that is already on screen (#1929).
|
|
1458
|
+
var fetchData = async function(runOptions) {
|
|
1459
|
+
var background = !!(runOptions && runOptions.background);
|
|
1433
1460
|
var resolvedUrl = typeof url === 'function' ? url() : url;
|
|
1434
|
-
if (!resolvedUrl) { loading.set(false); return; }
|
|
1461
|
+
if (!resolvedUrl) { if (!background) loading.set(false); isFetching.set(false); return; }
|
|
1435
1462
|
var key = cacheKey || resolvedUrl;
|
|
1436
1463
|
|
|
1437
1464
|
// Check cache
|
|
1438
1465
|
var cached = _queryCache[key];
|
|
1439
1466
|
if (cached && (Date.now() - cached.timestamp < staleTime)) {
|
|
1440
1467
|
data.set(cached.data);
|
|
1441
|
-
loading.set(false);
|
|
1468
|
+
if (!background) loading.set(false);
|
|
1469
|
+
isFetching.set(false);
|
|
1442
1470
|
isStale.set(false);
|
|
1443
1471
|
if (options.onSuccess) options.onSuccess(cached.data);
|
|
1444
1472
|
return;
|
|
@@ -1458,12 +1486,14 @@ else if (immediate) {
|
|
|
1458
1486
|
if (shared && shared.owner !== __instanceId) {
|
|
1459
1487
|
shared.refs++;
|
|
1460
1488
|
__joined = shared;
|
|
1461
|
-
loading.set(true);
|
|
1462
|
-
|
|
1489
|
+
if (!background) loading.set(true);
|
|
1490
|
+
isFetching.set(true);
|
|
1491
|
+
if (!background) error.set(null);
|
|
1463
1492
|
try {
|
|
1464
1493
|
var joinedResult = await shared.promise;
|
|
1465
1494
|
var joinedTransformed = options.transform ? options.transform(joinedResult) : joinedResult;
|
|
1466
1495
|
data.set(joinedTransformed);
|
|
1496
|
+
if (background) error.set(null);
|
|
1467
1497
|
isStale.set(false);
|
|
1468
1498
|
if (options.onSuccess) options.onSuccess(joinedTransformed);
|
|
1469
1499
|
}
|
|
@@ -1475,7 +1505,8 @@ else if (immediate) {
|
|
|
1475
1505
|
finally {
|
|
1476
1506
|
shared.refs--;
|
|
1477
1507
|
if (__joined === shared) __joined = null;
|
|
1478
|
-
loading.set(false);
|
|
1508
|
+
if (!background) loading.set(false);
|
|
1509
|
+
isFetching.set(false);
|
|
1479
1510
|
}
|
|
1480
1511
|
return;
|
|
1481
1512
|
}
|
|
@@ -1490,8 +1521,9 @@ else if (immediate) {
|
|
|
1490
1521
|
__joined = null;
|
|
1491
1522
|
__stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1492
1523
|
var __signal = __stxAbort ? __stxAbort.signal : undefined;
|
|
1493
|
-
loading.set(true);
|
|
1494
|
-
|
|
1524
|
+
if (!background) loading.set(true);
|
|
1525
|
+
isFetching.set(true);
|
|
1526
|
+
if (!background) error.set(null);
|
|
1495
1527
|
try {
|
|
1496
1528
|
var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) }, signal: __signal };
|
|
1497
1529
|
// Published BEFORE it is awaited, so a caller starting in the same tick
|
|
@@ -1515,6 +1547,7 @@ else if (immediate) {
|
|
|
1515
1547
|
if (__signal && __signal.aborted) return;
|
|
1516
1548
|
var transformed = options.transform ? options.transform(result) : result;
|
|
1517
1549
|
data.set(transformed);
|
|
1550
|
+
if (background) error.set(null);
|
|
1518
1551
|
isStale.set(false);
|
|
1519
1552
|
_queryCache[key] = { data: transformed, timestamp: Date.now() };
|
|
1520
1553
|
if (options.onSuccess) options.onSuccess(transformed);
|
|
@@ -1529,7 +1562,10 @@ catch (e) {
|
|
|
1529
1562
|
if (options.onError) options.onError(e);
|
|
1530
1563
|
}
|
|
1531
1564
|
finally {
|
|
1532
|
-
if (!__signal || !__signal.aborted)
|
|
1565
|
+
if (!__signal || !__signal.aborted) {
|
|
1566
|
+
if (!background) loading.set(false);
|
|
1567
|
+
isFetching.set(false);
|
|
1568
|
+
}
|
|
1533
1569
|
}
|
|
1534
1570
|
};
|
|
1535
1571
|
|
|
@@ -1537,13 +1573,20 @@ finally {
|
|
|
1537
1573
|
onMount(fetchData);
|
|
1538
1574
|
}
|
|
1539
1575
|
|
|
1576
|
+
// A refetch nobody asked for does not set loading. Both of these refresh
|
|
1577
|
+
// data that is ALREADY on screen, so driving the first-load state from them
|
|
1578
|
+
// put a spinner over a populated view once a minute, forever \u2014 the reason a
|
|
1579
|
+
// polling view could not use this composable at all (#1929). Bind
|
|
1580
|
+
// isFetching for a subtle in-flight indicator.
|
|
1581
|
+
var backgroundRun = { background: true };
|
|
1582
|
+
|
|
1540
1583
|
// refetchOnFocus \u2014 removed on destroy, like refetchInterval right below.
|
|
1541
1584
|
// It used to add a listener and never remove it, so every instance leaked
|
|
1542
1585
|
// one for the life of the page and each SPA navigation added more; every
|
|
1543
1586
|
// stale listener still fired, refetching for components long gone (#1818).
|
|
1544
1587
|
if (options.refetchOnFocus) {
|
|
1545
1588
|
var onVisibility = function() {
|
|
1546
|
-
if (!document.hidden) fetchData();
|
|
1589
|
+
if (!document.hidden) fetchData(backgroundRun);
|
|
1547
1590
|
};
|
|
1548
1591
|
document.addEventListener('visibilitychange', onVisibility);
|
|
1549
1592
|
onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
|
|
@@ -1557,7 +1600,7 @@ finally {
|
|
|
1557
1600
|
if (typeof document !== 'undefined' && document.hidden) return;
|
|
1558
1601
|
var en = options.enabled;
|
|
1559
1602
|
if (typeof en === 'function' ? !en() : en === false) return;
|
|
1560
|
-
var p = fetchData();
|
|
1603
|
+
var p = fetchData(backgroundRun);
|
|
1561
1604
|
if (options.stopOnError && p && typeof p.then === 'function') {
|
|
1562
1605
|
p.then(function() { if (error()) clearInterval(intervalId); });
|
|
1563
1606
|
}
|
|
@@ -1571,13 +1614,14 @@ finally {
|
|
|
1571
1614
|
return {
|
|
1572
1615
|
data: data,
|
|
1573
1616
|
loading: loading,
|
|
1617
|
+
isFetching: isFetching,
|
|
1574
1618
|
error: error,
|
|
1575
1619
|
isStale: isStale,
|
|
1576
1620
|
refetch: fetchData,
|
|
1577
|
-
invalidate: function() {
|
|
1621
|
+
invalidate: function(runOptions) {
|
|
1578
1622
|
var key = cacheKey || (typeof url === 'function' ? url() : url);
|
|
1579
1623
|
delete _queryCache[key];
|
|
1580
|
-
return fetchData();
|
|
1624
|
+
return fetchData(runOptions);
|
|
1581
1625
|
}
|
|
1582
1626
|
};
|
|
1583
1627
|
}
|
|
@@ -2145,11 +2189,18 @@ catch (e) {
|
|
|
2145
2189
|
if (!el || !el.getAttribute || value === undefined) return;
|
|
2146
2190
|
var scopeId = el.getAttribute('data-stx-scope');
|
|
2147
2191
|
var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
|
|
2148
|
-
if (!scopeVars) return;
|
|
2149
2192
|
var camelName = name.replace(/-([a-z0-9])/g, function(_, character) {
|
|
2150
2193
|
return character.toUpperCase();
|
|
2151
2194
|
});
|
|
2152
|
-
|
|
2195
|
+
// useReactiveProp signals are often intentionally aliased inside the
|
|
2196
|
+
// child, for example liveClosable = useReactiveProp('closable', true).
|
|
2197
|
+
// The public prop name therefore cannot be inferred from the variable
|
|
2198
|
+
// name exported by the component scope. Keep an element-owned registry as
|
|
2199
|
+
// the canonical bridge, with the scope lookup retained for older output.
|
|
2200
|
+
var propSignals = el.__stx_reactive_prop_signals;
|
|
2201
|
+
var propSignal = propSignals && (propSignals[name] || propSignals[camelName]);
|
|
2202
|
+
if (!propSignal && scopeVars)
|
|
2203
|
+
propSignal = scopeVars[name] || scopeVars[camelName];
|
|
2153
2204
|
if (!propSignal || !propSignal._isSignal || typeof propSignal.set !== 'function') return;
|
|
2154
2205
|
var directValues = el.__stx_direct_prop_values || (el.__stx_direct_prop_values = {});
|
|
2155
2206
|
var directValue = {
|
|
@@ -2477,13 +2528,12 @@ else if (part) {
|
|
|
2477
2528
|
}
|
|
2478
2529
|
|
|
2479
2530
|
// x-else / x-else-if are owned by their preceding x-if chain (#1734).
|
|
2480
|
-
//
|
|
2481
|
-
// iteration must be skipped
|
|
2482
|
-
//
|
|
2483
|
-
//
|
|
2484
|
-
//
|
|
2485
|
-
|
|
2486
|
-
if (el.__stx_chain_member && !el.isConnected) return;
|
|
2531
|
+
// An inactive chain member reached via the parent's snapshot child-
|
|
2532
|
+
// iteration must be skipped. Connectivity cannot identify inactivity:
|
|
2533
|
+
// an active inner branch is also disconnected while an outer conditional
|
|
2534
|
+
// temporarily detaches its parent. bindIfChain owns explicit branch state
|
|
2535
|
+
// so nested structural effects can still hydrate active detached subtrees.
|
|
2536
|
+
if (el.__stx_chain_member && !el.__stx_chain_active) return;
|
|
2487
2537
|
|
|
2488
2538
|
// An else/else-if with no preceding if is an orphan. Match Vue: warn,
|
|
2489
2539
|
// strip the attribute, and render it as a plain element (fall through).
|
|
@@ -4175,6 +4225,7 @@ catch (e) {
|
|
|
4175
4225
|
preserveConditionalComponentScopes(b.el, capturedComponentScope);
|
|
4176
4226
|
b.el.__stx_if_bound = true;
|
|
4177
4227
|
b.el.__stx_chain_member = true;
|
|
4228
|
+
b.el.__stx_chain_active = false;
|
|
4178
4229
|
b.capturedElementScope = findElementScope(b.el);
|
|
4179
4230
|
b.placeholder = document.createComment('stx-if-chain');
|
|
4180
4231
|
parent.insertBefore(b.placeholder, b.el);
|
|
@@ -4274,12 +4325,14 @@ catch (e2) {
|
|
|
4274
4325
|
// components under reactive conditionals (#1737). Permanent disposal is
|
|
4275
4326
|
// still handled by bindFor (item removal) and cleanupContainer (SPA nav).
|
|
4276
4327
|
if (currentIdx !== -1) {
|
|
4328
|
+
chain[currentIdx].el.__stx_chain_active = false;
|
|
4277
4329
|
chain[currentIdx].nodes.forEach(function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
|
|
4278
4330
|
}
|
|
4279
4331
|
|
|
4280
4332
|
// Insert + process the newly-picked branch.
|
|
4281
4333
|
if (pickedIdx !== -1) {
|
|
4282
4334
|
var pick = chain[pickedIdx];
|
|
4335
|
+
pick.el.__stx_chain_active = true;
|
|
4283
4336
|
// Insert this branch's node(s) before the placeholder's next sibling.
|
|
4284
4337
|
// The anchor is captured once so the nodes stack in source order.
|
|
4285
4338
|
var anchor = pick.placeholder.nextSibling;
|
|
@@ -5478,6 +5531,11 @@ catch (e) {} }
|
|
|
5478
5531
|
}
|
|
5479
5532
|
var s = state(initial);
|
|
5480
5533
|
if (!root) return s;
|
|
5534
|
+
var propSignals = root.__stx_reactive_prop_signals
|
|
5535
|
+
|| (root.__stx_reactive_prop_signals = {});
|
|
5536
|
+
propSignals[name] = s;
|
|
5537
|
+
propSignals[kebabName] = s;
|
|
5538
|
+
propSignals[lowerName] = s;
|
|
5481
5539
|
var observer = new MutationObserver(function() {
|
|
5482
5540
|
var attribute = readAttribute();
|
|
5483
5541
|
var hasAttr = attribute.has;
|
|
@@ -5500,7 +5558,12 @@ catch (e) {} }
|
|
|
5500
5558
|
if (next !== s()) s.set(next);
|
|
5501
5559
|
});
|
|
5502
5560
|
observer.observe(root, { attributes: true, attributeFilter: attributeNames });
|
|
5503
|
-
onDestroy(function() {
|
|
5561
|
+
onDestroy(function() {
|
|
5562
|
+
observer.disconnect();
|
|
5563
|
+
attributeNames.forEach(function(attributeName) {
|
|
5564
|
+
if (propSignals[attributeName] === s) delete propSignals[attributeName];
|
|
5565
|
+
});
|
|
5566
|
+
});
|
|
5504
5567
|
return s;
|
|
5505
5568
|
}
|
|
5506
5569
|
|
|
@@ -7156,6 +7219,49 @@ else {
|
|
|
7156
7219
|
});
|
|
7157
7220
|
var pageScopeSnapshot = { ...componentScope };
|
|
7158
7221
|
|
|
7222
|
+
/*
|
|
7223
|
+
* Any x-data scope the reactive bridge did not reach, initialized here.
|
|
7224
|
+
*
|
|
7225
|
+
* The bridge script addresses each scope by a selector it generated, and
|
|
7226
|
+
* that numbering restarts per render - so a page carrying three component
|
|
7227
|
+
* islands emits three scripts all looking for the same scope id, and two of
|
|
7228
|
+
* the three scopes are never initialized at all. The symptom is the
|
|
7229
|
+
* quietest one available: the markup is there, every binding shows its
|
|
7230
|
+
* initial value, and whatever x-init was meant to start never starts.
|
|
7231
|
+
*
|
|
7232
|
+
* Registered scopes are skipped, so the bridge stays authoritative where it
|
|
7233
|
+
* did run and this only picks up what it missed. The init expression is
|
|
7234
|
+
* read from data-stx-xinit, which the server preserves for exactly this.
|
|
7235
|
+
*
|
|
7236
|
+
* No backticks in this comment, deliberately: it lives inside the runtime
|
|
7237
|
+
* source that is embedded as a template literal, and one would end the
|
|
7238
|
+
* literal and take the whole file's syntax with it.
|
|
7239
|
+
*/
|
|
7240
|
+
if (window.__stx_reactive && window.__stx_reactive.initScope) {
|
|
7241
|
+
document.querySelectorAll('[data-stx-xdata]').forEach(function(el) {
|
|
7242
|
+
var scopeId = el.getAttribute('data-stx-scope');
|
|
7243
|
+
if (scopeId && window.stx._scopes && window.stx._scopes[scopeId]) return;
|
|
7244
|
+
if (el.__stx_scope_initialized) return;
|
|
7245
|
+
|
|
7246
|
+
var xdata = el.getAttribute('data-stx-xdata');
|
|
7247
|
+
if (!xdata) return;
|
|
7248
|
+
|
|
7249
|
+
if (!scopeId) {
|
|
7250
|
+
scopeId = '__stx_scope_island_' + (++window.stx._scopeCounter);
|
|
7251
|
+
el.setAttribute('data-stx-scope', scopeId);
|
|
7252
|
+
}
|
|
7253
|
+
|
|
7254
|
+
el.__stx_scope_initialized = true;
|
|
7255
|
+
|
|
7256
|
+
try {
|
|
7257
|
+
window.__stx_reactive.initScope(el, xdata, [], {}, el.getAttribute('data-stx-xinit') || null);
|
|
7258
|
+
}
|
|
7259
|
+
catch (e) {
|
|
7260
|
+
console.error('[stx] island scope init error:', e);
|
|
7261
|
+
}
|
|
7262
|
+
});
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7159
7265
|
// Process mount queue (from stx.mount() calls during loading) only after
|
|
7160
7266
|
// caller setup is available for Vue-style projected slot ownership.
|
|
7161
7267
|
mountQueue.forEach(function(fn) { fn(); });
|
|
@@ -7520,7 +7626,11 @@ catch (e) {
|
|
|
7520
7626
|
var walker = document.createTreeWalker(root, showText, null);
|
|
7521
7627
|
var node;
|
|
7522
7628
|
while ((node = walker.nextNode())) {
|
|
7523
|
-
|
|
7629
|
+
// Browsers expose Text.nodeValue as a string, while lightweight DOM and
|
|
7630
|
+
// native embedding shims can preserve an assigned number. Diagnostics
|
|
7631
|
+
// must normalize the boundary instead of aborting the entire audit on a
|
|
7632
|
+
// missing String#indexOf method.
|
|
7633
|
+
var text = node.nodeValue == null ? '' : String(node.nodeValue);
|
|
7524
7634
|
if (text.indexOf('{{') === -1 || text.indexOf('}}') === -1) continue;
|
|
7525
7635
|
if (auditSkipped(node)) continue;
|
|
7526
7636
|
found.push(text.trim().slice(0, 80));
|
|
@@ -7776,8 +7886,14 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
7776
7886
|
scopeId = '__stx_scope_spa_' + (++window.stx._scopeCounter);
|
|
7777
7887
|
el.setAttribute('data-stx-scope', scopeId);
|
|
7778
7888
|
}
|
|
7889
|
+
// The init expression, which used to be dropped here. A scope
|
|
7890
|
+
// re-initialized with its data and none of its setup renders every
|
|
7891
|
+
// binding at its initial value and starts nothing - a live region
|
|
7892
|
+
// showing its empty state forever, which is indistinguishable from one
|
|
7893
|
+
// where nothing has happened yet.
|
|
7894
|
+
var xinit = el.getAttribute('data-stx-xinit');
|
|
7779
7895
|
console.log('[stx:load] initializing x-data scope:', scopeId, xdata.substring(0, 40));
|
|
7780
|
-
window.__stx_reactive.initScope(el, xdata, [], {}, null);
|
|
7896
|
+
window.__stx_reactive.initScope(el, xdata, [], {}, xinit || null);
|
|
7781
7897
|
});
|
|
7782
7898
|
}
|
|
7783
7899
|
|
|
@@ -7868,8 +7984,10 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
7868
7984
|
if (el.nodeType !== Node.ELEMENT_NODE) return;
|
|
7869
7985
|
// Skip scoped elements - they were already processed
|
|
7870
7986
|
if (el.hasAttribute && el.hasAttribute('data-stx-scope')) return;
|
|
7871
|
-
//
|
|
7872
|
-
|
|
7987
|
+
// Inactive chain members are owned by bindIfChain. Use explicit branch
|
|
7988
|
+
// state because an active nested branch can live under a detached outer
|
|
7989
|
+
// conditional while its reactive effects continue to run.
|
|
7990
|
+
if (el.__stx_chain_member && !el.__stx_chain_active) return;
|
|
7873
7991
|
// Process this element's directives without recursing into children
|
|
7874
7992
|
// (we'll handle children manually to skip scoped ones)
|
|
7875
7993
|
const hasFor = el.hasAttribute && (el.hasAttribute('@for') || el.hasAttribute(':for') || el.hasAttribute('x-for'));
|