@stacksjs/stx 0.2.153 → 0.2.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation.js +6 -6
- package/dist/app-shell.js +1 -27
- package/dist/build-message.d.ts +10 -0
- package/dist/build-message.js +26 -0
- package/dist/builtins/attrs.js +6 -1
- package/dist/cli.js +1481 -1231
- package/dist/client-script-bundler.js +2 -4
- package/dist/client-script.d.ts +29 -0
- package/dist/client-script.js +140 -95
- package/dist/color-mode-boot.d.ts +31 -0
- package/dist/color-mode-boot.js +41 -5
- package/dist/component-renderer.js +10 -21
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/use-clipboard.d.ts +1 -5
- package/dist/composables/use-clipboard.js +57 -46
- package/dist/composables/use-color-mode.js +13 -5
- package/dist/composables/use-router.d.ts +28 -11
- package/dist/composables/use-router.js +63 -9
- package/dist/config.js +22 -7
- package/dist/craft.js +1029 -789
- package/dist/dev-server/crosswind.js +8 -6
- package/dist/dev-server/serve-app.js +4 -2
- package/dist/dev-server/serve-markdown.js +4 -1
- package/dist/dev-server/serve-multi.js +32 -20
- package/dist/document-shell.d.ts +65 -0
- package/dist/document-shell.js +59 -12
- package/dist/dynamic-components.js +123 -4
- package/dist/events.d.ts +26 -49
- package/dist/events.js +23 -9
- package/dist/expressions.d.ts +1 -1
- package/dist/expressions.js +3 -3
- package/dist/framework-composables.d.ts +41 -0
- package/dist/framework-composables.js +115 -0
- package/dist/hot-reload.js +8 -2
- package/dist/includes.js +2 -2
- package/dist/isolated-build.d.ts +12 -0
- package/dist/isolated-build.js +18 -0
- package/dist/plugin-system.d.ts +1 -1
- package/dist/process.js +40 -10
- package/dist/production-server.js +4 -1
- package/dist/prop-sentinels.d.ts +31 -0
- package/dist/prop-sentinels.js +7 -0
- package/dist/pwa.js +966 -726
- package/dist/reactive.js +1 -1
- package/dist/reactivity.d.ts +0 -15
- package/dist/reactivity.js +15 -6
- package/dist/runtime-globals.d.ts +40 -2
- package/dist/runtime-globals.js +90 -0
- package/dist/runtime-injection.d.ts +51 -0
- package/dist/runtime-injection.js +13 -0
- package/dist/script-emit.d.ts +20 -0
- package/dist/script-emit.js +11 -0
- package/dist/script-validation.js +12 -7
- package/dist/signal-processing.js +25 -40
- package/dist/signals-api.d.ts +30 -0
- package/dist/signals-api.js +18 -0
- package/dist/signals.js +258 -47
- package/dist/site-builder/build.js +21 -7
- package/dist/site-builder/seo.js +19 -10
- package/dist/site-builder/theme.js +5 -4
- package/dist/site-builder/types.d.ts +1 -0
- package/dist/ssg.js +986 -746
- package/dist/state-management.js +4 -1
- package/dist/store-loader.d.ts +0 -11
- package/dist/store-loader.js +27 -3
- package/dist/strict-lint.d.ts +9 -6
- package/dist/strict-lint.js +2 -21
- package/dist/stx.d.ts +111 -16
- package/dist/unresolved-identifiers.d.ts +50 -0
- package/dist/unresolved-identifiers.js +146 -0
- package/dist/utils.js +3 -3
- package/dist/variable-extractor.js +17 -1
- package/dist/x-element.js +1 -1
- package/package.json +5 -5
- package/stx.d.ts +111 -16
package/dist/signals.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { state, derived, effect, batch, onMount, onDestroy, isSignal, isDerived, untrack, peek } from "./signals-api.js";
|
|
2
|
+
import { runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
|
|
2
3
|
|
|
3
4
|
export * from "./signals-api.js";
|
|
4
5
|
export function generateSignalsRuntime() {
|
|
@@ -689,6 +690,12 @@ catch (e) { console.warn('[stx] dispose error:', e); } });
|
|
|
689
690
|
return typeof v === 'function' && v._isSignal === true;
|
|
690
691
|
}
|
|
691
692
|
|
|
693
|
+
// The derived brand is set at creation; this predicate was declared and
|
|
694
|
+
// auto-imported for years without ever existing here (#1804).
|
|
695
|
+
function isDerived(v) {
|
|
696
|
+
return typeof v === 'function' && v._isDerived === true;
|
|
697
|
+
}
|
|
698
|
+
|
|
692
699
|
function untrack(v) {
|
|
693
700
|
return isSignal(v) || (typeof v === 'function' && v._isDerived) ? v() : v;
|
|
694
701
|
}
|
|
@@ -873,7 +880,13 @@ finally {
|
|
|
873
880
|
|
|
874
881
|
function useFetch(urlOrFn, options = {}) {
|
|
875
882
|
const data = state(options.initialData ?? null);
|
|
876
|
-
|
|
883
|
+
// Starts false when nothing will be requested. It used to start true
|
|
884
|
+
// unconditionally while the only thing clearing it was the fetch that
|
|
885
|
+
// immediate:false suppresses \u2014 so the documented way to declare a deferred
|
|
886
|
+
// request produced a composable stuck loading forever, which is exactly the
|
|
887
|
+
// case where a template is most likely driving a spinner off it (#1818).
|
|
888
|
+
const immediate = options.immediate !== false;
|
|
889
|
+
const loading = state(immediate);
|
|
877
890
|
const error = state(null);
|
|
878
891
|
if (options.suspense) registerSuspense(loading, error);
|
|
879
892
|
|
|
@@ -927,20 +940,27 @@ finally {
|
|
|
927
940
|
}
|
|
928
941
|
};
|
|
929
942
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// If urlOrFn is a function, watch for changes and refetch
|
|
943
|
+
// A reactive URL drives its own fetching: effect() runs eagerly on creation,
|
|
944
|
+
// so the first run IS the mount fetch. Registering onMount as well issued
|
|
945
|
+
// two requests for every function URL (#1818).
|
|
936
946
|
if (typeof urlOrFn === 'function') {
|
|
947
|
+
var firstUrlRun = true;
|
|
937
948
|
effect(() => {
|
|
938
949
|
const url = urlOrFn();
|
|
939
|
-
if (url
|
|
940
|
-
|
|
950
|
+
if (!url) return;
|
|
951
|
+
// Honour immediate:false for the FIRST evaluation only. A later change
|
|
952
|
+
// to the URL is a new request the caller asked for by changing it, not
|
|
953
|
+
// the initial one they deferred.
|
|
954
|
+
if (firstUrlRun) {
|
|
955
|
+
firstUrlRun = false;
|
|
956
|
+
if (!immediate) return;
|
|
941
957
|
}
|
|
958
|
+
fetchData();
|
|
942
959
|
});
|
|
943
960
|
}
|
|
961
|
+
else if (immediate) {
|
|
962
|
+
onMount(fetchData);
|
|
963
|
+
}
|
|
944
964
|
|
|
945
965
|
return {
|
|
946
966
|
data,
|
|
@@ -973,17 +993,26 @@ finally {
|
|
|
973
993
|
// Navigation API
|
|
974
994
|
// ==========================================================================
|
|
975
995
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
996
|
+
// Second argument is an options object: { replace, reload }.
|
|
997
|
+
//
|
|
998
|
+
// It used to be a bare forceReload boolean while the shipped declaration
|
|
999
|
+
// promised { replace?: boolean } \u2014 so the one call the type system invited,
|
|
1000
|
+
// navigate(url, { replace: true }), took the truthy branch and did a full
|
|
1001
|
+
// document load that PUSHED a history entry: the opposite of replace, at the
|
|
1002
|
+
// cost of the SPA router, with no error (#1807).
|
|
1003
|
+
//
|
|
1004
|
+
// A bare boolean is still accepted and still means "full reload", because
|
|
1005
|
+
// that was the real shipped behaviour and docs and examples contain it.
|
|
1006
|
+
function navigate(url, options) {
|
|
1007
|
+
var opts = (options && typeof options === 'object') ? options : { reload: !!options };
|
|
1008
|
+
var replace = !!opts.replace;
|
|
1009
|
+
var reload = !!(opts.reload || opts.forceReload);
|
|
1010
|
+
if (!reload && window.stxRouter && typeof window.stxRouter.navigate === 'function') {
|
|
1011
|
+
window.stxRouter.navigate(url, { replace: replace });
|
|
979
1012
|
return;
|
|
980
1013
|
}
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
else {
|
|
985
|
-
window.location.href = url;
|
|
986
|
-
}
|
|
1014
|
+
if (replace) window.location.replace(url);
|
|
1015
|
+
else window.location.href = url;
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
1018
|
function goBack() { window.history.back(); }
|
|
@@ -1038,20 +1067,66 @@ else {
|
|
|
1038
1067
|
};
|
|
1039
1068
|
window.addEventListener('popstate', syncFromUrl);
|
|
1040
1069
|
window.addEventListener('stx:navigate', syncFromUrl);
|
|
1070
|
+
// Own-property reads only. The backing object comes from Object.fromEntries,
|
|
1071
|
+
// so it inherits Object.prototype \u2014 an unguarded params()[key] returned a
|
|
1072
|
+
// FUNCTION for 'toString', 'constructor', 'valueOf' and friends, which is
|
|
1073
|
+
// neither a string nor undefined and matches no declared return type.
|
|
1074
|
+
var own = function(key) {
|
|
1075
|
+
var current = params();
|
|
1076
|
+
return Object.prototype.hasOwnProperty.call(current, key);
|
|
1077
|
+
};
|
|
1078
|
+
// pushState by default; replaceState when the caller asks for it (#1825).
|
|
1079
|
+
//
|
|
1080
|
+
// The canonical reason to delete a param is to CONSUME a one-shot value \u2014
|
|
1081
|
+
// an OAuth callback result, ?checkout=success, a flash token \u2014 and the
|
|
1082
|
+
// point of consuming it is that it must not survive a Back press. Under
|
|
1083
|
+
// pushState the pre-delete URL, the one still carrying the param, becomes
|
|
1084
|
+
// the previous entry, so Back replays the callback and whatever consuming
|
|
1085
|
+
// it triggered runs again.
|
|
1086
|
+
//
|
|
1087
|
+
// Default stays pushState so nothing that works today changes, and the
|
|
1088
|
+
// option is spelled { replace } to match navigate(), which already takes it.
|
|
1089
|
+
var commit = function(url, options) {
|
|
1090
|
+
// A plain truthy read of the replace property looks equivalent and is
|
|
1091
|
+
// not: it asks whether the value HAS a replace property, and every string
|
|
1092
|
+
// has String.prototype.replace \u2014 so delete(key, 'push') would have
|
|
1093
|
+
// replaced, the exact opposite of what it reads as. Requiring an object
|
|
1094
|
+
// also keeps this identical to the module implementation in
|
|
1095
|
+
// composables/use-router.ts; with optional chaining alone the two
|
|
1096
|
+
// disagreed on the empty string, which short-circuits in one and hits
|
|
1097
|
+
// String.prototype.replace in the other.
|
|
1098
|
+
//
|
|
1099
|
+
// NB: no backticks anywhere in this block. This runtime is assembled as a
|
|
1100
|
+
// template literal, so a backtick in a COMMENT ends the string and the
|
|
1101
|
+
// rest of the runtime is parsed as code.
|
|
1102
|
+
var replace = !!(options && typeof options === 'object' && options.replace);
|
|
1103
|
+
if (replace)
|
|
1104
|
+
window.history.replaceState({}, '', url);
|
|
1105
|
+
else
|
|
1106
|
+
window.history.pushState({}, '', url);
|
|
1107
|
+
syncFromUrl();
|
|
1108
|
+
};
|
|
1041
1109
|
return {
|
|
1042
1110
|
data: params,
|
|
1043
|
-
get: function(key) { return params()[key]; },
|
|
1044
|
-
|
|
1111
|
+
get: function(key) { return own(key) ? params()[key] : undefined; },
|
|
1112
|
+
has: own,
|
|
1113
|
+
set: function(key, value, options) {
|
|
1045
1114
|
var url = new URL(window.location.href);
|
|
1046
1115
|
url.searchParams.set(key, value);
|
|
1047
|
-
|
|
1048
|
-
syncFromUrl();
|
|
1116
|
+
commit(url, options);
|
|
1049
1117
|
},
|
|
1050
|
-
|
|
1118
|
+
// delete and has were DECLARED but absent, so the compiler endorsed calls
|
|
1119
|
+
// that threw in the browser; setAll and data were present but undeclared
|
|
1120
|
+
// (#1806). Deleting writes history the same way set does, for consistency.
|
|
1121
|
+
'delete': function(key, options) {
|
|
1122
|
+
var url = new URL(window.location.href);
|
|
1123
|
+
url.searchParams['delete'](key);
|
|
1124
|
+
commit(url, options);
|
|
1125
|
+
},
|
|
1126
|
+
setAll: function(obj, options) {
|
|
1051
1127
|
var url = new URL(window.location.href);
|
|
1052
1128
|
Object.keys(obj).forEach(function(k) { url.searchParams.set(k, obj[k]); });
|
|
1053
|
-
|
|
1054
|
-
syncFromUrl();
|
|
1129
|
+
commit(url, options);
|
|
1055
1130
|
}
|
|
1056
1131
|
};
|
|
1057
1132
|
}
|
|
@@ -1139,11 +1214,16 @@ finally {
|
|
|
1139
1214
|
onMount(fetchData);
|
|
1140
1215
|
}
|
|
1141
1216
|
|
|
1142
|
-
// refetchOnFocus
|
|
1217
|
+
// refetchOnFocus \u2014 removed on destroy, like refetchInterval right below.
|
|
1218
|
+
// It used to add a listener and never remove it, so every instance leaked
|
|
1219
|
+
// one for the life of the page and each SPA navigation added more; every
|
|
1220
|
+
// stale listener still fired, refetching for components long gone (#1818).
|
|
1143
1221
|
if (options.refetchOnFocus) {
|
|
1144
|
-
|
|
1222
|
+
var onVisibility = function() {
|
|
1145
1223
|
if (!document.hidden) fetchData();
|
|
1146
|
-
}
|
|
1224
|
+
};
|
|
1225
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
1226
|
+
onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
|
|
1147
1227
|
}
|
|
1148
1228
|
|
|
1149
1229
|
// refetchInterval
|
|
@@ -2189,7 +2269,7 @@ catch (e2) {
|
|
|
2189
2269
|
// :attr still works for backward compat but is reserved for structural directives.
|
|
2190
2270
|
// x-text, x-html, x-model, x-show, x-if, x-for, x-cloak, x-ref, x-data are
|
|
2191
2271
|
// handled by their own code paths below \u2014 exclude them here.
|
|
2192
|
-
var X_HANDLED = {
|
|
2272
|
+
var X_HANDLED = ${runtimeHandledXAttrsLiteral()};
|
|
2193
2273
|
if (name.startsWith('@bind:') || name.startsWith('x-bind:')
|
|
2194
2274
|
|| (name.startsWith(':') && !name.startsWith('::') && !DIRECTIVE_NAMES[name.slice(1).split('.')[0]] && !EVENT_RE.test(name.slice(1)))
|
|
2195
2275
|
|| (name.startsWith('x-') && !X_HANDLED[name.split('.')[0]] && !X_HANDLED[name])) {
|
|
@@ -2657,7 +2737,11 @@ catch (e) {
|
|
|
2657
2737
|
});
|
|
2658
2738
|
}
|
|
2659
2739
|
else if (tag === 'select') {
|
|
2660
|
-
|
|
2740
|
+
// Same equality guard as the text branch. Harmless here \u2014 a select has
|
|
2741
|
+
// no caret \u2014 but an unconditional write is still a needless DOM mutation
|
|
2742
|
+
// on every signal change, and the inconsistency invites the text-branch
|
|
2743
|
+
// bug being reintroduced by copying this one (#1799).
|
|
2744
|
+
var syncSelect = () => { var next = getValue() ?? ''; if (el.value !== next) el.value = next; };
|
|
2661
2745
|
el.__stx_model_sync = syncSelect;
|
|
2662
2746
|
effect(syncSelect);
|
|
2663
2747
|
if (!el.__stx_model_observer && typeof window.MutationObserver !== 'undefined') {
|
|
@@ -2677,7 +2761,41 @@ catch (e) {
|
|
|
2677
2761
|
el.addEventListener('change', () => setValue(coerceValue(el.value)));
|
|
2678
2762
|
}
|
|
2679
2763
|
else {
|
|
2680
|
-
|
|
2764
|
+
// Writing el.value moves the text cursor to the END of the control, and
|
|
2765
|
+
// typing forms a loop: input -> setValue -> the signal changes -> this
|
|
2766
|
+
// effect re-runs -> the write. So every keystroke reset the caret
|
|
2767
|
+
// (stacksjs/stx#1799).
|
|
2768
|
+
//
|
|
2769
|
+
// Invisible while appending at the end of a field, which is why a login
|
|
2770
|
+
// form with x-model felt fine and a textarea people actually edit did
|
|
2771
|
+
// not. Worse inside :for, where key-based reuse updates the item signal
|
|
2772
|
+
// on reuse (#1669) and re-evaluates the reused row's bindings on every
|
|
2773
|
+
// character.
|
|
2774
|
+
//
|
|
2775
|
+
// The equality guard alone fixes typing, because the value this effect
|
|
2776
|
+
// computes IS the value just typed. The offset is preserved as well for
|
|
2777
|
+
// the case the guard cannot cover: a value that legitimately changed
|
|
2778
|
+
// from elsewhere while the field is focused \u2014 a formatter rewriting
|
|
2779
|
+
// input, a remote sync.
|
|
2780
|
+
effect(() => {
|
|
2781
|
+
var next = getValue() ?? '';
|
|
2782
|
+
if (el.value === next) return;
|
|
2783
|
+
var active = typeof document !== 'undefined' && document.activeElement === el;
|
|
2784
|
+
var start = null;
|
|
2785
|
+
var end = null;
|
|
2786
|
+
if (active) {
|
|
2787
|
+
// selectionStart throws on input types that do not support
|
|
2788
|
+
// selection (number, email, date), so it cannot be assumed present.
|
|
2789
|
+
try { start = el.selectionStart; end = el.selectionEnd; }
|
|
2790
|
+
catch (e) { start = null; end = null; }
|
|
2791
|
+
}
|
|
2792
|
+
el.value = next;
|
|
2793
|
+
if (active && start !== null && typeof el.setSelectionRange === 'function') {
|
|
2794
|
+
// Clamp: the incoming value may be shorter than the old offset.
|
|
2795
|
+
try { el.setSelectionRange(Math.min(start, next.length), Math.min(end == null ? start : end, next.length)); }
|
|
2796
|
+
catch (e) { /* type does not support selection */ }
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2681
2799
|
el.addEventListener(modifiers.includes('lazy') ? 'change' : 'input', () => setValue(coerceValue(el.value)));
|
|
2682
2800
|
}
|
|
2683
2801
|
|
|
@@ -3944,9 +4062,6 @@ catch (e2) {
|
|
|
3944
4062
|
const fullScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
3945
4063
|
const directSignal = fullScope[expr];
|
|
3946
4064
|
|
|
3947
|
-
if (directSignal) {
|
|
3948
|
-
console.log('[stx] bindIf direct signal for :if=' + expr, 'signal identity:', directSignal === componentScope[expr] ? 'SAME' : 'DIFFERENT', 'signal():', directSignal());
|
|
3949
|
-
}
|
|
3950
4065
|
effect(() => {
|
|
3951
4066
|
var value;
|
|
3952
4067
|
if (directSignal && (directSignal._isSignal || directSignal._isDerived)) {
|
|
@@ -4026,6 +4141,11 @@ else if (!value && isInserted) {
|
|
|
4026
4141
|
// if) fire too. The if attribute was already removed, so no recursion.
|
|
4027
4142
|
if (value && isInserted && !childrenProcessed) {
|
|
4028
4143
|
childrenProcessed = true;
|
|
4144
|
+
// Until this deferred pass runs, the shown subtree still holds literal
|
|
4145
|
+
// {{ }}. Flag it so the hydration audit doesn't report those as a
|
|
4146
|
+
// binding miss in the gap before the setTimeout fires \u2014 a :for nested
|
|
4147
|
+
// in a :if binds a macrotask after the synchronous stx:load audit (#1773).
|
|
4148
|
+
el.__stx_if_pending = true;
|
|
4029
4149
|
setTimeout(function() {
|
|
4030
4150
|
var childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
4031
4151
|
// A false single-element conditional is detached before the
|
|
@@ -4043,6 +4163,8 @@ else if (!value && isInserted) {
|
|
|
4043
4163
|
// :if children still have x-cloak and stay hidden.
|
|
4044
4164
|
el.removeAttribute('x-cloak');
|
|
4045
4165
|
el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
|
|
4166
|
+
// Deferred bind complete \u2014 the audit may inspect this subtree now.
|
|
4167
|
+
el.__stx_if_pending = false;
|
|
4046
4168
|
}, 0);
|
|
4047
4169
|
}
|
|
4048
4170
|
}
|
|
@@ -4078,10 +4200,25 @@ else if (!value && isInserted) {
|
|
|
4078
4200
|
|
|
4079
4201
|
// provide() \u2014 share values across components (like Vue's provide/inject)
|
|
4080
4202
|
// Usage: provide('Modal', Modal) \u2014 makes Modal accessible in all components
|
|
4203
|
+
//
|
|
4204
|
+
// inject() was auto-imported but implemented nowhere, so half of the pair
|
|
4205
|
+
// worked: provide() succeeded and the failure surfaced only in the consumer
|
|
4206
|
+
// (#1804). Values are recorded in a private registry as well as on window,
|
|
4207
|
+
// because the global write collides with native properties \u2014 provide('name',
|
|
4208
|
+
// x) lands on window.name, which the browser coerces to a string.
|
|
4209
|
+
var __stxProvided = Object.create(null);
|
|
4210
|
+
|
|
4081
4211
|
function provide(name, value) {
|
|
4212
|
+
__stxProvided[name] = value;
|
|
4082
4213
|
window[name] = value;
|
|
4083
4214
|
}
|
|
4084
4215
|
|
|
4216
|
+
function inject(name, defaultValue) {
|
|
4217
|
+
if (name in __stxProvided) return __stxProvided[name];
|
|
4218
|
+
if (name in window) return window[name];
|
|
4219
|
+
return defaultValue;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4085
4222
|
// ==========================================================================
|
|
4086
4223
|
// Timer & Utility Composables
|
|
4087
4224
|
// ==========================================================================
|
|
@@ -4363,6 +4500,33 @@ else if (timer === null) {
|
|
|
4363
4500
|
});
|
|
4364
4501
|
}
|
|
4365
4502
|
|
|
4503
|
+
// Watch several sources at once, calling back with (newValues, oldValues).
|
|
4504
|
+
// Declared and auto-imported but never implemented here (#1804). NOT an alias
|
|
4505
|
+
// of $watch, which spreads the values and passes no oldValues \u2014 callers
|
|
4506
|
+
// written against the declared contract would silently receive the wrong
|
|
4507
|
+
// arguments. Sources may be signals, Vue-style refs, or plain values.
|
|
4508
|
+
function watchMultiple(sources, callback, options) {
|
|
4509
|
+
options = options || {};
|
|
4510
|
+
var read = function() {
|
|
4511
|
+
return sources.map(function(s) {
|
|
4512
|
+
if (typeof s === 'function') return s();
|
|
4513
|
+
if (s && typeof s === 'object' && 'value' in s) return s.value;
|
|
4514
|
+
return s;
|
|
4515
|
+
});
|
|
4516
|
+
};
|
|
4517
|
+
var oldValues = read();
|
|
4518
|
+
var first = true;
|
|
4519
|
+
return effect(function() {
|
|
4520
|
+
var newValues = read();
|
|
4521
|
+
if (first) {
|
|
4522
|
+
first = false;
|
|
4523
|
+
if (!options.immediate) return;
|
|
4524
|
+
}
|
|
4525
|
+
callback(newValues, oldValues);
|
|
4526
|
+
oldValues = newValues;
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4366
4530
|
function useAsync(fn, options) {
|
|
4367
4531
|
options = options || {};
|
|
4368
4532
|
var asyncState = 'idle';
|
|
@@ -4409,16 +4573,31 @@ else if (timer === null) {
|
|
|
4409
4573
|
try { boot = window.__STX_COLOR_MODE__ || null; } catch (e) {}
|
|
4410
4574
|
// Explicit-undefined checks rather than ||, so an explicit darkClass:null
|
|
4411
4575
|
// or attribute:null opts out instead of falling through to the default.
|
|
4576
|
+
// An explicit null means "do not manage this", from the call site OR from
|
|
4577
|
+
// app.colorMode. The boot payload is generated by stx and always carries
|
|
4578
|
+
// every key, so absent and explicitly-null ARE distinguishable there, and
|
|
4579
|
+
// rejecting null made the boot global the one path that could not say it
|
|
4580
|
+
// (#1813). The asymmetry hid because it only bites where the fallback is
|
|
4581
|
+
// non-null: attribute:null looked fine, darkClass:null silently became
|
|
4582
|
+
// 'dark' \u2014 so opting out of the class required repeating the option at
|
|
4583
|
+
// every call site, defeating the single-source arrangement the global
|
|
4584
|
+
// exists for.
|
|
4412
4585
|
function pick(a, b, fallback) {
|
|
4413
4586
|
if (a !== undefined) return a;
|
|
4414
|
-
if (b !== undefined
|
|
4587
|
+
if (b !== undefined) return b;
|
|
4415
4588
|
return fallback;
|
|
4416
4589
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4590
|
+
// Read through a helper rather than a short-circuit on boot itself: that
|
|
4591
|
+
// expression yields undefined for an undefined boot but NULL for a null
|
|
4592
|
+
// one, which pick would now honour as an explicit opt-out.
|
|
4593
|
+
function fromBoot(key) {
|
|
4594
|
+
return (boot && typeof boot === 'object') ? boot[key] : undefined;
|
|
4595
|
+
}
|
|
4596
|
+
var storageKey = pick(options.storageKey, fromBoot('storageKey'), 'stx-color-mode');
|
|
4597
|
+
var initialMode = pick(options.initialMode, fromBoot('initialMode'), 'auto');
|
|
4598
|
+
var darkClass = pick(options.darkClass, fromBoot('darkClass'), 'dark');
|
|
4599
|
+
var attribute = pick(options.attribute, fromBoot('attribute'), null);
|
|
4600
|
+
var autoValueOption = pick(options.autoValue, fromBoot('autoValue'), undefined);
|
|
4422
4601
|
var disableTransitions = options.disableTransitions !== false;
|
|
4423
4602
|
var resolved = 'light';
|
|
4424
4603
|
var listeners = [];
|
|
@@ -5366,16 +5545,25 @@ catch (e) {} }
|
|
|
5366
5545
|
// Component mount system
|
|
5367
5546
|
var mountQueue = [];
|
|
5368
5547
|
|
|
5369
|
-
window.stx
|
|
5548
|
+
// MERGE, never replace (#1804). Three client bundles assign window.stx and
|
|
5549
|
+
// the last one used to erase the others. Object.assign also preserves the
|
|
5550
|
+
// object IDENTITY, which matters because the generated store runtime captures
|
|
5551
|
+
// window.stx in a closure variable \u2014 replacing the object leaves that closure
|
|
5552
|
+
// calling a detached one.
|
|
5553
|
+
window.stx = Object.assign(window.stx || {}, {
|
|
5370
5554
|
hydrate: hydrateSubtree,
|
|
5371
5555
|
state,
|
|
5372
5556
|
derived,
|
|
5373
5557
|
effect,
|
|
5374
5558
|
batch,
|
|
5375
5559
|
isSignal,
|
|
5560
|
+
isDerived,
|
|
5376
5561
|
untrack,
|
|
5377
5562
|
peek,
|
|
5563
|
+
inject,
|
|
5564
|
+
watchMultiple,
|
|
5378
5565
|
onMount,
|
|
5566
|
+
onBeforeMount: onMount,
|
|
5379
5567
|
onDestroy,
|
|
5380
5568
|
onMounted: onMount,
|
|
5381
5569
|
onBeforeUnmount: onDestroy,
|
|
@@ -5492,6 +5680,11 @@ catch (e) {} }
|
|
|
5492
5680
|
defineSlots: function() {
|
|
5493
5681
|
return window.__STX_CURRENT_SLOTS__ || {};
|
|
5494
5682
|
},
|
|
5683
|
+
// Vue spells the same lookup useSlots(); it was auto-imported with no
|
|
5684
|
+
// implementation behind it (#1804).
|
|
5685
|
+
useSlots: function() {
|
|
5686
|
+
return window.__STX_CURRENT_SLOTS__ || {};
|
|
5687
|
+
},
|
|
5495
5688
|
|
|
5496
5689
|
_mountCallbacks: mountCallbacks,
|
|
5497
5690
|
_destroyCallbacks: destroyCallbacks,
|
|
@@ -5908,7 +6101,7 @@ else {
|
|
|
5908
6101
|
doMount();
|
|
5909
6102
|
}
|
|
5910
6103
|
}
|
|
5911
|
-
};
|
|
6104
|
+
});
|
|
5912
6105
|
|
|
5913
6106
|
// Also expose globally for convenience
|
|
5914
6107
|
window.state = state;
|
|
@@ -6147,10 +6340,20 @@ else {
|
|
|
6147
6340
|
const setupName = el.getAttribute('data-stx');
|
|
6148
6341
|
console.log('[DOMContentLoaded] data-stx:', setupName, 'el:', el.tagName, 'fn exists:', !!(setupName && window[setupName]));
|
|
6149
6342
|
if (setupName && window[setupName]) {
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6343
|
+
// Contained, like the two sibling setup call sites. An uncaught throw
|
|
6344
|
+
// here aborted the whole forEach and everything after it in this
|
|
6345
|
+
// handler, so ONE bad name in ONE page's setup left every root on the
|
|
6346
|
+
// page unhydrated \u2014 the user sees raw {{ }}, and the console names the
|
|
6347
|
+
// identifier without saying it cost them the document (#1805).
|
|
6348
|
+
try {
|
|
6349
|
+
const result = window[setupName]();
|
|
6350
|
+
console.log('[DOMContentLoaded] setup returned:', result ? Object.keys(result).slice(0, 10) : 'null');
|
|
6351
|
+
if (typeof result === 'object' && result !== null) {
|
|
6352
|
+
Object.assign(componentScope, result);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
catch (e) {
|
|
6356
|
+
console.error('[stx] page setup "' + setupName + '" threw; this root will not hydrate:', e);
|
|
6154
6357
|
}
|
|
6155
6358
|
}
|
|
6156
6359
|
});
|
|
@@ -6242,7 +6445,7 @@ else {
|
|
|
6242
6445
|
componentScope = { ...componentScope, ...scopeVars };
|
|
6243
6446
|
}
|
|
6244
6447
|
|
|
6245
|
-
console.log('[stx] calling processElement on scope:', scopeId, 'componentScope
|
|
6448
|
+
console.log('[stx] calling processElement on scope:', scopeId, 'componentScope keys:', Object.keys(componentScope).length, 'scopeVars keys:', Object.keys(scopeVars).length);
|
|
6246
6449
|
var disposeEffects = trackEffects(function() { processElement(el); });
|
|
6247
6450
|
el.__stx_disposers = disposeEffects;
|
|
6248
6451
|
console.log('[stx] scope processed OK:', scopeId);
|
|
@@ -6500,6 +6703,14 @@ catch (e) {
|
|
|
6500
6703
|
if (p.nodeType !== Node.ELEMENT_NODE) continue;
|
|
6501
6704
|
if (AUDIT_SKIP_TAGS[p.tagName]) return true;
|
|
6502
6705
|
if (p.hasAttribute && p.hasAttribute('data-stx-ignore')) return true;
|
|
6706
|
+
// A :if whose deferred child-binding pass hasn't run yet legitimately holds
|
|
6707
|
+
// literal {{ }} that binds a macrotask later \u2014 not a hydration miss (#1773).
|
|
6708
|
+
if (p.__stx_if_pending) return true;
|
|
6709
|
+
// A deferred island (client=visible|idle|\u2026) is unhydrated ON PURPOSE until
|
|
6710
|
+
// its trigger fires, so its {{ }} legitimately stay literal in the meantime
|
|
6711
|
+
// \u2014 not a miss (#1746). The stranded-scope pass already skips stx-hydrate;
|
|
6712
|
+
// the literal sweep must too.
|
|
6713
|
+
if (p.hasAttribute && p.hasAttribute('stx-hydrate') && !p.__stx_hydrated) return true;
|
|
6503
6714
|
}
|
|
6504
6715
|
return false;
|
|
6505
6716
|
}
|
|
@@ -19,6 +19,7 @@ import { generateSitemap } from "./sitemap.js";
|
|
|
19
19
|
import { generateRobots } from "./robots.js";
|
|
20
20
|
import { injectRouterScript } from "./router.js";
|
|
21
21
|
import { injectThemeBootstrap } from "./theme.js";
|
|
22
|
+
import { buildIsolatingFailures } from "../isolated-build.js";
|
|
22
23
|
import {
|
|
23
24
|
applyTranslations,
|
|
24
25
|
buildAlternateLinks,
|
|
@@ -31,18 +32,31 @@ export async function buildStaticSite(options) {
|
|
|
31
32
|
if (!options.noClean)
|
|
32
33
|
rmSync(outDir, { recursive: !0, force: !0 });
|
|
33
34
|
mkdirSync(outDir, { recursive: !0 });
|
|
34
|
-
const glob = new Glob(`${pagesDir}/**/*.stx`),
|
|
35
|
+
const glob = new Glob(`${pagesDir}/**/*.stx`), discovered = await Array.fromAsync(glob.scan(".")), excludeDirs = options.excludeDirs ?? ["layouts", "partials", "components"], excluded = [], entrypoints = discovered.filter((file) => {
|
|
36
|
+
const rel = file.slice(pagesDir.length).replace(/^[\\/]+/, "");
|
|
37
|
+
if (rel.split(/[\\/]/).slice(0, -1).some((seg) => excludeDirs.includes(seg))) {
|
|
38
|
+
excluded.push(file);
|
|
39
|
+
return !1;
|
|
40
|
+
}
|
|
41
|
+
return !rel.split(/[\\/]/).some((seg) => seg.startsWith("_"));
|
|
42
|
+
});
|
|
43
|
+
if (excluded.length > 0)
|
|
44
|
+
console.warn(`[stx] ${excluded.length} file(s) under ${pagesDir}/ skipped as non-pages: ${excludeDirs.join(", ")}`);
|
|
35
45
|
if (entrypoints.length === 0)
|
|
36
46
|
throw Error(`No .stx files found in ${pagesDir}/`);
|
|
37
|
-
const
|
|
38
|
-
entrypoints,
|
|
47
|
+
const isolated = await buildIsolatingFailures(entrypoints, (subset) => Bun.build({
|
|
48
|
+
entrypoints: subset,
|
|
39
49
|
outdir: outDir,
|
|
40
50
|
plugins: [stxPlugin()],
|
|
41
51
|
naming: { entry: "[name].[ext]" }
|
|
42
|
-
});
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
}));
|
|
53
|
+
if (isolated.failed.length > 0) {
|
|
54
|
+
for (const { entrypoint, error } of isolated.failed)
|
|
55
|
+
console.error(`[stx] failed to build ${entrypoint}:
|
|
56
|
+
${error}`);
|
|
57
|
+
if (isolated.succeeded.length === 0)
|
|
58
|
+
throw Error(`Build failed: none of the ${entrypoints.length} page(s) could be built`);
|
|
59
|
+
console.error(`[stx] ${isolated.failed.length} of ${entrypoints.length} page(s) skipped; the rest were built.`);
|
|
46
60
|
}
|
|
47
61
|
pruneEmptyJsChunks(outDir);
|
|
48
62
|
const htmlFiles = listFiles(outDir).filter((f) => f.endsWith(".html")), i18n = resolveI18n(options, process.cwd()), knownPaths = new Set(htmlFiles.map(pathFromFile));
|
package/dist/site-builder/seo.js
CHANGED
|
@@ -18,30 +18,39 @@ export function injectSeo(html, site, page = {}, pagePath = "/") {
|
|
|
18
18
|
html = html.replace(stxAutoBlockRe, "");
|
|
19
19
|
html = html.replace(/<!--\s*SEO\s*-->[\s\S]*?<!--\s*\/SEO\s*-->\s*/g, "");
|
|
20
20
|
html = html.replace(/<meta\s+name="title"\s+content="stx Project"[^>]*>\s*/g, "").replace(/<meta\s+name="description"\s+content="A website built with stx[^"]*"[^>]*>\s*/g, "").replace(/<meta\s+property="og:title"\s+content="stx Project"[^>]*>\s*/g, "").replace(/<meta\s+property="og:description"\s+content="A website built with stx[^"]*"[^>]*>\s*/g, "").replace(/<meta\s+name="twitter:title"\s+content="stx Project"[^>]*>\s*/g, "").replace(/<meta\s+name="twitter:description"\s+content="A website built with stx[^"]*"[^>]*>\s*/g, "");
|
|
21
|
-
const headEnd = html.search(/<\/head>/i), head = headEnd === -1 ? html : html.slice(0, headEnd), declared = (tag) => {
|
|
21
|
+
const headEnd = html.search(/<\/head>/i), head = headEnd === -1 ? html : html.slice(0, headEnd), PLACEHOLDER_TITLES = /^(?:stx App|stx Project|Document|Untitled)$/i, existingTitle = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(head)?.[1]?.trim(), titleIsPlaceholder = existingTitle === void 0 || existingTitle === "" || PLACEHOLDER_TITLES.test(existingTitle), declared = (tag) => {
|
|
22
22
|
if (/^<title[\s>]/i.test(tag))
|
|
23
|
-
return /<title[\s>]/i.test(head);
|
|
23
|
+
return /<title[\s>]/i.test(head) && !titleIsPlaceholder;
|
|
24
24
|
if (/rel="canonical"/i.test(tag))
|
|
25
25
|
return /<link[^>]+rel=["']canonical["']/i.test(head);
|
|
26
26
|
const key = /(?:property|name)="([^"]+)"/i.exec(tag)?.[1];
|
|
27
27
|
if (!key)
|
|
28
28
|
return !1;
|
|
29
29
|
return new RegExp(`<meta[^>]+(?:property|name)=["']${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "i").test(head);
|
|
30
|
-
}
|
|
30
|
+
};
|
|
31
|
+
let missing = tags.filter((tag) => !declared(tag));
|
|
31
32
|
if (missing.length === 0)
|
|
32
33
|
return html;
|
|
34
|
+
let output = html;
|
|
35
|
+
const titleTag = missing.find((tag) => /^<title[\s>]/i.test(tag));
|
|
36
|
+
if (titleTag && titleIsPlaceholder && /<title[^>]*>[\s\S]*?<\/title>/i.test(output)) {
|
|
37
|
+
output = output.replace(/<title[^>]*>[\s\S]*?<\/title>/i, () => titleTag);
|
|
38
|
+
missing = missing.filter((tag) => tag !== titleTag);
|
|
39
|
+
if (missing.length === 0)
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
33
42
|
const block = `<!-- SEO -->
|
|
34
43
|
${missing.join(`
|
|
35
44
|
`)}
|
|
36
|
-
<!-- /SEO
|
|
37
|
-
if (
|
|
38
|
-
return `${
|
|
39
|
-
${
|
|
40
|
-
if (/<head[^>]*>/i.test(
|
|
41
|
-
return
|
|
45
|
+
<!-- /SEO -->`, outEnd = output.search(/<\/head>/i);
|
|
46
|
+
if (outEnd !== -1)
|
|
47
|
+
return `${output.slice(0, outEnd)}${block}
|
|
48
|
+
${output.slice(outEnd)}`;
|
|
49
|
+
if (/<head[^>]*>/i.test(output))
|
|
50
|
+
return output.replace(/<head([^>]*)>/i, `<head$1>
|
|
42
51
|
${block}`);
|
|
43
52
|
return `${block}
|
|
44
|
-
${
|
|
53
|
+
${output}`;
|
|
45
54
|
}
|
|
46
55
|
function escapeAttr(s) {
|
|
47
56
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { COLOR_MODE_BOOT_MARKER } from "../color-mode-boot.js";
|
|
1
2
|
const FAVICON_FN = `window.__stxThemeIcon=function(isDark){try{var want=isDark?'dark':'light';var pick=document.querySelector('link[rel~="icon"][media*="prefers-color-scheme: '+want+'"]');if(!pick)return;var live=document.querySelector('link[rel~="icon"]:not([media])');if(!live){live=document.createElement('link');live.setAttribute('rel','icon');document.head.appendChild(live)}if(pick.type)live.setAttribute('type',pick.type);var href=pick.getAttribute('href');if(live.getAttribute('href')!==href)live.setAttribute('href',href)}catch(e){}};`, FOUC_SCRIPT = (defaultTheme, storageKey, lightColor, darkColor) => `(function(){${FAVICON_FN}function read(){try{var k=${JSON.stringify(storageKey)};var t=localStorage.getItem(k);return t===null?(${JSON.stringify(defaultTheme)}==='dark'||(${JSON.stringify(defaultTheme)}==='auto'&&window.matchMedia('(prefers-color-scheme: dark)').matches)):t==='dark'}catch(e){return ${JSON.stringify(defaultTheme)}==='dark'}}function apply(){var isDark=read();var html=document.documentElement;html.classList[isDark?'add':'remove']('dark');var color=isDark?${JSON.stringify(darkColor)}:${JSON.stringify(lightColor)};if(!document.head)return;var tc=document.querySelector('meta[name="theme-color"]:not([media])');if(!tc){tc=document.createElement('meta');tc.setAttribute('name','theme-color');tc.setAttribute('data-stx-theme-meta','1')}tc.setAttribute('content',color);if(tc!==document.head.firstChild){document.head.insertBefore(tc,document.head.firstChild)}if(window.__stxThemeIcon)window.__stxThemeIcon(isDark)}apply();window.addEventListener('stx:navigate',apply);window.addEventListener('popstate',apply)})();`, TOGGLE_HANDLER = (storageKey, lightColor, darkColor) => `document.addEventListener('click',function(e){var t=e.target&&e.target.closest&&e.target.closest('#theme-toggle');if(!t)return;var html=document.documentElement;var isDark=html.classList.toggle('dark');try{localStorage.setItem(${JSON.stringify(storageKey)},isDark?'dark':'light')}catch(e){}var tc=document.querySelector('meta[name="theme-color"]:not([media])');if(tc)tc.setAttribute('content',isDark?${JSON.stringify(darkColor)}:${JSON.stringify(lightColor)});if(window.__stxThemeIcon)window.__stxThemeIcon(isDark)});`;
|
|
2
3
|
export function injectThemeBootstrap(html, site) {
|
|
3
4
|
if (site.theme === !1)
|
|
4
5
|
return html;
|
|
5
|
-
const opts = typeof site.theme === "object" && site.theme !== null ? site.theme : {}, defaultTheme = opts.default ?? "dark", storageKey = opts.storageKey ?? "theme", lightColor = opts.colors?.light ?? "#ffffff", darkColor = opts.colors?.dark ?? "#000000";
|
|
6
|
-
if (defaultTheme === "dark") {
|
|
6
|
+
const colorModeOwnsTheme = html.includes(COLOR_MODE_BOOT_MARKER), opts = typeof site.theme === "object" && site.theme !== null ? site.theme : {}, defaultTheme = opts.default ?? "dark", storageKey = opts.storageKey ?? "theme", lightColor = opts.colors?.light ?? "#ffffff", darkColor = opts.colors?.dark ?? "#000000";
|
|
7
|
+
if (defaultTheme === "dark" && !colorModeOwnsTheme) {
|
|
7
8
|
if (!/<html\b[^>]*\bclass=/.test(html))
|
|
8
9
|
html = html.replace(/<html\b([^>]*)>/i, '<html$1 class="dark">');
|
|
9
10
|
else if (!/<html\b[^>]*\bclass="[^"]*\bdark\b/.test(html))
|
|
@@ -19,12 +20,12 @@ export function injectThemeBootstrap(html, site) {
|
|
|
19
20
|
html = html.replace(/<head\b([^>]*)>/i, `<head$1>
|
|
20
21
|
${metas}`);
|
|
21
22
|
}
|
|
22
|
-
if (!html.includes("__stxThemeGuard") && /<head\b[^>]*>/i.test(html)) {
|
|
23
|
+
if (!colorModeOwnsTheme && !html.includes("__stxThemeGuard") && /<head\b[^>]*>/i.test(html)) {
|
|
23
24
|
const guard = `<script data-stx-theme-guard="1">${FOUC_SCRIPT(defaultTheme, storageKey, lightColor, darkColor)};window.__stxThemeGuard=1;</script>`;
|
|
24
25
|
html = html.replace(/<head\b([^>]*)>/i, `<head$1>
|
|
25
26
|
${guard}`);
|
|
26
27
|
}
|
|
27
|
-
if (!html.includes("__stxThemeToggle") && /<\/body>/i.test(html)) {
|
|
28
|
+
if (!colorModeOwnsTheme && !html.includes("__stxThemeToggle") && /<\/body>/i.test(html)) {
|
|
28
29
|
const handler = `<script data-stx-theme-toggle="1">${TOGGLE_HANDLER(storageKey, lightColor, darkColor)};window.__stxThemeToggle=1;</script>`;
|
|
29
30
|
html = html.replace(/<\/body>/i, `${handler}
|
|
30
31
|
</body>`);
|