@stacksjs/stx 0.2.153 → 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.
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
@@ -505,7 +505,10 @@ else {
505
505
  });
506
506
  };
507
507
 
508
- window.stx = s;
508
+ // Merge, never replace (#1804). s was seeded from window.stx above, so this
509
+ // keeps both any earlier keys and the object identity the waitForStore
510
+ // closure captured.
511
+ window.stx = Object.assign(window.stx || {}, s);
509
512
  })();
510
513
  `;
511
514
  }
@@ -1,14 +1,3 @@
1
- /**
2
- * Discover store files from storesDir and bundle them into a single
3
- * script that can be injected into the page.
4
- *
5
- * The bundle runs `defineStore(...)` for each store — since the signals
6
- * runtime is already loaded, `defineStore`/`state`/`derived` are globals.
7
- *
8
- * Pass an absolute `storesDir` to look up stores there. When omitted, falls
9
- * back to reading `storesDir` from the stx config at the current working
10
- * directory — which is only correct when run from the app's own root.
11
- */
12
1
  export declare function getStoreScript(storesDir?: string): Promise<string | null>;
13
2
  /**
14
3
  * Clear the cached store script (for dev mode hot reload).
@@ -1,7 +1,26 @@
1
1
  import path from "node:path";
2
2
  import { loadStxConfig } from "./config.js";
3
3
  import { getPublicEnvDefine } from "./public-env.js";
4
+ import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
5
+ import { transformStoreImports } from "./store-imports.js";
4
6
  const _cachedStoreScripts = new Map;
7
+ function collectDeclaredNames(code, into) {
8
+ for (const match of code.matchAll(/\b(?:const|let|var|function|class)\s+([A-Z_a-z$][\w$]*)/g))
9
+ if (match[1])
10
+ into.add(match[1]);
11
+ for (const match of code.matchAll(/\b(?:const|let|var)\s*\{([^{}]*)\}\s*=/g))
12
+ for (const part of (match[1] ?? "").split(",")) {
13
+ const name = ((part.includes(":") ? part.split(":").pop() : part) ?? "").replace(/=.*$/s, "").replace(/^\s*\.\.\./, "").trim();
14
+ if (/^[A-Z_a-z$][\w$]*$/.test(name))
15
+ into.add(name);
16
+ }
17
+ for (const match of code.matchAll(/\b(?:const|let|var)\s*\[([^\]]*)\]\s*=/g))
18
+ for (const part of (match[1] ?? "").split(",")) {
19
+ const name = part.replace(/=.*$/s, "").replace(/^\s*\.\.\./, "").trim();
20
+ if (/^[A-Z_a-z$][\w$]*$/.test(name))
21
+ into.add(name);
22
+ }
23
+ }
5
24
  export async function getStoreScript(storesDir) {
6
25
  let resolvedDir;
7
26
  if (storesDir)
@@ -44,11 +63,13 @@ export async function getStoreScript(storesDir) {
44
63
  return 0;
45
64
  return a.hasUseStore ? 1 : -1;
46
65
  });
47
- const sortedFiles = filesWithDeps.map((f) => f.file), chunks = [], transpiler = new Bun.Transpiler({ loader: "ts", target: "browser", define: getPublicEnvDefine() });
66
+ const sortedFiles = filesWithDeps.map((f) => f.file), chunks = [], declaredNames = new Set, transpiler = new Bun.Transpiler({ loader: "ts", target: "browser", define: getPublicEnvDefine() });
48
67
  for (const file of sortedFiles)
49
68
  try {
50
69
  let code = await Bun.file(file).text();
51
70
  const storeName = path.basename(file, ".ts");
71
+ collectDeclaredNames(code, declaredNames);
72
+ code = transformStoreImports(code);
52
73
  code = code.replace(/^import\s+.*from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
53
74
  code = code.replace(/^export\s+(default\s+)?/gm, "");
54
75
  code = transpiler.transformSync(code);
@@ -61,10 +82,13 @@ ${code.trim()}`);
61
82
  _cachedStoreScripts.set(resolvedDir, "");
62
83
  return null;
63
84
  }
64
- const code = `;(function(){
65
- ${chunks.join(`
85
+ const joinedChunks = chunks.join(`
66
86
 
87
+ `), code = `;(function(){
88
+ var __stx = window.stx || {};
89
+ ${STX_RUNTIME_GLOBALS.filter((name) => !declaredNames.has(name) && new RegExp(`\\b${name}\\b`).test(joinedChunks)).map((name) => `var ${name} = __stx[${JSON.stringify(name)}];`).join(`
67
90
  `)}
91
+ ${joinedChunks}
68
92
  })();`;
69
93
  _cachedStoreScripts.set(resolvedDir, code);
70
94
  return code;
@@ -33,17 +33,20 @@ export declare interface StxStrictOptions {
33
33
  * with low false-positive rate).
34
34
  *
35
35
  * Phase 1 covers the five highest-hit footguns from the issue:
36
- * 1. stx/no-bare-function-ref-in-event `@click="toggleLike"` (no parens)
37
- * 2. stx/no-view-level-script-client `<script client>` in a view file
38
- * 3. stx/store-value-imports-must-be-local value import escaping resources/stores/
39
- * 4. stx/no-signal-call-in-for-iteration-var calling the `:for` loop var like a signal
40
- * 5. stx/no-backticks-in-html-comments a backtick inside an HTML comment
36
+ * 1. stx/no-bare-function-ref-in-event `@click="store.method"` (member path)
37
+ * 2. stx/store-value-imports-must-be-local value import escaping resources/stores/
38
+ * 3. stx/no-signal-call-in-for-iteration-var calling the `:for` loop var like a signal
39
+ * 4. stx/no-backticks-in-html-comments a backtick inside an HTML comment
40
+ *
41
+ * `stx/no-view-level-script-client` was removed in #1815: it asserted at error
42
+ * severity that a view-level `<script client>` is silently dropped, which is
43
+ * false — it is the primary documented authoring pattern, and its symbols reach
44
+ * the built page. The rule told people to break working code.
41
45
  *
42
46
  * Remaining footguns (web-components, template tags, @include loop-var
43
47
  * forwarding, etc.) are Phase 2.
44
48
  */
45
49
  export type StxStrictRuleId = | 'stx/no-bare-function-ref-in-event'
46
- | 'stx/no-view-level-script-client'
47
50
  | 'stx/store-value-imports-must-be-local'
48
51
  | 'stx/no-signal-call-in-for-iteration-var'
49
52
  | 'stx/no-backticks-in-html-comments';
@@ -117,11 +117,11 @@ function ruleBareFunctionRef(src, spans, out) {
117
117
  if (DIRECTIVE_ATTR_NAMES.has(base))
118
118
  continue;
119
119
  const value = m[3].trim();
120
- if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value)) {
120
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) {
121
121
  const at = src.indexOf(m[3], m.index), { line, column } = offsetToLineCol(src, at < 0 ? m.index : at);
122
122
  out.push({
123
123
  ruleId: "stx/no-bare-function-ref-in-event",
124
- message: `Event handler @${m[1]}="${value}" is a bare reference and will not fire. Call it: @${m[1]}="${value}()".`,
124
+ message: `Event handler @${m[1]}="${value}" is a member path, which the runtime does not invoke \u2014 it only auto-calls a plain identifier. Call it: @${m[1]}="${value}($event)".`,
125
125
  line,
126
126
  column,
127
127
  severity: "error"
@@ -129,23 +129,6 @@ function ruleBareFunctionRef(src, spans, out) {
129
129
  }
130
130
  }
131
131
  }
132
- function ruleViewLevelScriptClient(src, fileKind, out) {
133
- if (fileKind !== "view")
134
- return;
135
- const re = /<script\b([^>]*)>/gi;
136
- let m;
137
- while ((m = re.exec(src)) !== null)
138
- if (/(?:^|\s)client(?=[\s/=]|$)/i.test(m[1])) {
139
- const { line, column } = offsetToLineCol(src, m.index);
140
- out.push({
141
- ruleId: "stx/no-view-level-script-client",
142
- message: "<script client> at the top level of a view is silently dropped \u2014 only components get client scopes. Move it into a component, or use <script> (server).",
143
- line,
144
- column,
145
- severity: "error"
146
- });
147
- }
148
- }
149
132
  function normalizePath(p) {
150
133
  const parts = p.split("/"), out = [];
151
134
  for (const seg of parts) {
@@ -268,8 +251,6 @@ export function lintStxStrict(source, options = {}) {
268
251
  const out = [], spans = rawTextSpans(source), filePath = (options.filePath ?? "").replace(/\\/g, "/"), fileKind = options.fileKind ?? (/\/components?\//i.test(filePath) ? "component" : /\/layouts?\//i.test(filePath) ? "layout" : "view");
269
252
  if (isRuleOn(options.rules, "stx/no-bare-function-ref-in-event"))
270
253
  ruleBareFunctionRef(source, spans, out);
271
- if (isRuleOn(options.rules, "stx/no-view-level-script-client"))
272
- ruleViewLevelScriptClient(source, fileKind, out);
273
254
  if (filePath && isRuleOn(options.rules, "stx/store-value-imports-must-be-local"))
274
255
  ruleStoreValueImports(source, filePath, out);
275
256
  if (isRuleOn(options.rules, "stx/no-signal-call-in-for-iteration-var"))
package/dist/stx.d.ts CHANGED
@@ -82,23 +82,49 @@ declare function onMount(_fn: () => void | StxCleanup): void
82
82
  declare function onDestroy(_fn: () => void): void
83
83
  declare function onBeforeMount(_fn: () => void): void
84
84
  declare function onMounted(_fn: () => void | StxCleanup): void
85
- declare function onBeforeUpdate(_fn: () => void): void
86
- declare function onUpdated(_fn: () => void): void
87
85
  declare function onBeforeUnmount(_fn: () => void): void
88
86
  declare function onUnmounted(_fn: () => void): void
89
- declare function onErrorCaptured(_fn: (error: unknown, instance: HTMLElement | null, info: string) => boolean | void): void
87
+ // onBeforeUpdate / onUpdated / onErrorCaptured are NOT declared: they exist in
88
+ // reactivity.ts and composition-api.ts but have no client-runtime counterpart,
89
+ // so declaring them promised a global that resolved to undefined (#1804).
90
90
 
91
91
  // ============================================================================
92
92
  // Template refs
93
93
  // ============================================================================
94
94
 
95
+ /** Stable per-component id, unique across instances. */
96
+ declare function useId(_prefix?: string): string
97
+ /**
98
+ * Signal tracking a parent-driven `:prop="signal()"` attribute on the
99
+ * component's root element. One-way: parent to child.
100
+ */
101
+ declare function useReactiveProp<T = unknown>(
102
+ _name: string,
103
+ _defaultValue?: T,
104
+ _options?: { parse?: (_value: string) => T },
105
+ ): StxSignal<T>
95
106
  declare function useRef<T = HTMLElement>(_name: string): { current: T | null }
96
107
 
97
108
  // ============================================================================
98
109
  // Routing / navigation
99
110
  // ============================================================================
100
111
 
101
- declare function navigate(_url: string, _options?: { replace?: boolean }): void
112
+ interface StxNavigateOptions {
113
+ /** Replace the current history entry instead of pushing a new one. */
114
+ replace?: boolean
115
+ /** Bypass the SPA router and perform a full document load. */
116
+ reload?: boolean
117
+ }
118
+
119
+ /**
120
+ * Navigate via the SPA router.
121
+ *
122
+ * The second argument is deliberately NOT typed to also accept a boolean. The
123
+ * legacy positional `forceReload` boolean still works at runtime — it was the
124
+ * shape that actually shipped — but keeping it out of the type means nothing
125
+ * that type-checks today changes meaning (#1807).
126
+ */
127
+ declare function navigate(_url: string, _options?: StxNavigateOptions): void
102
128
  declare function goBack(): void
103
129
  declare function goForward(): void
104
130
  declare function useRoute(): {
@@ -108,11 +134,39 @@ declare function useRoute(): {
108
134
  hash: string
109
135
  }
110
136
  declare function setRouteParams(_params: Record<string, string>): void
137
+ /** How a search-param mutation is written to session history (#1825). */
138
+ interface StxSearchParamsCommitOptions {
139
+ /** Replace the current history entry instead of pushing a new one. */
140
+ replace?: boolean
141
+ }
142
+
143
+ /**
144
+ * Reactive access to the URL query string.
145
+ *
146
+ * The declaration was wrong in both directions (#1806): it promised `delete`
147
+ * and `has`, which did not exist at runtime, and omitted `setAll` and `data`,
148
+ * which did. `delete`/`has` are now implemented rather than dropped; `get`
149
+ * returns `undefined` for a missing key, not `null`, which is what the runtime
150
+ * and the docs have always said.
151
+ */
111
152
  declare function useSearchParams(): {
112
- get: (key: string) => string | null
113
- set: (key: string, value: string) => void
114
- delete: (key: string) => void
115
- has: (key: string) => boolean
153
+ /** The backing signal — read it to make a binding reactive to the query. */
154
+ data: StxSignal<Record<string, string>>
155
+ get: (_key: string) => string | undefined
156
+ has: (_key: string) => boolean
157
+ /** Set a param. Pushes a history entry unless `{ replace: true }` is passed. */
158
+ set: (_key: string, _value: string, _options?: StxSearchParamsCommitOptions) => void
159
+ /**
160
+ * Remove a param. Pushes a history entry unless `{ replace: true }` is passed.
161
+ *
162
+ * CONSUMING a one-shot param — an OAuth callback result, `?checkout=success`,
163
+ * a flash token — needs `{ replace: true }`. Under the default push, the URL
164
+ * that still carries the param becomes the previous history entry, so Back
165
+ * replays the callback and re-runs whatever consuming it triggered (#1825).
166
+ */
167
+ delete: (_key: string, _options?: StxSearchParamsCommitOptions) => void
168
+ /** Set several params. Pushes a history entry unless `{ replace: true }` is passed. */
169
+ setAll: (_values: Record<string, string>, _options?: StxSearchParamsCommitOptions) => void
116
170
  }
117
171
 
118
172
  // ============================================================================
@@ -160,7 +214,6 @@ declare function useEventListener(
160
214
  declare function useScrollLock(
161
215
  _target?: HTMLElement | null | { current?: HTMLElement | null, value?: HTMLElement | null } | (() => HTMLElement | null | undefined),
162
216
  ): StxSignal<boolean>
163
- declare function useMeta(_meta: Record<string, string>): void
164
217
  declare function useClickOutside(_target: HTMLElement | StxRef<HTMLElement | null> | string | null, _handler: (event: MouseEvent) => void): StxCleanup
165
218
  declare function useFocus(_target: HTMLElement | StxRef<HTMLElement | null> | string | null): { focused: StxSignal<boolean>, focus: () => void, blur: () => void }
166
219
 
@@ -209,6 +262,44 @@ declare function useAsync<T>(_fn: () => Promise<T>): {
209
262
  declare function useLocalStorage<T>(_key: string, _defaultValue: T): StxSignal<T>
210
263
  declare function useSessionStorage<T>(_key: string, _defaultValue: T): StxSignal<T>
211
264
 
265
+ /**
266
+ * Options accepted by the auto-imported `useCookie` global.
267
+ *
268
+ * This describes the CLIENT RUNTIME's cookie composable, which is what a bare
269
+ * `useCookie(...)` in a `<script client>` block resolves to. The module export
270
+ * `@stacksjs/stx/composables` accepts a slightly wider `expires` (Date, epoch ms
271
+ * or parseable string); the runtime calls `.toUTCString()` on it, so only a Date
272
+ * works there. See #1710.
273
+ */
274
+ interface StxCookieOptions {
275
+ /** Max age in seconds. */
276
+ maxAge?: number
277
+ /** Expiration date. */
278
+ expires?: Date
279
+ /** Cookie path. Defaults to '/'. */
280
+ path?: string
281
+ /** Cookie domain. */
282
+ domain?: string
283
+ /** Secure flag. Defaults to true when location.protocol is https:. */
284
+ secure?: boolean
285
+ /** SameSite attribute. Defaults to 'Lax'. */
286
+ sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none'
287
+ /** Value returned when the cookie is absent. Defaults to ''. */
288
+ defaultValue?: string
289
+ /** Custom encoder. Defaults to encodeURIComponent. */
290
+ encode?: (_value: string) => string
291
+ /** Custom decoder. Defaults to decodeURIComponent. */
292
+ decode?: (_value: string) => string
293
+ }
294
+
295
+ /**
296
+ * Reactive binding to a single cookie (#1808).
297
+ *
298
+ * Returns a signal, like `useLocalStorage` — writes serialise straight to
299
+ * `document.cookie`, and `.set('')` deletes by emitting `max-age=0`.
300
+ */
301
+ declare function useCookie(_name: string, _options?: StxCookieOptions): StxSignal<string>
302
+
212
303
  // ============================================================================
213
304
  // WebSocket
214
305
  // ============================================================================
@@ -317,6 +408,8 @@ interface StxSeoMetaConfig {
317
408
 
318
409
  declare function useHead(_config: StxHeadConfig): void
319
410
  declare function useSeoMeta(_config: StxSeoMetaConfig): void
411
+ /** No-op on the client; the real one runs at SSR/SSG time. */
412
+ declare function definePageMeta(_meta: Record<string, unknown>): void
320
413
 
321
414
  // ============================================================================
322
415
  // Vue-style reactivity (alternative API)
@@ -348,9 +441,9 @@ declare function defineExpose<T extends Record<string, any>>(_exposed: T): void
348
441
  declare function defineSlots<T extends Record<string, (..._args: any[]) => any> = Record<string, (..._args: any[]) => any>>(): T
349
442
  declare function provide<T>(_key: string | symbol, _value: T): void
350
443
  declare function inject<T>(_key: string | symbol, _defaultValue?: T): T | undefined
351
- declare function getCurrentInstance(): any
352
444
  declare function useSlots(): Record<string, any>
353
- declare function useAttrs(): Record<string, any>
445
+ // getCurrentInstance / useAttrs are NOT declared — composition-api.ts exports
446
+ // them for server use; no client-runtime counterpart exists (#1804).
354
447
  declare function $computed<T>(_getter: () => T): StxRef<T>
355
448
  declare function $watch<T>(
356
449
  _source: (() => T) | StxRef<T> | StxSignal<T>,
@@ -462,16 +555,18 @@ declare function defineStore<S extends Record<string, any>, G extends Record<str
462
555
  },
463
556
  ): () => S & G & A
464
557
  declare function useStore<T = any>(_id: string): T
465
- declare function createStore<T>(_setup: () => T, _options?: StxStoreOptions): () => T
466
- declare function action<T extends (..._args: any[]) => any>(_fn: T): T
467
- declare function createSelector<T, R>(_selector: (state: T) => R): (state: T) => R
558
+ declare function registerStoresClient(_stores: Record<string, unknown>): void
559
+ // createStore / createSelector are exported by state-management.ts for server
560
+ // use and `action` has no implementation anywhere; none reaches window.stx, so
561
+ // none is declared as an ambient global (#1804).
468
562
 
469
563
  // ============================================================================
470
564
  // JSX runtime (Vue/React style)
471
565
  // ============================================================================
472
566
 
473
- declare function h(_tag: string | Function, _props?: Record<string, any> | null, ..._children: any[]): any
474
- declare const Fragment: unique symbol
567
+ // h / Fragment belong to the JSX runtime (jsx-runtime.ts) and are resolved by
568
+ // the transform, not destructured off window.stx — so they are not ambient
569
+ // globals (#1804). Import them explicitly if you call them directly.
475
570
 
476
571
  // ============================================================================
477
572
  // window.stx registry
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Find identifiers a script CALLS that resolve to nothing in the browser.
3
+ *
4
+ * Restricted to names we can name a cause for — exports of `composables/` with
5
+ * no runtime counterpart. A broader "looks like a hook and is not in scope"
6
+ * heuristic would fire on legitimate local helpers and app composables, and a
7
+ * warning people learn to ignore is worse than no warning.
8
+ *
9
+ * @param source the script body, after imports have been transformed
10
+ * @param provided names already in scope (runtime globals, locals, imports)
11
+ */
12
+ export declare function findUnresolvedIdentifiers(source: string, provided: Iterable<string>): UnresolvedIdentifier[];
13
+ /**
14
+ * Report unresolved identifiers against a file, once each.
15
+ *
16
+ * Returns the diagnostics so callers can assert on them without capturing
17
+ * console output.
18
+ */
19
+ export declare function reportUnresolvedIdentifiers(source: string, provided: Iterable<string>, filePath: string): UnresolvedIdentifier[];
20
+ /**
21
+ * Diagnose an identifier a client script calls that will not exist in the browser.
22
+ *
23
+ * `packages/stx/src/composables/` exports 149 functions. Only 29 of them reach
24
+ * the client runtime; the other 120 are module exports with no counterpart on
25
+ * `window.stx`. They type-check, they autocomplete, and calling one from a
26
+ * `<script client>` block throws `ReferenceError` — which used to take down the
27
+ * whole page (fixed in the DOMContentLoaded handler, stacksjs/stx#1805).
28
+ *
29
+ * Containing the throw stops one bad name costing the document, but the author
30
+ * still has to work out WHY a documented, exported, type-checking composable is
31
+ * not defined. This turns that into a message at compile time naming the
32
+ * identifier, the file, and the fix.
33
+ *
34
+ * Deliberately a warning, not a build failure. #1810 is a report of the dev
35
+ * server aborting on one bad view with no filename; adding a second way to abort
36
+ * the build would be solving one complaint by creating another.
37
+ */
38
+ /**
39
+ * Functions `composables/index.ts` exports that the client runtime does NOT
40
+ * provide.
41
+ *
42
+ * Generated — `test/unresolved-identifiers.test.ts` recomputes it from the real
43
+ * module surface and fails when this drifts, so it cannot silently rot the way
44
+ * the auto-import list did (#1804).
45
+ */
46
+ export declare const SERVER_ONLY_COMPOSABLES: readonly string[];
47
+ export declare interface UnresolvedIdentifier {
48
+ name: string
49
+ message: string
50
+ }
@@ -0,0 +1,146 @@
1
+ export const SERVER_ONLY_COMPOSABLES = [
2
+ "broadcast",
3
+ "calculateDistance",
4
+ "canNotify",
5
+ "clearCookies",
6
+ "clearFetchCache",
7
+ "clearStorage",
8
+ "copyToClipboard",
9
+ "createShareableFile",
10
+ "getBatteryLevel",
11
+ "getCookie",
12
+ "getCurrentPosition",
13
+ "getStorageKeys",
14
+ "getStorageSize",
15
+ "getVoices",
16
+ "hasBattery",
17
+ "hasCameraPermission",
18
+ "hasGeolocationPermission",
19
+ "hasMicrophonePermission",
20
+ "hasNotificationPermission",
21
+ "hasResizeObserver",
22
+ "hexToHsl",
23
+ "hexToRgb",
24
+ "isBroadcastChannelSupported",
25
+ "isCharging",
26
+ "isDeviceMotionSupported",
27
+ "isDeviceOrientationSupported",
28
+ "isEventSourceSupported",
29
+ "isEyeDropperSupported",
30
+ "isInFullscreen",
31
+ "isMutationObserverSupported",
32
+ "isPermissionGranted",
33
+ "isSpeechRecognitionSupported",
34
+ "isSpeechSynthesisSupported",
35
+ "isWakeLockSupported",
36
+ "isWebSocketSupported",
37
+ "notify",
38
+ "parseCookies",
39
+ "pickColor",
40
+ "prefetch",
41
+ "removeCookie",
42
+ "requestMediaPermissions",
43
+ "requestMotionPermission",
44
+ "requestNotificationPermission",
45
+ "requestOrientationPermission",
46
+ "setCookie",
47
+ "share",
48
+ "shareCurrentPage",
49
+ "shareFiles",
50
+ "shareText",
51
+ "shareURL",
52
+ "shareWithFallback",
53
+ "speak",
54
+ "stopSpeaking",
55
+ "toggleFullscreen",
56
+ "useAsyncData",
57
+ "useAttributeObserver",
58
+ "useAutoLogout",
59
+ "useAutoWakeLock",
60
+ "useBattery",
61
+ "useBreakpoint",
62
+ "useBroadcastChannel",
63
+ "useChannel",
64
+ "useChildListObserver",
65
+ "useClipboard",
66
+ "useColorHistory",
67
+ "useConditionalWakeLock",
68
+ "useCookies",
69
+ "useCopySelection",
70
+ "useDeviceMotion",
71
+ "useDeviceOrientation",
72
+ "useElementSize",
73
+ "useElementTextSelection",
74
+ "useElementVisibility",
75
+ "useEventSource",
76
+ "useEyeDropper",
77
+ "useFavicon",
78
+ "useFullscreen",
79
+ "useGeolocation",
80
+ "useGeolocationWatch",
81
+ "useHotkey",
82
+ "useIdle",
83
+ "useIdleState",
84
+ "useInfiniteScroll",
85
+ "useIntersectionObserver",
86
+ "useIntersectionObserverMultiple",
87
+ "useIsDesktop",
88
+ "useIsMobile",
89
+ "useIsTablet",
90
+ "useKeyPressed",
91
+ "useKeySequence",
92
+ "useKeyboard",
93
+ "useLastActive",
94
+ "useLazyLoad",
95
+ "useMeta",
96
+ "useMouse",
97
+ "useMouseInElement",
98
+ "useMutationObserver",
99
+ "useNetwork",
100
+ "useNotification",
101
+ "useOnline",
102
+ "useParallax",
103
+ "usePermission",
104
+ "usePermissions",
105
+ "usePointer",
106
+ "usePost",
107
+ "useResizeObserver",
108
+ "useResizeObserverMultiple",
109
+ "useSSE",
110
+ "useScroll",
111
+ "useSelectionPopup",
112
+ "useShare",
113
+ "useSpeechRecognition",
114
+ "useSpeechSynthesis",
115
+ "useStorage",
116
+ "useTextObserver",
117
+ "useTextSelection",
118
+ "useTitle",
119
+ "useVisibility",
120
+ "useWakeLock",
121
+ "useWindowSize"
122
+ ];
123
+ const SERVER_ONLY = new Set(SERVER_ONLY_COMPOSABLES);
124
+ export function findUnresolvedIdentifiers(source, provided) {
125
+ const inScope = provided instanceof Set ? provided : new Set(provided), found = new Map;
126
+ for (const match of source.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
127
+ const name = match[1];
128
+ if (found.has(name) || inScope.has(name) || !SERVER_ONLY.has(name))
129
+ continue;
130
+ if (new RegExp(`(?:const|let|var|function|class)\\s+${name}\\b`).test(source))
131
+ continue;
132
+ if (new RegExp(`import\\s[^;]*\\b${name}\\b[^;]*from`).test(source))
133
+ continue;
134
+ found.set(name, {
135
+ name,
136
+ message: `"${name}" is exported by @stacksjs/stx/composables but is not available in the browser \u2014 ` + `it is a server-only module export, so this call throws ReferenceError at runtime. Import it explicitly (import { ${name} } from '@stacksjs/stx/composables') so the bundler inlines it, or use a runtime equivalent from window.stx.`
137
+ });
138
+ }
139
+ return [...found.values()];
140
+ }
141
+ export function reportUnresolvedIdentifiers(source, provided, filePath) {
142
+ const unresolved = findUnresolvedIdentifiers(source, provided);
143
+ for (const item of unresolved)
144
+ console.warn(`[stx] ${filePath}: ${item.message}`);
145
+ return unresolved;
146
+ }
package/dist/utils.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { extractAndStripCssImports, extractBridgeData, injectBrowserCoreAutoImports, processClientScript, renderVendorStyleTags } from "./client-script.js";
4
4
  import { COMPONENT_CLIENT_FACTORIES_CONTEXT_KEY, registerComponentClientFactory } from "./component-client-factories.js";
5
- import { interpolateScriptExpressions, processExpressions } from "./expressions.js";
5
+ import { interpolateScriptExpressions, processExpressions, usesSignalsInScript } from "./expressions.js";
6
6
  import { COMPONENT_SCOPE_LOCAL_GLOBALS, buildRuntimeGlobalsDestructure } from "./runtime-globals.js";
7
7
  import { transformStoreImports } from "./store-imports.js";
8
8
  import { LRUCache } from "./performance-utils.js";
@@ -576,7 +576,7 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
576
576
  $slots,
577
577
  __processedComponents: branchComponents,
578
578
  __sections: {},
579
- ...slotContentHasExpressions(slotContent) && { __stx_force_signals: !0 }
579
+ __stx_signals_gate: usesSignalsInScript(componentContent) || slotContentHasExpressions(slotContent)
580
580
  };
581
581
  applyDestructuredPropDefaults(componentContent, componentContext);
582
582
  let workingContent = componentContent;
@@ -725,7 +725,7 @@ finally {
725
725
  }`, factoryId = hydrateTrigger ? null : registerComponentClientFactory(parentContext, factoryBody), wrappedContent = factoryId ? `window.__stxComponentFactories[${JSON.stringify(factoryId)}](${JSON.stringify(scopeId)});` : `(${factoryBody})(${JSON.stringify(scopeId)});`;
726
726
  if (hydrateTrigger)
727
727
  return `${vendorStyleTags}<script type="stx/island" data-stx-island="${scopeId}" data-stx-scoped${attrs}>${wrappedContent}</script>`;
728
- return `${vendorStyleTags}<script data-stx-scoped${attrs}>${wrappedContent}</script>`;
728
+ return `${vendorStyleTags}<script data-stx-scoped data-stx-run="always"${attrs}>${wrappedContent}</script>`;
729
729
  }));
730
730
  output = output + `
731
731
  ` + scopedScripts.join(`
@@ -307,7 +307,13 @@ export async function extractVariables(scriptContent, context, filePath, options
307
307
  }
308
308
  return cached;
309
309
  };
310
- }, effect = (_fn) => {}, batch = (fn) => fn(), onMount = (_fn) => {}, onDestroy = (_fn) => {}, definePageMeta = (_meta) => {}, useRoute = () => {
310
+ }, effect = (_fn) => {}, batch = (fn) => fn(), onMount = (_fn) => {}, onDestroy = (_fn) => {};
311
+ function definePageMeta(meta) {
312
+ const config = meta || {};
313
+ if (config.title !== void 0 || config.description !== void 0)
314
+ useSeoMeta({ title: config.title, description: config.description });
315
+ }
316
+ const useRoute = () => {
311
317
  const serveCtx = context.__stxServeContext, search = context.__stxServeSearch ?? serveCtx?.search ?? "", path = serveCtx?.path ?? "";
312
318
  return {
313
319
  params: context.params ?? {},
@@ -537,6 +543,7 @@ export function convertToCommonJS(scriptContent, filePath) {
537
543
  }
538
544
  if (line.startsWith("definePageMeta(")) {
539
545
  let depth = 0, done = !1;
546
+ const callStart = i;
540
547
  for (let j = i;j < lines.length && !done; j++) {
541
548
  let inString = null, escaped = !1;
542
549
  const text = lines[j];
@@ -573,6 +580,15 @@ export function convertToCommonJS(scriptContent, filePath) {
573
580
  }
574
581
  if (!done)
575
582
  i = lines.length;
583
+ const callText = lines.slice(callStart, i).join(`
584
+ `), lifted = [];
585
+ for (const key of ["title", "description"]) {
586
+ const match = new RegExp(`\\b${key}\\s*:\\s*(['"\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1`).exec(callText);
587
+ if (match)
588
+ lifted.push(`${key}: ${JSON.stringify(match[2])}`);
589
+ }
590
+ if (lifted.length > 0)
591
+ convertedLines.push(`useSeoMeta({ ${lifted.join(", ")} });`);
576
592
  continue;
577
593
  }
578
594
  if (line.startsWith("import ") || line === "import" || line.startsWith("import{")) {
package/dist/x-element.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export function generateXElementRuntime() {
2
- return `<script data-stx-scoped data-stx-x-element-runtime>
2
+ return `<script data-stx-scoped data-stx-x-element-runtime data-stx-run="always">
3
3
  (function() {
4
4
  'use strict';
5
5
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.153",
4
+ "version": "0.2.154",
5
5
  "description": "A performant UI Framework. Powered by Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -117,12 +117,12 @@
117
117
  "dependencies": {
118
118
  "@cwcss/crosswind": "^0.2.14",
119
119
  "@stacksjs/clapp": "^0.2.0",
120
- "@stacksjs/desktop": "0.2.153",
121
- "@stacksjs/sanitizer": "0.2.153",
120
+ "@stacksjs/desktop": "0.2.154",
121
+ "@stacksjs/sanitizer": "0.2.154",
122
122
  "@stacksjs/ts-i18n": "^0.1.11",
123
- "bun-plugin-stx": "0.2.153",
123
+ "bun-plugin-stx": "0.2.154",
124
124
  "bunfig": "^0.15.17",
125
- "stx-router": "0.2.153",
125
+ "stx-router": "0.2.154",
126
126
  "ts-broadcasting": "^0.0.9",
127
127
  "ts-images": "^0.1.9",
128
128
  "ts-syntax-highlighter": "^0.2.1"