@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.
Files changed (88) hide show
  1. package/dist/animation.js +6 -6
  2. package/dist/app-shell.js +2 -1
  3. package/dist/browser-composables.d.ts +8 -0
  4. package/dist/build-id.d.ts +49 -0
  5. package/dist/build-id.js +20 -0
  6. package/dist/build-message.d.ts +10 -0
  7. package/dist/build-message.js +26 -0
  8. package/dist/builtins/attrs.js +6 -1
  9. package/dist/cli.js +1743 -1255
  10. package/dist/client-script-bundler.js +2 -4
  11. package/dist/client-script.d.ts +29 -0
  12. package/dist/client-script.js +142 -97
  13. package/dist/color-mode-boot.d.ts +46 -10
  14. package/dist/color-mode-boot.js +53 -13
  15. package/dist/component-client-factories.js +3 -2
  16. package/dist/component-renderer.js +10 -21
  17. package/dist/composables/index.d.ts +8 -1
  18. package/dist/composables/use-clipboard.d.ts +1 -5
  19. package/dist/composables/use-clipboard.js +57 -46
  20. package/dist/composables/use-color-mode.d.ts +19 -4
  21. package/dist/composables/use-color-mode.js +49 -28
  22. package/dist/composables/use-router.d.ts +28 -11
  23. package/dist/composables/use-router.js +63 -9
  24. package/dist/composables/use-storage.d.ts +14 -15
  25. package/dist/composables/use-storage.js +63 -2
  26. package/dist/config.js +22 -7
  27. package/dist/craft.js +1319 -841
  28. package/dist/dev-server/crosswind.js +8 -6
  29. package/dist/dev-server/serve-app.js +13 -7
  30. package/dist/dev-server/serve-markdown.js +4 -1
  31. package/dist/dev-server/serve-multi.js +32 -20
  32. package/dist/dev-server/types.d.ts +8 -1
  33. package/dist/document-shell.d.ts +120 -0
  34. package/dist/document-shell.js +161 -14
  35. package/dist/dynamic-components.js +123 -4
  36. package/dist/events.d.ts +26 -49
  37. package/dist/events.js +23 -9
  38. package/dist/expressions.d.ts +1 -1
  39. package/dist/expressions.js +3 -3
  40. package/dist/framework-composables.d.ts +41 -0
  41. package/dist/framework-composables.js +115 -0
  42. package/dist/head.js +5 -1
  43. package/dist/hot-reload.js +8 -2
  44. package/dist/includes.js +4 -4
  45. package/dist/index.d.ts +17 -0
  46. package/dist/index.js +9 -0
  47. package/dist/isolated-build.d.ts +12 -0
  48. package/dist/isolated-build.js +18 -0
  49. package/dist/plugin-system.d.ts +1 -1
  50. package/dist/process.js +46 -12
  51. package/dist/production-builder.js +3 -1
  52. package/dist/production-server.js +4 -1
  53. package/dist/prop-sentinels.d.ts +31 -0
  54. package/dist/prop-sentinels.js +7 -0
  55. package/dist/pwa.js +1258 -780
  56. package/dist/reactive.js +1 -1
  57. package/dist/reactivity.d.ts +0 -15
  58. package/dist/reactivity.js +15 -6
  59. package/dist/runtime-globals.d.ts +40 -2
  60. package/dist/runtime-globals.js +90 -0
  61. package/dist/runtime-injection.d.ts +51 -0
  62. package/dist/runtime-injection.js +13 -0
  63. package/dist/script-emit.d.ts +20 -0
  64. package/dist/script-emit.js +11 -0
  65. package/dist/script-validation.js +12 -7
  66. package/dist/signal-processing.js +25 -40
  67. package/dist/signals-api.d.ts +30 -0
  68. package/dist/signals-api.js +18 -0
  69. package/dist/signals.js +532 -85
  70. package/dist/site-builder/build.js +21 -7
  71. package/dist/site-builder/seo.js +19 -10
  72. package/dist/site-builder/theme.js +5 -4
  73. package/dist/site-builder/types.d.ts +1 -0
  74. package/dist/ssg.js +1255 -777
  75. package/dist/state-management.js +4 -1
  76. package/dist/store-loader.d.ts +0 -11
  77. package/dist/store-loader.js +27 -3
  78. package/dist/strict-lint.d.ts +9 -6
  79. package/dist/strict-lint.js +2 -21
  80. package/dist/stx.d.ts +125 -22
  81. package/dist/types/config-types.d.ts +1 -0
  82. package/dist/unresolved-identifiers.d.ts +50 -0
  83. package/dist/unresolved-identifiers.js +146 -0
  84. package/dist/utils.js +3 -3
  85. package/dist/variable-extractor.js +17 -1
  86. package/dist/x-element.js +1 -1
  87. package/package.json +5 -5
  88. package/stx.d.ts +125 -22
@@ -3,6 +3,115 @@ import { safeEvaluate } from "./safe-evaluator.js";
3
3
  function escapeHtmlComment(value) {
4
4
  return value.replace(/--/g, "- -").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5
5
  }
6
+ const HTML_TAGS = new Set([
7
+ "a",
8
+ "abbr",
9
+ "address",
10
+ "article",
11
+ "aside",
12
+ "audio",
13
+ "b",
14
+ "bdi",
15
+ "bdo",
16
+ "blockquote",
17
+ "button",
18
+ "canvas",
19
+ "caption",
20
+ "cite",
21
+ "code",
22
+ "col",
23
+ "colgroup",
24
+ "data",
25
+ "datalist",
26
+ "dd",
27
+ "del",
28
+ "details",
29
+ "dfn",
30
+ "dialog",
31
+ "div",
32
+ "dl",
33
+ "dt",
34
+ "em",
35
+ "fieldset",
36
+ "figcaption",
37
+ "figure",
38
+ "footer",
39
+ "form",
40
+ "h1",
41
+ "h2",
42
+ "h3",
43
+ "h4",
44
+ "h5",
45
+ "h6",
46
+ "header",
47
+ "hgroup",
48
+ "hr",
49
+ "i",
50
+ "iframe",
51
+ "img",
52
+ "input",
53
+ "ins",
54
+ "kbd",
55
+ "label",
56
+ "legend",
57
+ "li",
58
+ "main",
59
+ "mark",
60
+ "menu",
61
+ "meter",
62
+ "nav",
63
+ "ol",
64
+ "optgroup",
65
+ "option",
66
+ "output",
67
+ "p",
68
+ "picture",
69
+ "pre",
70
+ "progress",
71
+ "q",
72
+ "rp",
73
+ "rt",
74
+ "ruby",
75
+ "s",
76
+ "samp",
77
+ "section",
78
+ "select",
79
+ "small",
80
+ "span",
81
+ "strong",
82
+ "sub",
83
+ "summary",
84
+ "sup",
85
+ "table",
86
+ "tbody",
87
+ "td",
88
+ "textarea",
89
+ "tfoot",
90
+ "th",
91
+ "thead",
92
+ "time",
93
+ "tr",
94
+ "u",
95
+ "ul",
96
+ "video"
97
+ ]), VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
98
+ function resolveIsName(expr, context) {
99
+ const mustache = /^\s*\{\{\s*([\s\S]+?)\s*\}\}\s*$/.exec(expr), source = mustache ? mustache[1] : expr, evaluated = safeEvaluate(source, context);
100
+ if (evaluated && typeof evaluated === "string")
101
+ return evaluated;
102
+ const literal = source.trim();
103
+ return HTML_TAGS.has(literal) ? literal : null;
104
+ }
105
+ function displayExpression(expr) {
106
+ const mustache = /^\s*\{\{\s*([\s\S]+?)\s*\}\}\s*$/.exec(expr);
107
+ return mustache ? mustache[1] : expr;
108
+ }
109
+ function renderAsElement(name, attrs, slotContent) {
110
+ const cleanedAttrs = attrs.replace(/\s*(?::is|v-bind:is)\s*=\s*"[^"]*"/gi, "").trim(), open = cleanedAttrs ? `<${name} ${cleanedAttrs}>` : `<${name}>`;
111
+ if (VOID_TAGS.has(name))
112
+ return cleanedAttrs ? `<${name} ${cleanedAttrs}>` : `<${name}>`;
113
+ return `${open}${slotContent}</${name}>`;
114
+ }
6
115
  export async function processDynamicComponents(template, context, filePath, options, dependencies) {
7
116
  let result = template;
8
117
  const componentPattern = /<component\s+(?::is|v-bind:is)\s*=\s*"([^"]*)"([^>]*)>([\s\S]*?)<\/component>/gi, matches = [];
@@ -18,12 +127,17 @@ export async function processDynamicComponents(template, context, filePath, opti
18
127
  for (let i = matches.length - 1;i >= 0; i--) {
19
128
  const m = matches[i];
20
129
  try {
21
- const componentName = safeEvaluate(m.expr, context);
130
+ const componentName = resolveIsName(m.expr, context);
22
131
  if (!componentName || typeof componentName !== "string") {
23
- const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(m.expr)}" -->`;
132
+ const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(displayExpression(m.expr))}" -->`;
24
133
  result = result.slice(0, m.index) + replacement + result.slice(m.index + m.full.length);
25
134
  continue;
26
135
  }
136
+ if (HTML_TAGS.has(componentName)) {
137
+ const rendered = renderAsElement(componentName, m.attrs, m.slotContent);
138
+ result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
139
+ continue;
140
+ }
27
141
  const propsStr = m.attrs.trim(), props = {}, propsPattern = /(?::([a-zA-Z_][a-zA-Z0-9_-]*)|([a-zA-Z_][a-zA-Z0-9_-]*))\s*=\s*"([^"]*)"/g;
28
142
  let propMatch;
29
143
  while ((propMatch = propsPattern.exec(propsStr)) !== null) {
@@ -61,12 +175,17 @@ async function processSelfClosingDynamicComponents(template, context, filePath,
61
175
  for (let i = matches.length - 1;i >= 0; i--) {
62
176
  const m = matches[i];
63
177
  try {
64
- const componentName = safeEvaluate(m.expr, context);
178
+ const componentName = resolveIsName(m.expr, context);
65
179
  if (!componentName || typeof componentName !== "string") {
66
- const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(m.expr)}" -->`;
180
+ const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(displayExpression(m.expr))}" -->`;
67
181
  result = result.slice(0, m.index) + replacement + result.slice(m.index + m.full.length);
68
182
  continue;
69
183
  }
184
+ if (HTML_TAGS.has(componentName)) {
185
+ const rendered = renderAsElement(componentName, m.attrs, "");
186
+ result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
187
+ continue;
188
+ }
70
189
  const componentsDir = options.componentsDir || "components", rendered = await renderComponentWithSlot(componentName, {}, "", componentsDir, context, filePath, options, new Set, dependencies ?? new Set);
71
190
  result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
72
191
  } catch (error) {
package/dist/events.d.ts CHANGED
@@ -1,3 +1,29 @@
1
+ /**
2
+ * `@click="doThing"` names a function, so it must be INVOKED — not evaluated.
3
+ *
4
+ * The declarative runtime learned this in #1695: without it the expression falls
5
+ * through to a generic `new Function` and runs as a discarded identifier
6
+ * statement, so the click silently does nothing. Every IMPERATIVE binding path
7
+ * still evaluated it, in three separate places — the inline `else` branch
8
+ * emitted by client-script.ts, the `__stx_runHandler` fallback below, and
9
+ * `__stx_execute` in the x-data bridge — so the declarative path disagreed with
10
+ * all three (#1824).
11
+ *
12
+ * Normalising here, at emit time, rather than inside each of those runtimes:
13
+ * the handler text is already known when it is emitted, so one transform covers
14
+ * every path, and the rule stays out of the generated template literals.
15
+ *
16
+ * Restricted to a plain identifier, matching the runtime's own check exactly —
17
+ * a dotted path would require `obj` to exist before `typeof obj.fn` could be
18
+ * asked. The `_isSignal` exclusion is the runtime's too, and it matters because
19
+ * signals ARE functions: without it `@click="count"` would read the signal
20
+ * instead of doing nothing.
21
+ */
22
+ export declare function asInvocableStatement(handler: string): string;
23
+ /**
24
+ * Generate the complete runtime script
25
+ */
26
+ export declare function generateRuntimeScript(events: ParsedEvent[]): string;
1
27
  /**
2
28
  * Extract and process event attributes from a template without generating
3
29
  * a standalone script. Returns the modified template (with @event attributes
@@ -33,55 +59,6 @@ export declare function processEventDirectives(template: string, context: Record
33
59
  * Check if a template contains event directives
34
60
  */
35
61
  export declare function hasEventDirectives(template: string): boolean;
36
- /* eslint-disable prefer-const, style/max-statements-per-line, no-super-linear-backtracking, regexp/no-unused-capturing-group */
37
- /**
38
- * Event Directives Module
39
- *
40
- * Provides Alpine.js-style event handling for STX templates.
41
- * Transforms `@click`, `@keydown.enter`, etc. into client-side event listeners.
42
- *
43
- * ## Supported Syntax
44
- *
45
- * ### Basic Events
46
- * ```html
47
- * <button @click="count++">Increment</button>
48
- * <button @click="handleClick()">Click me</button>
49
- * <form @submit="handleSubmit()">...</form>
50
- * ```
51
- *
52
- * ### Event Modifiers
53
- * ```html
54
- * <form @submit.prevent="handleSubmit()">...</form>
55
- * <a @click.prevent.stop="navigate()">Link</a>
56
- * <button @click.once="initialize()">Init</button>
57
- * <div @click.self="onlyThis()">...</div>
58
- * <input @keydown.capture="log()">
59
- * ```
60
- *
61
- * ### Key Modifiers
62
- * ```html
63
- * <input @keydown.enter="submit()">
64
- * <input @keyup.escape="cancel()">
65
- * <input @keydown.space="toggle()">
66
- * <input @keydown.ctrl.s="save()">
67
- * <input @keydown.meta.enter="send()">
68
- * ```
69
- *
70
- * ### Mouse Modifiers
71
- * ```html
72
- * <div @click.left="handleLeft()">
73
- * <div @click.right="handleRight()">
74
- * <div @click.middle="handleMiddle()">
75
- * ```
76
- *
77
- * ### Special Variables
78
- * ```html
79
- * <input @input="value = $event.target.value">
80
- * <button @click="handleClick($el)">Uses element</button>
81
- * ```
82
- *
83
- * @module events
84
- */
85
62
  // =============================================================================
86
63
  // Types
87
64
  // =============================================================================
package/dist/events.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { templateHasReactiveContext } from "./runtime-globals.js";
1
2
  const DOM_EVENTS = new Set([
2
3
  "click",
3
4
  "dblclick",
@@ -185,8 +186,14 @@ function wrapHandler(handler, modifiers) {
185
186
  wrapped = `__stx_throttle(() => { ${wrapped} }, ${modifiers.throttle})`;
186
187
  return wrapped;
187
188
  }
189
+ export function asInvocableStatement(handler) {
190
+ const name = handler.trim();
191
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
192
+ return handler;
193
+ return `if (typeof ${name} === 'function' && !${name}._isSignal) ${name}($event);`;
194
+ }
188
195
  function generateEventListener(event) {
189
- const { modifiers } = event, elementId = event.elementId.replace(/\\/g, "\\\\").replace(/'/g, "\\'"), checks = generateModifierChecks(event), handler = wrapHandler(event.handler, modifiers), options = [];
196
+ const { modifiers } = event, elementId = event.elementId.replace(/\\/g, "\\\\").replace(/'/g, "\\'"), checks = generateModifierChecks(event), handler = wrapHandler(asInvocableStatement(event.handler), modifiers), options = [];
190
197
  if (modifiers.once)
191
198
  options.push("once: true");
192
199
  if (modifiers.capture)
@@ -254,8 +261,8 @@ else {
254
261
  }${optionsStr});
255
262
  })();`;
256
263
  }
257
- function generateRuntimeScript(elements) {
258
- if (elements.length === 0)
264
+ export function generateRuntimeScript(events) {
265
+ if (events.length === 0)
259
266
  return "";
260
267
  return `
261
268
  <script data-stx-events>
@@ -284,9 +291,16 @@ function generateRuntimeScript(elements) {
284
291
  };
285
292
  }
286
293
 
287
- // Non-reactive fallback runner. The handler is embedded HTML-entity-encoded so
288
- // the surrounding <script> can never contain raw markup (e.g. </script>); we
289
- // decode it to JS only in memory here, then run it with $event/$el in scope.
294
+ // Non-reactive fallback runner. The handler is embedded HTML-entity-encoded
295
+ // so the surrounding script element can never contain raw markup, including
296
+ // its own closing tag; we decode it to JS only in memory here, then run it
297
+ // with $event/$el in scope.
298
+ //
299
+ // This comment must not spell that closing tag out. It used to, as an
300
+ // example, and a browser ends the script element at the FIRST one it sees \u2014
301
+ // so everything below this line, __stx_runHandler included, was emitted as
302
+ // page text and no listener was ever bound. The comment about escaping was
303
+ // the thing that needed escaping.
290
304
  function __stx_runHandler(h, $event, $el) {
291
305
  var code = h
292
306
  .replace(/&lt;/g, '<')
@@ -305,7 +319,7 @@ function generateRuntimeScript(elements) {
305
319
  }
306
320
 
307
321
  // Event listeners
308
- ${elements.flatMap((el) => el.events).map(generateEventListener).join(`
322
+ ${events.map(generateEventListener).join(`
309
323
  `)}
310
324
  })();
311
325
  </script>`;
@@ -338,7 +352,7 @@ export function extractAndProcessEvents(template) {
338
352
  return { template: output, bindings: allBindings };
339
353
  }
340
354
  export function processEventDirectives(template, context, _filePath) {
341
- if (/\b(?:state|derived|effect)\s*(?:<[^<>()]*>)?\s*\(/.test(template) || /@(?:model|show|for|if)\s*=/.test(template) || /data-stx(?:-auto)?(?![-\w])/.test(template))
355
+ if (templateHasReactiveContext(template))
342
356
  return template;
343
357
  if (context.__stx_sfc_mode) {
344
358
  if (/x-data\s*=\s*["']/.test(template))
@@ -369,7 +383,7 @@ export function processEventDirectives(template, context, _filePath) {
369
383
  }
370
384
  output = output.slice(0, element.startIndex) + newTag + output.slice(element.endIndex);
371
385
  }
372
- const script = generateRuntimeScript(elements), bodyIdx = output.lastIndexOf("</body>");
386
+ const script = generateRuntimeScript(elements.flatMap((el) => el.events)), bodyIdx = output.lastIndexOf("</body>");
373
387
  if (bodyIdx !== -1)
374
388
  output = output.slice(0, bodyIdx) + script + `
375
389
  ` + output.slice(bodyIdx);
@@ -80,7 +80,7 @@ export declare function interpolateScriptsInTemplate(template: string, context:
80
80
  /**
81
81
  * Process template expressions including variables, filters, and operations
82
82
  */
83
- export declare function processExpressions(template: string, context: Record<string, any>, filePath: string, options?: { forceSignals?: boolean }): string;
83
+ export declare function processExpressions(template: string, context: Record<string, any>, filePath: string): string;
84
84
  /**
85
85
  * Apply filters to a value
86
86
  */
@@ -318,7 +318,7 @@ export function interpolateScriptExpressions(scriptBody, context) {
318
318
  const value = evaluateExpression(expr, context);
319
319
  if (value === void 0)
320
320
  return match;
321
- return JSON.stringify(value);
321
+ return JSON.stringify(value).replace(/</g, "\\u003c");
322
322
  } catch {
323
323
  return match;
324
324
  }
@@ -344,7 +344,7 @@ export function interpolateScriptsInTemplate(template, context, options = {}) {
344
344
  });
345
345
  return restoreStashedScripts(output, interpolatedScripts);
346
346
  }
347
- export function processExpressions(template, context, filePath, options) {
347
+ export function processExpressions(template, context, filePath) {
348
348
  let output = template;
349
349
  const styleMatcher = (html, i) => {
350
350
  const end = matchStyleElement(html, i);
@@ -359,7 +359,7 @@ export function processExpressions(template, context, filePath, options) {
359
359
  output = maskInto(output, styleMatcher, styleBlocks, (n) => `<!--__STX_STYLE_${n}__-->`);
360
360
  const scriptBlocks = [];
361
361
  output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
362
- const hasSignals = options?.forceSignals === !0 || context?.__stx_force_signals === !0 || Array.isArray(context?.__stx_client_signal_names) || usesSignalsInScript(template), clientSignalNames = new Set(Array.isArray(context?.__stx_client_signal_names) ? context.__stx_client_signal_names : []), preservedClientExpressions = new Set(Array.isArray(context?.__stx_preserved_client_expressions) ? context.__stx_preserved_client_expressions : []);
362
+ const hasSignals = context?.__stx_signals_gate === !0 || usesSignalsInScript(template), clientSignalNames = new Set(Array.isArray(context?.__stx_client_signal_names) ? context.__stx_client_signal_names : []), preservedClientExpressions = new Set(Array.isArray(context?.__stx_preserved_client_expressions) ? context.__stx_preserved_client_expressions : []);
363
363
  output = output.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (match, expr, offset) => {
364
364
  try {
365
365
  const value = evaluateExpression(expr, context);
@@ -0,0 +1,41 @@
1
+ /** Clear the cache. Dev HMR calls this when composables change on disk. */
2
+ export declare function clearFrameworkComposableCache(): void;
3
+ /**
4
+ * Choose which files in the composables directory carry runtime code.
5
+ *
6
+ * This ran against a `*.ts` glob, which is only ever right when stx runs from
7
+ * `src/`. A released package runs from `dist/`, where the directory holds 37
8
+ * `.js` files and 37 `.d.ts` files and not one plain `.ts` — so every file the
9
+ * bundler opened was type-only, nothing was ever emitted, and every requested
10
+ * name was reported as unbundlable. The one configuration where the old glob
11
+ * worked is the one the repo's own tests run in, which is why it never showed
12
+ * up here (stacksjs/stx#1832).
13
+ *
14
+ * Two compounding details, both fixed by going through this function:
15
+ *
16
+ * - `.d.ts` can never contribute runtime code, so it is excluded outright
17
+ * rather than left to be caught downstream.
18
+ * - The barrel exclusion compared `endsWith('index.ts')`, and `'index.d.ts'`
19
+ * does not end with that. The one file the filter existed to skip was the
20
+ * only one it let through — and being a barrel of `export … from …`, it was
21
+ * also the one shape the export-stripping below cannot parse.
22
+ *
23
+ * Exported so the selection can be tested against a dist-shaped listing without
24
+ * a built package.
25
+ */
26
+ export declare function selectComposableFiles(files: string[]): string[];
27
+ /**
28
+ * Which framework composables does this source actually call?
29
+ *
30
+ * Restricted to the server-only set, so a page calling `useLocalStorage` (which
31
+ * the runtime provides) never drags in a module copy.
32
+ */
33
+ export declare function referencedFrameworkComposables(source: string): string[];
34
+ /**
35
+ * Build a script exposing `names` on `window.stx` and as bare globals.
36
+ *
37
+ * Returns null when nothing is needed.
38
+ */
39
+ export declare function getFrameworkComposableScript(source: string, composablesDir?: string): Promise<string | null>;
40
+ /** @internal test-only — declare names the runtime provides. */
41
+ export declare function __setRuntimeOwnedForTest(names: string[]): void;
@@ -0,0 +1,115 @@
1
+ import path from "node:path";
2
+ import { getPublicEnvDefine } from "./public-env.js";
3
+ import { SERVER_ONLY_COMPOSABLES } from "./unresolved-identifiers.js";
4
+ const _cache = new Map, RUNTIME_OWNED = new Set, SERVER_ONLY = new Set(SERVER_ONLY_COMPOSABLES);
5
+ export function clearFrameworkComposableCache() {
6
+ _cache.clear();
7
+ }
8
+ export function selectComposableFiles(files) {
9
+ const byBase = new Map;
10
+ for (const file of files) {
11
+ const base = path.basename(file);
12
+ if (base.endsWith(".d.ts"))
13
+ continue;
14
+ const stem = base.replace(/\.[jt]s$/, "");
15
+ if (stem === "index")
16
+ continue;
17
+ if (stem === base)
18
+ continue;
19
+ const existing = byBase.get(stem);
20
+ if (existing && !existing.endsWith(".js"))
21
+ continue;
22
+ byBase.set(stem, file);
23
+ }
24
+ return [...byBase.values()].sort();
25
+ }
26
+ function declaresLocally(source, name) {
27
+ const escaped = name.replace(/[$()*+.?[\\\]^{|}-]/g, "\\$&");
28
+ return new RegExp(`(?:^|[^\\w$.])(?:const|let|var|function|class)\\s+${escaped}\\b`).test(source);
29
+ }
30
+ export function referencedFrameworkComposables(source) {
31
+ if (!source)
32
+ return [];
33
+ const authored = source.replace(/<script\b[^>]*\bdata-stx-runtime\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-framework-composables\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-stores\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-composables\b[^>]*>[\s\S]*?<\/script>/gi, ""), found = [];
34
+ for (const name of SERVER_ONLY) {
35
+ if (RUNTIME_OWNED.has(name))
36
+ continue;
37
+ const escaped = name.replace(/[$()*+.?[\\\]^{|}-]/g, "\\$&");
38
+ if (new RegExp(`(^|[^\\w$.])${escaped}\\s*\\(`).test(authored))
39
+ found.push(name);
40
+ }
41
+ return found.sort();
42
+ }
43
+ export async function getFrameworkComposableScript(source, composablesDir) {
44
+ const names = referencedFrameworkComposables(source);
45
+ if (names.length === 0)
46
+ return null;
47
+ const dir = composablesDir ?? path.join(import.meta.dir, "composables"), key = `${dir}\x00${names.join(",")}`, cached = _cache.get(key);
48
+ if (cached !== void 0)
49
+ return cached || null;
50
+ const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser", define: getPublicEnvDefine() }), wanted = new Set(names), chunks = [], emitted = new Set;
51
+ let files = [];
52
+ try {
53
+ const glob = new Bun.Glob("*.{ts,js}");
54
+ for await (const file of glob.scan({ cwd: dir, absolute: !0 }))
55
+ files.push(file);
56
+ } catch {
57
+ _cache.set(key, "");
58
+ return null;
59
+ }
60
+ files = selectComposableFiles(files);
61
+ for (const file of files) {
62
+ let code;
63
+ try {
64
+ code = await Bun.file(file).text();
65
+ } catch {
66
+ continue;
67
+ }
68
+ let exports = [];
69
+ try {
70
+ exports = transpiler.scan(code).exports.filter((n) => n !== "default");
71
+ } catch {
72
+ continue;
73
+ }
74
+ if (!exports.some((n) => wanted.has(n)))
75
+ continue;
76
+ try {
77
+ code = code.replace(/^import\s+.*from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
78
+ code = code.replace(/^import\s+type\s+.*$/gm, "");
79
+ code = code.replace(/^export\s+\*\s+(?:as\s+\w+\s+)?from\s*['"][^'"]+['"]\s*;?\s*$/gm, "");
80
+ code = code.replace(/^export\s+(?:type\s+)?\{[\s\S]*?\}\s*(?:from\s*['"][^'"]+['"]\s*)?;?\s*$/gm, "");
81
+ code = code.replace(/^export\s+default\s+/gm, "");
82
+ code = code.replace(/^export\s+(?=(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\b)/gm, "");
83
+ chunks.push(`// ${path.basename(file).replace(/\.[jt]s$/, "")}
84
+ ${transpiler.transformSync(code).trim()}`);
85
+ for (const name of exports)
86
+ emitted.add(name);
87
+ } catch (error) {
88
+ console.warn(`[stx] framework composable ${path.basename(file)} could not be bundled:`, error);
89
+ }
90
+ }
91
+ const missing = names.filter((n) => !emitted.has(n) && !declaresLocally(source, n));
92
+ if (missing.length > 0)
93
+ console.warn(`[stx] could not bundle ${missing.join(", ")} from @stacksjs/stx/composables \u2014 ` + "calls to them will throw ReferenceError in the browser. Import them explicitly so the bundler inlines them, or use a runtime equivalent from window.stx.");
94
+ if (chunks.length === 0) {
95
+ _cache.set(key, "");
96
+ return null;
97
+ }
98
+ const assignments = [...emitted].map((name) => ` try { if (!(${JSON.stringify(name)} in __s)) { __s[${JSON.stringify(name)}] = ${name}; if (!(${JSON.stringify(name)} in window)) window[${JSON.stringify(name)}] = ${name}; } } catch (e) {}`).join(`
99
+ `), script = `<script data-stx-framework-composables>
100
+ ;(function(){
101
+ var __s = window.stx = window.stx || {};
102
+ ${chunks.join(`
103
+
104
+ `)}
105
+ ${assignments}
106
+ })();
107
+ </script>`;
108
+ _cache.set(key, script);
109
+ return script;
110
+ }
111
+ export function __setRuntimeOwnedForTest(names) {
112
+ RUNTIME_OWNED.clear();
113
+ for (const n of names)
114
+ RUNTIME_OWNED.add(n);
115
+ }
package/dist/head.js CHANGED
@@ -194,7 +194,11 @@ export function applyHead(config = currentHead) {
194
194
  }
195
195
  if (config.htmlAttrs)
196
196
  for (const [key, value] of Object.entries(config.htmlAttrs))
197
- document.documentElement.setAttribute(key, value);
197
+ if (key === "class")
198
+ for (const cls of String(value).split(/\s+/).filter(Boolean))
199
+ document.documentElement.classList.add(cls);
200
+ else
201
+ document.documentElement.setAttribute(key, value);
198
202
  if (config.bodyAttrs)
199
203
  for (const [key, value] of Object.entries(config.bodyAttrs))
200
204
  document.body.setAttribute(key, value);
@@ -198,8 +198,14 @@ catch (e) {
198
198
  console.log('[stx] Reloading...', msg.path || '');
199
199
  // Use SPA router for progressive update instead of full page reload.
200
200
  // Falls back to hard reload only when the router isn't available.
201
- if (typeof window.navigate === 'function') {
202
- window.navigate(window.location.pathname + window.location.search, false, true);
201
+ // Call the ROUTER directly, not window.navigate: the runtime helper
202
+ // takes (url, options) and silently dropped the third argument, so
203
+ // force never arrived and the same-URL early return made this a
204
+ // no-op on every page that had the router (#1807). pushState=false
205
+ // keeps the address bar untouched (this is a reload, not a
206
+ // navigation) and force=true bypasses the early return.
207
+ if (window.stxRouter && typeof window.stxRouter.navigate === 'function') {
208
+ window.stxRouter.navigate(window.location.pathname + window.location.search, false, true);
203
209
  } else {
204
210
  window.location.reload();
205
211
  }
package/dist/includes.js CHANGED
@@ -560,6 +560,7 @@ export async function processIncludes(template, context, filePath, options, depe
560
560
  workingContent = workingContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
561
561
  workingContent = workingContent.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
562
562
  const includeContext = { ...context };
563
+ includeContext.__stx_signals_gate = usesSignalsInScript(partialContent);
563
564
  for (const [key, value] of Object.entries(localVars))
564
565
  includeContext[key] = value;
565
566
  const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi, scriptMatches = [...partialContent.matchAll(scriptRegex)];
@@ -597,7 +598,7 @@ export async function processIncludes(template, context, filePath, options, depe
597
598
  if (isSignalScript && !isServerScript) {
598
599
  signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
599
600
  const resolvedContent = transformStoreImports(scriptContent), transformedScript = transformSignalScript(resolvedContent, signalScopeId);
600
- preservedScript += `${vendorStyleTags}<script data-stx-scoped>${transformedScript}</script>
601
+ preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always">${transformedScript}</script>
601
602
  `;
602
603
  continue;
603
604
  }
@@ -613,7 +614,7 @@ export async function processIncludes(template, context, filePath, options, depe
613
614
  const extraAttrs = scriptAttrs.replace(/\bclient\b/i, "").trim();
614
615
  signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
615
616
  const resolvedContent = transformStoreImports(scriptContent), tail = buildScopeRegistrationTail(resolvedContent, signalScopeId), wrapped = wrapClientScript(resolvedContent, tail);
616
- preservedScript += `${vendorStyleTags}<script data-stx-scoped${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
617
+ preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always"${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
617
618
  `;
618
619
  }
619
620
  }
@@ -630,8 +631,7 @@ export async function processIncludes(template, context, filePath, options, depe
630
631
  const { processLoops } = await import("./loops.js");
631
632
  let processedContent = processLoops(workingContent, includeContext, includeFilePath);
632
633
  processedContent = processConditionals(processedContent, includeContext, includeFilePath);
633
- const partialHasSignals = usesSignalsInScript(partialContent);
634
- processedContent = processExpressions(processedContent, includeContext, includeFilePath, { forceSignals: partialHasSignals });
634
+ processedContent = processExpressions(processedContent, includeContext, includeFilePath);
635
635
  if (preservedStyle)
636
636
  processedContent += `
637
637
  ` + preservedStyle;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,22 @@ export { type HydrationOptions } from './hydration';
28
28
  // shapes with different field sets; those are kept private to their modules
29
29
  // to avoid silently shadowing the user-facing one. Surfaced by stacksjs/stx#1707.
30
30
  export { type CookieOptions } from './composables/use-cookie';
31
+ // Storage composables were reachable ONLY via the './composables' subpath, so
32
+ // `import { useLocalStorage } from '@stacksjs/stx'` failed with TS2614 while
33
+ // `state`/`derived` imported fine — and a `functions/` composable (which the
34
+ // docs recommend for anything with persistence) had to reach for the subpath,
35
+ // where the same identifier used to return a different shape. See #1797.
36
+ export {
37
+ clearStorage,
38
+ getStorageKeys,
39
+ getStorageSize,
40
+ type StorageRef,
41
+ type StorageType,
42
+ useLocalStorage,
43
+ useSessionStorage,
44
+ useStorage,
45
+ type UseStorageOptions,
46
+ } from './composables/use-storage';
31
47
  export { formatSize, getTotalSize } from './deploy/index';
32
48
  export { type ImageRenderResult, type ImageVariant, type ProcessedImage, clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from './image-optimization/index';
33
49
  export { type AnalysisResult } from './analyzer';
@@ -62,6 +78,7 @@ export * from './async-components';
62
78
  export * from './auth';
63
79
  export * from './build';
64
80
  export * from './build-assets';
81
+ export * from './build-id';
65
82
  export * from './build-mode-detector';
66
83
  export * from './build-optimizer';
67
84
  export * from './broadcasting';
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ export {
2
+ clearStorage,
3
+ getStorageKeys,
4
+ getStorageSize,
5
+ useLocalStorage,
6
+ useSessionStorage,
7
+ useStorage
8
+ } from "./composables/use-storage.js";
1
9
  export { formatSize, getTotalSize } from "./deploy/index.js";
2
10
  export { clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from "./image-optimization/index.js";
3
11
  export { fileExists } from "./utils.js";
@@ -21,6 +29,7 @@ export * from "./async-components.js";
21
29
  export * from "./auth.js";
22
30
  export * from "./build.js";
23
31
  export * from "./build-assets.js";
32
+ export * from "./build-id.js";
24
33
  export * from "./build-mode-detector.js";
25
34
  export * from "./build-optimizer.js";
26
35
  export * from "./broadcasting.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Run `build` over `entrypoints`, falling back to one-at-a-time on failure.
3
+ *
4
+ * @param entrypoints absolute paths to build
5
+ * @param build performs a Bun.build over the given subset; should throw or
6
+ * reject on failure, which is what Bun.build does by default
7
+ */
8
+ export declare function buildIsolatingFailures(entrypoints: string[], build: (subset: string[]) => Promise<unknown>): Promise<IsolatedBuildResult>;
9
+ export declare interface IsolatedBuildResult {
10
+ succeeded: string[]
11
+ failed: Array<{ entrypoint: string, error: string }>
12
+ }
@@ -0,0 +1,18 @@
1
+ import { formatBuildFailure } from "./build-message.js";
2
+ export async function buildIsolatingFailures(entrypoints, build) {
3
+ if (entrypoints.length === 0)
4
+ return { succeeded: [], failed: [] };
5
+ try {
6
+ await build(entrypoints);
7
+ return { succeeded: [...entrypoints], failed: [] };
8
+ } catch {}
9
+ const succeeded = [], failed = [];
10
+ for (const entrypoint of entrypoints)
11
+ try {
12
+ await build([entrypoint]);
13
+ succeeded.push(entrypoint);
14
+ } catch (error) {
15
+ failed.push({ entrypoint, error: formatBuildFailure(error, entrypoint) });
16
+ }
17
+ return { succeeded, failed };
18
+ }