@stacksjs/stx 0.2.146 → 0.2.147

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.
@@ -11,6 +11,20 @@ export declare function classifyScript(attrs: string, content: string, fullTag:
11
11
  /**
12
12
  * Classify all `<script>` tags in a template.
13
13
  *
14
+ * The scan walks the template rather than running a global regex so that HTML
15
+ * comments are inert. A comment whose prose merely mentions a script tag — e.g.
16
+ * `<!-- the <script server> block owner-scopes the switcher -->` — used to be
17
+ * matched as a REAL opening tag, and the non-greedy regex then ran on to the
18
+ * next `</script>`. The resulting bogus `fullTag` spanned from inside the comment
19
+ * to the end of an unrelated script, so callers that delete `fullTag`
20
+ * (app-shell's `cleanTemplate`, serve-app's `templateContent`) erased the
21
+ * comment's `-->` terminator AND every real script in between — producing a
22
+ * runaway comment node that swallowed the rest of the document.
23
+ *
24
+ * `<template>` contents are deliberately still scanned: stx SFC files wrap their
25
+ * whole markup in `<template>`, and callers rely on scripts authored inside it
26
+ * being classified so they can be stripped from the rendered output.
27
+ *
14
28
  * Returns arrays of scripts grouped by type.
15
29
  */
16
30
  export declare function classifyAllScripts(content: string): {
@@ -24,12 +38,4 @@ export declare interface ScriptClassification {
24
38
  content: string
25
39
  attrs: string
26
40
  }
27
- /**
28
- * Script Classification
29
- *
30
- * Single source of truth for classifying `<script>` tags in `.stx` files.
31
- * Used by bun-plugin, dev-server, and process.ts to ensure consistent behavior.
32
- *
33
- * @module script-classifier
34
- */
35
41
  export type ScriptType = 'signals' | 'client' | 'server';
@@ -1,3 +1,4 @@
1
+ import { matchHtmlComment, matchScriptElement } from "./html-masking.js";
1
2
  const SIGNALS_PATTERN = /\b(?:state|derived|effect|batch|ref|reactive|computed|watch|watchEffect)\s*(?:<[^>]*>)?\s*\(/;
2
3
  export function classifyScript(attrs, content, fullTag) {
3
4
  const result = { fullTag, content, attrs };
@@ -11,22 +12,40 @@ export function classifyScript(attrs, content, fullTag) {
11
12
  return { ...result, type: "client" };
12
13
  return { ...result, type: "server" };
13
14
  }
15
+ const SCRIPT_PARTS = /^<script\b([^>]*)>([\s\S]*)<\/script\s*>$/i;
14
16
  export function classifyAllScripts(content) {
15
- const signals = [], client = [], server = [], scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
16
- let match;
17
- while ((match = scriptRegex.exec(content)) !== null) {
18
- const classification = classifyScript(match[1], match[2], match[0]);
19
- switch (classification.type) {
20
- case "signals":
21
- signals.push(classification);
22
- break;
23
- case "client":
24
- client.push(classification);
25
- break;
26
- case "server":
27
- server.push(classification);
28
- break;
17
+ const signals = [], client = [], server = [];
18
+ let i = 0;
19
+ while (i < content.length) {
20
+ const lt = content.indexOf("<", i);
21
+ if (lt === -1)
22
+ break;
23
+ const commentEnd = matchHtmlComment(content, lt);
24
+ if (commentEnd > lt) {
25
+ i = commentEnd;
26
+ continue;
29
27
  }
28
+ const scriptEnd = matchScriptElement(content, lt);
29
+ if (scriptEnd > lt) {
30
+ const fullTag = content.slice(lt, scriptEnd), parts = SCRIPT_PARTS.exec(fullTag);
31
+ if (parts) {
32
+ const classification = classifyScript(parts[1], parts[2], fullTag);
33
+ switch (classification.type) {
34
+ case "signals":
35
+ signals.push(classification);
36
+ break;
37
+ case "client":
38
+ client.push(classification);
39
+ break;
40
+ case "server":
41
+ server.push(classification);
42
+ break;
43
+ }
44
+ }
45
+ i = scriptEnd;
46
+ continue;
47
+ }
48
+ i = lt + (content.startsWith("<!--", lt) ? 4 : 1);
30
49
  }
31
50
  return { signals, client, server };
32
51
  }
package/dist/signals.js CHANGED
@@ -4402,10 +4402,18 @@ else if (timer === null) {
4402
4402
 
4403
4403
  function useColorMode(options) {
4404
4404
  options = options || {};
4405
- var storageKey = options.storageKey || 'stx-color-mode';
4406
- var initialMode = options.initialMode || 'auto';
4407
- var darkClass = options.darkClass || 'dark';
4408
- var attribute = options.attribute || null;
4405
+ // The pre-paint boot script (stacksjs/stx#1794) publishes the app's
4406
+ // configured options here. Reading them makes a bare useColorMode() agree
4407
+ // with whatever already landed on <html> before first paint \u2014 without this
4408
+ // the composable would fall back to its own defaults, read a different
4409
+ // storage key, and undo the theme on hydration, reintroducing the very
4410
+ // flash the boot script exists to prevent.
4411
+ var boot = null;
4412
+ try { boot = window.__STX_COLOR_MODE__ || null; } catch (e) {}
4413
+ var storageKey = options.storageKey || (boot && boot.storageKey) || 'stx-color-mode';
4414
+ var initialMode = options.initialMode || (boot && boot.initialMode) || 'auto';
4415
+ var darkClass = options.darkClass || (boot && boot.darkClass) || 'dark';
4416
+ var attribute = options.attribute || (boot && boot.attribute) || null;
4409
4417
  var disableTransitions = options.disableTransitions !== false;
4410
4418
  var preference = initialMode;
4411
4419
  var resolved = 'light';
@@ -4571,15 +4579,60 @@ catch (e) {}
4571
4579
  var watch = $watch;
4572
4580
  var watchEffect = function(fn) { return effect(fn); };
4573
4581
 
4582
+ // Storage access is guarded end to end (stacksjs/stx#1793). Every one of
4583
+ // these can throw, and each throw took down the whole client script for the
4584
+ // page rather than just the composable:
4585
+ //
4586
+ // getItem - SecurityError in a sandboxed iframe or with storage disabled
4587
+ // parse - any non-JSON value already sitting at the key
4588
+ // setItem - QuotaExceededError, Safari private mode
4589
+ //
4590
+ // The parse fallback is deliberately LENIENT: a value that isn't JSON is
4591
+ // returned as the raw string rather than discarded for defaultValue. A key
4592
+ // that previously held a bare 'list' keeps reading back 'list' and is
4593
+ // rewritten as JSON on the next set, so migrating an existing key onto this
4594
+ // composable no longer needs the key to be renamed. This also matches
4595
+ // composables/use-storage.ts, whose default serializer already returns the
4596
+ // raw string on parse failure \u2014 the two implementations have to agree.
4597
+ function stxParseStored(raw, fallback, key, label) {
4598
+ if (raw === null || raw === undefined) return fallback;
4599
+ try { return JSON.parse(raw); }
4600
+ catch (e) {
4601
+ console.warn('[stx] ' + label + ': "' + key + '" holds a value that is not JSON (' + String(raw).slice(0, 64) + '); using it as a raw string. Write it with the same composable to store it as JSON.');
4602
+ return raw;
4603
+ }
4604
+ }
4605
+ function stxStorageRead(store, key, fallback, label) {
4606
+ var raw;
4607
+ try { raw = store.getItem(key); }
4608
+ catch (e) {
4609
+ console.warn('[stx] ' + label + ': cannot read "' + key + '" (' + (e && e.message ? e.message : e) + '); using the default.');
4610
+ return fallback;
4611
+ }
4612
+ return stxParseStored(raw, fallback, key, label);
4613
+ }
4614
+ function stxStorageWrite(store, key, value, label) {
4615
+ try { store.setItem(key, JSON.stringify(value)); }
4616
+ catch (e) {
4617
+ console.warn('[stx] ' + label + ': cannot persist "' + key + '" (' + (e && e.message ? e.message : e) + '); the value stays in memory only.');
4618
+ }
4619
+ }
4620
+
4574
4621
  function useLocalStorage(key, defaultValue) {
4575
- var stored = localStorage.getItem(key);
4576
- var initial = stored !== null ? JSON.parse(stored) : defaultValue;
4577
- var s = state(initial);
4622
+ var s = state(stxStorageRead(localStorage, key, defaultValue, 'useLocalStorage'));
4578
4623
  effect(function() {
4579
- localStorage.setItem(key, JSON.stringify(s()));
4624
+ stxStorageWrite(localStorage, key, s(), 'useLocalStorage');
4580
4625
  });
4581
4626
  var handler = function(e) {
4582
- if (e.key === key) s.set(e.newValue !== null ? JSON.parse(e.newValue) : defaultValue);
4627
+ // Fires from ANOTHER tab at an arbitrary later time, so an unguarded
4628
+ // parse here throws inside a window listener with a stack pointing at
4629
+ // the runtime rather than at the consumer.
4630
+ // storageArea is also matched so a sessionStorage write to the same key
4631
+ // name can't clobber this signal \u2014 useSessionStorage already filtered,
4632
+ // this side didn't. Synthetic events (tests) leave storageArea null and
4633
+ // are still accepted; real browsers always populate it.
4634
+ if (e.key === key && (!e.storageArea || e.storageArea === localStorage))
4635
+ s.set(stxParseStored(e.newValue, defaultValue, key, 'useLocalStorage'));
4583
4636
  };
4584
4637
  window.addEventListener('storage', handler);
4585
4638
  onDestroy(function() { window.removeEventListener('storage', handler); });
@@ -4593,15 +4646,13 @@ catch (e) {}
4593
4646
  // event (filtered to sessionStorage's storageArea \u2014 localStorage events
4594
4647
  // share the same listener but fire from a different area).
4595
4648
  function useSessionStorage(key, defaultValue) {
4596
- var stored = sessionStorage.getItem(key);
4597
- var initial = stored !== null ? JSON.parse(stored) : defaultValue;
4598
- var s = state(initial);
4649
+ var s = state(stxStorageRead(sessionStorage, key, defaultValue, 'useSessionStorage'));
4599
4650
  effect(function() {
4600
- sessionStorage.setItem(key, JSON.stringify(s()));
4651
+ stxStorageWrite(sessionStorage, key, s(), 'useSessionStorage');
4601
4652
  });
4602
4653
  var handler = function(e) {
4603
- if (e.key === key && e.storageArea === sessionStorage)
4604
- s.set(e.newValue !== null ? JSON.parse(e.newValue) : defaultValue);
4654
+ if (e.key === key && (!e.storageArea || e.storageArea === sessionStorage))
4655
+ s.set(stxParseStored(e.newValue, defaultValue, key, 'useSessionStorage'));
4605
4656
  };
4606
4657
  window.addEventListener('storage', handler);
4607
4658
  onDestroy(function() { window.removeEventListener('storage', handler); });