@stacksjs/stx 0.2.152 → 0.2.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation.js +6 -6
- package/dist/app-shell.js +2 -1
- package/dist/browser-composables.d.ts +8 -0
- package/dist/build-id.d.ts +49 -0
- package/dist/build-id.js +20 -0
- package/dist/build-message.d.ts +10 -0
- package/dist/build-message.js +26 -0
- package/dist/builtins/attrs.js +6 -1
- package/dist/cli.js +1743 -1255
- package/dist/client-script-bundler.js +2 -4
- package/dist/client-script.d.ts +29 -0
- package/dist/client-script.js +142 -97
- package/dist/color-mode-boot.d.ts +46 -10
- package/dist/color-mode-boot.js +53 -13
- package/dist/component-client-factories.js +3 -2
- package/dist/component-renderer.js +10 -21
- package/dist/composables/index.d.ts +8 -1
- package/dist/composables/use-clipboard.d.ts +1 -5
- package/dist/composables/use-clipboard.js +57 -46
- package/dist/composables/use-color-mode.d.ts +19 -4
- package/dist/composables/use-color-mode.js +49 -28
- package/dist/composables/use-router.d.ts +28 -11
- package/dist/composables/use-router.js +63 -9
- package/dist/composables/use-storage.d.ts +14 -15
- package/dist/composables/use-storage.js +63 -2
- package/dist/config.js +22 -7
- package/dist/craft.js +1319 -841
- package/dist/dev-server/crosswind.js +8 -6
- package/dist/dev-server/serve-app.js +13 -7
- package/dist/dev-server/serve-markdown.js +4 -1
- package/dist/dev-server/serve-multi.js +32 -20
- package/dist/dev-server/types.d.ts +8 -1
- package/dist/document-shell.d.ts +120 -0
- package/dist/document-shell.js +161 -14
- package/dist/dynamic-components.js +123 -4
- package/dist/events.d.ts +26 -49
- package/dist/events.js +23 -9
- package/dist/expressions.d.ts +1 -1
- package/dist/expressions.js +3 -3
- package/dist/framework-composables.d.ts +41 -0
- package/dist/framework-composables.js +115 -0
- package/dist/head.js +5 -1
- package/dist/hot-reload.js +8 -2
- package/dist/includes.js +4 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/isolated-build.d.ts +12 -0
- package/dist/isolated-build.js +18 -0
- package/dist/plugin-system.d.ts +1 -1
- package/dist/process.js +46 -12
- package/dist/production-builder.js +3 -1
- package/dist/production-server.js +4 -1
- package/dist/prop-sentinels.d.ts +31 -0
- package/dist/prop-sentinels.js +7 -0
- package/dist/pwa.js +1258 -780
- package/dist/reactive.js +1 -1
- package/dist/reactivity.d.ts +0 -15
- package/dist/reactivity.js +15 -6
- package/dist/runtime-globals.d.ts +40 -2
- package/dist/runtime-globals.js +90 -0
- package/dist/runtime-injection.d.ts +51 -0
- package/dist/runtime-injection.js +13 -0
- package/dist/script-emit.d.ts +20 -0
- package/dist/script-emit.js +11 -0
- package/dist/script-validation.js +12 -7
- package/dist/signal-processing.js +25 -40
- package/dist/signals-api.d.ts +30 -0
- package/dist/signals-api.js +18 -0
- package/dist/signals.js +532 -85
- package/dist/site-builder/build.js +21 -7
- package/dist/site-builder/seo.js +19 -10
- package/dist/site-builder/theme.js +5 -4
- package/dist/site-builder/types.d.ts +1 -0
- package/dist/ssg.js +1255 -777
- package/dist/state-management.js +4 -1
- package/dist/store-loader.d.ts +0 -11
- package/dist/store-loader.js +27 -3
- package/dist/strict-lint.d.ts +9 -6
- package/dist/strict-lint.js +2 -21
- package/dist/stx.d.ts +125 -22
- package/dist/types/config-types.d.ts +1 -0
- package/dist/unresolved-identifiers.d.ts +50 -0
- package/dist/unresolved-identifiers.js +146 -0
- package/dist/utils.js +3 -3
- package/dist/variable-extractor.js +17 -1
- package/dist/x-element.js +1 -1
- package/package.json +5 -5
- package/stx.d.ts +125 -22
package/dist/reactive.js
CHANGED
|
@@ -438,7 +438,7 @@ function generateScopeInitializers(scopes, context) {
|
|
|
438
438
|
}).join(`
|
|
439
439
|
`);
|
|
440
440
|
return `
|
|
441
|
-
<script data-stx-scoped data-stx-reactive>
|
|
441
|
+
<script data-stx-scoped data-stx-reactive data-stx-run="always">
|
|
442
442
|
${generateReactiveRuntime()}
|
|
443
443
|
${initializers}
|
|
444
444
|
</script>`;
|
package/dist/reactivity.d.ts
CHANGED
|
@@ -49,21 +49,6 @@ export declare function reactive<T extends object>(target: T): T;
|
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
51
|
export declare function computed<T>(getter: () => T): Ref<T>;
|
|
52
|
-
/**
|
|
53
|
-
* Watch a reactive source and run a callback on changes.
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```typescript
|
|
57
|
-
* const count = ref(0)
|
|
58
|
-
*
|
|
59
|
-
* watch(count, (newVal, oldVal) => {
|
|
60
|
-
* console.log(`Changed from ${oldVal} to ${newVal}`)
|
|
61
|
-
* })
|
|
62
|
-
*
|
|
63
|
-
* // With options
|
|
64
|
-
* watch(count, callback, { immediate: true, deep: true })
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
52
|
export declare function watch<T>(source: Ref<T> | (() => T), callback: WatchCallback<T>, options?: WatchOptions): () => void;
|
|
68
53
|
/**
|
|
69
54
|
* Watch multiple sources.
|
package/dist/reactivity.js
CHANGED
|
@@ -197,6 +197,15 @@ export function computed(getter) {
|
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
199
|
}
|
|
200
|
+
function afterRender(fn) {
|
|
201
|
+
if (typeof requestAnimationFrame === "function") {
|
|
202
|
+
requestAnimationFrame(() => {
|
|
203
|
+
requestAnimationFrame(fn);
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setTimeout(fn, 0);
|
|
208
|
+
}
|
|
200
209
|
export function watch(source, callback, options = {}) {
|
|
201
210
|
const { immediate = !1, deep = !1, flush = "post" } = options;
|
|
202
211
|
let oldValue, cleanup, stopped = !1;
|
|
@@ -221,9 +230,7 @@ export function watch(source, callback, options = {}) {
|
|
|
221
230
|
else if (flush === "pre")
|
|
222
231
|
Promise.resolve().then(fn);
|
|
223
232
|
else
|
|
224
|
-
|
|
225
|
-
requestAnimationFrame(fn);
|
|
226
|
-
});
|
|
233
|
+
afterRender(fn);
|
|
227
234
|
};
|
|
228
235
|
let scheduled = !1;
|
|
229
236
|
const onChange = () => {
|
|
@@ -509,8 +516,10 @@ export function generateClientRuntime() {
|
|
|
509
516
|
window[name] = value;
|
|
510
517
|
}
|
|
511
518
|
|
|
512
|
-
// Expose globally for <script client> blocks
|
|
513
|
-
window.stx
|
|
519
|
+
// Expose globally for <script client> blocks.
|
|
520
|
+
// MERGE, never replace (#1804): several client bundles write window.stx
|
|
521
|
+
// and a wholesale assignment erases whichever ran first.
|
|
522
|
+
window.stx = Object.assign(window.stx || {}, {
|
|
514
523
|
// Reactivity
|
|
515
524
|
ref,
|
|
516
525
|
reactive,
|
|
@@ -546,7 +555,7 @@ export function generateClientRuntime() {
|
|
|
546
555
|
// SSR detection
|
|
547
556
|
isClient: function() { return true; },
|
|
548
557
|
isServer: function() { return false; }
|
|
549
|
-
};
|
|
558
|
+
});
|
|
550
559
|
window.provide = provide;
|
|
551
560
|
})();
|
|
552
561
|
</script>`;
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* surface.
|
|
15
15
|
*/
|
|
16
16
|
export declare function buildRuntimeGlobalsDestructure(declaration?: 'const' | 'var', exclude?: readonly string[], source?: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Does this script call anything that needs the reactive runtime?
|
|
19
|
+
*
|
|
20
|
+
* Matches a CALL, not a mention: a bare word can be a property, a string or a
|
|
21
|
+
* comment. Tolerates a generic argument list, since `state<Foo>(…)` is valid
|
|
22
|
+
* TypeScript and common.
|
|
23
|
+
*/
|
|
24
|
+
export declare function usesReactiveRuntime(source: string): boolean;
|
|
17
25
|
/**
|
|
18
26
|
* Runtime Globals
|
|
19
27
|
*
|
|
@@ -31,8 +39,16 @@ export declare function buildRuntimeGlobalsDestructure(declaration?: 'const' | '
|
|
|
31
39
|
*
|
|
32
40
|
* Two entries were also phantoms - `inject` and `nextTick` were destructured by
|
|
33
41
|
* one site but never defined on `window.stx` at all, so they silently evaluated
|
|
34
|
-
* to `undefined` and threw "is not a function" when called.
|
|
35
|
-
* signals
|
|
42
|
+
* to `undefined` and threw "is not a function" when called. Both are now
|
|
43
|
+
* implemented by the signals runtime (`inject` as of #1804).
|
|
44
|
+
*
|
|
45
|
+
* This list is also the source of `STX_AUTO_IMPORTS` (client-script.ts), which
|
|
46
|
+
* used to be a fourth hand-maintained copy for the classic `<script client>`
|
|
47
|
+
* path. It had drifted to 82 names against a runtime surface of 66, so 16 names
|
|
48
|
+
* — `h`, `Fragment`, `createStore`, `useMeta`, `getCurrentInstance` and others —
|
|
49
|
+
* were destructured off `window.stx` for every client script and silently bound
|
|
50
|
+
* to `undefined`, failing at the call site as "undefined is not a function" with
|
|
51
|
+
* nothing pointing at the missing name. One list, one guard (#1804).
|
|
36
52
|
*
|
|
37
53
|
* Adding a runtime global is now a one-line change here. `runtime-globals.test.ts`
|
|
38
54
|
* asserts every name actually exists on `window.stx` and that all three call
|
|
@@ -62,3 +78,25 @@ export declare const STX_RUNTIME_GLOBALS: readonly string[];
|
|
|
62
78
|
* #1785 fixed, so it is declared here rather than left implicit.
|
|
63
79
|
*/
|
|
64
80
|
export declare const COMPONENT_SCOPE_LOCAL_GLOBALS: readonly string[];
|
|
81
|
+
/**
|
|
82
|
+
* Runtime globals that are COMPILE-TIME macros rather than reactive APIs.
|
|
83
|
+
*
|
|
84
|
+
* Calling one of these does not make a script reactive — they are consumed by
|
|
85
|
+
* the compiler and produce no runtime subscription — so the auto-mount test
|
|
86
|
+
* below must not treat them as evidence that a block needs mounting.
|
|
87
|
+
*/
|
|
88
|
+
export declare const COMPILE_TIME_ONLY_GLOBALS: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Names whose presence in a client script means it needs the signals runtime
|
|
91
|
+
* and an `stx.mount()` wrapper.
|
|
92
|
+
*
|
|
93
|
+
* DERIVED, not hand-maintained (stacksjs/stx#1819). This was a literal regex
|
|
94
|
+
* alternation of 36 names against 71 runtime globals, so a block built out of
|
|
95
|
+
* the newer reactive composables — `useStore`, `useQuery`, `useMutation`,
|
|
96
|
+
* `useCookie`, `watchMultiple` and a dozen more — was classified as "not using
|
|
97
|
+
* signals", skipped mounting, and fell through to the legacy IIFE path with no
|
|
98
|
+
* reactivity at all.
|
|
99
|
+
*
|
|
100
|
+
* Exactly the drift #1804 removed for STX_AUTO_IMPORTS, one file away.
|
|
101
|
+
*/
|
|
102
|
+
export declare const REACTIVE_RUNTIME_GLOBALS: readonly string[];
|
package/dist/runtime-globals.js
CHANGED
|
@@ -11,18 +11,25 @@ export const STX_RUNTIME_GLOBALS = [
|
|
|
11
11
|
"effect",
|
|
12
12
|
"goBack",
|
|
13
13
|
"goForward",
|
|
14
|
+
"inject",
|
|
15
|
+
"isDerived",
|
|
16
|
+
"isSignal",
|
|
14
17
|
"navigate",
|
|
15
18
|
"nextTick",
|
|
19
|
+
"onBeforeMount",
|
|
16
20
|
"onBeforeUnmount",
|
|
17
21
|
"onDestroy",
|
|
18
22
|
"onMount",
|
|
19
23
|
"onMounted",
|
|
20
24
|
"onUnmounted",
|
|
25
|
+
"peek",
|
|
21
26
|
"provide",
|
|
22
27
|
"reactive",
|
|
23
28
|
"ref",
|
|
24
29
|
"registerStoresClient",
|
|
30
|
+
"setRouteParams",
|
|
25
31
|
"state",
|
|
32
|
+
"untrack",
|
|
26
33
|
"useAsync",
|
|
27
34
|
"useClickOutside",
|
|
28
35
|
"useColorMode",
|
|
@@ -53,6 +60,7 @@ export const STX_RUNTIME_GLOBALS = [
|
|
|
53
60
|
"useSearchParams",
|
|
54
61
|
"useSeoMeta",
|
|
55
62
|
"useSessionStorage",
|
|
63
|
+
"useSlots",
|
|
56
64
|
"useStore",
|
|
57
65
|
"useThrottle",
|
|
58
66
|
"useTimeout",
|
|
@@ -60,6 +68,7 @@ export const STX_RUNTIME_GLOBALS = [
|
|
|
60
68
|
"useWebSocket",
|
|
61
69
|
"watch",
|
|
62
70
|
"watchEffect",
|
|
71
|
+
"watchMultiple",
|
|
63
72
|
"withDefaults"
|
|
64
73
|
], COMPONENT_SCOPE_LOCAL_GLOBALS = ["onMount", "onDestroy"];
|
|
65
74
|
export function buildRuntimeGlobalsDestructure(declaration = "const", exclude = [], source) {
|
|
@@ -71,3 +80,84 @@ export function buildRuntimeGlobalsDestructure(declaration = "const", exclude =
|
|
|
71
80
|
});
|
|
72
81
|
return `${declaration} { ${names.join(", ")} } = window.stx;`;
|
|
73
82
|
}
|
|
83
|
+
export const COMPILE_TIME_ONLY_GLOBALS = [
|
|
84
|
+
"defineEmits",
|
|
85
|
+
"defineExpose",
|
|
86
|
+
"definePageMeta",
|
|
87
|
+
"defineProps",
|
|
88
|
+
"defineSlots",
|
|
89
|
+
"withDefaults"
|
|
90
|
+
], REACTIVE_RUNTIME_GLOBALS = STX_RUNTIME_GLOBALS.filter((name) => !COMPILE_TIME_ONLY_GLOBALS.includes(name));
|
|
91
|
+
export function usesReactiveRuntime(source) {
|
|
92
|
+
if (!source)
|
|
93
|
+
return !1;
|
|
94
|
+
return REACTIVE_RUNTIME_GLOBALS.some((name) => new RegExp(`\\b${name}\\s*(?:<[^>]*>)?\\s*\\(`).test(source));
|
|
95
|
+
}
|
|
96
|
+
const TEMPLATE_REACTIVE_PATTERNS = [
|
|
97
|
+
/@if\s*=/,
|
|
98
|
+
/@for\s*=/,
|
|
99
|
+
/@show\s*=/,
|
|
100
|
+
/@model\s*=/,
|
|
101
|
+
/@bind:/,
|
|
102
|
+
/@class\s*=/,
|
|
103
|
+
/@style\s*=/,
|
|
104
|
+
/\bx-data\s*=/,
|
|
105
|
+
/\bstate\s*(?:<[^>]*>)?\s*\(/,
|
|
106
|
+
/\bderived\s*(?:<[^>]*>)?\s*\(/,
|
|
107
|
+
/\beffect\s*(?:<[^>]*>)?\s*\(/,
|
|
108
|
+
/\bref\s*(?:<[^>]*>)?\s*\(/,
|
|
109
|
+
/\bcomputed\s*(?:<[^>]*>)?\s*\(/,
|
|
110
|
+
/\breactive\s*(?:<[^>]*>)?\s*\(/,
|
|
111
|
+
/\bwatch\s*(?:<[^>]*>)?\s*\(/,
|
|
112
|
+
/\bwatchEffect\s*(?:<[^>]*>)?\s*\(/,
|
|
113
|
+
/\s:[a-z][\w-]*\s*=\s*["']/i,
|
|
114
|
+
/\s:(?:if|else-if|else|for|show|text|html|model)\s*=/,
|
|
115
|
+
/data-stx(?:-auto)?(?![-\w])/,
|
|
116
|
+
/data-stx-scoped/
|
|
117
|
+
], HAS_EVENT_HANDLER = /\s@[a-z][\w.:-]*\s*=/i;
|
|
118
|
+
export function templateHasReactiveContext(template) {
|
|
119
|
+
if (!template)
|
|
120
|
+
return !1;
|
|
121
|
+
const live = blankInertHtmlRegions(template);
|
|
122
|
+
if (TEMPLATE_REACTIVE_PATTERNS.some((pattern) => pattern.test(live)))
|
|
123
|
+
return !0;
|
|
124
|
+
return usesReactiveRuntime(live);
|
|
125
|
+
}
|
|
126
|
+
export function templateNeedsRuntime(template) {
|
|
127
|
+
if (!template)
|
|
128
|
+
return !1;
|
|
129
|
+
return HAS_EVENT_HANDLER.test(blankInertHtmlRegions(template)) || templateHasReactiveContext(template);
|
|
130
|
+
}
|
|
131
|
+
export const RUNTIME_HANDLED_X_ATTRS = [
|
|
132
|
+
"x-text",
|
|
133
|
+
"x-html",
|
|
134
|
+
"x-model",
|
|
135
|
+
"x-show",
|
|
136
|
+
"x-if",
|
|
137
|
+
"x-for",
|
|
138
|
+
"x-cloak",
|
|
139
|
+
"x-ref",
|
|
140
|
+
"x-data",
|
|
141
|
+
"x-bind",
|
|
142
|
+
"x-class",
|
|
143
|
+
"x-style",
|
|
144
|
+
"x-tooltip",
|
|
145
|
+
"x-tooltip-position"
|
|
146
|
+
], ALPINE_BRIDGE_X_ATTRS = [
|
|
147
|
+
"x-init",
|
|
148
|
+
"x-transition",
|
|
149
|
+
"x-effect",
|
|
150
|
+
"x-on"
|
|
151
|
+
], COMPONENT_PASSTHROUGH_X_ATTRS = [
|
|
152
|
+
...RUNTIME_HANDLED_X_ATTRS,
|
|
153
|
+
...ALPINE_BRIDGE_X_ATTRS
|
|
154
|
+
];
|
|
155
|
+
export function runtimeHandledXAttrsLiteral() {
|
|
156
|
+
return `{${RUNTIME_HANDLED_X_ATTRS.map((n) => `'${n}':1`).join(",")}}`;
|
|
157
|
+
}
|
|
158
|
+
export function blankInertHtmlRegions(html) {
|
|
159
|
+
if (!html)
|
|
160
|
+
return html;
|
|
161
|
+
const blankText = (segment) => segment.replace(/(<[^>]*>)|([^<]+)/g, (_match, tag, text) => tag ?? " ".repeat(text.length));
|
|
162
|
+
return html.replace(/(<pre\b[^>]*>)([\s\S]*?)(<\/pre>)/gi, (_m, open, body, close) => open + blankText(body) + close).replace(/(<code\b[^>]*>)([\s\S]*?)(<\/code>)/gi, (_m, open, body, close) => open + blankText(body) + close).replace(/<!--[\s\S]*?-->/g, (match) => " ".repeat(match.length));
|
|
163
|
+
}
|
|
@@ -8,6 +8,57 @@ import type { StxOptions } from './types';
|
|
|
8
8
|
* Server scripts should use the server ORM directly, not browser functions.
|
|
9
9
|
*/
|
|
10
10
|
export declare function injectBrowserRuntime(template: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Does this rendered page ship the signals runtime?
|
|
13
|
+
*
|
|
14
|
+
* Answers the question the SPA router cannot: a fragment never carries the
|
|
15
|
+
* runtime, so swapping one into a page that has none leaves it permanently
|
|
16
|
+
* unhydrated. The router used to guess from markers in the fragment markup and
|
|
17
|
+
* missed the shape that has no markers at all — reactive syntax with no setup
|
|
18
|
+
* function, whose only stamp is `data-stx-auto` on `<body>`, which the fragment
|
|
19
|
+
* excludes (stacksjs/stx#1827). The server knows, because it is the thing that
|
|
20
|
+
* decided; it reports the answer in `X-STX-Runtime` on fragment responses.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately keyed on the same marker `injectSignalsRuntime` writes and
|
|
23
|
+
* checks below, so the two cannot disagree about what "has a runtime" means.
|
|
24
|
+
* Pass the FULL page, before it is reduced to the container's inner content.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pageShipsSignalsRuntime(fullPageHtml: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Does this rendered output contain a script that will THROW without the
|
|
29
|
+
* signals runtime?
|
|
30
|
+
*
|
|
31
|
+
* The safety net for stacksjs/stx#1820. Two independent decisions used to be
|
|
32
|
+
* made about the same page — `hasSignalsSyntax` decided whether to inject the
|
|
33
|
+
* runtime, and the client-script pass separately decided to emit
|
|
34
|
+
* `window.stx.mount(...)` — and nothing reconciled them. When they disagreed
|
|
35
|
+
* the page died on its first client line with "Cannot read properties of
|
|
36
|
+
* undefined (reading 'mount')", taking every handler defined in that block
|
|
37
|
+
* with it.
|
|
38
|
+
*
|
|
39
|
+
* Widening the gate does not fix that; it only makes the disagreement rarer,
|
|
40
|
+
* and it has already been widened twice. The gate is also evaluated against a
|
|
41
|
+
* template `processEventDirectives` has already rewritten, so an `@event=`
|
|
42
|
+
* page cannot match it however many patterns are added. This asks the opposite
|
|
43
|
+
* question, of the finished output, where no ordering can hide the answer: did
|
|
44
|
+
* we emit something that requires the runtime?
|
|
45
|
+
*
|
|
46
|
+
* Keyed on an UNGUARDED reach for `window.stx`, because that is precisely what
|
|
47
|
+
* throws. The guarded spellings the compiler also emits are all safe and must
|
|
48
|
+
* not trigger injection:
|
|
49
|
+
*
|
|
50
|
+
* var { state } = window.stx || window; // falls back
|
|
51
|
+
* var s = window.stx || {}; // falls back
|
|
52
|
+
* window.stx = window.stx || {}; // self-initializing
|
|
53
|
+
* if (window.stx) window.stx._latestSetup = null; // guarded
|
|
54
|
+
*
|
|
55
|
+
* The appearance bootstrap is the reason this runs before that directive is
|
|
56
|
+
* processed: it emits a self-contained `data-stx-scoped` script on pages with
|
|
57
|
+
* no reactivity at all, so keying on the presence of a client script — the
|
|
58
|
+
* obvious invariant, and the one the issue suggested — would pull the runtime
|
|
59
|
+
* onto every static page that sets a theme.
|
|
60
|
+
*/
|
|
61
|
+
export declare function outputNeedsSignalsRuntime(html: string): boolean;
|
|
11
62
|
/**
|
|
12
63
|
* Inject STX signals runtime into the template.
|
|
13
64
|
* The runtime provides client-side reactivity.
|
|
@@ -60,6 +60,19 @@ ${browserScript}`);
|
|
|
60
60
|
return browserScript + `
|
|
61
61
|
` + template;
|
|
62
62
|
}
|
|
63
|
+
export function pageShipsSignalsRuntime(fullPageHtml) {
|
|
64
|
+
return fullPageHtml.includes("data-stx-runtime");
|
|
65
|
+
}
|
|
66
|
+
export function outputNeedsSignalsRuntime(html) {
|
|
67
|
+
const reach = /window\.stx\s*\.\s*[A-Za-z_$]|\}\s*=\s*window\.stx\s*;/g;
|
|
68
|
+
for (let m = reach.exec(html);m !== null; m = reach.exec(html)) {
|
|
69
|
+
const preceding = html.slice(Math.max(0, m.index - 80), m.index);
|
|
70
|
+
if (/if\s*\(\s*window\.stx\s*\)|window\.stx\s*&&/.test(preceding))
|
|
71
|
+
continue;
|
|
72
|
+
return !0;
|
|
73
|
+
}
|
|
74
|
+
return !1;
|
|
75
|
+
}
|
|
63
76
|
export async function injectSignalsRuntime(template, options) {
|
|
64
77
|
if (template.includes("data-stx-runtime") || template.includes("window.stx.state") || template.includes("STX Signals Runtime"))
|
|
65
78
|
return template;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutralise closing script tags in a JavaScript body about to be embedded in
|
|
3
|
+
* an HTML `<script>` element.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent: an already-escaped `<\/script` contains no `</script`, so
|
|
6
|
+
* re-running this is a no-op rather than a double-escape.
|
|
7
|
+
*/
|
|
8
|
+
export declare function escapeScriptBody(js: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Warn when a body still carries a raw closing tag at emit time.
|
|
11
|
+
*
|
|
12
|
+
* A truncated script is never the intent, so this is worth saying out loud —
|
|
13
|
+
* both of the reported production incidents were diagnosed from the browser's
|
|
14
|
+
* "Unexpected end of input" rather than from anything stx said.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately a warning rather than the `StxSyntaxError` the issue proposes: a
|
|
17
|
+
* hard throw here could brick a production build over a vendor bundle inlined
|
|
18
|
+
* through `stx-inline`, which is a worse failure than the one being reported.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertNoRawClose(js: string, site: string, filePath?: string): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const RAW_CLOSE = /<\/(script)/gi;
|
|
2
|
+
export function escapeScriptBody(js) {
|
|
3
|
+
if (!js || js.indexOf("</") === -1)
|
|
4
|
+
return js;
|
|
5
|
+
return js.replace(RAW_CLOSE, "<\\/$1");
|
|
6
|
+
}
|
|
7
|
+
export function assertNoRawClose(js, site, filePath) {
|
|
8
|
+
if (!js || !/<\/script/i.test(js))
|
|
9
|
+
return;
|
|
10
|
+
console.warn(`[stx] ${filePath ? `${filePath}: ` : ""}a script body emitted by ${site} still contains a literal closing script tag. The HTML parser will end the element there, so everything after it becomes page text and every declaration below it silently disappears. See stacksjs/stx#1792.`);
|
|
11
|
+
}
|
|
@@ -60,9 +60,9 @@ export const PROHIBITED_DOM_PATTERNS = [
|
|
|
60
60
|
suggestion: "Use useEventListener() or @click directives"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
|
-
pattern: /
|
|
63
|
+
pattern: /(?:\bwindow\.)?localStorage(?![A-Za-z])/g,
|
|
64
64
|
message: "window.localStorage is prohibited",
|
|
65
|
-
suggestion: "Use useLocalStorage() from composables"
|
|
65
|
+
suggestion: "Use useLocalStorage() from composables \u2014 or @appearanceBootstrap if this must run before first paint, where the runtime does not exist yet"
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
pattern: /window\.sessionStorage(?![A-Za-z])/g,
|
|
@@ -125,10 +125,10 @@ export const PROHIBITED_DOM_PATTERNS = [
|
|
|
125
125
|
suggestion: "Use navigate() instead"
|
|
126
126
|
}
|
|
127
127
|
];
|
|
128
|
-
export function validateClientScript(content, filePath, strict) {
|
|
128
|
+
export function validateClientScript(content, filePath, strict, originFilePath) {
|
|
129
129
|
const strictConfig = typeof strict === "boolean" ? { enabled: strict, failOnViolation: strict } : strict ?? { enabled: !1 }, allowPatterns = strictConfig.allowPatterns ?? [], errors = [];
|
|
130
130
|
for (const { pattern, message, suggestion } of PROHIBITED_DOM_PATTERNS) {
|
|
131
|
-
if (allowPatterns.some((allowed) =>
|
|
131
|
+
if (allowPatterns.some((allowed) => /\w/.test(allowed) && message.includes(allowed)))
|
|
132
132
|
continue;
|
|
133
133
|
pattern.lastIndex = 0;
|
|
134
134
|
const matches = content.match(pattern);
|
|
@@ -140,13 +140,18 @@ export function validateClientScript(content, filePath, strict) {
|
|
|
140
140
|
if (pattern.test(line))
|
|
141
141
|
lineNumbers.push(index + 1);
|
|
142
142
|
});
|
|
143
|
-
const locationInfo = lineNumbers.length > 0 ? ` (line${lineNumbers.length > 1 ? "s" : ""}
|
|
143
|
+
const locationInfo = lineNumbers.length > 0 ? ` (script line${lineNumbers.length > 1 ? "s" : ""} ${lineNumbers.join(", ")})` : "", quoted = lineNumbers.slice(0, 3).map((n) => ` ${n} | ${(lines[n - 1] ?? "").trim().slice(0, 100)}`).join(`
|
|
144
|
+
`), snippet = quoted ? `
|
|
145
|
+
${quoted}` : "";
|
|
144
146
|
errors.push(` \u2717 ${message}${locationInfo}
|
|
145
|
-
\u2192 ${suggestion}`);
|
|
147
|
+
\u2192 ${suggestion}${snippet}`);
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
if (errors.length > 0 && strictConfig.enabled) {
|
|
149
|
-
const
|
|
151
|
+
const rel = (p) => {
|
|
152
|
+
const cwd = `${process.cwd()}/`;
|
|
153
|
+
return p.startsWith(cwd) ? p.slice(cwd.length) : p;
|
|
154
|
+
}, baseMessage = `[STX] DOM API violation in ${originFilePath && originFilePath !== filePath ? `${rel(originFilePath)} (composed into ${rel(filePath)})` : rel(filePath)}:
|
|
150
155
|
${errors.join(`
|
|
151
156
|
`)}
|
|
152
157
|
Tip: prefer useRef(), navigate(), and composables for component code`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateServerDataBridge, injectBrowserCoreAutoImports } from "./client-script.js";
|
|
2
2
|
import { findIfBlocks } from "./parser/index.js";
|
|
3
|
-
import { buildRuntimeGlobalsDestructure } from "./runtime-globals.js";
|
|
3
|
+
import { buildRuntimeGlobalsDestructure, templateNeedsRuntime, usesReactiveRuntime } from "./runtime-globals.js";
|
|
4
|
+
import { escapeScriptBody } from "./script-emit.js";
|
|
4
5
|
import { transformStoreImports } from "./store-imports.js";
|
|
5
6
|
import { shouldTranspileTypeScript, transpileTypeScript } from "./utils.js";
|
|
6
7
|
import { injectSignalsRuntime } from "./runtime-injection.js";
|
|
@@ -62,29 +63,7 @@ function parseItemSpec(spec) {
|
|
|
62
63
|
return { itemVar: null, indexVar: null };
|
|
63
64
|
}
|
|
64
65
|
export function hasSignalsSyntax(template) {
|
|
65
|
-
return
|
|
66
|
-
/@if\s*=/,
|
|
67
|
-
/@for\s*=/,
|
|
68
|
-
/@show\s*=/,
|
|
69
|
-
/@model\s*=/,
|
|
70
|
-
/@bind:/,
|
|
71
|
-
/@class\s*=/,
|
|
72
|
-
/@style\s*=/,
|
|
73
|
-
/\bx-data\s*=/,
|
|
74
|
-
/\bstate\s*(?:<[^>]*>)?\s*\(/,
|
|
75
|
-
/\bderived\s*(?:<[^>]*>)?\s*\(/,
|
|
76
|
-
/\beffect\s*(?:<[^>]*>)?\s*\(/,
|
|
77
|
-
/\bref\s*(?:<[^>]*>)?\s*\(/,
|
|
78
|
-
/\bcomputed\s*(?:<[^>]*>)?\s*\(/,
|
|
79
|
-
/\breactive\s*(?:<[^>]*>)?\s*\(/,
|
|
80
|
-
/\bwatch\s*(?:<[^>]*>)?\s*\(/,
|
|
81
|
-
/\bwatchEffect\s*(?:<[^>]*>)?\s*\(/,
|
|
82
|
-
/\buseId\s*\(/,
|
|
83
|
-
/\buseReactiveProp\s*(?:<[^>]*>)?\s*\(/,
|
|
84
|
-
/\s:(?:if|else-if|else|for|show|text|html|model)\s*=/,
|
|
85
|
-
/data-stx(?:-auto)?(?![-\w])/,
|
|
86
|
-
/data-stx-scoped/
|
|
87
|
-
].some((pattern) => pattern.test(template));
|
|
66
|
+
return templateNeedsRuntime(template);
|
|
88
67
|
}
|
|
89
68
|
function hasTopLevelElseBranch(content) {
|
|
90
69
|
const tokenRegex = /@(if\s*\(|endif|elseif\s*\(|else\b)/g;
|
|
@@ -461,7 +440,7 @@ export function convertSignalLoopsToAttributes(template, context) {
|
|
|
461
440
|
let output = template;
|
|
462
441
|
const forDirectiveStart = /@(for|foreach)\s*\(/g;
|
|
463
442
|
let match;
|
|
464
|
-
const replacements = [];
|
|
443
|
+
const replacements = [], serverScopes = [];
|
|
465
444
|
while ((match = forDirectiveStart.exec(output)) !== null) {
|
|
466
445
|
const directive = match[1], startIdx = match.index, exprStart = startIdx + match[0].length;
|
|
467
446
|
let depth = 1, i = exprStart;
|
|
@@ -475,20 +454,7 @@ export function convertSignalLoopsToAttributes(template, context) {
|
|
|
475
454
|
if (depth !== 0)
|
|
476
455
|
continue;
|
|
477
456
|
let expr = output.substring(exprStart, i - 1).trim();
|
|
478
|
-
const afterExpr = i, parsed = parseLoopExpression(expr);
|
|
479
|
-
if (parsed.iterable) {
|
|
480
|
-
const iter = parsed.iterable.trim();
|
|
481
|
-
if (iter.startsWith("[") || iter.startsWith("{") || iter.startsWith("'") || iter.startsWith('"') || iter.startsWith("`") || /^-?\d/.test(iter))
|
|
482
|
-
continue;
|
|
483
|
-
if (context) {
|
|
484
|
-
const rootVar = iter.split(/[.[(]/)[0].trim();
|
|
485
|
-
if (rootVar && rootVar in context)
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (parsed.style === "as")
|
|
490
|
-
expr = parsed.indexVar ? `(${parsed.itemVar}, ${parsed.indexVar}) in ${parsed.iterable}` : `${parsed.itemVar} in ${parsed.iterable}`;
|
|
491
|
-
const endTag = directive === "for" ? "@endfor" : "@endforeach";
|
|
457
|
+
const afterExpr = i, parsed = parseLoopExpression(expr), endTag = directive === "for" ? "@endfor" : "@endforeach";
|
|
492
458
|
let forDepth = 1, endIdx = afterExpr;
|
|
493
459
|
const endRegex = directive === "for" ? new RegExp("@(for(?!each)\\s*\\(|endfor(?!each))", "g") : new RegExp(`@(${directive}\\s*\\(|end${directive})`, "g");
|
|
494
460
|
endRegex.lastIndex = afterExpr;
|
|
@@ -505,6 +471,25 @@ export function convertSignalLoopsToAttributes(template, context) {
|
|
|
505
471
|
}
|
|
506
472
|
if (forDepth !== 0)
|
|
507
473
|
continue;
|
|
474
|
+
const keepServerSide = () => {
|
|
475
|
+
const vars = [parsed.itemVar, parsed.indexVar].filter(Boolean);
|
|
476
|
+
if (vars.length)
|
|
477
|
+
serverScopes.push({ start: startIdx, end: endIdx, vars });
|
|
478
|
+
};
|
|
479
|
+
if (parsed.iterable) {
|
|
480
|
+
const iter = parsed.iterable.trim();
|
|
481
|
+
if (iter.startsWith("[") || iter.startsWith("{") || iter.startsWith("'") || iter.startsWith('"') || iter.startsWith("`") || /^-?\d/.test(iter)) {
|
|
482
|
+
keepServerSide();
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const rootVar = iter.split(/[.[(]/)[0].trim(), fromContext = context ? rootVar in context : !1, fromEnclosingLoop = serverScopes.some((s) => startIdx > s.start && startIdx < s.end && s.vars.includes(rootVar));
|
|
486
|
+
if (rootVar && (fromContext || fromEnclosingLoop)) {
|
|
487
|
+
keepServerSide();
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (parsed.style === "as")
|
|
492
|
+
expr = parsed.indexVar ? `(${parsed.itemVar}, ${parsed.indexVar}) in ${parsed.iterable}` : `${parsed.itemVar} in ${parsed.iterable}`;
|
|
508
493
|
const content = output.substring(afterExpr, endIdx - endTag.length).trim(), singleElementMatch = content.match(SINGLE_ELEMENT_RE);
|
|
509
494
|
let replacement;
|
|
510
495
|
if (singleElementMatch) {
|
|
@@ -690,7 +675,7 @@ ${resolved}`);
|
|
|
690
675
|
<script data-stx-scoped>
|
|
691
676
|
function ${setupFnName}() {
|
|
692
677
|
${buildRuntimeGlobalsDestructure("const", mergedExportNames)}
|
|
693
|
-
${bridge}${mergedContent}
|
|
678
|
+
${escapeScriptBody(`${bridge}${mergedContent}`)}
|
|
694
679
|
return { ${mergedExports} };
|
|
695
680
|
}
|
|
696
681
|
window.__stx_latestSetup=${setupFnName};
|
package/dist/signals-api.d.ts
CHANGED
|
@@ -199,6 +199,36 @@ export declare function onMount(callback: LifecycleCallback): void;
|
|
|
199
199
|
* ```
|
|
200
200
|
*/
|
|
201
201
|
export declare function onDestroy(callback: LifecycleCallback): void;
|
|
202
|
+
/**
|
|
203
|
+
* Run and clear every callback registered with {@link onMount}.
|
|
204
|
+
*
|
|
205
|
+
* Both queues used to be write-only: `onMount` and `onDestroy` pushed, and
|
|
206
|
+
* nothing anywhere drained them (stacksjs/stx#1811). Two consequences, and the
|
|
207
|
+
* first is the one that bites — a composable registering cleanup via
|
|
208
|
+
* `onDestroy` got no cleanup, silently. The code reads as correct, passes
|
|
209
|
+
* review, and does nothing. The second is a slow leak: nothing cleared the
|
|
210
|
+
* arrays, so every registration retained its closure for the life of the
|
|
211
|
+
* process, and the retained objects were precisely the ones the author expected
|
|
212
|
+
* to be released.
|
|
213
|
+
*
|
|
214
|
+
* The client runtime drains its equivalents, so leaving these undrained also
|
|
215
|
+
* broke the dual-implementation parity CLAUDE.md item 40 requires: the same
|
|
216
|
+
* public API did something on one side and nothing on the other.
|
|
217
|
+
*
|
|
218
|
+
* Drains before invoking, so a callback that registers another is queued for
|
|
219
|
+
* the next drain rather than running inside this one.
|
|
220
|
+
*
|
|
221
|
+
* Errors are contained per callback: one throwing hook must not swallow the
|
|
222
|
+
* rest, which is the same lesson as the page-setup loop in #1805.
|
|
223
|
+
*/
|
|
224
|
+
export declare function runMountCallbacks(): void;
|
|
225
|
+
/** Run and clear every callback registered with {@link onDestroy}. */
|
|
226
|
+
export declare function runDestroyCallbacks(): void;
|
|
227
|
+
/**
|
|
228
|
+
* How many callbacks are queued. Exposed for tests and diagnostics — a queue
|
|
229
|
+
* that only ever grows is the shape of the defect above.
|
|
230
|
+
*/
|
|
231
|
+
export declare function pendingLifecycleCounts(): { mount: number, destroy: number };
|
|
202
232
|
/**
|
|
203
233
|
* Checks if a value is a signal.
|
|
204
234
|
*
|
package/dist/signals-api.js
CHANGED
|
@@ -116,6 +116,24 @@ export function onMount(callback) {
|
|
|
116
116
|
export function onDestroy(callback) {
|
|
117
117
|
destroyCallbacks.push(callback);
|
|
118
118
|
}
|
|
119
|
+
export function runMountCallbacks() {
|
|
120
|
+
drain(mountCallbacks, "onMount");
|
|
121
|
+
}
|
|
122
|
+
export function runDestroyCallbacks() {
|
|
123
|
+
drain(destroyCallbacks, "onDestroy");
|
|
124
|
+
}
|
|
125
|
+
function drain(queue, label) {
|
|
126
|
+
const pending = queue.splice(0, queue.length);
|
|
127
|
+
for (const callback of pending)
|
|
128
|
+
try {
|
|
129
|
+
callback();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`[stx] ${label} callback threw:`, error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function pendingLifecycleCounts() {
|
|
135
|
+
return { mount: mountCallbacks.length, destroy: destroyCallbacks.length };
|
|
136
|
+
}
|
|
119
137
|
export function isSignal(value) {
|
|
120
138
|
return typeof value === "function" && value._isSignal === !0;
|
|
121
139
|
}
|