sygnal 5.1.1 → 5.1.2

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.
package/README.md CHANGED
@@ -21,12 +21,13 @@ A reactive component framework with pure functions, zero side effects, and autom
21
21
  **Scaffold a new project:**
22
22
 
23
23
  ```bash
24
- npx degit tpresley/sygnal-template my-app
24
+ npm create sygnal-app my-app
25
25
  cd my-app
26
- npm install
27
26
  npm run dev
28
27
  ```
29
28
 
29
+ Choose from Vite (SPA), Vike (SSR), or Astro templates during setup.
30
+
30
31
  **Or add to an existing project:**
31
32
 
32
33
  ```bash
@@ -386,6 +387,25 @@ import Counter from '../components/Counter.jsx'
386
387
  <Counter client:load />
387
388
  ```
388
389
 
390
+ ### Vike Integration
391
+
392
+ File-based routing with SSR, client-side navigation, and automatic hydration:
393
+
394
+ ```javascript
395
+ // vite.config.js
396
+ import sygnal from 'sygnal/vite'
397
+ import vike from 'vike/plugin'
398
+ export default defineConfig({ plugins: [sygnal({ disableHmr: true }), vike()] })
399
+ ```
400
+
401
+ ```javascript
402
+ // pages/+config.js
403
+ import vikeSygnal from 'sygnal/config'
404
+ export default { extends: [vikeSygnal] }
405
+ ```
406
+
407
+ Pages are standard Sygnal components in `pages/*/+Page.jsx`. Supports layouts, data fetching, and SPA mode.
408
+
389
409
  ### TypeScript
390
410
 
391
411
  Full type definitions included:
@@ -443,6 +463,7 @@ h('div', [h('h1', 'Hello'), h('button.btn', 'Click')])
443
463
  |---------|-------------|
444
464
  | [Getting Started](./examples/getting-started) | Interactive guide with live demos (Astro) |
445
465
  | [Kanban Board](./examples/kanban) | Drag-and-drop with Collections and cross-component communication |
466
+ | [Vike SSR](./examples/vike) | File-based routing with SSR, layouts, and data fetching |
446
467
  | [Advanced Features](./examples/advanced-feature-tests) | Portals, slots, disposal, suspense, lazy loading |
447
468
  | [TypeScript 2048](./examples/ts-example-2048) | Full game in TypeScript |
448
469
  | [AI Discussion Panel](./examples/ai-panel-spa) | Complex SPA with custom drivers |
@@ -5462,7 +5462,18 @@ function wrapDOMSource(domSource) {
5462
5462
  }
5463
5463
  });
5464
5464
  }
5465
- const ABORT = Symbol('ABORT');
5465
+ const ABORT = Symbol.for('sygnal.ABORT');
5466
+ /**
5467
+ * Check if a value is the ABORT sentinel.
5468
+ * Uses Symbol.for() identity first, then falls back to description check
5469
+ * in case bundlers (e.g. Vite) create duplicate module instances with
5470
+ * separate Symbol.for() registries.
5471
+ */
5472
+ function isAbort(value) {
5473
+ if (value === ABORT)
5474
+ return true;
5475
+ return typeof value === 'symbol' && value.description === 'sygnal.ABORT';
5476
+ }
5466
5477
  function normalizeCalculatedEntry(field, entry) {
5467
5478
  if (typeof entry === 'function') {
5468
5479
  return { fn: entry, deps: null };
@@ -6184,7 +6195,7 @@ class Component {
6184
6195
  const enhancedState = this.addCalculated(_state);
6185
6196
  props.state = enhancedState;
6186
6197
  const newState = reducer(enhancedState, data, next, props);
6187
- if (newState === ABORT)
6198
+ if (isAbort(newState))
6188
6199
  return _state;
6189
6200
  return this.cleanupCalculated(newState);
6190
6201
  }
@@ -6213,7 +6224,7 @@ class Component {
6213
6224
  return ABORT;
6214
6225
  }
6215
6226
  }
6216
- }).filter((result) => result !== ABORT);
6227
+ }).filter((result) => !isAbort(result));
6217
6228
  }
6218
6229
  else if (reducer === undefined || reducer === true) {
6219
6230
  returnStream$ = filtered$.map(({ data }) => data);
@@ -5460,7 +5460,18 @@ function wrapDOMSource(domSource) {
5460
5460
  }
5461
5461
  });
5462
5462
  }
5463
- const ABORT = Symbol('ABORT');
5463
+ const ABORT = Symbol.for('sygnal.ABORT');
5464
+ /**
5465
+ * Check if a value is the ABORT sentinel.
5466
+ * Uses Symbol.for() identity first, then falls back to description check
5467
+ * in case bundlers (e.g. Vite) create duplicate module instances with
5468
+ * separate Symbol.for() registries.
5469
+ */
5470
+ function isAbort(value) {
5471
+ if (value === ABORT)
5472
+ return true;
5473
+ return typeof value === 'symbol' && value.description === 'sygnal.ABORT';
5474
+ }
5464
5475
  function normalizeCalculatedEntry(field, entry) {
5465
5476
  if (typeof entry === 'function') {
5466
5477
  return { fn: entry, deps: null };
@@ -6182,7 +6193,7 @@ class Component {
6182
6193
  const enhancedState = this.addCalculated(_state);
6183
6194
  props.state = enhancedState;
6184
6195
  const newState = reducer(enhancedState, data, next, props);
6185
- if (newState === ABORT)
6196
+ if (isAbort(newState))
6186
6197
  return _state;
6187
6198
  return this.cleanupCalculated(newState);
6188
6199
  }
@@ -6211,7 +6222,7 @@ class Component {
6211
6222
  return ABORT;
6212
6223
  }
6213
6224
  }
6214
- }).filter((result) => result !== ABORT);
6225
+ }).filter((result) => !isAbort(result));
6215
6226
  }
6216
6227
  else if (reducer === undefined || reducer === true) {
6217
6228
  returnStream$ = filtered$.map(({ data }) => data);
@@ -1,10 +1,12 @@
1
- import { Stream, MemoryStream } from 'xstream';
1
+ import { MemoryStream } from 'xstream';
2
2
  import { EventsFnOptions } from './DOMSource';
3
+ import { EnrichedEventStream } from './enrichEventStream';
3
4
  export declare class DocumentDOMSource {
4
5
  private _name;
5
- constructor(_name: string);
6
+ private _selector;
7
+ constructor(_name: string, selector?: string);
6
8
  select(selector: string): DocumentDOMSource;
7
- elements(): MemoryStream<Array<Document>>;
8
- element(): MemoryStream<Document>;
9
- events<K extends keyof DocumentEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): Stream<DocumentEventMap[K]>;
9
+ elements(): MemoryStream<Array<Document | Element>>;
10
+ element(): MemoryStream<Document | Element | null>;
11
+ events<K extends keyof DocumentEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<DocumentEventMap[K]>;
10
12
  }
@@ -3,6 +3,7 @@ import { EventsFnOptions } from './DOMSource';
3
3
  import { DocumentDOMSource } from './DocumentDOMSource';
4
4
  import { BodyDOMSource } from './BodyDOMSource';
5
5
  import { VNode } from './snabbdom';
6
+ import { EnrichedEventStream } from './enrichEventStream';
6
7
  import { Scope, IsolateSink } from './isolate';
7
8
  import { IsolateModule } from './IsolateModule';
8
9
  import { EventDelegator } from './EventDelegator';
@@ -24,7 +25,7 @@ export declare class MainDOMSource {
24
25
  get namespace(): Array<Scope>;
25
26
  select<T extends keyof SpecialSelector>(selector: T): SpecialSelector[T];
26
27
  select(selector: string): MainDOMSource;
27
- events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): Stream<HTMLElementEventMap[K]>;
28
+ events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<HTMLElementEventMap[K]>;
28
29
  dispose(): void;
29
30
  isolateSource: (source: MainDOMSource, scope: string) => MainDOMSource;
30
31
  isolateSink: IsolateSink<VNode>;
@@ -0,0 +1,24 @@
1
+ import { Stream } from 'xstream';
2
+ export interface EnrichedEventStream<T = Event> extends Stream<T> {
3
+ value(): EnrichedEventStream<string>;
4
+ value<R>(fn: (val: string) => R): EnrichedEventStream<R>;
5
+ checked(): EnrichedEventStream<boolean>;
6
+ checked<R>(fn: (val: boolean) => R): EnrichedEventStream<R>;
7
+ data(name: string): EnrichedEventStream<string | undefined>;
8
+ data<R>(name: string, fn: (val: string | undefined) => R): EnrichedEventStream<R>;
9
+ target(): EnrichedEventStream<EventTarget | null>;
10
+ target<R>(fn: (el: EventTarget | null) => R): EnrichedEventStream<R>;
11
+ key(): EnrichedEventStream<string>;
12
+ key<R>(fn: (key: string) => R): EnrichedEventStream<R>;
13
+ }
14
+ /**
15
+ * Adds chainable convenience methods to a DOM event stream.
16
+ *
17
+ * DOM.select('.input').events('input').value()
18
+ * DOM.input('.input').value()
19
+ * DOM.select('.item').events('click').data('id')
20
+ * DOM.click('.item').data('id', Number)
21
+ * DOM.change('.checkbox').checked()
22
+ * DOM.keydown('.field').key()
23
+ */
24
+ export declare function enrichEventStream(stream$: any): any;
@@ -514,6 +514,12 @@ function vnodeToHtml(vnode) {
514
514
  if (VOID_ELEMENTS.has(tag)) {
515
515
  return html;
516
516
  }
517
+ // If innerHTML is set via props, use it as raw content (no escaping)
518
+ if (vnode.data?.props?.innerHTML != null) {
519
+ html += String(vnode.data.props.innerHTML);
520
+ html += `</${tag}>`;
521
+ return html;
522
+ }
517
523
  // Children — snabbdom uses `text` for single text children (even when
518
524
  // `children` holds a text element object). Prioritize `text` when set.
519
525
  if (vnode.text != null) {
@@ -510,6 +510,12 @@ function vnodeToHtml(vnode) {
510
510
  if (VOID_ELEMENTS.has(tag)) {
511
511
  return html;
512
512
  }
513
+ // If innerHTML is set via props, use it as raw content (no escaping)
514
+ if (vnode.data?.props?.innerHTML != null) {
515
+ html += String(vnode.data.props.innerHTML);
516
+ html += `</${tag}>`;
517
+ return html;
518
+ }
513
519
  // Children — snabbdom uses `text` for single text children (even when
514
520
  // `children` holds a text element object). Prioritize `text` when set.
515
521
  if (vnode.text != null) {
@@ -1,10 +1,12 @@
1
- import { Stream, MemoryStream } from 'xstream';
1
+ import { MemoryStream } from 'xstream';
2
2
  import { EventsFnOptions } from './DOMSource';
3
+ import { EnrichedEventStream } from './enrichEventStream';
3
4
  export declare class DocumentDOMSource {
4
5
  private _name;
5
- constructor(_name: string);
6
+ private _selector;
7
+ constructor(_name: string, selector?: string);
6
8
  select(selector: string): DocumentDOMSource;
7
- elements(): MemoryStream<Array<Document>>;
8
- element(): MemoryStream<Document>;
9
- events<K extends keyof DocumentEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): Stream<DocumentEventMap[K]>;
9
+ elements(): MemoryStream<Array<Document | Element>>;
10
+ element(): MemoryStream<Document | Element | null>;
11
+ events<K extends keyof DocumentEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<DocumentEventMap[K]>;
10
12
  }
@@ -3,6 +3,7 @@ import { EventsFnOptions } from './DOMSource';
3
3
  import { DocumentDOMSource } from './DocumentDOMSource';
4
4
  import { BodyDOMSource } from './BodyDOMSource';
5
5
  import { VNode } from './snabbdom';
6
+ import { EnrichedEventStream } from './enrichEventStream';
6
7
  import { Scope, IsolateSink } from './isolate';
7
8
  import { IsolateModule } from './IsolateModule';
8
9
  import { EventDelegator } from './EventDelegator';
@@ -24,7 +25,7 @@ export declare class MainDOMSource {
24
25
  get namespace(): Array<Scope>;
25
26
  select<T extends keyof SpecialSelector>(selector: T): SpecialSelector[T];
26
27
  select(selector: string): MainDOMSource;
27
- events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): Stream<HTMLElementEventMap[K]>;
28
+ events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<HTMLElementEventMap[K]>;
28
29
  dispose(): void;
29
30
  isolateSource: (source: MainDOMSource, scope: string) => MainDOMSource;
30
31
  isolateSink: IsolateSink<VNode>;
@@ -0,0 +1,24 @@
1
+ import { Stream } from 'xstream';
2
+ export interface EnrichedEventStream<T = Event> extends Stream<T> {
3
+ value(): EnrichedEventStream<string>;
4
+ value<R>(fn: (val: string) => R): EnrichedEventStream<R>;
5
+ checked(): EnrichedEventStream<boolean>;
6
+ checked<R>(fn: (val: boolean) => R): EnrichedEventStream<R>;
7
+ data(name: string): EnrichedEventStream<string | undefined>;
8
+ data<R>(name: string, fn: (val: string | undefined) => R): EnrichedEventStream<R>;
9
+ target(): EnrichedEventStream<EventTarget | null>;
10
+ target<R>(fn: (el: EventTarget | null) => R): EnrichedEventStream<R>;
11
+ key(): EnrichedEventStream<string>;
12
+ key<R>(fn: (key: string) => R): EnrichedEventStream<R>;
13
+ }
14
+ /**
15
+ * Adds chainable convenience methods to a DOM event stream.
16
+ *
17
+ * DOM.select('.input').events('input').value()
18
+ * DOM.input('.input').value()
19
+ * DOM.select('.item').events('click').data('id')
20
+ * DOM.click('.item').data('id', Number)
21
+ * DOM.change('.checkbox').checked()
22
+ * DOM.keydown('.field').key()
23
+ */
24
+ export declare function enrichEventStream(stream$: any): any;
package/dist/index.cjs.js CHANGED
@@ -2205,7 +2205,18 @@ function wrapDOMSource(domSource) {
2205
2205
  }
2206
2206
  });
2207
2207
  }
2208
- const ABORT = Symbol('ABORT');
2208
+ const ABORT = Symbol.for('sygnal.ABORT');
2209
+ /**
2210
+ * Check if a value is the ABORT sentinel.
2211
+ * Uses Symbol.for() identity first, then falls back to description check
2212
+ * in case bundlers (e.g. Vite) create duplicate module instances with
2213
+ * separate Symbol.for() registries.
2214
+ */
2215
+ function isAbort(value) {
2216
+ if (value === ABORT)
2217
+ return true;
2218
+ return typeof value === 'symbol' && value.description === 'sygnal.ABORT';
2219
+ }
2209
2220
  function normalizeCalculatedEntry(field, entry) {
2210
2221
  if (typeof entry === 'function') {
2211
2222
  return { fn: entry, deps: null };
@@ -2927,7 +2938,7 @@ class Component {
2927
2938
  const enhancedState = this.addCalculated(_state);
2928
2939
  props.state = enhancedState;
2929
2940
  const newState = reducer(enhancedState, data, next, props);
2930
- if (newState === ABORT)
2941
+ if (isAbort(newState))
2931
2942
  return _state;
2932
2943
  return this.cleanupCalculated(newState);
2933
2944
  }
@@ -2956,7 +2967,7 @@ class Component {
2956
2967
  return ABORT;
2957
2968
  }
2958
2969
  }
2959
- }).filter((result) => result !== ABORT);
2970
+ }).filter((result) => !isAbort(result));
2960
2971
  }
2961
2972
  else if (reducer === undefined || reducer === true) {
2962
2973
  returnStream$ = filtered$.map(({ data }) => data);
@@ -6326,6 +6337,12 @@ function vnodeToHtml(vnode) {
6326
6337
  if (VOID_ELEMENTS.has(tag)) {
6327
6338
  return html;
6328
6339
  }
6340
+ // If innerHTML is set via props, use it as raw content (no escaping)
6341
+ if (vnode.data?.props?.innerHTML != null) {
6342
+ html += String(vnode.data.props.innerHTML);
6343
+ html += `</${tag}>`;
6344
+ return html;
6345
+ }
6329
6346
  // Children — snabbdom uses `text` for single text children (even when
6330
6347
  // `children` holds a text element object). Prioritize `text` when set.
6331
6348
  if (vnode.text != null) {
package/dist/index.esm.js CHANGED
@@ -2184,7 +2184,18 @@ function wrapDOMSource(domSource) {
2184
2184
  }
2185
2185
  });
2186
2186
  }
2187
- const ABORT = Symbol('ABORT');
2187
+ const ABORT = Symbol.for('sygnal.ABORT');
2188
+ /**
2189
+ * Check if a value is the ABORT sentinel.
2190
+ * Uses Symbol.for() identity first, then falls back to description check
2191
+ * in case bundlers (e.g. Vite) create duplicate module instances with
2192
+ * separate Symbol.for() registries.
2193
+ */
2194
+ function isAbort(value) {
2195
+ if (value === ABORT)
2196
+ return true;
2197
+ return typeof value === 'symbol' && value.description === 'sygnal.ABORT';
2198
+ }
2188
2199
  function normalizeCalculatedEntry(field, entry) {
2189
2200
  if (typeof entry === 'function') {
2190
2201
  return { fn: entry, deps: null };
@@ -2906,7 +2917,7 @@ class Component {
2906
2917
  const enhancedState = this.addCalculated(_state);
2907
2918
  props.state = enhancedState;
2908
2919
  const newState = reducer(enhancedState, data, next, props);
2909
- if (newState === ABORT)
2920
+ if (isAbort(newState))
2910
2921
  return _state;
2911
2922
  return this.cleanupCalculated(newState);
2912
2923
  }
@@ -2935,7 +2946,7 @@ class Component {
2935
2946
  return ABORT;
2936
2947
  }
2937
2948
  }
2938
- }).filter((result) => result !== ABORT);
2949
+ }).filter((result) => !isAbort(result));
2939
2950
  }
2940
2951
  else if (reducer === undefined || reducer === true) {
2941
2952
  returnStream$ = filtered$.map(({ data }) => data);
@@ -6305,6 +6316,12 @@ function vnodeToHtml(vnode) {
6305
6316
  if (VOID_ELEMENTS.has(tag)) {
6306
6317
  return html;
6307
6318
  }
6319
+ // If innerHTML is set via props, use it as raw content (no escaping)
6320
+ if (vnode.data?.props?.innerHTML != null) {
6321
+ html += String(vnode.data.props.innerHTML);
6322
+ html += `</${tag}>`;
6323
+ return html;
6324
+ }
6308
6325
  // Children — snabbdom uses `text` for single text children (even when
6309
6326
  // `children` holds a text element object). Prioritize `text` when set.
6310
6327
  if (vnode.text != null) {