sygnal 5.3.7 → 5.4.0

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 (226) hide show
  1. package/README.md +53 -38
  2. package/dist/astro/client.cjs.js +18 -8106
  3. package/dist/astro/client.cjs.js.map +1 -0
  4. package/dist/astro/client.mjs +17 -8105
  5. package/dist/astro/client.mjs.map +1 -0
  6. package/dist/astro/index.cjs.js +768 -9
  7. package/dist/astro/index.cjs.js.map +1 -0
  8. package/dist/astro/index.mjs +768 -9
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/server.cjs.js +4 -1
  11. package/dist/astro/server.cjs.js.map +1 -0
  12. package/dist/astro/server.mjs +4 -1
  13. package/dist/astro/server.mjs.map +1 -0
  14. package/dist/diagnostics.cjs.js +1297 -0
  15. package/dist/diagnostics.cjs.js.map +1 -0
  16. package/dist/diagnostics.esm.js +1267 -0
  17. package/dist/diagnostics.esm.js.map +1 -0
  18. package/dist/index.cjs.js +2981 -1386
  19. package/dist/index.cjs.js.map +1 -0
  20. package/dist/index.d.ts +848 -111
  21. package/dist/index.esm.js +2997 -1411
  22. package/dist/index.esm.js.map +1 -0
  23. package/dist/jsx-dev-runtime.cjs.js +15 -9
  24. package/dist/jsx-dev-runtime.cjs.js.map +1 -0
  25. package/dist/jsx-dev-runtime.esm.js +15 -9
  26. package/dist/jsx-dev-runtime.esm.js.map +1 -0
  27. package/dist/jsx-runtime.cjs.js +15 -9
  28. package/dist/jsx-runtime.cjs.js.map +1 -0
  29. package/dist/jsx-runtime.esm.js +15 -9
  30. package/dist/jsx-runtime.esm.js.map +1 -0
  31. package/dist/jsx.cjs.js +15 -9
  32. package/dist/jsx.cjs.js.map +1 -0
  33. package/dist/jsx.esm.js +15 -9
  34. package/dist/jsx.esm.js.map +1 -0
  35. package/dist/sygnal.min.js +2 -1
  36. package/dist/sygnal.min.js.map +1 -0
  37. package/dist/vike/+config.cjs.js +1 -0
  38. package/dist/vike/+config.cjs.js.map +1 -0
  39. package/dist/vike/+config.js +1 -0
  40. package/dist/vike/+config.js.map +1 -0
  41. package/dist/vike/ClientOnly.cjs.js +1 -0
  42. package/dist/vike/ClientOnly.cjs.js.map +1 -0
  43. package/dist/vike/ClientOnly.mjs +1 -0
  44. package/dist/vike/ClientOnly.mjs.map +1 -0
  45. package/dist/vike/onRenderClient.cjs.js +1 -0
  46. package/dist/vike/onRenderClient.cjs.js.map +1 -0
  47. package/dist/vike/onRenderClient.mjs +1 -0
  48. package/dist/vike/onRenderClient.mjs.map +1 -0
  49. package/dist/vike/onRenderHtml.cjs.js +1 -0
  50. package/dist/vike/onRenderHtml.cjs.js.map +1 -0
  51. package/dist/vike/onRenderHtml.mjs +1 -0
  52. package/dist/vike/onRenderHtml.mjs.map +1 -0
  53. package/dist/vite/plugin.cjs.js +670 -44
  54. package/dist/vite/plugin.cjs.js.map +1 -0
  55. package/dist/vite/plugin.mjs +670 -44
  56. package/dist/vite/plugin.mjs.map +1 -0
  57. package/llms.txt +250 -0
  58. package/package.json +15 -6
  59. package/src/astro/client.ts +13 -2
  60. package/src/astro/index.d.ts +24 -1
  61. package/src/astro/index.ts +40 -9
  62. package/src/astro/server.ts +3 -1
  63. package/src/collection.ts +5 -2
  64. package/src/component.ts +375 -330
  65. package/src/cycle/dom/MainDOMSource.ts +4 -0
  66. package/src/cycle/dom/classNameModule.ts +74 -0
  67. package/src/cycle/dom/controlledInputModule.ts +33 -0
  68. package/src/cycle/dom/enrichEventStream.ts +9 -3
  69. package/src/cycle/dom/makeDOMDriver.ts +1 -2
  70. package/src/cycle/dom/mockDOMSource.ts +62 -8
  71. package/src/cycle/dom/modules.ts +5 -1
  72. package/src/cycle/state/Collection.ts +13 -3
  73. package/src/cycle/state/StateSource.ts +1 -1
  74. package/src/cycle/state/pickCombine.ts +35 -1
  75. package/src/cycle/state/withState.ts +1 -1
  76. package/src/extra/devtools.ts +8 -0
  77. package/src/extra/diagnostics/checks/collections.ts +67 -0
  78. package/src/extra/diagnostics/checks/dom.ts +241 -0
  79. package/src/extra/diagnostics/checks/events.ts +68 -0
  80. package/src/extra/diagnostics/checks/index.ts +109 -0
  81. package/src/extra/diagnostics/checks/inspect.ts +319 -0
  82. package/src/extra/diagnostics/checks/props.ts +51 -0
  83. package/src/extra/diagnostics/checks/public.d.ts +211 -0
  84. package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
  85. package/src/extra/diagnostics/checks/shared.ts +119 -0
  86. package/src/extra/diagnostics/checks/state.ts +55 -0
  87. package/src/extra/diagnostics/checks/strict.ts +112 -0
  88. package/src/extra/diagnostics/checks/wiring.ts +65 -0
  89. package/src/extra/diagnostics/codes.ts +232 -0
  90. package/src/extra/diagnostics/index.ts +328 -0
  91. package/src/extra/diagnostics/legacy.ts +59 -0
  92. package/src/extra/driverFactories.ts +106 -58
  93. package/src/extra/eventDriver.ts +4 -0
  94. package/src/extra/flatten.ts +75 -0
  95. package/src/extra/reducers.ts +12 -5
  96. package/src/extra/run.ts +11 -0
  97. package/src/extra/testing.ts +890 -180
  98. package/src/extra/xstreamExtras.ts +269 -0
  99. package/src/index.d.ts +428 -17
  100. package/src/index.ts +7 -6
  101. package/src/pragma/index.ts +14 -9
  102. package/src/switchable.ts +10 -8
  103. package/src/vike/types.ts +21 -4
  104. package/src/vite/plugin.d.ts +95 -2
  105. package/src/vite/plugin.ts +720 -54
  106. package/dist/astro/astro/client.d.ts +0 -5
  107. package/dist/astro/astro/index.d.ts +0 -16
  108. package/dist/astro/astro/server.d.ts +0 -12
  109. package/dist/astro/client.d.ts +0 -5
  110. package/dist/astro/client.esm.js +0 -4665
  111. package/dist/astro/collection.d.ts +0 -12
  112. package/dist/astro/component.d.ts +0 -22
  113. package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
  114. package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
  115. package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
  116. package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
  117. package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
  118. package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
  119. package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
  120. package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
  121. package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
  122. package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
  123. package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
  124. package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
  125. package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
  126. package/dist/astro/cycle/dom/index.d.ts +0 -8
  127. package/dist/astro/cycle/dom/isolate.d.ts +0 -9
  128. package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
  129. package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
  130. package/dist/astro/cycle/dom/modules.d.ts +0 -5
  131. package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
  132. package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
  133. package/dist/astro/cycle/dom/thunk.d.ts +0 -11
  134. package/dist/astro/cycle/dom/utils.d.ts +0 -8
  135. package/dist/astro/cycle/isolate/index.d.ts +0 -42
  136. package/dist/astro/cycle/run/adapt.d.ts +0 -6
  137. package/dist/astro/cycle/run/index.d.ts +0 -36
  138. package/dist/astro/cycle/run/internals.d.ts +0 -8
  139. package/dist/astro/cycle/run/types.d.ts +0 -49
  140. package/dist/astro/cycle/state/Collection.d.ts +0 -29
  141. package/dist/astro/cycle/state/StateSource.d.ts +0 -20
  142. package/dist/astro/cycle/state/index.d.ts +0 -5
  143. package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
  144. package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
  145. package/dist/astro/cycle/state/types.d.ts +0 -18
  146. package/dist/astro/cycle/state/withState.d.ts +0 -17
  147. package/dist/astro/extra/classes.d.ts +0 -7
  148. package/dist/astro/extra/devtools.d.ts +0 -78
  149. package/dist/astro/extra/dragDriver.d.ts +0 -20
  150. package/dist/astro/extra/driverFactories.d.ts +0 -12
  151. package/dist/astro/extra/eventDriver.d.ts +0 -9
  152. package/dist/astro/extra/exactState.d.ts +0 -1
  153. package/dist/astro/extra/hmr.d.ts +0 -17
  154. package/dist/astro/extra/logDriver.d.ts +0 -2
  155. package/dist/astro/extra/processDrag.d.ts +0 -19
  156. package/dist/astro/extra/processForm.d.ts +0 -15
  157. package/dist/astro/extra/run.d.ts +0 -13
  158. package/dist/astro/extra/xstreamCompat.d.ts +0 -5
  159. package/dist/astro/index.d.ts +0 -19
  160. package/dist/astro/index.esm.js +0 -27
  161. package/dist/astro/jsx-dev-runtime.d.ts +0 -1
  162. package/dist/astro/jsx-runtime.d.ts +0 -4
  163. package/dist/astro/jsx.d.ts +0 -2
  164. package/dist/astro/pragma/fn.d.ts +0 -7
  165. package/dist/astro/pragma/index.d.ts +0 -7
  166. package/dist/astro/pragma/is.d.ts +0 -10
  167. package/dist/astro/server.d.ts +0 -12
  168. package/dist/astro/server.esm.js +0 -25
  169. package/dist/astro/switchable.d.ts +0 -7
  170. package/dist/collection.d.ts +0 -12
  171. package/dist/component.d.ts +0 -22
  172. package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
  173. package/dist/cycle/dom/DOMSource.d.ts +0 -11
  174. package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
  175. package/dist/cycle/dom/ElementFinder.d.ts +0 -8
  176. package/dist/cycle/dom/EventDelegator.d.ts +0 -34
  177. package/dist/cycle/dom/IsolateModule.d.ts +0 -23
  178. package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
  179. package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
  180. package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
  181. package/dist/cycle/dom/SymbolTree.d.ts +0 -9
  182. package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
  183. package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
  184. package/dist/cycle/dom/fromEvent.d.ts +0 -6
  185. package/dist/cycle/dom/index.d.ts +0 -8
  186. package/dist/cycle/dom/isolate.d.ts +0 -9
  187. package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
  188. package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
  189. package/dist/cycle/dom/modules.d.ts +0 -5
  190. package/dist/cycle/dom/snabbdom.d.ts +0 -22
  191. package/dist/cycle/dom/styleModule.d.ts +0 -13
  192. package/dist/cycle/dom/thunk.d.ts +0 -11
  193. package/dist/cycle/dom/utils.d.ts +0 -8
  194. package/dist/cycle/isolate/index.d.ts +0 -42
  195. package/dist/cycle/run/adapt.d.ts +0 -6
  196. package/dist/cycle/run/index.d.ts +0 -36
  197. package/dist/cycle/run/internals.d.ts +0 -8
  198. package/dist/cycle/run/types.d.ts +0 -49
  199. package/dist/cycle/state/Collection.d.ts +0 -29
  200. package/dist/cycle/state/StateSource.d.ts +0 -20
  201. package/dist/cycle/state/index.d.ts +0 -5
  202. package/dist/cycle/state/pickCombine.d.ts +0 -3
  203. package/dist/cycle/state/pickMerge.d.ts +0 -3
  204. package/dist/cycle/state/types.d.ts +0 -18
  205. package/dist/cycle/state/withState.d.ts +0 -17
  206. package/dist/extra/classes.d.ts +0 -7
  207. package/dist/extra/devtools.d.ts +0 -78
  208. package/dist/extra/dragDriver.d.ts +0 -20
  209. package/dist/extra/driverFactories.d.ts +0 -12
  210. package/dist/extra/eventDriver.d.ts +0 -9
  211. package/dist/extra/exactState.d.ts +0 -1
  212. package/dist/extra/hmr.d.ts +0 -17
  213. package/dist/extra/logDriver.d.ts +0 -2
  214. package/dist/extra/processDrag.d.ts +0 -19
  215. package/dist/extra/processForm.d.ts +0 -15
  216. package/dist/extra/run.d.ts +0 -13
  217. package/dist/extra/xstreamCompat.d.ts +0 -5
  218. package/dist/jsx-dev-runtime.d.ts +0 -1
  219. package/dist/jsx-dev-runtime.js +0 -224
  220. package/dist/jsx-runtime.d.ts +0 -4
  221. package/dist/jsx-runtime.js +0 -224
  222. package/dist/jsx.d.ts +0 -2
  223. package/dist/pragma/fn.d.ts +0 -7
  224. package/dist/pragma/index.d.ts +0 -7
  225. package/dist/pragma/is.d.ts +0 -10
  226. package/dist/switchable.d.ts +0 -7
package/src/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { MainDOMSource } from './cycle/dom/MainDOMSource'
2
2
  import type { EnrichedEventStream } from './cycle/dom/enrichEventStream'
3
3
  import type { StateSource } from './cycle/state/index'
4
4
  import xsDefault from 'xstream'
5
+ import type { InspectGraph } from './extra/diagnostics/checks/public'
5
6
  import type { MemoryStream, Stream } from 'xstream'
6
7
 
7
8
  export declare const ABORT: unique symbol
@@ -60,6 +61,72 @@ type StateOnlyReducer<STATE, RETURN = any> = (
60
61
 
61
62
  export type Event<DATA = any> = { type: string; data: DATA }
62
63
 
64
+ // ── EVENTS registry ────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Registry of global EVENTS bus event names and their payload types.
68
+ *
69
+ * Empty by default, which keeps the EVENTS bus untyped (`any`). Augment it to
70
+ * type-check `EVENTS.select()`, `event()`, `emit()` and raw EVENTS sink returns:
71
+ *
72
+ * declare module 'sygnal' {
73
+ * interface SygnalEvents {
74
+ * DELETE_LANE: { laneId: string }
75
+ * RESET: void // no payload: event('RESET')
76
+ * }
77
+ * }
78
+ *
79
+ * Once the registry has at least one entry, unregistered event names are type errors.
80
+ * The augmenting file must be a module (have at least one import or export).
81
+ */
82
+ export interface SygnalEvents {}
83
+
84
+ /** Valid event names: `string` while the registry is empty, otherwise the registered names. */
85
+ export type EventName = keyof SygnalEvents extends never ? string : keyof SygnalEvents & string
86
+
87
+ /** Payload type of a registered event (`any` while the registry is empty). */
88
+ export type EventPayload<TYPE extends string = string> = keyof SygnalEvents extends never
89
+ ? any
90
+ : TYPE extends keyof SygnalEvents ? SygnalEvents[TYPE] : never
91
+
92
+ /**
93
+ * An event object as put on the EVENTS bus. While the registry is empty this is `Event<any>`;
94
+ * otherwise it is the union of `{ type, data }` for every registered event.
95
+ */
96
+ export type RegisteredEvent = keyof SygnalEvents extends never
97
+ ? Event<any>
98
+ : { [TYPE in keyof SygnalEvents & string]: { type: TYPE; data: SygnalEvents[TYPE] } }[keyof SygnalEvents & string]
99
+
100
+ /** The `{ type, data }` object produced by `event(type, ...)` / `emit(type, ...)`. */
101
+ export type EmittedEvent<TYPE extends string = string> = keyof SygnalEvents extends never
102
+ ? { type: TYPE; data: any }
103
+ : { type: TYPE; data: EventPayload<TYPE> }
104
+
105
+ /** The `next()` function as seen by an event payload function. */
106
+ type EventNextFunction = (action: string, data?: any, delay?: number) => void
107
+
108
+ /** Payload function for `event()` / `emit()`: receives the reducer arguments, returns the event data. */
109
+ export type EventPayloadFunction<TYPE extends string = string, STATE = any, DATA = any> =
110
+ (state: STATE, data: DATA, next: EventNextFunction, props: any) => EventPayload<TYPE>
111
+
112
+ /**
113
+ * Remaining arguments of `event()` / `emit()` when the payload is a static value.
114
+ * The payload may be omitted when the registry is empty or the event's payload
115
+ * type accepts `undefined` (e.g. `void`).
116
+ */
117
+ type StaticEventArgs<TYPE extends string> = keyof SygnalEvents extends never
118
+ ? [payload?: any]
119
+ : undefined extends EventPayload<TYPE>
120
+ ? [payload?: EventPayload<TYPE>]
121
+ : [payload: EventPayload<TYPE>]
122
+
123
+ /**
124
+ * The sink function returned by `event()`. Use it as the value of an `EVENTS` key in a
125
+ * model entry: `ACTION: { STATE: ..., EVENTS: event('TYPE', fn) }`.
126
+ */
127
+ export type EventSink<TYPE extends string = string, STATE = any, DATA = any> =
128
+ (state: STATE, data: DATA, next: any, props: any) => EmittedEvent<TYPE>
129
+
63
130
  export type NonStateSinkReturns = {
64
131
  EVENTS?: unknown;
65
132
  LOG?: unknown;
@@ -67,7 +134,7 @@ export type NonStateSinkReturns = {
67
134
  }
68
135
 
69
136
  type ResolvedNonStateSinkReturns<SINK_RETURNS extends NonStateSinkReturns = {}> = {
70
- EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN : Event<any>;
137
+ EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN : RegisteredEvent;
71
138
  LOG: SINK_RETURNS extends { LOG: infer LOG_RETURN } ? LOG_RETURN : any;
72
139
  PARENT: SINK_RETURNS extends { PARENT: infer PARENT_RETURN } ? PARENT_RETURN : any;
73
140
  }
@@ -149,7 +216,43 @@ type ComponentModel<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, SINK_RETURNS ext
149
216
  >
150
217
  }
151
218
 
219
+ type TrimSpaces<S extends string> =
220
+ S extends ` ${infer REST}` ? TrimSpaces<REST>
221
+ : S extends `${infer REST} ` ? TrimSpaces<REST>
222
+ : S
223
+
224
+ /** Value type produced by a PARENT sink value (a reducer's return, minus ABORT/undefined). */
225
+ type ParentSinkValueReturn<VALUE> =
226
+ VALUE extends (...args: any[]) => infer RETURN ? Exclude<RETURN, ABORT | undefined | void> : never
227
+
228
+ type ParentPayloadFromEntry<ENTRY> =
229
+ ENTRY extends (...args: any[]) => any ? never
230
+ : ENTRY extends object
231
+ ? 'PARENT' extends keyof ENTRY ? ParentSinkValueReturn<NonNullable<ENTRY['PARENT']>> : never
232
+ : never
233
+
234
+ type ParentPayloadsOfModel<MODEL> = {
235
+ [ACTION_KEY in keyof MODEL]-?: ACTION_KEY extends `${string}|${infer SINK}`
236
+ ? TrimSpaces<SINK> extends 'PARENT' ? ParentSinkValueReturn<NonNullable<MODEL[ACTION_KEY]>> : never
237
+ : ParentPayloadFromEntry<NonNullable<MODEL[ACTION_KEY]>>
238
+ }[keyof MODEL]
239
+
240
+ type AnyIfNever<T> = [T] extends [never] ? any : T
241
+
242
+ /**
243
+ * The value type a component sends to its parent through the `PARENT` sink, inferred from the
244
+ * component's `model` (object-form `{ PARENT: fn }` entries and `'ACTION | PARENT'` shorthand).
245
+ * Falls back to `any` when it can't be inferred (no model, a `Component<...>` annotation without
246
+ * a `PARENT` entry in its `SINK_RETURNS`, or `PARENT: true` pass-through entries only).
247
+ */
248
+ export type ParentPayloadOf<COMPONENT> =
249
+ COMPONENT extends { model?: infer MODEL }
250
+ ? 0 extends (1 & MODEL) ? any : AnyIfNever<ParentPayloadsOfModel<NonNullable<MODEL>>>
251
+ : any
252
+
152
253
  type ChildSource = {
254
+ /** Typed: the stream type is inferred from the child's PARENT sink (falls back to `any`). */
255
+ select<COMPONENT extends (...args: any[]) => any>(component: COMPONENT): Stream<ParentPayloadOf<COMPONENT>>;
153
256
  select<T = any>(component: (...args: any[]) => any): Stream<T>;
154
257
  select<T = any>(name: string): Stream<T>;
155
258
  }
@@ -158,9 +261,11 @@ export type SygnalDOMSource = MainDOMSource & {
158
261
  [eventName: string]: (selector: string) => EnrichedEventStream<globalThis.Event>
159
262
  }
160
263
 
161
- export type EventsSource<EVENTS = any> = Stream<Event<EVENTS>> & {
162
- select<T = any>(type: string): Stream<T>;
163
- }
264
+ type EventsSelect = keyof SygnalEvents extends never
265
+ ? { select<T = any>(type: string): Stream<T>; }
266
+ : { select<TYPE extends keyof SygnalEvents & string>(type: TYPE): Stream<SygnalEvents[TYPE]>; }
267
+
268
+ export type EventsSource<EVENTS = any> = Stream<Event<EVENTS>> & EventsSelect
164
269
 
165
270
  export type DefaultDrivers<STATE, EVENTS = any> = {
166
271
  STATE: {
@@ -217,6 +322,42 @@ export type FixDrivers<DRIVERS> =
217
322
 
218
323
  type CombinedSources<STATE, DRIVERS> = Sources<DefaultDrivers<STATE> & DRIVERS> & { dispose$: Stream<boolean> }
219
324
 
325
+ /**
326
+ * The sources object an intent function receives (DOM, STATE, EVENTS, CHILD, dispose$, plus
327
+ * any custom drivers). Use it to annotate an intent declared before its component:
328
+ *
329
+ * const intent = ({ DOM }: IntentSources<State>) => ({ INC: DOM.click('.inc') })
330
+ */
331
+ export type IntentSources<STATE = any, DRIVERS = {}> = CombinedSources<STATE, FixDrivers<DRIVERS>>
332
+
333
+ type StreamPayload<STREAM> = STREAM extends Stream<infer T> ? T : any
334
+
335
+ type IntentReturnToActions<RETURN> = {
336
+ [ACTION_KEY in keyof RETURN & string]-?: StreamPayload<Exclude<RETURN[ACTION_KEY], undefined>>
337
+ }
338
+
339
+ /**
340
+ * Derives the ACTIONS map (action name → payload type) from an intent function's type
341
+ * (or from its return object type): each key's `Stream<T>` becomes `T`.
342
+ *
343
+ * const intent = ({ DOM }: IntentSources<State>) => ({
344
+ * INC: DOM.click('.inc').mapTo(1), // Stream<number>
345
+ * NAME: DOM.input('.name').value(), // Stream<string>
346
+ * })
347
+ * const Counter: Component<State, {}, {}, ActionsOf<typeof intent>> = ...
348
+ * Counter.intent = intent
349
+ * Counter.model = { INC: (state, n) => ..., NAME: (state, name) => ... } // n: number, name: string
350
+ *
351
+ * With it, model keys not returned by the intent are type errors (the built-ins BOOTSTRAP,
352
+ * INITIALIZE, HYDRATE and DISPOSE stay allowed). Actions reached only through `next()` are
353
+ * added explicitly:
354
+ *
355
+ * type Actions = ActionsOf<typeof intent> & { SAVED: { id: string } }
356
+ */
357
+ export type ActionsOf<INTENT> = INTENT extends (...args: any[]) => infer RETURN
358
+ ? IntentReturnToActions<RETURN>
359
+ : IntentReturnToActions<INTENT>
360
+
220
361
  interface ComponentIntent<STATE, DRIVERS, ACTIONS> {
221
362
  (args: CombinedSources<STATE, DRIVERS>): Partial<IntentActions<ACTIONS>>
222
363
  }
@@ -267,6 +408,12 @@ export type Component<
267
408
  model?: ComponentModel<STATE, PROPS, FixDrivers<DRIVERS>, ACTIONS, CALCULATED, SINK_RETURNS, CONTEXT>;
268
409
  intent?: ComponentIntent<STATE & CALCULATED, FixDrivers<DRIVERS>, ACTIONS>;
269
410
  initialState?: STATE;
411
+ /**
412
+ * Give a sub-component its own state instead of the slice its parent passes in.
413
+ * Required to use `initialState` on a sub-component (otherwise SYG405). Without a
414
+ * `state` prop the state is local to the instance and never written to the parent.
415
+ */
416
+ isolatedState?: boolean;
270
417
  calculated?: Calculated<STATE, CALCULATED>;
271
418
  storeCalculatedInState?: boolean;
272
419
  context?: Context<STATE & CALCULATED, CONTEXT>;
@@ -294,12 +441,37 @@ export type RootComponent<
294
441
  */
295
442
  type AnyComponent = ((...args: any[]) => any) & Record<string, any>
296
443
 
297
- export type CollectionProps<PROPS = any> = {
444
+ /** Keys of STATE whose value is an array (optional/nullable arrays included). */
445
+ export type ArrayKeysOf<STATE> = {
446
+ [KEY in keyof STATE]-?: NonNullable<STATE[KEY]> extends ReadonlyArray<any> ? KEY : never
447
+ }[keyof STATE] & string
448
+
449
+ /**
450
+ * Valid `from` values for a Collection: any string or Lense while the parent STATE is unknown
451
+ * (`any`); otherwise an array-valued key of STATE or a Lense over STATE.
452
+ */
453
+ export type CollectionFrom<STATE = any> = 0 extends (1 & STATE)
454
+ ? string | Lense
455
+ : ArrayKeysOf<STATE> | Lense<STATE, any>
456
+
457
+ /**
458
+ * Collection props. Pass the parent component's state type as STATE to type-check `from`:
459
+ *
460
+ * const TaskCollection = Collection<{}, LaneState> // instantiation expression
461
+ * <TaskCollection of={TaskCard} from="tasks" /> // 'tasks' must be an array key of LaneState
462
+ */
463
+ export type CollectionProps<PROPS = any, STATE = any> = {
298
464
  of: AnyComponent;
299
- from: string | Lense;
465
+ from: CollectionFrom<STATE>;
300
466
  filter?: Filter;
301
467
  sort?: string | SortFunction | SortObject;
302
- } & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort'>
468
+ /**
469
+ * Item field used as the key that tracks each item (its component instance, isolation
470
+ * scope and DOM) across updates. Items are keyed by `id` by default; keys should be
471
+ * unique and stable (an item without the field is keyed by its index).
472
+ */
473
+ idfield?: string;
474
+ } & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort' | 'idfield'>
303
475
 
304
476
  export type SwitchableProps<PROPS = any> = {
305
477
  of: Record<string, AnyComponent>;
@@ -319,6 +491,16 @@ export type TransitionProps = {
319
491
  children?: any;
320
492
  }
321
493
 
494
+ export type SuspenseProps = {
495
+ /**
496
+ * Shown (wrapped in `<div data-sygnal-suspense="pending">`) while any child is not ready:
497
+ * a `lazy()` component still loading, or a component with an explicit READY model entry
498
+ * that hasn't emitted true. A string renders as text. Without it the children render as-is.
499
+ */
500
+ fallback?: JSX.Element | string;
501
+ children?: any;
502
+ }
503
+
322
504
  export type SlotProps = {
323
505
  name?: string;
324
506
  children?: any;
@@ -326,12 +508,93 @@ export type SlotProps = {
326
508
 
327
509
  export type ClassesType = (string | string[] | { [className: string]: boolean | undefined })[]
328
510
 
511
+ /**
512
+ * Diagnostics mode.
513
+ * - 'off' — no checks, no collection (default in production)
514
+ * - 'collect' — collect diagnostics silently (read with getDiagnostics())
515
+ * - 'warn' — collect and print warn/error diagnostics to the console (default in Vite dev)
516
+ * - 'error' — collect and throw on warn/error diagnostics
517
+ */
518
+ export type DiagnosticsMode = 'off' | 'collect' | 'warn' | 'error'
519
+
520
+ /** Stable diagnostic code, e.g. 'SYG101'. See https://sygnal.js.org/reference/errors */
521
+ export type DiagnosticCode = `SYG${number}`
522
+
523
+ export type DiagnosticSeverity = 'error' | 'warn' | 'info'
524
+
525
+ export type Diagnostic = {
526
+ code: DiagnosticCode;
527
+ severity: DiagnosticSeverity;
528
+ /** Name of the component the diagnostic is about */
529
+ component?: string;
530
+ /** What is wrong */
531
+ message: string;
532
+ /** How to fix it */
533
+ fix?: string;
534
+ /** Structured payload (check-specific) */
535
+ data?: any;
536
+ /** Link to the docs entry for this code */
537
+ docsUrl: string;
538
+ /** Fully formatted message: `[Sygnal SYG123] Component: message. fix docsUrl` */
539
+ text: string;
540
+ timestamp: number;
541
+ }
542
+
543
+ export type DiagnosticsOptions = {
544
+ mode?: DiagnosticsMode;
545
+ /** Codes to ignore entirely */
546
+ ignore?: DiagnosticCode[];
547
+ }
548
+
329
549
  export type RunOptions = {
330
550
  mountPoint?: string;
331
551
  fragments?: boolean;
332
552
  useDefaultDrivers?: boolean;
553
+ /**
554
+ * Runtime diagnostics. Takes precedence over `globalThis.__SYGNAL_DEV__`
555
+ * (set by the Sygnal Vite plugin in dev), which enables 'warn'. Default: 'off'.
556
+ */
557
+ diagnostics?: DiagnosticsMode | DiagnosticsOptions;
333
558
  }
334
559
 
560
+ /** All diagnostics collected so far (most recent last). */
561
+ export function getDiagnostics(): Diagnostic[]
562
+
563
+ /** Clear the collected diagnostics. */
564
+ export function clearDiagnostics(): void
565
+
566
+ /** Subscribe to diagnostics as they are reported. Returns an unsubscribe function. */
567
+ export function onDiagnostic(callback: (diagnostic: Diagnostic) => void): () => void
568
+
569
+ export type {
570
+ InspectGraph,
571
+ InspectComponent,
572
+ InspectAction,
573
+ InspectActionTrigger,
574
+ InspectChild,
575
+ InspectSelector,
576
+ InspectDiagnostic,
577
+ } from './extra/diagnostics/checks/public'
578
+
579
+ /**
580
+ * The Sygnal DevTools bridge (also `window.__SYGNAL_DEVTOOLS__` once run() has
581
+ * initialized it in a browser). Only the stable, documented members are typed.
582
+ */
583
+ export interface SygnalDevTools {
584
+ /** true while the browser extension is connected */
585
+ readonly connected: boolean
586
+ /** Diagnostics collected so far (same as getDiagnostics()) */
587
+ getDiagnostics(): Diagnostic[]
588
+ /**
589
+ * The machine-readable app graph of the live components. Present only when the
590
+ * 'sygnal/diagnostics' dev entry is loaded (it attaches this method); needs diagnostics on.
591
+ */
592
+ inspect?(): InspectGraph
593
+ }
594
+
595
+ /** The DevTools bridge singleton. */
596
+ export function getDevTools(): SygnalDevTools | undefined
597
+
335
598
  export type SygnalSinks<STATE = any, DRIVERS = {}> = {
336
599
  [SINK_NAME in keyof (DefaultDrivers<STATE> & FixDrivers<DRIVERS>) | string]?: Stream<any>
337
600
  }
@@ -406,16 +669,52 @@ export function set<S = any>(
406
669
  */
407
670
  export function toggle<S = any>(field: keyof S & string): (state: S) => S
408
671
 
672
+ type EmitEntry<TYPE extends string> = keyof SygnalEvents extends never
673
+ ? { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
674
+ : { EVENTS: (state: any, actionData: any, next: Function, props: any) => EmittedEvent<TYPE> }
675
+
409
676
  /**
410
677
  * Create a model entry that emits an EVENTS bus event.
678
+ * Prefer `event()` inside the object form: `ACTION: { EVENTS: event('TYPE', fn) }`.
411
679
  *
412
680
  * `emit('DELETE_LANE', (state) => ({ laneId: state.id }))`
413
681
  * `emit('REFRESH')`
682
+ *
683
+ * With a `SygnalEvents` registry, `type` and the payload are checked against it.
414
684
  */
415
- export function emit(
416
- type: string,
417
- data?: any | ((state: any, actionData: any, next: Function, props: any) => any)
418
- ): { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
685
+ export function emit<TYPE extends EventName>(
686
+ type: TYPE,
687
+ data: (state: any, actionData: any, next: Function, props: any) => EventPayload<TYPE>
688
+ ): EmitEntry<TYPE>
689
+ export function emit<TYPE extends EventName>(
690
+ type: TYPE,
691
+ ...data: StaticEventArgs<TYPE>
692
+ ): EmitEntry<TYPE>
693
+
694
+ /**
695
+ * Create an EVENTS sink function that puts `{ type, data }` on the EVENTS bus.
696
+ * Use it as the `EVENTS` value inside an object-form model entry:
697
+ *
698
+ * DELETE: {
699
+ * STATE: (state) => ({ ...state, deleting: true }),
700
+ * EVENTS: event('DELETE_LANE', (state) => ({ laneId: state.id })),
701
+ * }
702
+ *
703
+ * The payload is either a function `(state, data, next, props) => payload` or a static value:
704
+ * `event('RESET')`, `event('SET_MODE', 'dark')`.
705
+ *
706
+ * With a `SygnalEvents` registry, `type` must be a registered name and the payload must match
707
+ * its type. When used inside a model, the payload function's `state` and `data` parameters are
708
+ * typed from the component.
709
+ */
710
+ export function event<TYPE extends EventName, STATE = any, DATA = any>(
711
+ type: TYPE,
712
+ payload: EventPayloadFunction<TYPE, STATE, DATA>
713
+ ): EventSink<TYPE, STATE, DATA>
714
+ export function event<TYPE extends EventName>(
715
+ type: TYPE,
716
+ ...payload: StaticEventArgs<TYPE>
717
+ ): EventSink<TYPE>
419
718
 
420
719
  /**
421
720
  * Any object with an events() method (e.g., DOM.select('form')).
@@ -552,18 +851,43 @@ export function collection(...args: any[]): any
552
851
  export function switchable(...args: any[]): any
553
852
  export function portal(...args: any[]): any
554
853
 
555
- export function Collection<PROPS extends { [prop: string]: any }>(props: CollectionProps<PROPS>): JSX.Element
854
+ export function Collection<PROPS extends { [prop: string]: any }, STATE = any>(props: CollectionProps<PROPS, STATE>): JSX.Element
556
855
  export function Switchable<PROPS extends { [prop: string]: any }>(props: SwitchableProps<PROPS>): JSX.Element
557
856
  export function Portal(props: PortalProps): JSX.Element
558
857
  export function Transition(props: TransitionProps): JSX.Element
858
+ export function Suspense(props: SuspenseProps): JSX.Element
559
859
  export function Slot(props: SlotProps): JSX.Element
560
860
 
861
+ /**
862
+ * What `lazy()` returns: a sub-component used in JSX with its own props only
863
+ * (`<Chart title="Sales" />`; `state` is optional, as for any sub-component), that
864
+ * still carries the component statics (`model`, `intent`, …) once loaded.
865
+ */
866
+ export type LazyComponent<PROPS = any> = ((
867
+ props: PROPS & { state?: any; children?: JSX.Element | JSX.Element[] }
868
+ ) => JSX.Element) & Omit<Component<any, PROPS>, never>
869
+
561
870
  export function lazy<PROPS = any>(
562
871
  loadFn: () => Promise<{ default: Component<any, PROPS> } | Component<any, PROPS>>
563
- ): Component<any, PROPS>
872
+ ): LazyComponent<PROPS>
873
+
874
+ /** Payload on `errors()` of a driverFromAsync source when a request fails */
875
+ export type AsyncDriverError<INCOMING = any> = {
876
+ /** The rejection reason (or what `post` threw) */
877
+ error: any;
878
+ /** The request that failed */
879
+ request: INCOMING;
880
+ /** The request's selector property (default 'category') is copied here */
881
+ [selectorProperty: string]: any;
882
+ }
564
883
 
565
884
  export type AsyncDriverFromFunction<INCOMING = any, OUTGOING = any> = {
566
885
  select: (selector?: string | ((value: OUTGOING) => boolean)) => Stream<OUTGOING>
886
+ /**
887
+ * Failed requests (rejected promise, rejected/throwing `post`). Filters like
888
+ * `select()`. Failures are only console.error'd while nothing listens here.
889
+ */
890
+ errors: (selector?: string | ((error: AsyncDriverError<INCOMING>) => boolean)) => Stream<AsyncDriverError<INCOMING>>
567
891
  }
568
892
 
569
893
  export type DriverFromAsyncOptions<INCOMING = any, OUTGOING = any, RETURN = any> = {
@@ -629,13 +953,43 @@ export interface InstallPrompt {
629
953
 
630
954
  export function createInstallPrompt(): InstallPrompt
631
955
 
956
+ export interface SimulatedEventInit {
957
+ /** Merged into `event.target` (value, checked, dataset, ...) */
958
+ target?: Record<string, any>;
959
+ /** Shorthand for target.value */
960
+ value?: any;
961
+ /** Shorthand for target.checked */
962
+ checked?: boolean;
963
+ /** Merged into target.dataset (values become strings, like the DOM) */
964
+ dataset?: Record<string, any>;
965
+ /** Alias for dataset */
966
+ data?: Record<string, any>;
967
+ /** Keyboard key (e.key) */
968
+ key?: string;
969
+ /**
970
+ * Don't fail when the selector matches no rendered element: wait up to 300ms for it, then
971
+ * drop the event with SYG103 (info). Not copied onto the event.
972
+ */
973
+ allowMissing?: boolean;
974
+ /** Any other event properties are copied onto the event */
975
+ [prop: string]: any;
976
+ }
977
+
632
978
  export interface RenderOptions {
633
979
  /** Override initial state (defaults to component's .initialState) */
634
980
  initialState?: any;
635
981
  /** Mock DOM configuration — maps selectors to event streams */
636
982
  mockConfig?: Record<string, any>;
637
- /** Additional drivers beyond DOM, EVENTS, STATE, and LOG */
983
+ /** Additional drivers beyond DOM, EVENTS, STATE, and LOG (model sinks without a driver get a no-op one) */
638
984
  drivers?: Record<string, any>;
985
+ /**
986
+ * Diagnostics mode while rendered. Default: 'collect' (error-severity messages still print),
987
+ * or the current mode when diagnostics are already on. Restored when the last instance is
988
+ * disposed. Dev checks require `import 'sygnal/diagnostics'` (the Vite plugin adds it under Vitest).
989
+ */
990
+ diagnostics?: DiagnosticsMode;
991
+ /** Enable strict (canonical-form) runtime checks while rendered (requires 'sygnal/diagnostics') */
992
+ strict?: boolean;
639
993
  }
640
994
 
641
995
  export interface RenderResult {
@@ -649,14 +1003,68 @@ export interface RenderResult {
649
1003
  sinks: Record<string, any>;
650
1004
  /** All source objects by driver name */
651
1005
  sources: Record<string, any>;
652
- /** Push an action directly into the intent→model pipeline */
1006
+ /** Push an action into the intent→model pipeline under its real name (all sinks of the entry run) */
653
1007
  simulateAction: (actionName: string, data?: any) => void;
654
- /** Wait for state to satisfy a predicate (resolves with the matching state) */
1008
+ /**
1009
+ * Dispatch a synthetic DOM event through the mock DOM source so the component's real intent
1010
+ * streams fire (DOM.click('.x'), DOM.select('.x').events('click'), .value(), .data()).
1011
+ * Targets the first rendered element matching `selector` and bubbles within its isolation scope.
1012
+ * Selectors match the rendered tree like the real DOM: tag, .class, #id, [attr], [attr="v"]
1013
+ * (^= $= *= ~=), :first-child, :last-child, :only-child, :nth-child(an+b|odd|even),
1014
+ * :nth-last-child(), :first/last/only/nth-of-type, :not(), descendant ' ' and child '>'
1015
+ * combinators, ',' lists. Other syntax (:has(), '+', '~', pseudo-elements...) throws.
1016
+ * If nothing matches yet, the event waits (up to 300ms, re-checked on every render) for a
1017
+ * matching element, then targets the first one. If none renders, the test fails with an
1018
+ * error naming the selector: it rejects the pending next()/waitForState()/settle(), or is
1019
+ * thrown by the next t.* call or dispose() (or by simulateEvent itself when nothing is
1020
+ * pending and the tree is quiet). Pass `{ allowMissing: true }` to drop the event with SYG103
1021
+ * instead. It is never sent to every listener with that selector string. It also waits until
1022
+ * the listeners it reaches are subscribed (a just-mounted child subscribes a few ms late).
1023
+ * `'document'` / `'body'` go to the DOM.select('document' | 'body') listeners.
1024
+ * simulateAction/simulateEvent calls are delivered in call order; a waiting event holds the
1025
+ * calls after it. Reports SYG104 (selector only matches inside a child component).
1026
+ */
1027
+ simulateEvent: (selector: string, eventType: string, eventInit?: SimulatedEventInit) => void;
1028
+ /** Resolves once the component is subscribed (earlier simulate* calls are buffered and replayed) */
1029
+ ready: () => Promise<void>;
1030
+ /**
1031
+ * Wait for a state that satisfies the predicate. Matches the recorded HISTORY too: a state
1032
+ * from before the call resolves it (e.g. `count === 0` right after a reset resolves at once
1033
+ * with the initial state). Resolves with the matching state once the whole tree (children
1034
+ * included) has rendered it. Use `next()` to wait for a new state.
1035
+ */
655
1036
  waitForState: (predicate: (state: any) => boolean, timeoutMs?: number) => Promise<any>;
1037
+ /**
1038
+ * Wait for the next state emitted AFTER this call that satisfies the predicate (default: any
1039
+ * state). Resolves with it once the whole tree (children included) has rendered it; rejects
1040
+ * after timeoutMs (default 2000).
1041
+ */
1042
+ next: (predicate?: (state: any) => boolean, timeoutMs?: number) => Promise<any>;
1043
+ /**
1044
+ * Resolves once nothing is pending: the component is ready, no simulated input is waiting,
1045
+ * and nothing in the tree has rendered, reduced or changed state for 20ms (longer than
1046
+ * next()'s default delay). Rejects after timeoutMs (default 2000) if it never calms down.
1047
+ */
1048
+ settle: (timeoutMs?: number) => Promise<void>;
656
1049
  /** Collected state values — grows as new states are emitted */
657
1050
  states: any[];
658
- /** Tear down the component and clean up all listeners */
1051
+ /** Live array of values emitted on a sink (EVENTS as {type, data}, PARENT unwrapped, custom drivers) */
1052
+ sinkValues: (sinkName: string) => any[];
1053
+ /** Live array of EVENTS sink emissions ({type, data}) */
1054
+ emitted: Array<{ type: string; data: any }>;
1055
+ /** Live array of diagnostics reported while rendered */
1056
+ diagnostics: Diagnostic[];
1057
+ /** Throws (with the formatted texts) if any warn/error diagnostics were collected */
1058
+ expectNoDiagnostics: () => void;
1059
+ /** Latest rendered VNode serialized to HTML ('' before the first render) */
1060
+ html: () => string;
1061
+ /** Tear down the component, clean up listeners and restore the diagnostics mode */
659
1062
  dispose: () => void;
1063
+ /**
1064
+ * The app graph of the rendered tree (components, actions, selectors with the mock DOM's
1065
+ * match / isolation results, EVENTS, diagnostics). Throws unless 'sygnal/diagnostics' is loaded.
1066
+ */
1067
+ inspect: () => InspectGraph;
660
1068
  }
661
1069
 
662
1070
  export function renderComponent(componentDef: any, options?: RenderOptions): RenderResult
@@ -688,6 +1096,9 @@ export { default as throttle } from 'xstream/extra/throttle.js'
688
1096
  export { default as delay } from 'xstream/extra/delay.js'
689
1097
  export { default as dropRepeats } from 'xstream/extra/dropRepeats.js'
690
1098
  export { default as sampleCombine } from 'xstream/extra/sampleCombine.js'
1099
+ export { default as flattenConcurrently } from 'xstream/extra/flattenConcurrently.js'
1100
+ export { default as flattenSequentially } from 'xstream/extra/flattenSequentially.js'
1101
+ export { default as concat } from 'xstream/extra/concat.js'
691
1102
 
692
1103
  export * from './cycle/dom/index'
693
1104
  export type { MemoryStream, Stream }
package/src/index.ts CHANGED
@@ -21,18 +21,19 @@ export { createElement } from './pragma/index'
21
21
  export { createCommand } from './extra/command'
22
22
  export { createRef, createRef$ } from './extra/ref'
23
23
  export { renderComponent } from './extra/testing'
24
- export { set, toggle, emit } from './extra/reducers'
24
+ export { set, toggle, emit, event } from './extra/reducers'
25
25
  export { makeServiceWorkerDriver, onlineStatus$, createInstallPrompt } from './extra/pwa'
26
26
  export { renderToString } from './extra/ssr'
27
27
  export { default as xs } from './extra/xstreamCompat'
28
28
  export { getDevTools } from './extra/devtools'
29
+ export { getDiagnostics, clearDiagnostics, onDiagnostic } from './extra/diagnostics/index'
29
30
 
30
31
  // export dom helper functions (h, makeDOMDriver, etc.)
31
32
  export * from './cycle/dom/index'
32
33
 
33
34
  // export xstream and most used extra operators
34
- export { default as debounce } from "xstream/extra/debounce.js"
35
- export { default as throttle } from 'xstream/extra/throttle.js'
36
- export { default as delay } from "xstream/extra/delay.js"
37
- export { default as dropRepeats } from "xstream/extra/dropRepeats.js"
38
- export { default as sampleCombine } from 'xstream/extra/sampleCombine.js'
35
+ // ESM ports of xstream/extra/* (behaviour-identical). The CJS originals are not callable from
36
+ // the rollup CJS build or native Node ESM (their default export is `{ default: fn }` there),
37
+ // and ESM classes tree-shake when an app does not use them.
38
+ export { concat, debounce, throttle, delay, dropRepeats, sampleCombine } from './extra/xstreamExtras'
39
+ export { flattenConcurrently, flattenSequentially } from './extra/flatten'
@@ -166,9 +166,12 @@ export const createElementWithModules = (modules: Record<string, any>) => {
166
166
  return (sel: any, data: any, ...children: any[]) => {
167
167
  if (typeof sel === 'undefined') {
168
168
  sel = 'UNDEFINED'
169
- console.error('JSX Error: Capitalized HTML element without corresponding factory function. Components with names where the first letter is capital MUST be defined or included at the parent component\'s file scope.')
169
+ // Pre-formatted (formatDiagnostic output) instead of diagnostics/legacy: the JSX
170
+ // runtime entries bundle this file standalone and must not carry a second diagnostics core.
171
+ console.error('[Sygnal SYG420] JSX: A JSX tag is undefined, so <UNDEFINED> is rendered instead. Import or define the component in this file. https://sygnal.js.org/reference/errors#syg420')
170
172
  }
171
- if (is.fun(sel)) {
173
+ const isComponent = is.fun(sel)
174
+ if (isComponent) {
172
175
  if ((sel as any).__sygnalFragment || sel.name === 'Fragment') {
173
176
  return sel(data || {}, children)
174
177
  }
@@ -178,13 +181,12 @@ export const createElementWithModules = (modules: Record<string, any>) => {
178
181
  const view = sel
179
182
  const { model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug, preventInstantiation } = sel as any
180
183
  if (preventInstantiation) {
181
- const text = sanitizeText(children)
182
- const sanitized = data ? sanitizeData(data, modules) : {}
184
+ // children always stay an array here: Portal/Suspense/... read vnode.children
183
185
  return considerSvg({
184
186
  sel: name,
185
- data: sanitized,
186
- children: typeof text !== 'undefined' ? createTextElement(text) : sanitizeChildren(children),
187
- text,
187
+ data: data ? sanitizeData(data, modules) : {},
188
+ children: sanitizeChildren(children),
189
+ text: undefined,
188
190
  elm: undefined,
189
191
  key: data ? data.key : undefined
190
192
  })
@@ -198,11 +200,14 @@ export const createElementWithModules = (modules: Record<string, any>) => {
198
200
  data.sygnalFactory = factory
199
201
  }
200
202
  }
201
- const text = sanitizeText(children)
203
+ // B-011: a vnode is either text-only (`text`, no children) or has a children array,
204
+ // never both; snabbdom's diff mishandles a vnode with both. A component placeholder
205
+ // keeps an array so the component receives its text child via `children`.
206
+ const text = isComponent ? undefined : sanitizeText(children)
202
207
  return considerSvg({
203
208
  sel,
204
209
  data: data ? sanitizeData(data, modules) : {},
205
- children: typeof text !== 'undefined' ? createTextElement(text) : sanitizeChildren(children),
210
+ children: typeof text !== 'undefined' ? undefined : sanitizeChildren(children),
206
211
  text,
207
212
  elm: undefined,
208
213
  key: data ? data.key : undefined