@stacksjs/stx 0.2.153 → 0.2.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/animation.js +6 -6
  2. package/dist/app-shell.js +1 -27
  3. package/dist/build-message.d.ts +10 -0
  4. package/dist/build-message.js +26 -0
  5. package/dist/builtins/attrs.js +6 -1
  6. package/dist/cli.js +1481 -1231
  7. package/dist/client-script-bundler.js +2 -4
  8. package/dist/client-script.d.ts +29 -0
  9. package/dist/client-script.js +140 -95
  10. package/dist/color-mode-boot.d.ts +31 -0
  11. package/dist/color-mode-boot.js +41 -5
  12. package/dist/component-renderer.js +10 -21
  13. package/dist/composables/index.d.ts +2 -0
  14. package/dist/composables/use-clipboard.d.ts +1 -5
  15. package/dist/composables/use-clipboard.js +57 -46
  16. package/dist/composables/use-color-mode.js +13 -5
  17. package/dist/composables/use-router.d.ts +28 -11
  18. package/dist/composables/use-router.js +63 -9
  19. package/dist/config.js +22 -7
  20. package/dist/craft.js +1029 -789
  21. package/dist/dev-server/crosswind.js +8 -6
  22. package/dist/dev-server/serve-app.js +4 -2
  23. package/dist/dev-server/serve-markdown.js +4 -1
  24. package/dist/dev-server/serve-multi.js +32 -20
  25. package/dist/document-shell.d.ts +65 -0
  26. package/dist/document-shell.js +59 -12
  27. package/dist/dynamic-components.js +123 -4
  28. package/dist/events.d.ts +26 -49
  29. package/dist/events.js +23 -9
  30. package/dist/expressions.d.ts +1 -1
  31. package/dist/expressions.js +3 -3
  32. package/dist/framework-composables.d.ts +41 -0
  33. package/dist/framework-composables.js +115 -0
  34. package/dist/hot-reload.js +8 -2
  35. package/dist/includes.js +2 -2
  36. package/dist/isolated-build.d.ts +12 -0
  37. package/dist/isolated-build.js +18 -0
  38. package/dist/plugin-system.d.ts +1 -1
  39. package/dist/process.js +40 -10
  40. package/dist/production-server.js +4 -1
  41. package/dist/prop-sentinels.d.ts +31 -0
  42. package/dist/prop-sentinels.js +7 -0
  43. package/dist/pwa.js +966 -726
  44. package/dist/reactive.js +1 -1
  45. package/dist/reactivity.d.ts +0 -15
  46. package/dist/reactivity.js +15 -6
  47. package/dist/runtime-globals.d.ts +40 -2
  48. package/dist/runtime-globals.js +90 -0
  49. package/dist/runtime-injection.d.ts +51 -0
  50. package/dist/runtime-injection.js +13 -0
  51. package/dist/script-emit.d.ts +20 -0
  52. package/dist/script-emit.js +11 -0
  53. package/dist/script-validation.js +12 -7
  54. package/dist/signal-processing.js +25 -40
  55. package/dist/signals-api.d.ts +30 -0
  56. package/dist/signals-api.js +18 -0
  57. package/dist/signals.js +258 -47
  58. package/dist/site-builder/build.js +21 -7
  59. package/dist/site-builder/seo.js +19 -10
  60. package/dist/site-builder/theme.js +5 -4
  61. package/dist/site-builder/types.d.ts +1 -0
  62. package/dist/ssg.js +986 -746
  63. package/dist/state-management.js +4 -1
  64. package/dist/store-loader.d.ts +0 -11
  65. package/dist/store-loader.js +27 -3
  66. package/dist/strict-lint.d.ts +9 -6
  67. package/dist/strict-lint.js +2 -21
  68. package/dist/stx.d.ts +111 -16
  69. package/dist/unresolved-identifiers.d.ts +50 -0
  70. package/dist/unresolved-identifiers.js +146 -0
  71. package/dist/utils.js +3 -3
  72. package/dist/variable-extractor.js +17 -1
  73. package/dist/x-element.js +1 -1
  74. package/package.json +5 -5
  75. package/stx.d.ts +111 -16
package/dist/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
+ }
@@ -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)];
@@ -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;
@@ -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
+ }
@@ -89,7 +89,7 @@ export declare interface StxPlugin {
89
89
  registerFilters?: () => Record<string, (value: unknown, ...args: unknown[]) => unknown>
90
90
  onRegister?: (options: StxOptions) => void | Promise<void>
91
91
  onUnregister?: () => void | Promise<void>
92
- components?: string
92
+ components?: string | string[]
93
93
  functions?: string
94
94
  stores?: string
95
95
  pages?: string
package/dist/process.js CHANGED
@@ -7,7 +7,7 @@ import { processTemplateBindings } from "./reactive-bindings.js";
7
7
  import { injectAnalytics } from "./analytics.js";
8
8
  import { injectHeatmap } from "./heatmap.js";
9
9
  import { processAnimationDirectives } from "./animation.js";
10
- import { processEventDirectives } from "./events.js";
10
+ import { generateRuntimeScript, processEventDirectives } from "./events.js";
11
11
  import { extractBridgeData, processClientScript } from "./client-script.js";
12
12
  import { processReactiveDirectives } from "./reactive.js";
13
13
  import { processMarkdownFileDirectives } from "./assets.js";
@@ -36,9 +36,9 @@ import { processServerBindings } from "./server-bindings.js";
36
36
  import { processVueTemplate } from "./vue-template.js";
37
37
  import { processDynamicComponents } from "./dynamic-components.js";
38
38
  import { dedupeScopedStyles, processScopedStyles } from "./style-scoping.js";
39
- import { injectColorModeBootScript } from "./color-mode-boot.js";
39
+ import { injectColorModeBootScript, normalizeCriticalHeadOrder } from "./color-mode-boot.js";
40
40
  import { injectBuildId } from "./build-id.js";
41
- import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs } from "./document-shell.js";
41
+ import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs, startsDocument } from "./document-shell.js";
42
42
  import { hasSignalsSyntax, convertSignalDirectivesToAttributes, convertSignalLoopsToAttributes, preEvalLiteralReactiveIfs, processSignals } from "./signal-processing.js";
43
43
  import { processComponents } from "./component-renderer.js";
44
44
  import { processInlineAssets } from "./inline-assets.js";
@@ -51,6 +51,7 @@ import { deriveLayoutGroup } from "stx-router/layout-metadata";
51
51
  import { initializeComponentClientFactories, injectComponentClientFactories } from "./component-client-factories.js";
52
52
  import { importOnce } from "./lazy-module.js";
53
53
  export { injectRouterScript } from "./runtime-injection.js";
54
+ import { injectSignalsRuntime, outputNeedsSignalsRuntime, pageShipsSignalsRuntime } from "./runtime-injection.js";
54
55
  export { processJsonDirective, processOnceDirective } from "./misc-directives.js";
55
56
  export { validateClientScript } from "./script-validation.js";
56
57
  const PARENT_PLACEHOLDER = "<!--PARENT_CONTENT_PLACEHOLDER-->";
@@ -246,6 +247,8 @@ function placeSignalsRuntimeBeforeScripts(html) {
246
247
  export async function processDirectives(template, context, filePath, options, dependencies) {
247
248
  if (options.ssr === !1 && !options.autoShell && options.buildMode === "spa")
248
249
  return template;
250
+ if (context.__stx_signals_gate === void 0)
251
+ context.__stx_signals_gate = usesSignalsInScript(template);
249
252
  const isTopLevel = !context.__stxProcessingDepth;
250
253
  if (isTopLevel)
251
254
  initializeComponentClientFactories(context);
@@ -269,7 +272,7 @@ export async function processDirectives(template, context, filePath, options, de
269
272
  pageHeadTitle = rawTitleMatch[1].trim();
270
273
  pageHeadRaw = pageHeadRaw.replace(rawTitleMatch[0], "").trim();
271
274
  }
272
- const sectionTitle = context.__sections?.title, pageTitle = runtimeHead.title || pageHeadTitle || sectionTitle, headConfig = {
275
+ const sectionTitle = context.__sections?.title, pageTitle = runtimeHead.title || pageHeadTitle || sectionTitle, titleIsHtml = !runtimeHead.title && (!!pageHeadTitle || !!sectionTitle), headConfig = {
273
276
  ...baseHeadConfig,
274
277
  ...pageTitle && { title: pageTitle },
275
278
  meta: [...baseHeadConfig.meta || [], ...runtimeHead.meta || []],
@@ -281,6 +284,7 @@ export async function processDirectives(template, context, filePath, options, de
281
284
  ...runtimeHead.bodyAttrs && { bodyAttrs: { ...baseHeadConfig.bodyAttrs || {}, ...runtimeHead.bodyAttrs } }
282
285
  }, alreadyShelled = hasDocumentShell(result);
283
286
  result = ensureDocumentShell(result, headConfig, {
287
+ ...titleIsHtml && { titleIsHtml: !0 },
284
288
  ...options.app?.colorMode && { colorMode: options.app.colorMode }
285
289
  });
286
290
  if (alreadyShelled) {
@@ -290,7 +294,7 @@ export async function processDirectives(template, context, filePath, options, de
290
294
  const layoutComment = result.match(/<!-- stx-layout: ([^ ]+) -->/);
291
295
  if (layoutComment && result.includes("<head")) {
292
296
  const layoutName = layoutComment[1], layoutGroup = deriveLayoutGroup(layoutName);
293
- result = result.replace("<head>", `<head>
297
+ result = result.replace(/<head\b[^>]*>/i, (match) => `${match}
294
298
  <meta name="stx-layout" content="${escapeHtmlAttribute(layoutName)}">
295
299
  <meta name="stx-layout-group" content="${escapeHtmlAttribute(layoutGroup)}">`);
296
300
  }
@@ -319,6 +323,16 @@ export async function processDirectives(template, context, filePath, options, de
319
323
  ` + storeTag + result.slice(runtime.end);
320
324
  }
321
325
  } catch {}
326
+ if (isTopLevel)
327
+ try {
328
+ const { getFrameworkComposableScript } = await importOnce("stx/framework-composables", () => import("./framework-composables.js")), frameworkScript = await getFrameworkComposableScript(result);
329
+ if (frameworkScript) {
330
+ const anchor = findScriptBlockByAttribute(result, "data-stx-stores") || findScriptBlockByAttribute(result, "data-stx-runtime");
331
+ if (anchor)
332
+ result = result.slice(0, anchor.end) + `
333
+ ` + frameworkScript + result.slice(anchor.end);
334
+ }
335
+ } catch {}
322
336
  if (isTopLevel)
323
337
  try {
324
338
  const { getComposableScript } = await importOnce("stx/composable-loader", () => import("./composable-loader.js")), resolvedComposablesDir = options.composablesDir, composableCode = await getComposableScript(resolvedComposablesDir);
@@ -329,6 +343,8 @@ export async function processDirectives(template, context, filePath, options, de
329
343
  ` + composableTag + result.slice(anchor.end);
330
344
  }
331
345
  } catch {}
346
+ if (isTopLevel)
347
+ result = normalizeCriticalHeadOrder(result);
332
348
  if (isTopLevel && context.__stx_inject_css !== !1)
333
349
  result = await injectCrosswindCSS(result, void 0, options.buildMode === "serve");
334
350
  if (isTopLevel)
@@ -384,8 +400,8 @@ async function processDirectivesInternal(template, context, filePath, options, d
384
400
  output = output.replace(/@(?:layout|extends)\(\s*['"]([^'"]+)['"]\s*\)/g, "");
385
401
  }
386
402
  if (!layoutPath && !hasNoLayout && opts.defaultLayout) {
387
- const hasDoctype = /<!DOCTYPE\s/i.test(output), hasSections = /@section\s*\(/.test(output);
388
- if (!hasDoctype || hasSections) {
403
+ const isDocument = startsDocument(output), hasSections = /@section\s*\(/.test(output);
404
+ if (!isDocument || hasSections) {
389
405
  let autoLayoutPath = "", searchDir = path.dirname(filePath);
390
406
  const rootDir = opts.layoutsDir ? path.dirname(opts.layoutsDir) : process.cwd();
391
407
  for (let i = 0;i < 20; i++) {
@@ -602,8 +618,8 @@ async function processOtherDirectives(template, context, filePath, options, depe
602
618
  const { processStreamDirectives } = await importOnce("stx/streaming", () => import("./streaming.js"));
603
619
  output = processStreamDirectives(output, context);
604
620
  output = processLoops(output, context, filePath, opts);
605
- output = await processComponents(output, context, filePath, opts, dependencies);
606
621
  output = await processDynamicComponents(output, context, filePath, opts, dependencies);
622
+ output = await processComponents(output, context, filePath, opts, dependencies);
607
623
  output = processAnimationDirectives(output, context, filePath, opts);
608
624
  const { processDeferDirectives } = await importOnce("stx/defer", () => import("./defer.js"));
609
625
  output = processDeferDirectives(output, context, filePath);
@@ -739,7 +755,7 @@ ${snippet}
739
755
  }
740
756
  const hasClientScripts = /<script\b(?![^>]*\bserver\b)[^>]*>[\s\S]*?<\/script>/i.test(output), hasStxUsage = /\bSTX\.\w+/.test(output), alreadyHasRuntime = /window\.STX\s*=/.test(output);
741
757
  if (hasClientScripts && hasStxUsage && !alreadyHasRuntime) {
742
- const runtimeScript = `<script data-stx-scoped>
758
+ const runtimeScript = `<script data-stx-scoped data-stx-run="once">
743
759
  ${generateLifecycleRuntime()}
744
760
  </script>`;
745
761
  if (output.includes("</head>")) {
@@ -789,12 +805,26 @@ ${runtimeScript}`);
789
805
  for (const { match, attrs, content } of clientScriptMatches) {
790
806
  clientScriptsTransformed = !0;
791
807
  let processedContent = content;
792
- validateClientScript(content, filePath, options.strict);
808
+ validateClientScript(content, filePath, options.strict, context.__originalFilePath);
793
809
  const scriptResult = await processClientScript(content, { eventBindings, attrs, hasColonDirectives, templateContent: template, filePath, projectRoot: process.cwd(), production: options.buildMode === "compile", serverData });
794
810
  output = output.replace(match, () => scriptResult);
795
811
  }
796
812
  if (clientScriptsTransformed)
797
813
  context.__stx_event_bindings = [];
814
+ if (!clientScriptsTransformed && context.__stxProcessingDepth === 1) {
815
+ const orphaned = context.__stx_event_bindings || [];
816
+ if (orphaned.length > 0) {
817
+ const script = generateRuntimeScript(orphaned);
818
+ if (script) {
819
+ const bodyIdx = output.lastIndexOf("</body>");
820
+ output = bodyIdx === -1 ? output + script : output.slice(0, bodyIdx) + script + `
821
+ ` + output.slice(bodyIdx);
822
+ }
823
+ context.__stx_event_bindings = [];
824
+ }
825
+ }
826
+ if (!pageShipsSignalsRuntime(output) && outputNeedsSignalsRuntime(output))
827
+ output = await injectSignalsRuntime(output, opts);
798
828
  if (opts.csp?.enabled && opts.csp.useNonce && typeof context.cspNonce === "string")
799
829
  output = addNonceToInlineContent(output, context.cspNonce);
800
830
  return output;
@@ -4,6 +4,7 @@ import { serve } from "bun";
4
4
  import { loadManifest } from "./manifest.js";
5
5
  import { hydrateTemplateStream, hydrateFragment } from "./template-hydrator.js";
6
6
  import { extractLayoutMetadata } from "./app-shell.js";
7
+ import { pageShipsSignalsRuntime } from "./runtime-injection.js";
7
8
  import { patternToRegex } from "stx-router";
8
9
  const MIME_TYPES = {
9
10
  ".js": "application/javascript",
@@ -41,12 +42,13 @@ export async function startProductionServer(options = {}) {
41
42
  process.exit(1);
42
43
  }
43
44
  console.log(`[stx] Production server loading ${manifest.routes.length} routes...`);
44
- const compiledTemplates = new Map, fragmentCache = new Map, layoutMetadataCache = new Map;
45
+ const compiledTemplates = new Map, fragmentCache = new Map, layoutMetadataCache = new Map, runtimeCache = new Map;
45
46
  for (const route of manifest.routes)
46
47
  try {
47
48
  const compiledPath = path.join(outputDir, route.compiledPath), compiled = JSON.parse(await Bun.file(compiledPath).text());
48
49
  compiledTemplates.set(route.pattern, compiled);
49
50
  layoutMetadataCache.set(route.pattern, extractLayoutMetadata(compiled.html));
51
+ runtimeCache.set(route.pattern, pageShipsSignalsRuntime(compiled.html));
50
52
  const fragmentPath = path.join(outputDir, route.fragmentPath);
51
53
  if (await Bun.file(fragmentPath).exists())
52
54
  fragmentCache.set(route.pattern, await Bun.file(fragmentPath).text());
@@ -129,6 +131,7 @@ export async function startProductionServer(options = {}) {
129
131
  headers: {
130
132
  "Content-Type": "text/html",
131
133
  "X-STX-Fragment": "true",
134
+ ...runtimeCache.has(matchedRoute.pattern) ? { "X-STX-Runtime": runtimeCache.get(matchedRoute.pattern) ? "true" : "false" } : {},
132
135
  ...layoutMetadata ? {
133
136
  "X-STX-Layout": layoutMetadata.layout,
134
137
  "X-STX-Layout-Group": layoutMetadata.group
@@ -0,0 +1,31 @@
1
+ /** Is this value the bare-boolean-attribute marker? */
2
+ export declare function isBooleanAttributeSentinel(value: unknown): boolean;
3
+ /**
4
+ * Last-resort guard for anything about to be written into a response.
5
+ *
6
+ * Nothing should reach output carrying a NUL, but the cost of one escaping is
7
+ * an invalid document and a silent grep, which is far worse than the cost of
8
+ * this check.
9
+ */
10
+ export declare function stripControlChars(value: string): string;
11
+ /**
12
+ * Sentinel values used while resolving component props.
13
+ *
14
+ * A leaf module on purpose: `component-renderer.ts` imports `./builtins`, so a
15
+ * builtin importing the renderer back would be a cycle. Both sides import this
16
+ * instead.
17
+ *
18
+ * @module prop-sentinels
19
+ */
20
+ /**
21
+ * Marks a prop that was written as a bare HTML boolean attribute — `<X open>`
22
+ * rather than `<X open="true">`.
23
+ *
24
+ * It carries a leading NUL precisely so it cannot collide with an author-written
25
+ * value, which also means it must never survive into a response: a NUL byte
26
+ * makes the document invalid UTF-8, and tools treat the file as binary. `grep`
27
+ * in particular goes SILENT on it — not zero matches, no output at all — so a
28
+ * verification step that greps rendered HTML returns an empty string and reads
29
+ * as a pass (stacksjs/stx#1816).
30
+ */
31
+ export declare const BOOLEAN_ATTRIBUTE_SENTINEL: '\0stx-boolean-attribute';
@@ -0,0 +1,7 @@
1
+ export const BOOLEAN_ATTRIBUTE_SENTINEL = "\x00stx-boolean-attribute";
2
+ export function isBooleanAttributeSentinel(value) {
3
+ return value === BOOLEAN_ATTRIBUTE_SENTINEL;
4
+ }
5
+ export function stripControlChars(value) {
6
+ return value.indexOf("\x00") === -1 ? value : value.replace(/\0/g, "");
7
+ }