@stacksjs/stx 0.2.175 → 0.2.177

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 (59) hide show
  1. package/dist/a11y.d.ts +72 -2
  2. package/dist/a11y.js +25 -10
  3. package/dist/builtins/attrs.d.ts +19 -0
  4. package/dist/builtins/attrs.js +18 -0
  5. package/dist/builtins/safe-image.js +2 -1
  6. package/dist/builtins/stx-image.js +2 -1
  7. package/dist/builtins/stx-link.js +2 -1
  8. package/dist/cli-flags.d.ts +34 -0
  9. package/dist/cli-flags.js +20 -0
  10. package/dist/cli.js +1132 -895
  11. package/dist/client-script.js +2 -1
  12. package/dist/component-registry.d.ts +9 -0
  13. package/dist/component-renderer.js +20 -1
  14. package/dist/composables/use-fetch.d.ts +6 -1
  15. package/dist/composables/use-fetch.js +40 -18
  16. package/dist/composables/use-query.d.ts +7 -2
  17. package/dist/cookie-serialize.d.ts +40 -0
  18. package/dist/cookie-serialize.js +18 -0
  19. package/dist/craft.js +670 -442
  20. package/dist/crosswind-config.d.ts +13 -0
  21. package/dist/crosswind-config.js +6 -2
  22. package/dist/dev-server/crosswind.js +2 -1
  23. package/dist/edge-runtime.d.ts +2 -9
  24. package/dist/edge-runtime.js +2 -16
  25. package/dist/includes.js +8 -4
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +3 -0
  28. package/dist/page-action.d.ts +10 -20
  29. package/dist/page-action.js +33 -9
  30. package/dist/process.js +2 -1
  31. package/dist/production-server.js +8 -7
  32. package/dist/props.d.ts +51 -8
  33. package/dist/pwa.js +637 -409
  34. package/dist/reactive.js +2 -2
  35. package/dist/resolve-stx-targets.d.ts +49 -0
  36. package/dist/resolve-stx-targets.js +38 -0
  37. package/dist/runtime-globals.d.ts +24 -0
  38. package/dist/runtime-globals.js +16 -0
  39. package/dist/runtime-injection.d.ts +36 -0
  40. package/dist/runtime-injection.js +14 -0
  41. package/dist/server-bindings.js +3 -0
  42. package/dist/signal-processing.js +38 -7
  43. package/dist/signals.js +157 -35
  44. package/dist/ssg.d.ts +23 -0
  45. package/dist/ssg.js +615 -383
  46. package/dist/ssr.js +2 -16
  47. package/dist/stx-virtual-ts.d.ts +72 -4
  48. package/dist/stx-virtual-ts.js +111 -5
  49. package/dist/stx.d.ts +66 -7
  50. package/dist/template-hydrator.d.ts +1 -1
  51. package/dist/template-hydrator.js +6 -6
  52. package/dist/theme-tokens.d.ts +88 -0
  53. package/dist/theme-tokens.js +71 -0
  54. package/dist/typecheck.d.ts +6 -1
  55. package/dist/typecheck.js +102 -67
  56. package/dist/types/config-types.d.ts +24 -0
  57. package/dist/utils.js +5 -4
  58. package/package.json +5 -5
  59. package/stx.d.ts +66 -7
package/dist/typecheck.js CHANGED
@@ -3,25 +3,42 @@ import path from "node:path";
3
3
  import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
4
4
  import { stateDir } from "./state-dir.js";
5
5
  import {
6
+ absolutizeRelativeSpecifiers,
6
7
  buildVirtualTypeScript,
7
8
  extractScriptBlocks,
8
9
  resolvePosition,
9
10
  clientPayloadDeclarations,
10
11
  scrapedBridgeNames,
11
12
  serverContextDeclarations,
13
+ blankScriptDirectives,
12
14
  substituteInterpolationsInPlace
13
15
  } from "./stx-virtual-ts.js";
14
16
  export {
15
17
  extractScriptBlocks,
16
18
  extractTemplateExpressions
17
19
  } from "./stx-virtual-ts.js";
18
- export function buildVirtualSource(block, serverCode = "") {
20
+ export function buildVirtualSource(block, serverCode = "", originDir = "") {
19
21
  const leadingNewlines = block.startLine > 1 ? `
20
- `.repeat(block.startLine - 1) : "", code = substituteInterpolationsInPlace(block.code), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode) : "";
21
- return `${leadingNewlines + code}
22
+ `.repeat(block.startLine - 1) : "", code = blankScriptDirectives(substituteInterpolationsInPlace(block.code)), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode, block.code) : "";
23
+ return absolutizeRelativeSpecifiers(`${leadingNewlines + code}
22
24
  ${payload}
23
25
  export {}
24
- `;
26
+ `, originDir);
27
+ }
28
+ function virtualStxModuleDeclaration(files) {
29
+ const anchor = files.length > 0 ? path.dirname(path.resolve(files[0])) : process.cwd();
30
+ try {
31
+ Bun.resolveSync("@stacksjs/stx", anchor);
32
+ } catch {
33
+ return "";
34
+ }
35
+ return [
36
+ "// Generated \u2014 `stx` is the virtual specifier client-script.ts strips.",
37
+ "declare module 'stx' {",
38
+ " export * from '@stacksjs/stx'",
39
+ "}"
40
+ ].join(`
41
+ `);
25
42
  }
26
43
  export function virtualPathFor(filePath, kind, index) {
27
44
  return `${filePath}.__stx_${kind}${index}.ts`;
@@ -39,6 +56,9 @@ export function findRuntimeTypeDeclarations() {
39
56
  return null;
40
57
  }
41
58
  const TSC_LINE_RE = /^(.*?)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.*)$/;
59
+ function isSyntactic(code) {
60
+ return code >= 1000 && code < 2000;
61
+ }
42
62
  function readProjectPathAliases(startDir) {
43
63
  let dir = path.resolve(startDir), configPath;
44
64
  for (;; ) {
@@ -130,17 +150,15 @@ export async function typecheckStxFiles(files, options = {}) {
130
150
  const source = await Bun.file(file).text(), blocks = extractScriptBlocks(source).filter((b) => {
131
151
  if (b.kind === "server")
132
152
  return checkServer;
133
- if (b.kind === "client")
134
- return checkClient;
135
- return !1;
153
+ return checkClient;
136
154
  }), serverCode = extractScriptBlocks(source).filter((block) => block.kind === "server").map((block) => block.code).join(`
137
155
  `);
138
156
  if (checkClient && serverCode) {
139
- const clientBlocks = blocks.filter((block) => block.kind === "client"), names = [...new Set(clientBlocks.flatMap((block) => scrapedBridgeNames(serverCode, block.code)))];
157
+ const clientBlocks = blocks.filter((block) => block.kind !== "server"), names = [...new Set(clientBlocks.flatMap((block) => scrapedBridgeNames(serverCode, block.code)))];
140
158
  if (names.length > 0)
141
- scrapedByFile.set(file, { names, line: clientBlocks[0].startLine });
159
+ scrapedByFile.set(file, { names, line: clientBlocks[0].startLine, kind: clientBlocks[0].kind });
142
160
  }
143
- const templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
161
+ const originDir = path.dirname(path.resolve(file)), templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes, originDir }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
144
162
  if (blocks.length === 0 && expressions === 0)
145
163
  continue;
146
164
  checkedFiles.push(file);
@@ -148,7 +166,7 @@ export async function typecheckStxFiles(files, options = {}) {
148
166
  expressionCount += expressions;
149
167
  blocks.forEach((block, i) => {
150
168
  virtualFiles.set(virtualPathFor(file, block.kind, i), {
151
- source: buildVirtualSource(block, serverCode),
169
+ source: buildVirtualSource(block, serverCode, originDir),
152
170
  origin: file,
153
171
  kind: block.kind
154
172
  });
@@ -169,8 +187,9 @@ export async function typecheckStxFiles(files, options = {}) {
169
187
  const globalsDts = `${stateDir()}/typecheck/__stx_globals.d.ts`, globalDecls = [
170
188
  "// Generated \u2014 the context serve.ts injects into <script server> blocks.",
171
189
  serverContextDeclarations(),
172
- ...runtimeTypes ? [] : ["declare const window: any", ...STX_RUNTIME_GLOBALS.map((name) => `declare const ${name}: any`)]
173
- ].join(`
190
+ ...runtimeTypes ? [] : ["declare const window: any", ...STX_RUNTIME_GLOBALS.map((name) => `declare const ${name}: any`)],
191
+ virtualStxModuleDeclaration(files)
192
+ ].filter(Boolean).join(`
174
193
  `), workDir = `${stateDir()}/typecheck`;
175
194
  await Bun.$`rm -rf ${workDir}`.quiet().nothrow();
176
195
  await Bun.$`mkdir -p ${workDir}`.quiet().nothrow();
@@ -182,64 +201,80 @@ export async function typecheckStxFiles(files, options = {}) {
182
201
  await Bun.write(`${workDir}/${safe}`, meta.source);
183
202
  writtenToOrigin.set(safe, meta);
184
203
  }
185
- const tsconfig = {
186
- compilerOptions: {
187
- target: "ESNext",
188
- module: "ESNext",
189
- moduleResolution: "bundler",
190
- strict: !0,
191
- noEmit: !0,
192
- skipLibCheck: !0,
193
- allowJs: !0,
194
- esModuleInterop: !0,
195
- resolveJsonModule: !0,
196
- lib: ["ESNext", "DOM", "DOM.Iterable"],
197
- types: [],
198
- ...readProjectPathAliases(files.length > 0 ? path.dirname(path.resolve(files[0])) : process.cwd()),
199
- ...options.compilerOptions ?? {}
200
- },
201
- files: [...writtenToOrigin.keys()].map((name) => `${workDir}/${name}`).concat(globalsDts, ...ambient)
202
- }, tsconfigPath = `${workDir}/tsconfig.json`;
203
- await Bun.write(tsconfigPath, JSON.stringify(tsconfig, null, 2));
204
- const proc = Bun.spawnSync(["bun", "x", "tsc", "-p", tsconfigPath, "--pretty", "false"], {
205
- stdout: "pipe",
206
- stderr: "pipe"
207
- }), output = new TextDecoder().decode(proc.stdout) + new TextDecoder().decode(proc.stderr), diagnostics = [], exitCode = proc.exitCode ?? 0;
204
+ const compilerOptions = {
205
+ target: "ESNext",
206
+ module: "ESNext",
207
+ moduleResolution: "bundler",
208
+ strict: !0,
209
+ noEmit: !0,
210
+ skipLibCheck: !0,
211
+ allowJs: !0,
212
+ esModuleInterop: !0,
213
+ resolveJsonModule: !0,
214
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
215
+ types: [],
216
+ ...readProjectPathAliases(files.length > 0 ? path.dirname(path.resolve(files[0])) : process.cwd()),
217
+ ...options.compilerOptions ?? {}
218
+ }, runTsc = async (names, pass) => {
219
+ const tsconfigPath = `${workDir}/tsconfig${pass === 1 ? "" : `.${pass}`}.json`;
220
+ await Bun.write(tsconfigPath, JSON.stringify({
221
+ compilerOptions,
222
+ files: names.map((name) => `${workDir}/${name}`).concat(globalsDts, ...ambient)
223
+ }, null, 2));
224
+ const proc = Bun.spawnSync(["bun", "x", "tsc", "-p", tsconfigPath, "--pretty", "false"], {
225
+ stdout: "pipe",
226
+ stderr: "pipe"
227
+ });
228
+ return {
229
+ output: new TextDecoder().decode(proc.stdout) + new TextDecoder().decode(proc.stderr),
230
+ exitCode: proc.exitCode ?? 0
231
+ };
232
+ }, first = await runTsc([...writtenToOrigin.keys()], 1), output = first.output, diagnostics = [], exitCode = first.exitCode;
208
233
  let printedDiagnostics = 0;
209
234
  const ambientBasenames = new Set(ambient.map((lib) => lib.split("/").pop()));
210
235
  let ambientDiagnostics = 0;
211
- for (const rawLine of output.split(`
236
+ const unparseable = new Set, parseTscOutput = (text) => {
237
+ for (const rawLine of text.split(`
212
238
  `)) {
213
- const m = rawLine.match(TSC_LINE_RE);
214
- if (!m)
215
- continue;
216
- printedDiagnostics++;
217
- if (ambientBasenames.has(m[1].split("/").pop()))
218
- ambientDiagnostics++;
219
- const [, filePart, lineStr, colStr, category, code, message] = m, entry = writtenToOrigin.get(filePart.split("/").pop());
220
- if (!entry)
221
- continue;
222
- let line = Number(lineStr), column = Number(colStr), expression;
223
- if (entry.virtual) {
224
- const at = resolvePosition(entry.virtual, line, column);
225
- if (!at?.expression)
239
+ const m = rawLine.match(TSC_LINE_RE);
240
+ if (!m)
226
241
  continue;
227
- if (Number(code) === 2774)
242
+ printedDiagnostics++;
243
+ if (ambientBasenames.has(m[1].split("/").pop()))
244
+ ambientDiagnostics++;
245
+ const [, filePart, lineStr, colStr, category, code, message] = m, written = filePart.split("/").pop(), entry = writtenToOrigin.get(written);
246
+ if (!entry)
228
247
  continue;
229
- line = at.line;
230
- column = at.column;
231
- expression = at.expression.code.trim();
248
+ if (isSyntactic(Number(code)))
249
+ unparseable.add(written);
250
+ let line = Number(lineStr), column = Number(colStr), expression;
251
+ if (entry.virtual) {
252
+ const at = resolvePosition(entry.virtual, line, column);
253
+ if (!at?.expression)
254
+ continue;
255
+ if (Number(code) === 2774)
256
+ continue;
257
+ line = at.line;
258
+ column = at.column;
259
+ expression = at.expression.code.trim();
260
+ }
261
+ diagnostics.push({
262
+ file: entry.origin,
263
+ line,
264
+ column,
265
+ code: Number(code),
266
+ message,
267
+ category: category === "error" ? "error" : "warning",
268
+ blockKind: entry.kind,
269
+ expression
270
+ });
232
271
  }
233
- diagnostics.push({
234
- file: entry.origin,
235
- line,
236
- column,
237
- code: Number(code),
238
- message,
239
- category: category === "error" ? "error" : "warning",
240
- blockKind: entry.kind,
241
- expression
242
- });
272
+ };
273
+ parseTscOutput(output);
274
+ const firstPassPrinted = printedDiagnostics, firstPassAmbient = ambientDiagnostics;
275
+ if (unparseable.size > 0 && unparseable.size < writtenToOrigin.size) {
276
+ const survivors = [...writtenToOrigin.keys()].filter((name) => !unparseable.has(name));
277
+ parseTscOutput((await runTsc(survivors, 2)).output);
243
278
  }
244
279
  if (checkClient)
245
280
  for (const [file, scraped] of scrapedByFile) {
@@ -252,7 +287,7 @@ export async function typecheckStxFiles(files, options = {}) {
252
287
  code: 0,
253
288
  message: `${scraped.names.length === 1 ? "1 value reaches" : `${scraped.names.length} values reach`} this block on the implicit bridge and ${scraped.names.length === 1 ? "is" : "are"} untyped: ${scraped.names.join(", ")}. Declare them with defineClientPayload({ ${scraped.names.join(", ")} }) in the server block to have them checked.`,
254
289
  category: "warning",
255
- blockKind: "client"
290
+ blockKind: scraped.kind
256
291
  });
257
292
  }
258
293
  const seen = new Set, unique = diagnostics.filter((d) => {
@@ -263,7 +298,7 @@ export async function typecheckStxFiles(files, options = {}) {
263
298
  return !0;
264
299
  });
265
300
  unique.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
266
- const brokenLib = ambientDiagnostics > 0 && unique.length === 0, failure = brokenLib || exitCode !== 0 && printedDiagnostics === 0 ? `${brokenLib ? "a file passed with --lib has errors of its own, so nothing in your code was checked" : `tsc exited ${exitCode} without reporting any diagnostic, so nothing was checked`}. ` + "A syntax error in an ambient declaration file does this \u2014 skipLibCheck suppresses " + `SEMANTIC diagnostics only. tsc said:
301
+ const brokenLib = firstPassAmbient > 0 && unique.length === 0, failure = brokenLib || exitCode !== 0 && firstPassPrinted === 0 ? `${brokenLib ? "a file passed with --lib has errors of its own, so nothing in your code was checked" : `tsc exited ${exitCode} without reporting any diagnostic, so nothing was checked`}. ` + "A syntax error in an ambient declaration file does this \u2014 skipLibCheck suppresses " + `SEMANTIC diagnostics only. tsc said:
267
302
  ${output.trim().split(`
268
303
  `).slice(0, 12).join(`
269
304
  `) || "(no output)"}` : void 0;
@@ -271,7 +306,7 @@ ${output.trim().split(`
271
306
  }
272
307
  export function formatTypecheckDiagnostics(diagnostics) {
273
308
  return diagnostics.map((d) => {
274
- const where = d.blockKind === "template" ? `[template: ${d.expression ?? ""}]` : `[<script ${d.blockKind}>]`;
309
+ const where = d.blockKind === "template" ? `[template: ${d.expression ?? ""}]` : `[${d.blockKind === "plain" ? "<script>" : `<script ${d.blockKind}>`}]`;
275
310
  return `${d.file}:${d.line}:${d.column} ${d.category} TS${d.code} ${d.message} ${where}`;
276
311
  }).join(`
277
312
  `);
@@ -594,6 +594,30 @@ export interface StxConfig {
594
594
  enabled: boolean;
595
595
  /** Path to partials directory, defaults to 'partials' in the same directory as the template */
596
596
  partialsDir: string;
597
+ /**
598
+ * Called when an `@include` fails, instead of the failure being silent.
599
+ *
600
+ * A failed include renders the error where the markup should have been. In dev
601
+ * that is the right call — you see it as soon as you look. In a build it
602
+ * produces a file nobody would knowingly deploy, and nothing else can see it:
603
+ * one app shipped pre-rendered pages whose header, footer, hero and CTA were
604
+ * all replaced by an ENOENT banner while the build printed `Failed: 0` and
605
+ * exited 0 (stacksjs/stx#1921).
606
+ *
607
+ * The rendered output is unchanged — this only carries the fact out, so a
608
+ * caller that builds files can decide to fail. `partialsDir` is included
609
+ * because "it looked in the wrong place" is the usual cause and the resolved
610
+ * path is what makes it a ten-second fix.
611
+ */
612
+ onIncludeError?: (failure: {
613
+ /** The path as written in the directive, e.g. `Bench/ConfirmHost`. */
614
+ includePath: string;
615
+ /** The template the directive appeared in. */
616
+ templatePath: string;
617
+ message: string;
618
+ /** The directory the include was resolved against. */
619
+ partialsDir: string;
620
+ }) => void;
597
621
  /** Path to components directory, defaults to 'components' in the same directory as the template */
598
622
  componentsDir: string;
599
623
  /** @internal Configured component root retained while nested components resolve locally. */
package/dist/utils.js CHANGED
@@ -558,7 +558,8 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
558
558
  const internalKeysToPreserve = new Set([
559
559
  "__originalFilePath",
560
560
  "__importedComponents",
561
- COMPONENT_CLIENT_FACTORIES_CONTEXT_KEY
561
+ COMPONENT_CLIENT_FACTORIES_CONTEXT_KEY,
562
+ "__stx_client_signal_names"
562
563
  ]), filteredParentContext = {};
563
564
  for (const [key, val] of Object.entries(parentContext))
564
565
  if (!key.startsWith("__") || internalKeysToPreserve.has(key))
@@ -648,9 +649,9 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
648
649
  templateContent = templateContent.replace(/\{\{\s*text\s*\|\|\s*slot\s*\}\}/g, () => slotContent);
649
650
  const { applySlots } = await importOnce("stx/slots", () => import("./slots.js")), { defaultSlot, namedSlots } = parsedSlotContent;
650
651
  templateContent = await applySlots(templateContent, defaultSlot, namedSlots, componentContext);
651
- const hasSignalScripts = clientScripts.some((s) => /\b(?:state|derived|effect|ref|reactive|computed|watch|watchEffect|useReactiveProp|defineProps|withDefaults|defineEmits|defineExpose|defineSlots)\s*(?:<[^<>()]*>)?\s*\(/.test(s)), clientSignalNames = componentClientSignalNames(clientScripts);
652
- if (clientSignalNames.length > 0)
653
- componentContext.__stx_client_signal_names = clientSignalNames;
652
+ const hasSignalScripts = clientScripts.some((s) => /\b(?:state|derived|effect|ref|reactive|computed|watch|watchEffect|useReactiveProp|defineProps|withDefaults|defineEmits|defineExpose|defineSlots)\s*(?:<[^<>()]*>)?\s*\(/.test(s)), clientSignalNames = componentClientSignalNames(clientScripts), inheritedClientSignalNames = Array.isArray(componentContext.__stx_client_signal_names) ? componentContext.__stx_client_signal_names : [];
653
+ if (clientSignalNames.length > 0 || inheritedClientSignalNames.length > 0)
654
+ componentContext.__stx_client_signal_names = [...new Set([...inheritedClientSignalNames, ...clientSignalNames])];
654
655
  const componentOptions = {
655
656
  ...options,
656
657
  _rootComponentsDir: options._rootComponentsDir || options.componentsDir,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.175",
4
+ "version": "0.2.177",
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.175",
121
- "@stacksjs/sanitizer": "0.2.175",
120
+ "@stacksjs/desktop": "0.2.177",
121
+ "@stacksjs/sanitizer": "0.2.177",
122
122
  "@stacksjs/ts-i18n": "^0.1.11",
123
- "bun-plugin-stx": "0.2.175",
123
+ "bun-plugin-stx": "0.2.177",
124
124
  "bunfig": "^0.15.17",
125
- "stx-router": "0.2.175",
125
+ "stx-router": "0.2.177",
126
126
  "ts-broadcasting": "^0.0.9",
127
127
  "ts-images": "^0.1.9",
128
128
  "ts-syntax-highlighter": "^0.2.1"
package/stx.d.ts CHANGED
@@ -199,11 +199,42 @@ declare function useSearchParams(): {
199
199
  // Data fetching
200
200
  // ============================================================================
201
201
 
202
- interface StxQueryResult<T> {
202
+ /** How one run of a request should behave. */
203
+ interface StxRefetchOptions {
204
+ /**
205
+ * Refresh without touching `loading` or clearing the current error.
206
+ *
207
+ * A poll refreshes data that is already on screen, so driving the first-load
208
+ * state from it puts a spinner over a populated view on every tick — the
209
+ * reason a polling view could not use these primitives at all (#1929). Bind
210
+ * `isFetching` for a subtle in-flight indicator instead.
211
+ */
212
+ background?: boolean
213
+ }
214
+
215
+ interface StxFetchResult<T> {
203
216
  data: StxSignal<T | null>
217
+ /** True while a first-load request is in flight. A background one leaves it alone. */
204
218
  loading: StxSignal<boolean>
219
+ /** True while ANY request is in flight, background ones included. */
220
+ isFetching: StxSignal<boolean>
205
221
  error: StxSignal<Error | null>
206
- refetch: () => Promise<void>
222
+ refetch: (_options?: StxRefetchOptions) => Promise<void>
223
+ }
224
+
225
+ /*
226
+ * `useQuery` returns more than `useFetch` does, and declaring one shape for
227
+ * both hid it: `isStale` and `invalidate` existed at runtime and were invisible
228
+ * to the type, so reading either was an error on a value that was really there
229
+ * (#1929). Extending rather than widening the shared interface, because the
230
+ * inverse mistake is worse — declaring `invalidate` on `useFetch` would
231
+ * type-check a call to something that is undefined at runtime.
232
+ */
233
+ interface StxQueryResult<T> extends StxFetchResult<T> {
234
+ /** True when cached data is on screen while a revalidation runs. */
235
+ isStale: StxSignal<boolean>
236
+ /** Drop this key from the cache and refetch. */
237
+ invalidate: (_options?: StxRefetchOptions) => Promise<void>
207
238
  }
208
239
 
209
240
  interface StxMutationResult<T> {
@@ -213,7 +244,7 @@ interface StxMutationResult<T> {
213
244
  mutate: (body?: unknown) => Promise<T>
214
245
  }
215
246
 
216
- declare function useFetch<T = any>(_url: string, _options?: any): StxQueryResult<T>
247
+ declare function useFetch<T = any>(_url: string, _options?: any): StxFetchResult<T>
217
248
  declare function useQuery<T = any>(_url: string, _options?: any): StxQueryResult<T>
218
249
  declare function useMutation<T = any>(_url: string, _options?: any): StxMutationResult<T>
219
250
 
@@ -271,17 +302,45 @@ declare function useOptimistic<T = any, A = any>(
271
302
  // DOM utilities
272
303
  // ============================================================================
273
304
 
305
+ /**
306
+ * Options the runtime reads, including the target it binds to.
307
+ *
308
+ * The target travels HERE rather than in a leading parameter, because that is
309
+ * what the implementation does: `var target = (options && options.target) || window`.
310
+ */
311
+ interface StxEventListenerOptions {
312
+ /** Element, selector, or document/window. Defaults to `window`. */
313
+ target?: Window | Document | HTMLElement | string | null
314
+ capture?: boolean
315
+ passive?: boolean
316
+ once?: boolean
317
+ }
318
+
319
+ /*
320
+ * Event first, then handler, then options (stacksjs/stx#1923).
321
+ *
322
+ * This was declared target-first while the runtime has always been
323
+ * event-first, so the two disagreed in the worst possible direction: `_target`
324
+ * accepted `string`, so the CORRECT call got as far as reading 'keydown' as a
325
+ * selector before failing on the handler, and an author who trusted the type
326
+ * and added a target wrote `useEventListener(window, 'keydown', fn)` — which
327
+ * binds an event named "window" with the string 'keydown' as its handler, and
328
+ * silently attaches nothing.
329
+ *
330
+ * `browser-composables.ts` had it right all along; the package shipped two
331
+ * declarations of one name with different arities and the registry got the
332
+ * wrong one. Same shape as the `reactive` split: one name, two conventions, and
333
+ * no way for the call site to tell which it has.
334
+ */
274
335
  declare function useEventListener<K extends keyof WindowEventMap>(
275
- _target: Window | Document | HTMLElement | string | null,
276
336
  _event: K,
277
337
  _handler: (event: WindowEventMap[K]) => void,
278
- _options?: boolean | AddEventListenerOptions,
338
+ _options?: StxEventListenerOptions,
279
339
  ): StxCleanup
280
340
  declare function useEventListener(
281
- _target: Window | Document | HTMLElement | string | null,
282
341
  _event: string,
283
342
  _handler: (event: Event) => void,
284
- _options?: boolean | AddEventListenerOptions,
343
+ _options?: StxEventListenerOptions,
285
344
  ): StxCleanup
286
345
  declare function useScrollLock(
287
346
  _target?: HTMLElement | null | { current?: HTMLElement | null, value?: HTMLElement | null } | (() => HTMLElement | null | undefined),