@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.
Files changed (75) hide show
  1. package/dist/animation.js +6 -6
  2. package/dist/app-shell.js +1 -27
  3. package/dist/build-message.d.ts +10 -0
  4. package/dist/build-message.js +26 -0
  5. package/dist/builtins/attrs.js +6 -1
  6. package/dist/cli.js +1481 -1231
  7. package/dist/client-script-bundler.js +2 -4
  8. package/dist/client-script.d.ts +29 -0
  9. package/dist/client-script.js +140 -95
  10. package/dist/color-mode-boot.d.ts +31 -0
  11. package/dist/color-mode-boot.js +41 -5
  12. package/dist/component-renderer.js +10 -21
  13. package/dist/composables/index.d.ts +2 -0
  14. package/dist/composables/use-clipboard.d.ts +1 -5
  15. package/dist/composables/use-clipboard.js +57 -46
  16. package/dist/composables/use-color-mode.js +13 -5
  17. package/dist/composables/use-router.d.ts +28 -11
  18. package/dist/composables/use-router.js +63 -9
  19. package/dist/config.js +22 -7
  20. package/dist/craft.js +1029 -789
  21. package/dist/dev-server/crosswind.js +8 -6
  22. package/dist/dev-server/serve-app.js +4 -2
  23. package/dist/dev-server/serve-markdown.js +4 -1
  24. package/dist/dev-server/serve-multi.js +32 -20
  25. package/dist/document-shell.d.ts +65 -0
  26. package/dist/document-shell.js +59 -12
  27. package/dist/dynamic-components.js +123 -4
  28. package/dist/events.d.ts +26 -49
  29. package/dist/events.js +23 -9
  30. package/dist/expressions.d.ts +1 -1
  31. package/dist/expressions.js +3 -3
  32. package/dist/framework-composables.d.ts +41 -0
  33. package/dist/framework-composables.js +115 -0
  34. package/dist/hot-reload.js +8 -2
  35. package/dist/includes.js +2 -2
  36. package/dist/isolated-build.d.ts +12 -0
  37. package/dist/isolated-build.js +18 -0
  38. package/dist/plugin-system.d.ts +1 -1
  39. package/dist/process.js +40 -10
  40. package/dist/production-server.js +4 -1
  41. package/dist/prop-sentinels.d.ts +31 -0
  42. package/dist/prop-sentinels.js +7 -0
  43. package/dist/pwa.js +966 -726
  44. package/dist/reactive.js +1 -1
  45. package/dist/reactivity.d.ts +0 -15
  46. package/dist/reactivity.js +15 -6
  47. package/dist/runtime-globals.d.ts +40 -2
  48. package/dist/runtime-globals.js +90 -0
  49. package/dist/runtime-injection.d.ts +51 -0
  50. package/dist/runtime-injection.js +13 -0
  51. package/dist/script-emit.d.ts +20 -0
  52. package/dist/script-emit.js +11 -0
  53. package/dist/script-validation.js +12 -7
  54. package/dist/signal-processing.js +25 -40
  55. package/dist/signals-api.d.ts +30 -0
  56. package/dist/signals-api.js +18 -0
  57. package/dist/signals.js +258 -47
  58. package/dist/site-builder/build.js +21 -7
  59. package/dist/site-builder/seo.js +19 -10
  60. package/dist/site-builder/theme.js +5 -4
  61. package/dist/site-builder/types.d.ts +1 -0
  62. package/dist/ssg.js +986 -746
  63. package/dist/state-management.js +4 -1
  64. package/dist/store-loader.d.ts +0 -11
  65. package/dist/store-loader.js +27 -3
  66. package/dist/strict-lint.d.ts +9 -6
  67. package/dist/strict-lint.js +2 -21
  68. package/dist/stx.d.ts +111 -16
  69. package/dist/unresolved-identifiers.d.ts +50 -0
  70. package/dist/unresolved-identifiers.js +146 -0
  71. package/dist/utils.js +3 -3
  72. package/dist/variable-extractor.js +17 -1
  73. package/dist/x-element.js +1 -1
  74. package/package.json +5 -5
  75. package/stx.d.ts +111 -16
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>`;
@@ -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.
@@ -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
- requestAnimationFrame(() => {
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. `nextTick` now has a
35
- * signals-runtime implementation; `inject` remains omitted until it does too.
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[];
@@ -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: /window\.localStorage(?![A-Za-z])/g,
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) => message.includes(allowed) || pattern.source.includes(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" : ""}: ${lineNumbers.join(", ")})` : "";
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 baseMessage = `[STX] DOM API violation in ${filePath.split("/").pop() || filePath}:
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};
@@ -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
  *
@@ -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
  }