sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
package/src/component.ts DELETED
@@ -1,2301 +0,0 @@
1
- import isolate from './cycle/isolate/index';
2
- import collection from './collection';
3
- import switchable from './switchable';
4
- import {StateSource} from './cycle/state/index';
5
- import {init as snabbdomInit} from './cycle/dom/snabbdom';
6
- import defaultModules from './cycle/dom/modules';
7
- import {makeCommandSource} from './extra/command';
8
- import type {Command} from './extra/command';
9
- // [diagnostics hook] shared diagnostics core — hooks are no-ops when diagnostics are off
10
- import * as diag from './extra/diagnostics/index';
11
- import {warn, error as logError, fail} from './extra/diagnostics/legacy';
12
-
13
- import xs, {Stream} from './extra/xstreamCompat';
14
- import {delay, concat, debounce, dropRepeats} from './extra/xstreamExtras';
15
-
16
- declare var process: { env: Record<string, any> };
17
-
18
- const ENVIRONMENT: any =
19
- (typeof window != 'undefined' && window) || (typeof process !== 'undefined' && process.env) || {};
20
-
21
- const BOOTSTRAP_ACTION = 'BOOTSTRAP';
22
- const INITIALIZE_ACTION = 'INITIALIZE';
23
- const HYDRATE_ACTION = 'HYDRATE';
24
- const DISPOSE_ACTION = 'DISPOSE';
25
- const PARENT_SINK_NAME = 'PARENT';
26
- const CHILD_SOURCE_NAME = 'CHILD';
27
- const READY_SINK_NAME = 'READY';
28
- const EFFECT_SINK_NAME = 'EFFECT';
29
- // fix hint for coded messages that carry the caught error (printed after the text, or diagnostic.data)
30
- const ERR_FIX = 'See the attached error'
31
- // B-003: the state an action's non-STATE sinks see (stamped by initModel$)
32
- const STATE_SNAPSHOT = Symbol('sygnal.stateSnapshot');
33
-
34
- let COMPONENT_COUNT = 0;
35
- // 1H-1: STATE reducers emitted but not applied yet (in any component)
36
- let pendingReducers = 0;
37
-
38
- function wrapDOMSource(domSource: any): any {
39
- return new Proxy(domSource, {
40
- get(target, prop, receiver) {
41
- if (typeof prop === 'symbol' || prop in target) {
42
- return Reflect.get(target, prop, receiver)
43
- }
44
- return (selector: any) => target.select(selector).events(prop)
45
- }
46
- })
47
- }
48
-
49
-
50
- export const ABORT = Symbol.for('sygnal.ABORT')
51
-
52
- /**
53
- * Check if a value is the ABORT sentinel.
54
- * Uses Symbol.for() identity first, then falls back to description check
55
- * in case bundlers (e.g. Vite) create duplicate module instances with
56
- * separate Symbol.for() registries.
57
- */
58
- function isAbort(value: any): boolean {
59
- if (value === ABORT) return true
60
- return typeof value === 'symbol' && value.description === 'sygnal.ABORT'
61
- }
62
-
63
-
64
- function normalizeCalculatedEntry(field: string, entry: any): {fn: (...args: any[]) => any; deps: string[] | null} {
65
- if (typeof entry === 'function') {
66
- return { fn: entry, deps: null }
67
- }
68
- if (Array.isArray(entry) && entry.length === 2
69
- && Array.isArray(entry[0]) && typeof entry[1] === 'function') {
70
- return { fn: entry[1], deps: entry[0] }
71
- }
72
- fail('SYG206', undefined, `Invalid calculated field '${field}'`, 'Use fn or [deps, fn]')
73
- }
74
-
75
- export interface ComponentOptions {
76
- name?: string;
77
- sources?: Record<string, any>;
78
- intent?: ((sources: Record<string, any>) => any) | Record<string, any>;
79
- model?: Record<string, any>;
80
- hmrActions?: string | string[];
81
- context?: Record<string, boolean | ((state: any) => any)>;
82
- response?: Record<string, any>;
83
- view?: (...args: any[]) => any;
84
- peers?: Record<string, (...args: any[]) => any>;
85
- components?: Record<string, any>;
86
- initialState?: Record<string, any>;
87
- calculated?: Record<string, ((state: any) => any) | [string[], (...args: any[]) => any]>;
88
- storeCalculatedInState?: boolean;
89
- DOMSourceName?: string;
90
- stateSourceName?: string;
91
- requestSourceName?: string;
92
- isolateOpts?: string | boolean | Record<string, any>;
93
- isolatedState?: boolean;
94
- onError?: (error: Error, info: { componentName: string }) => any;
95
- debug?: boolean;
96
- }
97
-
98
- export default function component(opts: ComponentOptions): any {
99
- const { name, sources, isolateOpts, stateSourceName='STATE' } = opts
100
-
101
- if (sources && !isObj(sources)) {
102
- fail('SYG601', name, 'Invalid sources', 'Pass sources from run()')
103
- }
104
-
105
- let fixedIsolateOpts
106
- if (typeof isolateOpts == 'string') {
107
- fixedIsolateOpts = { [stateSourceName]: isolateOpts }
108
- } else {
109
- if (isolateOpts === true) {
110
- fixedIsolateOpts = {}
111
- } else {
112
- fixedIsolateOpts = isolateOpts
113
- }
114
- }
115
-
116
- const currySources = typeof sources === 'undefined'
117
- let returnFunction
118
-
119
- if (isObj(fixedIsolateOpts)) {
120
- const wrapped = (sources: any) => {
121
- const fixedOpts = { ...opts, sources }
122
- const instance = new Component(fixedOpts)
123
- instance.sinks.__dispose = () => instance.dispose()
124
- return instance.sinks
125
- }
126
- returnFunction = currySources ? isolate(wrapped, fixedIsolateOpts) : isolate(wrapped, fixedIsolateOpts)(sources)
127
- } else {
128
- if (currySources) {
129
- returnFunction = (sources: any) => {
130
- const instance = new Component({ ...opts, sources })
131
- instance.sinks.__dispose = () => instance.dispose()
132
- return instance.sinks
133
- }
134
- } else {
135
- const instance = new Component(opts)
136
- instance.sinks.__dispose = () => instance.dispose()
137
- returnFunction = instance.sinks
138
- }
139
- }
140
-
141
- returnFunction.componentName = name
142
- returnFunction.isSygnalComponent = true
143
-
144
- return returnFunction
145
- }
146
-
147
-
148
-
149
-
150
-
151
- class Component {
152
- _componentNumber: number;
153
- name: string;
154
- sources: any;
155
- intent: any;
156
- model: any;
157
- hmrActions: any;
158
- context: any;
159
- response: any;
160
- view: any;
161
- peers: Record<string, any>;
162
- components: Record<string, any>;
163
- initialState: any;
164
- calculated: any;
165
- storeCalculatedInState: boolean;
166
- DOMSourceName: string;
167
- stateSourceName: string;
168
- requestSourceName: string;
169
- sourceNames: string[];
170
- _debug: boolean;
171
- onError: ((error: Error, info: { componentName: string }) => any) | undefined;
172
- isolatedState: boolean;
173
- isSubComponent: boolean;
174
- currentState: any;
175
- currentProps: any;
176
- currentChildren: any;
177
- currentSlots: Record<string, any[]>;
178
- currentContext: any;
179
- intent$: any;
180
- hmrAction$: any;
181
- action$: any;
182
- model$: any;
183
- context$: any;
184
- peers$: any;
185
- subComponentSink$: any;
186
- subComponentsRendered$: any;
187
- vdom$: any;
188
- sinks: any;
189
- log: any;
190
- addCalculated: (state: any) => any;
191
- newChildSources!: (sources: any) => void;
192
- newSubComponentSinks!: (sinks: any) => void;
193
- triggerSubComponentsRendered!: (() => void);
194
- _calculatedNormalized: Record<string, {fn: (...args: any[]) => any; deps: string[] | null}> | null;
195
- _calculatedFieldNames: Set<string> | null;
196
- _calculatedOrder: Array<[string, {fn: (...args: any[]) => any; deps: string[] | null}]> | null;
197
- _calculatedFieldCache: Record<string, {lastDepValues: any; lastResult: any}> | null;
198
- _subscriptions: any[];
199
- _processedChildren$: any;
200
- _disposeListener: any;
201
- _dispose$: any;
202
- _disposed?: boolean;
203
- _activeSubComponents: Map<string, any>;
204
- _childReadyState: Record<string, boolean>;
205
- _readyChanged$: any;
206
- _readyChangedListener: any;
207
-
208
- constructor({name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false}: ComponentOptions) {
209
- if (!sources || !isObj(sources)) fail('SYG601', name, 'Missing or invalid sources', 'Pass sources from run()')
210
-
211
- this._componentNumber = COMPONENT_COUNT++
212
-
213
- this.name = name
214
- this.sources = sources
215
- this.intent = intent
216
- this.model = model
217
- this.hmrActions = hmrActions
218
- this.context = context
219
- this.response = response
220
- this.view = view
221
- this.peers = peers
222
- this.components = components
223
- this.initialState = initialState
224
- this.calculated = calculated
225
- this.storeCalculatedInState = storeCalculatedInState
226
- this.DOMSourceName = DOMSourceName
227
- this.stateSourceName = stateSourceName
228
- this.requestSourceName = requestSourceName
229
- this.sourceNames = Object.keys(sources)
230
- this.onError = onError
231
- this.isolatedState = isolatedState
232
- this._debug = debug
233
-
234
- // Warn if calculated fields shadow base state keys
235
- if (this.calculated && this.initialState
236
- && isObj(this.calculated) && isObj(this.initialState)) {
237
- for (const key of Object.keys(this.calculated)) {
238
- if (key in this.initialState) {
239
- warn('SYG207', name, `Calculated field '${key}' overwrites the initialState key of the same name`, 'Rename one of them')
240
- }
241
- }
242
- }
243
-
244
- // Normalize calculated entries, build dependency graph, topological sort
245
- if (this.calculated && isObj(this.calculated)) {
246
- const calcEntries = Object.entries(this.calculated)
247
-
248
- // Normalize all entries to { fn, deps } shape
249
- this._calculatedNormalized = {}
250
- for (const [field, entry] of calcEntries) {
251
- this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry)
252
- }
253
-
254
- this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized))
255
-
256
- // Warn on deps referencing nonexistent keys
257
- for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
258
- if (deps !== null) {
259
- for (const dep of deps) {
260
- if (!this._calculatedFieldNames.has(dep)
261
- && this.initialState && !(dep in this.initialState)) {
262
- warn('SYG208', name, `Calculated field '${field}' depends on unknown key '${dep}'`, 'Add it to initialState')
263
- }
264
- }
265
- }
266
- }
267
-
268
- // Build adjacency: for each field, which other calculated fields must run first?
269
- const calcDeps: Record<string, string[]> = {}
270
- for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
271
- if (deps === null) {
272
- calcDeps[field] = []
273
- } else {
274
- calcDeps[field] = deps.filter(d => this._calculatedFieldNames!.has(d))
275
- }
276
- }
277
-
278
- // Kahn's algorithm for topological sort
279
- const inDegree: Record<string, number> = {}
280
- const reverseGraph: Record<string, string[]> = {}
281
- for (const field of this._calculatedFieldNames) {
282
- inDegree[field] = 0
283
- reverseGraph[field] = []
284
- }
285
- for (const [field, depList] of Object.entries(calcDeps)) {
286
- inDegree[field] = depList.length
287
- for (const dep of depList) {
288
- reverseGraph[dep].push(field)
289
- }
290
- }
291
-
292
- const queue = []
293
- for (const [field, degree] of Object.entries(inDegree)) {
294
- if (degree === 0) queue.push(field)
295
- }
296
-
297
- const sorted: string[] = []
298
- while (queue.length > 0) {
299
- const current: string = queue.shift()!
300
- sorted.push(current)
301
- for (const dependent of reverseGraph[current]) {
302
- inDegree[dependent]--
303
- if (inDegree[dependent] === 0) queue.push(dependent)
304
- }
305
- }
306
-
307
- if (sorted.length !== this._calculatedFieldNames.size) {
308
- // Cycle detected — build error message with cycle path
309
- const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f))
310
- const visited = new Set()
311
- const path: string[] = []
312
- const traceCycle = (node: any) => {
313
- if (visited.has(node)) { path.push(node); return true }
314
- visited.add(node)
315
- path.push(node)
316
- for (const dep of calcDeps[node]) {
317
- if (inCycle.includes(dep) && traceCycle(dep)) return true
318
- }
319
- path.pop()
320
- visited.delete(node)
321
- return false
322
- }
323
- traceCycle(inCycle[0])
324
- const start = path[path.length - 1]
325
- const cycle = path.slice(path.indexOf(start))
326
- fail('SYG209', name, `Circular calculated dependency: ${cycle.join(' \u2192 ')}`, 'Break the cycle')
327
- }
328
-
329
- this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized![f]])
330
-
331
- // Initialize per-field memoization caches for fields with declared deps
332
- this._calculatedFieldCache = {}
333
- for (const [field, { deps }] of this._calculatedOrder) {
334
- if (deps !== null) {
335
- this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined }
336
- }
337
- }
338
- } else {
339
- this._calculatedOrder = null
340
- this._calculatedNormalized = null
341
- this._calculatedFieldNames = null
342
- this._calculatedFieldCache = null
343
- }
344
-
345
- this.isSubComponent = this.sourceNames.includes('props$')
346
-
347
- const state$ = sources[stateSourceName] && sources[stateSourceName].stream
348
-
349
- this.currentSlots = {}
350
-
351
- if (state$) {
352
- this.currentState = initialState || {}
353
- this.sources[stateSourceName] = new StateSource(state$.map((val: any) => {
354
- this.currentState = val
355
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
356
- window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val)
357
- }
358
- return val
359
- }), stateSourceName)
360
- }
361
-
362
- const props$ = sources.props$
363
- if (props$) {
364
- this.sources.props$ = props$.map((val: any) => {
365
- const { sygnalFactory, sygnalOptions, ...sanitizedProps }: any = val
366
- this.currentProps = sanitizedProps
367
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
368
- window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps)
369
- }
370
- return val
371
- })
372
- }
373
-
374
- const children$ = sources.children$
375
- if (children$) {
376
- // Process children and extract slots once, share across subscribers
377
- this._processedChildren$ = children$.map((val: any) => {
378
- if (Array.isArray(val)) {
379
- const { slots, defaultChildren } = extractSlots(val)
380
- this.currentSlots = slots
381
- this.currentChildren = defaultChildren
382
- return { children: defaultChildren, slots }
383
- } else {
384
- this.currentSlots = {}
385
- this.currentChildren = val
386
- return { children: val, slots: {} }
387
- }
388
- })
389
- this.sources.children$ = this._processedChildren$.map((p: any) => p.children)
390
- }
391
-
392
- if (this.sources[DOMSourceName]) {
393
- this.sources[DOMSourceName] = wrapDOMSource(this.sources[DOMSourceName])
394
- }
395
-
396
- // Ensure that the root component has an intent and model
397
- // This is necessary to ensure that the component tree's state sink is subscribed to
398
- if (!this.isSubComponent && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
399
- this.initialState = initialState || true
400
- this.intent = (_: any) => ({__NOOP_ACTION__:xs.never()})
401
- this.model = {
402
- __NOOP_ACTION__: (state: any) => state
403
- }
404
- }
405
-
406
- this._subscriptions = []
407
- this._activeSubComponents = new Map()
408
- this._childReadyState = {}
409
- this._readyChangedListener = null
410
- this._readyChanged$ = xs.create({
411
- start: (listener: any) => { this._readyChangedListener = listener },
412
- stop: () => {},
413
- })
414
- this._disposeListener = null
415
- this._dispose$ = xs.create({
416
- start: (listener: any) => { this._disposeListener = listener },
417
- stop: () => {},
418
- })
419
- this.sources.dispose$ = this._dispose$
420
-
421
- this.addCalculated = this.createMemoizedAddCalculated()
422
- this.log = makeLog(`${this._componentNumber} | ${name}`)
423
-
424
- this.initChildSources$()
425
- this.initIntent$()
426
- this.initHmrActions()
427
- this.initAction$()
428
- this.initState()
429
- this.initContext()
430
- this.initModel$()
431
- this.initPeers$()
432
- this.initSubComponentSink$()
433
- this.initSubComponentsRendered$()
434
- this.initVdom$()
435
- this.initSinks()
436
-
437
- this.sinks.__index = this._componentNumber
438
-
439
- this.log(`Instantiated`, true)
440
-
441
- // Hook 1: Register with DevTools
442
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
443
- window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this)
444
-
445
- // Hook 1b: Register parent-child relationship
446
- const parentNum = sources?.__parentComponentNumber
447
- if (typeof parentNum === 'number') {
448
- window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber)
449
- }
450
- }
451
- }
452
-
453
- dispose(): void {
454
- if (this._disposed) return
455
- this._disposed = true
456
- // [diagnostics hook]
457
- diag.onDispose(this)
458
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
459
- window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name)
460
- }
461
- // Fire the DISPOSE built-in action so model handlers can run cleanup logic
462
- const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION))
463
- if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
464
- try {
465
- this.action$.shamefullySendNext({ type: DISPOSE_ACTION })
466
- } catch (_) {}
467
- }
468
- // Signal disposal to the component via dispose$ stream (for advanced use cases)
469
- if (this._disposeListener) {
470
- try {
471
- this._disposeListener.next(true)
472
- this._disposeListener.complete()
473
- } catch (_) {}
474
- this._disposeListener = null
475
- }
476
- // Dispose the sub-components now (R3), so the whole subtree's DISPOSE actions and
477
- // onDispose hooks run within this call (e.g. before renderComponent restores diagnostics)
478
- this._activeSubComponents.forEach((entry) => entry?.sink$?.__dispose?.())
479
- this._activeSubComponents.clear()
480
- // Tear down streams on next macrotask to allow DISPOSE/cleanup actions to process
481
- setTimeout(() => {
482
- // Complete the action$ stream to stop the entire component cycle
483
- if (this.action$ && typeof this.action$.shamefullySendComplete === 'function') {
484
- try { this.action$.shamefullySendComplete() } catch (_) {}
485
- }
486
- // Complete the vdom$ stream to stop rendering
487
- if (this.vdom$ && typeof this.vdom$.shamefullySendComplete === 'function') {
488
- try { this.vdom$.shamefullySendComplete() } catch (_) {}
489
- }
490
- // Unsubscribe tracked internal subscriptions
491
- for (const sub of this._subscriptions) {
492
- if (sub && typeof sub.unsubscribe === 'function') {
493
- try { sub.unsubscribe() } catch (_) {}
494
- }
495
- }
496
- this._subscriptions = []
497
- }, 0)
498
- }
499
-
500
- get debug(): boolean {
501
- return this._debug || (ENVIRONMENT.SYGNAL_DEBUG === 'true' || ENVIRONMENT.SYGNAL_DEBUG === true)
502
- }
503
-
504
- initIntent$(): void {
505
- if (!this.intent) {
506
- // [diagnostics hook]
507
- diag.onIntent(this, [], undefined)
508
- return
509
- }
510
- if (typeof this.intent != 'function') {
511
- fail('SYG602', this, 'intent must be a function', 'Use intent = sources => ({ ACTION: stream$ })')
512
- }
513
-
514
- this.intent$ = this.intent(this.sources)
515
-
516
- if (!(this.intent$ instanceof Stream) && (!isObj(this.intent$))) {
517
- fail('SYG603', this, 'intent must return a stream or an object of streams', 'Return { ACTION: stream$ }')
518
- }
519
-
520
- // [diagnostics hook] TODO(1A): selectorsUsed via MainDOMSource.select instrumentation
521
- diag.onIntent(this, this.intent$ instanceof Stream ? [] : Object.keys(this.intent$), undefined)
522
- }
523
-
524
- initHmrActions(): void {
525
- if (typeof this.hmrActions === 'undefined') {
526
- this.hmrAction$ = xs.of().filter((_: any) => false)
527
- return
528
- }
529
- if (typeof this.hmrActions === 'string') {
530
- this.hmrActions = [this.hmrActions]
531
- }
532
- if (!Array.isArray(this.hmrActions) || this.hmrActions.some(action => typeof action !== 'string')) {
533
- fail('SYG604', this, 'hmrActions must be an action name or an array of action names', "Use hmrActions = ['ACTION']")
534
- }
535
- this.hmrAction$ = xs.fromArray(this.hmrActions.map(action => ({ type: action })))
536
- }
537
-
538
- initAction$(): void {
539
- const requestSource = (this.sources && this.sources[this.requestSourceName]) || null
540
-
541
- if (!this.intent$) {
542
- this.action$ = xs.never()
543
- return
544
- }
545
-
546
- let runner
547
- if (this.intent$ instanceof Stream) {
548
- runner = this.intent$
549
- } else {
550
- // Validate that no intent action names contain '|' (reserved for model shorthand)
551
- for (const key of Object.keys(this.intent$)) {
552
- if (key.includes('|')) {
553
- fail('SYG605', this, `Intent action '${key}' contains '|', which is reserved for model shorthand`, 'Rename the action')
554
- }
555
- }
556
- const mapped = Object.entries(this.intent$)
557
- .map(([type, data$]: [string, any]) => data$.map((data: any) => ({type, data})))
558
- runner = mapped.length > 0 ? xs.merge(...mapped) : xs.never()
559
- }
560
-
561
- const action$ = ((runner instanceof Stream) ? runner : (runner.apply && runner(this.sources) || xs.never()))
562
- const bootstrap$ = xs.of({ type: BOOTSTRAP_ACTION }).compose(delay(10))
563
- const _hmrUpdating = typeof window !== 'undefined' && window.__SYGNAL_HMR_UPDATING === true
564
- const hmrAction$ = _hmrUpdating ? this.hmrAction$ : xs.of().filter((_: any) => false)
565
- const wrapped$ = (this.model[BOOTSTRAP_ACTION] && !_hmrUpdating) ? concat(bootstrap$, action$) : concat(xs.of().compose(delay(1)).filter((_: any) => false), hmrAction$, action$)
566
-
567
-
568
- let initialApiData
569
- if (!_hmrUpdating && requestSource && typeof requestSource.select == 'function') {
570
- initialApiData = requestSource.select('initial')
571
- .flatten()
572
- } else {
573
- initialApiData = xs.never()
574
- }
575
-
576
- const hydrate$ = initialApiData.map((data: any) => ({ type: HYDRATE_ACTION, data }))
577
-
578
- this.action$ = xs.merge(wrapped$, hydrate$)
579
- .compose(this.log(({ type }: any) => `<${type}> Action triggered`))
580
- .map((action: any) => {
581
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
582
- window.__SYGNAL_DEVTOOLS__.onActionDispatched(
583
- this._componentNumber, this.name, action.type, action.data
584
- )
585
- }
586
- return action
587
- })
588
- }
589
-
590
- initState(): void {
591
- if (this.model !== undefined) {
592
- if (this.model[INITIALIZE_ACTION] === undefined) {
593
- this.model[INITIALIZE_ACTION] = {
594
- [this.stateSourceName]: (_: any, data: any) => ({ ...this.addCalculated(data) })
595
- }
596
- } else if (isObj(this.model[INITIALIZE_ACTION])) {
597
- Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
598
- if (name !== this.stateSourceName) {
599
- warn('SYG210', this, `${INITIALIZE_ACTION} only supports the ${this.stateSourceName} sink; ignoring '${name}'`, 'Use another action')
600
- delete this.model[INITIALIZE_ACTION][name]
601
- }
602
- })
603
- }
604
- }
605
- }
606
-
607
- initContext(): void {
608
- if (!this.context && !this.sources.__parentContext$) {
609
- this.context$ = xs.of({})
610
- return
611
- }
612
-
613
- const state$ = this.sources[this.stateSourceName]?.stream.startWith({}).compose(dropRepeats(objIsEqual)) || xs.never()
614
- const parentContext$ = this.sources.__parentContext$?.startWith({}).compose(dropRepeats(objIsEqual)) || xs.of({})
615
- if (this.context && !isObj(this.context)) {
616
- logError('SYG402', this, `context must be an object, got ${typeof this.context}; ignoring it`, 'Use context = { name: state => value }')
617
- }
618
- this.context$ = xs.combine(state$, parentContext$)
619
- .map(([_, parent]: [any, any]) => {
620
- const _parent = isObj(parent) ? parent : {}
621
- const context = isObj(this.context) ? this.context : {}
622
- const state = this.currentState
623
- const values = Object.entries(context).reduce((acc, current) => {
624
- const [name, value] = current
625
- let _value
626
- const valueType = typeof value
627
- if (valueType === 'string') {
628
- _value = state[value]
629
- } else if (valueType === 'boolean') {
630
- _value = state[name]
631
- } else if (valueType === 'function') {
632
- _value = value(state)
633
- } else {
634
- logError('SYG403', this, `Invalid context entry '${name}'; skipping it`, 'Use a state key or state => value')
635
- return acc
636
- }
637
- acc[name] = _value
638
- return acc
639
- }, {} as Record<string, any>)
640
- const newContext = { ..._parent, ...values }
641
- this.currentContext = newContext
642
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
643
- window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext)
644
- }
645
- return newContext
646
- })
647
- .compose(dropRepeats(objIsEqual))
648
- .startWith({})
649
- this._subscriptions.push(this.context$.subscribe({ next: (_: any) => _, error: (err: any) => logError('SYG404', this, 'Context stream errored; context stops updating', 'Check the context functions', err) }))
650
- }
651
-
652
- initModel$(): void {
653
- if (typeof this.model == 'undefined') {
654
- this.model$ = this.sourceNames.reduce((a: Record<string, any>, s) => {
655
- a[s] = xs.never()
656
- return a
657
- }, {} as Record<string, any>)
658
- // [diagnostics hook]
659
- diag.onModel(this, {})
660
- return
661
- }
662
-
663
- const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE
664
- const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
665
- const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
666
- if (this.isSubComponent && this.initialState && !this.isolatedState) {
667
- warn('SYG405', this, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
668
- }
669
- const hasInitialState = (typeof effectiveInitialState !== 'undefined')
670
- const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined')
671
- // Only INITIALIZE is delayed (user actions start >= 1ms later), so the other actions, and
672
- // their non-STATE sinks, stay synchronous with the event that caused them (1H-1).
673
- const shimmed$ = shouldInjectInitialState ? xs.merge(xs.of(initial).compose(delay(0)), this.action$) : this.action$
674
- // B-003: STATE reducers are applied later (withState queues each one in a microtask), so
675
- // while one is pending a non-STATE sink must not read this.currentState. Then, before the
676
- // action reaches any reducer, queue a microtask (ahead of this action's own STATE reducer,
677
- // behind every earlier one) that snapshots the state and runs the non-STATE sinks.
678
- // Otherwise run them now, so EFFECT can still preventDefault() the live event (1H-1).
679
- let snapListener: any = null
680
- const sequenced$ = shimmed$.map((action: any) => {
681
- const l = snapListener
682
- const run = () => l.next({ ...action, [STATE_SNAPSHOT]: this.currentState })
683
- if (l) pendingReducers ? queueMicrotask(run) : run()
684
- return action
685
- })
686
- let snapSub: any
687
- const snapshotted$ = xs.create({
688
- start: (l: any) => { snapListener = l; snapSub = sequenced$.subscribe({}) },
689
- stop: () => { snapListener = null; snapSub?.unsubscribe() },
690
- })
691
- const onState = () => this.makeOnAction(sequenced$, true, this.action$)
692
- const onNormal = () => this.makeOnAction(snapshotted$, false, this.action$)
693
-
694
-
695
- const modelEntries = Object.entries(this.model)
696
-
697
- const reducers: Record<string, any[]> = {}
698
- const seenActionSinks = new Set<string>()
699
- const modelMap: Record<string, string[]> = {} // [diagnostics hook]
700
-
701
- modelEntries.forEach((entry) => {
702
- let [action, sinks]: [string, any] = entry
703
-
704
- // ACTION | DRIVER shorthand: 'MY_ACTION | EVENTS': (state) => ({ type: 'foo', data: 'bar' })
705
- if (action.includes('|')) {
706
- const parts = action.split('|').map((s: string) => s.trim())
707
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
708
- fail('SYG211', this, `Invalid shorthand model entry '${action}'`, "Use 'ACTION | SINK'")
709
- }
710
- action = parts[0]
711
- sinks = { [parts[1]]: sinks }
712
- }
713
-
714
- if (typeof sinks === 'function') {
715
- sinks = { [this.stateSourceName]: sinks }
716
- }
717
-
718
- if (!isObj(sinks)) {
719
- fail('SYG212', this, `Model entry '${action}' must be a function or an object`, 'Use { STATE: reducer }')
720
- }
721
-
722
- const sinkEntries = Object.entries(sinks)
723
-
724
- sinkEntries.forEach((entry) => {
725
- const [sink, reducer] = entry
726
-
727
- const actionSinkKey = `${action}::${sink}`
728
- if (seenActionSinks.has(actionSinkKey)) {
729
- warn('SYG213', this, `Duplicate model entry for action '${action}' on sink '${sink}'; both run`, 'Remove the duplicate')
730
- }
731
- seenActionSinks.add(actionSinkKey)
732
- ;(modelMap[action] ||= []).push(sink) // [diagnostics hook]
733
-
734
- // EFFECT sink: run the reducer for side effects only, no state change or sink output
735
- if (sink === EFFECT_SINK_NAME) {
736
- const effect$ = this.makeEffectHandler(snapshotted$, action, reducer, this.action$)
737
- if (Array.isArray(reducers[sink])) {
738
- reducers[sink].push(effect$)
739
- } else {
740
- reducers[sink] = [effect$]
741
- }
742
- return
743
- }
744
-
745
- const isStateSink = (sink === this.stateSourceName)
746
- const isParentSink = (sink === PARENT_SINK_NAME)
747
-
748
- const on = isStateSink ? onState() : onNormal()
749
- const on$ = isParentSink ? on(action, reducer).map((value: any) => ({ name: this.name, component: this.view, value })) : on(action, reducer)
750
-
751
- const wrapped$ = on$
752
- .compose(this.log((data: any) => {
753
- if (isStateSink) {
754
- return `<${action}> State reducer added`
755
- } else if (isParentSink) {
756
- return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
757
- } else {
758
- const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data)
759
- return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
760
- }
761
- }))
762
-
763
- if (Array.isArray(reducers[sink])) {
764
- reducers[sink].push(wrapped$)
765
- } else {
766
- reducers[sink] = [wrapped$]
767
- }
768
- })
769
- })
770
-
771
- const model$ = Object.entries(reducers).reduce((acc: Record<string, any>, entry: [string, any]) => {
772
- const [sink, streams] = entry
773
- acc[sink] = streams.length === 1 ? streams[0] : xs.merge(...streams)
774
- return acc
775
- }, {} as Record<string, any>)
776
-
777
- this.model$ = model$
778
-
779
- // [diagnostics hook]
780
- diag.onModel(this, modelMap)
781
- }
782
-
783
- initPeers$(): void {
784
- const initial: Record<string, any> = this.sourceNames.reduce((acc: Record<string, any>, name) => {
785
- if (name == this.DOMSourceName) {
786
- acc[name] = {}
787
- } else {
788
- acc[name] = []
789
- }
790
- return acc
791
- }, {} as Record<string, any>)
792
-
793
- this.peers$ = Object.entries(this.peers).reduce((acc: Record<string, any>, [peerName, peerFactory]) => {
794
- const peer$ = peerFactory(this.sources)
795
- this.sourceNames.forEach(source => {
796
- if (source == this.DOMSourceName) {
797
- acc[source][peerName] = peer$[source]
798
- } else {
799
- acc[source].push(peer$[source])
800
- }
801
- })
802
- return acc
803
- }, initial)
804
- }
805
-
806
- initChildSources$(): void {
807
- let newSourcesNext: any
808
- const childSources$ = xs.create({
809
- start: (listener: any) => {
810
- newSourcesNext = listener.next.bind(listener)
811
- },
812
- stop: (_: any) => {
813
-
814
- }
815
- }).map((sources: any) => xs.merge(...sources)).flatten()
816
-
817
- this.sources[CHILD_SOURCE_NAME] = {
818
- select: (nameOrComponent: any) => {
819
- const all$ = childSources$
820
- const filtered$ = typeof nameOrComponent === 'function'
821
- ? all$.filter((entry: any) => entry.component === nameOrComponent)
822
- : nameOrComponent
823
- ? all$.filter((entry: any) => entry.name === nameOrComponent)
824
- : all$
825
- const unwrapped$ = filtered$.map((entry: any) => entry.value)
826
- return unwrapped$
827
- }
828
- }
829
-
830
- this.newChildSources = (sources: any) => {
831
- if (typeof newSourcesNext === 'function') newSourcesNext(sources)
832
- }
833
- }
834
-
835
- initSubComponentSink$(): void {
836
- const subComponentSink$ = xs.create({
837
- start: (listener: any) => {
838
- this.newSubComponentSinks = listener.next.bind(listener)
839
- },
840
- stop: (_: any) => {
841
-
842
- }
843
- })
844
- this._subscriptions.push(subComponentSink$.subscribe({ next: (_: any) => _, error: (err: any) => logError('SYG901', this, 'Sub-component sink stream errored', ERR_FIX, err) }))
845
- this.subComponentSink$ = subComponentSink$.filter((sinks: any) => Object.keys(sinks).length > 0)
846
- }
847
-
848
- initSubComponentsRendered$(): void {
849
- const stream = xs.create({
850
- start: (listener: any) => {
851
- this.triggerSubComponentsRendered = listener.next.bind(listener)
852
- },
853
- stop: (_: any) => {
854
-
855
- }
856
- })
857
- this.subComponentsRendered$ = stream.startWith(null)
858
- }
859
-
860
- initVdom$(): void {
861
- if (typeof this.view != 'function') {
862
- this.vdom$ = xs.of(null)
863
- return
864
- }
865
-
866
- const renderParameters$ = this.collectRenderParameters()
867
-
868
- this.vdom$ = renderParameters$
869
- .map((params: any) => {
870
- const { props, state, children, slots, context, ...peers }: any = params
871
- const { sygnalFactory, sygnalOptions, ...sanitizedProps}: any = props || {}
872
- try {
873
- return this.view({ ...sanitizedProps, state, children, slots: slots || {}, context, peers }, state, context, peers)
874
- } catch (err) {
875
- const error = err instanceof Error ? err : new Error(String(err))
876
- // B-022: an error handled by .onError is a warning, without the "add .onError" hint
877
- if (typeof this.onError === 'function') {
878
- try {
879
- const fallback = this.onError(error, { componentName: this.name })
880
- warn('SYG406', this, 'View threw; rendered the onError fallback', undefined, error)
881
- return fallback
882
- } catch (fallbackErr) {
883
- logError('SYG406', this, 'View threw; rendering the error fallback', undefined, error)
884
- logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fallbackErr)
885
- }
886
- } else logError('SYG406', this, 'View threw; rendering the error fallback', 'Add .onError for a custom fallback', error)
887
- return { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
888
- }
889
- })
890
- .compose(this.log('View rendered'))
891
- .map((vDom: any) => vDom || { sel: 'div', data: {}, children: [] })
892
- .map((vdom: any) => preprocessVdom(vdom, this))
893
- .compose(this.instantiateSubComponents.bind(this))
894
- .filter((val: any) => val !== undefined)
895
- .compose(this.renderVdom.bind(this))
896
-
897
- }
898
-
899
- initSinks(): void {
900
- // Stamp this component's own EVENTS emissions with its info for devtools, before they are
901
- // merged with the sub-components' (B-023: stamping the merged sink made every ancestor
902
- // re-stamp, so the emitter was always the root). Non-enumerable (G-020), so sink values
903
- // still toEqual what the model returned.
904
- const ev$ = this.model$.EVENTS
905
- if (ev$) this.model$.EVENTS = ev$.map((ev: any) => Object.defineProperties({...ev}, {
906
- __emitterId: { value: this._componentNumber, configurable: true },
907
- __emitterName: { value: this.name, configurable: true },
908
- }))
909
- this.sinks = this.sourceNames.reduce((acc: Record<string, any>, name) => {
910
- if (name == this.DOMSourceName) return acc
911
- const subComponentSink$ = (this.subComponentSink$ && name !== PARENT_SINK_NAME) ? this.subComponentSink$.map((sinks: any) => sinks[name]).filter((sink: any) => !!sink).flatten() : xs.never()
912
- if (name === this.stateSourceName) {
913
- acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter((_: any) => false), ...(this.peers$[name] || []))
914
- } else {
915
- acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []))
916
- }
917
- return acc
918
- }, {} as Record<string, any>)
919
-
920
- this.sinks[this.DOMSourceName] = this.vdom$
921
- this.sinks[PARENT_SINK_NAME] = this.model$[PARENT_SINK_NAME] || xs.never()
922
-
923
- // EFFECT sink: subscribe to trigger side effects but don't expose as a driver sink
924
- if (this.model$[EFFECT_SINK_NAME]) {
925
- const effectSub = this.model$[EFFECT_SINK_NAME].subscribe({
926
- next: () => {},
927
- error: (err: any) => logError('SYG902', this, 'EFFECT stream errored; effects stop running', ERR_FIX, err),
928
- })
929
- this._subscriptions.push(effectSub)
930
- delete this.sinks[EFFECT_SINK_NAME]
931
- }
932
- // READY sink: if the component explicitly defined READY model entries, use them;
933
- // otherwise auto-emit true. Check the raw model object, not model$ (which always has keys for all sources).
934
- const hasExplicitReady = this.model && isObj(this.model) && Object.values(this.model).some(
935
- (sinks: any) => {
936
- if (isObj(sinks) && READY_SINK_NAME in sinks) return true
937
- return false
938
- }
939
- )
940
- if (hasExplicitReady) {
941
- this.sinks[READY_SINK_NAME] = this.model$[READY_SINK_NAME]
942
- this.sinks[READY_SINK_NAME].__explicitReady = true
943
- } else {
944
- this.sinks[READY_SINK_NAME] = xs.of(true)
945
- }
946
- }
947
-
948
- makeOnAction(action$: any, isStateSink: boolean = true, rootAction$?: any): (name: string, reducer: any) => any {
949
- rootAction$ = rootAction$ || action$
950
- return (name, reducer) => {
951
- const filtered$ = action$.filter(({type}: any) => type == name)
952
-
953
- let returnStream$
954
- if (typeof reducer === 'function') {
955
- returnStream$ = filtered$.map((action: any) => {
956
- const next = (type: any, data: any, delay=10) => {
957
- if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
958
- // put the "next" action request at the end of the event loop so the "current" action completes first
959
- setTimeout(() => {
960
- // push the "next" action request into the action$ stream
961
- rootAction$.shamefullySendNext({ type, data })
962
- }, delay)
963
- this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true)
964
- }
965
-
966
- const props = { ...this.currentProps, children: this.currentChildren, slots: this.currentSlots || {}, context: this.currentContext }
967
-
968
- let data = action.data
969
- if (isStateSink) {
970
- // withState applies reducers in microtasks, so none is legitimately pending at the
971
- // next macrotask: reset then, in case one was never applied (e.g. app disposed)
972
- if (!pendingReducers++) setTimeout(() => { pendingReducers = 0 })
973
- let applied = false
974
- return (state: any) => {
975
- if (!applied && pendingReducers) { applied = true; pendingReducers-- }
976
- // Reduce from the fresh argument, not currentState (B-013, 1H-3): below a
977
- // Collection, currentState lags behind instantiateCollection's debounce, so a
978
- // second same-tick action would start from the pre-update state. The parents'
979
- // lenses add their calculated fields, so it matches what the view gets.
980
- const fresh = typeof state !== 'undefined'
981
- const _state = this.isSubComponent && !fresh ? this.currentState : state
982
- try {
983
- const enhancedState = this.addCalculated(_state)
984
- props.state = enhancedState
985
- const newState = reducer(enhancedState, data, next, props)
986
- if (isAbort(newState)) return _state
987
- // [diagnostics hook]
988
- diag.onReducer(this, name, _state, newState, this.stateSourceName)
989
- const result = this.cleanupCalculated(newState)
990
- // B-013: later same-tick actions' non-STATE sinks snapshot currentState (B-003)
991
- if (fresh) this.currentState = result
992
- return result
993
- } catch (err) {
994
- logError('SYG216', this, `Reducer for '${name}' threw; state unchanged`, ERR_FIX, err)
995
- return _state
996
- }
997
- }
998
- } else {
999
- try {
1000
- const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
1001
- props.state = enhancedState
1002
- const reduced = reducer(enhancedState, data, next, props)
1003
- // B-029: ABORT = send nothing (filtered below), before the type checks
1004
- if (isAbort(reduced)) return reduced
1005
- const type = typeof reduced
1006
- if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
1007
- if (type === 'undefined') {
1008
- warn('SYG217', this, `Reducer for '${name}' sent undefined to the driver`, 'Return a value, or ABORT to send nothing')
1009
- return reduced
1010
- }
1011
- fail('SYG218', this, `Reducer for '${name}' returned a ${type}`, 'Return a value, or ABORT to send nothing')
1012
- } catch (err) {
1013
- logError('SYG216', this, `Reducer for '${name}' threw; nothing sent`, ERR_FIX, err)
1014
- return ABORT
1015
- }
1016
- }
1017
- }).filter((result: any) => !isAbort(result))
1018
- } else if (reducer === undefined || reducer === true) {
1019
- returnStream$ = filtered$.map(({data}: any) => data)
1020
- } else {
1021
- const value = reducer
1022
- returnStream$ = filtered$.mapTo(value)
1023
- }
1024
-
1025
- return returnStream$
1026
- }
1027
- }
1028
-
1029
- makeEffectHandler(action$: any, name: string, reducer: any, rootAction$: any = action$): any {
1030
- const filtered$ = action$.filter(({type}: any) => type == name)
1031
-
1032
- return filtered$.map((action: any) => {
1033
- if (typeof reducer === 'function') {
1034
- const next = (type: any, data: any, delay=10) => {
1035
- if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
1036
- setTimeout(() => {
1037
- rootAction$.shamefullySendNext({ type, data })
1038
- }, delay)
1039
- this.log(`<${name}> EFFECT triggered a next() action: <${type}> ${delay}ms delay`, true)
1040
- }
1041
-
1042
- try {
1043
- const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
1044
- const props = { ...this.currentProps, children: this.currentChildren, slots: this.currentSlots || {}, context: this.currentContext, state: enhancedState }
1045
- const result = reducer(enhancedState, action.data, next, props)
1046
- if (result !== undefined && !isAbort(result)) {
1047
- warn('SYG219', this, `EFFECT handler '${name}' returned a value, which is ignored`, 'Use a STATE or driver sink')
1048
- }
1049
- } catch (err) {
1050
- logError('SYG214', this, `EFFECT handler '${name}' threw`, ERR_FIX, err)
1051
- }
1052
- }
1053
- return null
1054
- }).filter((_: any) => false) // EFFECT never emits — stream is consumed but produces no output
1055
- }
1056
-
1057
- createMemoizedAddCalculated(): (state: any) => any {
1058
- let lastState: any
1059
- let lastResult: any
1060
-
1061
- return function(this: Component, state: any) {
1062
- if (!this.calculated || !isObj(state) || Array.isArray(state)) return state
1063
- if (state === lastState) {
1064
- return lastResult
1065
- }
1066
- if (!isObj(this.calculated)) fail('SYG606', this, 'calculated must be an object', 'Use calculated = { field: state => value }')
1067
-
1068
- const calculated = this.getCalculatedValues(state)
1069
- if (!calculated) {
1070
- lastState = state
1071
- lastResult = state
1072
- return state
1073
- }
1074
-
1075
- const newState = { ...state, ...calculated }
1076
-
1077
- lastState = state
1078
- lastResult = newState
1079
-
1080
- return newState
1081
- }
1082
- }
1083
-
1084
- getCalculatedValues(state: any): Record<string, any> | undefined {
1085
- if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
1086
- return
1087
- }
1088
-
1089
- const mergedState: Record<string, any> = { ...state }
1090
- const computedSoFar: Record<string, any> = {}
1091
-
1092
- for (const [field, { fn, deps }] of this._calculatedOrder) {
1093
- if (deps !== null && this._calculatedFieldCache) {
1094
- const cache = this._calculatedFieldCache[field]
1095
- const currentDepValues = deps.map(d => mergedState[d])
1096
-
1097
- if (cache.lastDepValues !== undefined) {
1098
- let unchanged = true
1099
- for (let i = 0; i < currentDepValues.length; i++) {
1100
- if (currentDepValues[i] !== cache.lastDepValues[i]) {
1101
- unchanged = false
1102
- break
1103
- }
1104
- }
1105
- if (unchanged) {
1106
- computedSoFar[field] = cache.lastResult
1107
- mergedState[field] = cache.lastResult
1108
- continue
1109
- }
1110
- }
1111
-
1112
- try {
1113
- const result = fn(mergedState)
1114
- cache.lastDepValues = currentDepValues
1115
- cache.lastResult = result
1116
- computedSoFar[field] = result
1117
- mergedState[field] = result
1118
- } catch (e: unknown) {
1119
- warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
1120
- }
1121
- } else {
1122
- // No deps declared — always recompute
1123
- try {
1124
- const result = fn(mergedState)
1125
- computedSoFar[field] = result
1126
- mergedState[field] = result
1127
- } catch (e: unknown) {
1128
- warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
1129
- }
1130
- }
1131
- }
1132
-
1133
- return computedSoFar
1134
- }
1135
-
1136
- cleanupCalculated(incomingState: any): any {
1137
- if (!incomingState || !isObj(incomingState) || Array.isArray(incomingState)) return incomingState
1138
- const state = this.storeCalculatedInState ? this.addCalculated(incomingState) : incomingState
1139
- const copy = { ...state }
1140
- if (!this.calculated || this.storeCalculatedInState) return copy
1141
- const keys = Object.keys(this.calculated)
1142
- keys.forEach(key => {
1143
- if (this.initialState && typeof this.initialState[key] !== 'undefined') {
1144
- copy[key] = this.initialState[key]
1145
- } else {
1146
- delete copy[key]
1147
- }
1148
- })
1149
- return copy
1150
- }
1151
-
1152
- collectRenderParameters(): any {
1153
- const state = this.sources[this.stateSourceName]
1154
- const renderParams = { ...this.peers$[this.DOMSourceName] }
1155
-
1156
- const enhancedState = state && state.isolateSource(state, { get: (state: any) => this.addCalculated(state) })
1157
- const stateStream = (enhancedState && enhancedState.stream) || xs.never()
1158
-
1159
-
1160
- renderParams.state = stateStream.compose(dropRepeats(objIsEqual))
1161
-
1162
- if (this.sources.props$) {
1163
- renderParams.props = this.sources.props$.compose(dropRepeats(propsIsEqual))
1164
- }
1165
-
1166
- if (this._processedChildren$) {
1167
- renderParams.children = this._processedChildren$.map((p: any) => p.children).compose(dropRepeats(objIsEqual))
1168
- renderParams.slots = this._processedChildren$.map((p: any) => p.slots).compose(dropRepeats(objIsEqual))
1169
- }
1170
-
1171
- if (this.context$) {
1172
- renderParams.context = this.context$.compose(dropRepeats(objIsEqual))
1173
- }
1174
-
1175
- const names: string[] = []
1176
- const streams: any[] = []
1177
-
1178
- Object.entries(renderParams).forEach(([name, stream]: [string, any]) => {
1179
- names.push(name)
1180
- streams.push(stream)
1181
- })
1182
-
1183
- const combined = xs.combine(...streams)
1184
- .compose(debounce(1))
1185
- // map the streams from an array back to an object with the render parameter names as the keys
1186
- .map((arr: any) => {
1187
- const params = names.reduce((acc: Record<string, any>, name, index) => {
1188
- acc[name] = arr[index]
1189
- if (name === 'state') {
1190
- acc[this.stateSourceName] = arr[index]
1191
- acc.calculated = (arr[index] && this.getCalculatedValues(arr[index])) || {}
1192
- }
1193
- return acc
1194
- }, {} as Record<string, any>)
1195
- return params
1196
- })
1197
-
1198
- return combined
1199
- }
1200
-
1201
- instantiateSubComponents(vDom$: any): any {
1202
- // Build the component name Set once outside the fold — avoids rebuilding per render
1203
- const componentNameSet = new Set(['collection', 'switchable', 'sygnal-factory', ...Object.keys(this.components)])
1204
-
1205
- return vDom$.fold((previousComponents: any, vDom: any) => {
1206
- const foundComponents = getComponents(vDom, componentNameSet)
1207
- const entries = Object.entries(foundComponents)
1208
-
1209
- const rootEntry: Record<string, any> = { '::ROOT::': vDom }
1210
-
1211
- if (entries.length === 0) {
1212
- // Dispose any previously active sub-components
1213
- this._activeSubComponents.forEach((entry) => {
1214
- if (entry?.sink$?.__dispose) entry.sink$.__dispose()
1215
- })
1216
- this._activeSubComponents.clear()
1217
- return rootEntry
1218
- }
1219
-
1220
- const sinkArrsByType: Record<string, any[]> = {}
1221
- const childSources: any[] = []
1222
- let newInstanceCount = 0
1223
-
1224
- const newComponents = entries.reduce((acc, [id, el]) => {
1225
- const data = el.data
1226
- const props = data.props || {}
1227
- // a string-tag component with a single text child is a text-only vnode (B-011)
1228
- const children = el.children || (el.text != null ? [{ text: el.text }] : [])
1229
-
1230
- const isCollection = data.isCollection || false
1231
- const isSwitchable = data.isSwitchable || false
1232
-
1233
- const addSinks = (sinks: any) => {
1234
- Object.entries(sinks).forEach(([name, stream]: [string, any]) => {
1235
- sinkArrsByType[name] ||= []
1236
- if (name === PARENT_SINK_NAME) {
1237
- childSources.push(stream)
1238
- } else if (name !== this.DOMSourceName && name !== READY_SINK_NAME) {
1239
- sinkArrsByType[name].push(stream)
1240
- }
1241
- })
1242
- }
1243
-
1244
-
1245
- if (previousComponents[id]) {
1246
- const entry = previousComponents[id]
1247
- acc[id] = entry
1248
- entry.props$.shamefullySendNext(props)
1249
- entry.children$.shamefullySendNext(children)
1250
- addSinks(entry.sink$)
1251
- return acc
1252
- }
1253
-
1254
- const props$ = xs.create().startWith(props)
1255
- const children$ = xs.create().startWith(children)
1256
-
1257
- let instantiator
1258
-
1259
- if (isCollection) {
1260
- instantiator = this.instantiateCollection.bind(this)
1261
- } else if (isSwitchable) {
1262
- instantiator = this.instantiateSwitchable.bind(this)
1263
- } else {
1264
- instantiator = this.instantiateCustomComponent.bind(this)
1265
- }
1266
-
1267
- newInstanceCount++
1268
-
1269
- let sink$
1270
- try {
1271
- sink$ = instantiator(el, props$, children$)
1272
- } catch (err) {
1273
- const error = err instanceof Error ? err : new Error(String(err))
1274
- logError('SYG408', this, 'Sub-component threw; rendering the error fallback', ERR_FIX, error)
1275
- let fallbackVNode = { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
1276
- if (typeof this.onError === 'function') {
1277
- try {
1278
- fallbackVNode = this.onError(error, { componentName: this.name }) || fallbackVNode
1279
- } catch (fallbackErr) {
1280
- logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fallbackErr)
1281
- }
1282
- }
1283
- sink$ = { [this.DOMSourceName]: xs.of(fallbackVNode) }
1284
- }
1285
-
1286
- sink$[this.DOMSourceName] = sink$[this.DOMSourceName] ? this.makeCoordinatedSubComponentDomSink(sink$[this.DOMSourceName]) : xs.never()
1287
-
1288
- acc[id] = { sink$, props$, children$ }
1289
- this._activeSubComponents.set(id, acc[id])
1290
-
1291
- addSinks(sink$)
1292
-
1293
- return acc
1294
- }, rootEntry)
1295
-
1296
- const mergedSinksByType = Object.entries(sinkArrsByType).reduce((acc: Record<string, any>, [name, streamArr]: [string, any]) => {
1297
- if (streamArr.length === 0) return acc
1298
- acc[name] = streamArr.length === 1 ? streamArr[0] : xs.merge(...streamArr)
1299
- return acc
1300
- }, {} as Record<string, any>)
1301
-
1302
- // Dispose removed sub-components
1303
- const currentIds = new Set(Object.keys(newComponents))
1304
- this._activeSubComponents.forEach((entry, id) => {
1305
- if (!currentIds.has(id)) {
1306
- if (entry?.sink$?.__dispose) entry.sink$.__dispose()
1307
- this._activeSubComponents.delete(id)
1308
- delete this._childReadyState[id]
1309
- }
1310
- })
1311
-
1312
- this.newSubComponentSinks(mergedSinksByType)
1313
- this.newChildSources(childSources)
1314
-
1315
-
1316
- if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true)
1317
-
1318
- return newComponents
1319
- }, {})
1320
- }
1321
-
1322
- makeCoordinatedSubComponentDomSink(domSink$: any): any {
1323
- return domSink$
1324
- .debug((_: any) => this.triggerSubComponentsRendered())
1325
- .remember()
1326
- }
1327
-
1328
- // A child's reducers get their state through its lens from this component's raw reducer
1329
- // state; adding the calculated fields here gives them what the child's view gets (1H-3).
1330
- withCalculated(lense: any): any {
1331
- return this.calculated ? { get: (state: any) => lense.get(isObj(state) ? this.addCalculated(state) : state), set: lense.set } : lense
1332
- }
1333
-
1334
- createSubComponentLense(stateField: any, componentType: string, defaultState?: any): any {
1335
- const baseLense = {
1336
- get: (state: any) => state,
1337
- set: (_oldState: any, newState: any) => newState
1338
- }
1339
-
1340
- if (typeof stateField === 'undefined') return baseLense
1341
-
1342
- if (typeof stateField === 'string') {
1343
- return {
1344
- get: (state: any) => {
1345
- const slice = state[stateField]
1346
- if (typeof slice === 'undefined' && defaultState) return defaultState
1347
- return slice
1348
- },
1349
- set: (oldState: any, newState: any) => {
1350
- if (this.calculated && stateField in this.calculated) {
1351
- warn('SYG409', this, `${componentType} tried to update calculated field '${stateField}'; ignored`, 'Bind it to a non-calculated field')
1352
- return oldState
1353
- }
1354
- return { ...oldState, [stateField]: newState }
1355
- }
1356
- }
1357
- }
1358
-
1359
- if (isObj(stateField)) {
1360
- if (typeof stateField.get !== 'function') {
1361
- logError('SYG410', this, `${componentType} 'state' prop has no get(); it gets the parent's whole state`, 'Use a state key string or { get, set }')
1362
- return baseLense
1363
- }
1364
- return { get: stateField.get, set: stateField.set }
1365
- }
1366
-
1367
- logError('SYG410', this, `${componentType} 'state' prop is a ${typeof stateField}; it gets the parent's whole state`, 'Use a state key string or { get, set }')
1368
- return baseLense
1369
- }
1370
-
1371
- instantiateCollection(el: any, props$: any, children$: any): any {
1372
- const data = el.data
1373
- const props = data.props || {}
1374
- let filter = typeof props.filter === 'function' ? props.filter : undefined
1375
- let sort = sortFunctionFromProp(props.sort)
1376
-
1377
- const arrayOperators = {
1378
- filter,
1379
- sort
1380
- }
1381
-
1382
- const state$ = xs.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$.startWith(props))
1383
- // this debounce is important. it forces state and prop updates to happen at the same time
1384
- // without this, changes to sort or filter won't happen properly
1385
- .compose(debounce(1))
1386
- .map(([state, props]: [any, any]) => {
1387
- if (props.filter !== arrayOperators.filter) {
1388
- arrayOperators.filter = typeof props.filter === 'function' ? props.filter : undefined
1389
- }
1390
- if (props.sort !== arrayOperators.sort) {
1391
- arrayOperators.sort = sortFunctionFromProp(props.sort)
1392
- }
1393
- // calculated fields are added by the lens (B-013)
1394
- return state
1395
- })
1396
-
1397
- const stateSource = new StateSource(state$, this.stateSourceName)
1398
- const stateField = props.from
1399
- const collectionOf = props.of
1400
- const idField = props.idfield || 'id'
1401
-
1402
- let lense
1403
- let factory
1404
-
1405
- if (typeof collectionOf === 'function') {
1406
- if (collectionOf.isSygnalComponent) {
1407
- factory = collectionOf
1408
- } else {
1409
- const name = collectionOf.componentName || collectionOf.label || collectionOf.name || 'FUNCTION_COMPONENT'
1410
- const view = collectionOf
1411
- const { model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = collectionOf
1412
- const options = { name, view, model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
1413
- factory = component(options)
1414
- }
1415
- } else if (this.components[collectionOf]) {
1416
- factory = this.components[collectionOf]
1417
- } else {
1418
- fail('SYG411', this, `Collection 'of' is not a component: ${collectionOf}`, 'Use of={ItemComponent}')
1419
- }
1420
-
1421
- const fieldLense = {
1422
- get: (state: any) => {
1423
- if (!Array.isArray(state[stateField])) return []
1424
- const items = state[stateField]
1425
- const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items
1426
- const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered
1427
- const mapped = sorted.map((item: any, index: any) => {
1428
- return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
1429
- })
1430
-
1431
- return mapped
1432
- },
1433
- set: (oldState: any, newState: any) => {
1434
- if (this.calculated && stateField in this.calculated) {
1435
- warn('SYG409', this, `Collection tried to update calculated field '${stateField}'; ignored`, 'Bind it to a non-calculated field')
1436
- return oldState
1437
- }
1438
- const updated = []
1439
- for (const oldItem of oldState[stateField].map((item: any, index: any) => (isObj(item) ? { ...item, [idField]: item[idField] || index } : { __primitive: true, value: item, [idField]: index }))) {
1440
- if (typeof arrayOperators.filter === 'function' && !arrayOperators.filter(oldItem)) {
1441
- updated.push(oldItem.__primitive ? oldItem.value : oldItem)
1442
- } else {
1443
- const newItem = newState.find((item: any) => item[idField] === oldItem[idField])
1444
- if (typeof newItem !== 'undefined') updated.push(oldItem.__primitive ? newItem.value : newItem)
1445
- }
1446
- }
1447
- return { ...oldState, [stateField]: updated }
1448
- }
1449
- }
1450
-
1451
- if (stateField === undefined) {
1452
- lense = {
1453
- get: (state: any) => {
1454
- if (!(state instanceof Array) && state.value && state.value instanceof Array) return state.value
1455
- return state
1456
- },
1457
- set: (oldState: any, newState: any) => {
1458
- return newState
1459
- }
1460
- }
1461
- } else if (typeof stateField === 'string') {
1462
- if (isObj(this.currentState)) {
1463
- if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
1464
- const arrayFields = Object.keys(this.currentState).filter(k => Array.isArray(this.currentState[k]))
1465
- logError('SYG401', this, `Collection from="${stateField}" is not in state${arrayFields.length ? ` (array fields: '${arrayFields.join("', '")}')` : ''}; it renders nothing`, 'Set it to an array in initialState')
1466
- lense = undefined
1467
- } else if (!Array.isArray(this.currentState[stateField])) {
1468
- warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
1469
- lense = fieldLense
1470
- } else {
1471
- lense = fieldLense
1472
- }
1473
- } else {
1474
- if (!Array.isArray(this.currentState[stateField])) {
1475
- warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
1476
- lense = fieldLense
1477
- } else {
1478
- lense = fieldLense
1479
- }
1480
- }
1481
- } else if (isObj(stateField)) {
1482
- if (typeof stateField.get !== 'function') {
1483
- logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
1484
- lense = undefined
1485
- } else {
1486
- lense = {
1487
- get: (state: any) => {
1488
- const newState = stateField.get(state)
1489
- if (!Array.isArray(newState)) {
1490
- warn('SYG401', this, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', newState)
1491
- return []
1492
- }
1493
- return newState
1494
- },
1495
- set: stateField.set
1496
- }
1497
- }
1498
- } else {
1499
- logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
1500
- lense = undefined
1501
- }
1502
-
1503
- // B-013: add this component's calculated fields inside the lens, not on state$, so the
1504
- // items' reducers (which read through the lens from the raw state) see the same item
1505
- // array as their views, e.g. for from={calculatedField} or a custom get().
1506
- if (lense) lense = this.withCalculated(lense)
1507
-
1508
- // Strip collection-specific props and forward only user-defined extra props to each item
1509
- const collectionKeys = ['of', 'from', 'filter', 'sort', 'idfield', 'className']
1510
- const itemProps$ = props$.map((p: any) => {
1511
- if (!p || typeof p !== 'object') return {}
1512
- const itemProps: Record<string, any> = {}
1513
- for (const key in p) {
1514
- if (!collectionKeys.includes(key)) itemProps[key] = p[key]
1515
- }
1516
- return itemProps
1517
- })
1518
-
1519
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$: itemProps$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber }
1520
- const sink$ = collection(factory, lense as any, { container: null as any })(sources)
1521
- if (!isObj(sink$)) {
1522
- fail('SYG903', this, 'Collection factory returned invalid sinks', 'Return a sinks object')
1523
- }
1524
-
1525
- // Notify devtools of collection mount
1526
- if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
1527
- const itemName = typeof collectionOf === 'function'
1528
- ? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
1529
- : String(collectionOf)
1530
- ;(window as any).__SYGNAL_DEVTOOLS__.onCollectionMounted(
1531
- this._componentNumber, this.name, itemName,
1532
- typeof stateField === 'string' ? stateField : null
1533
- )
1534
- }
1535
-
1536
- return sink$
1537
- }
1538
-
1539
- instantiateSwitchable(el: any, props$: any, children$: any): any {
1540
- const data = el.data
1541
- const props = data.props || {}
1542
-
1543
- const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
1544
- .map((state: any) => {
1545
- return isObj(state) ? this.addCalculated(state) : state
1546
- })
1547
-
1548
- const stateSource = new StateSource(state$, this.stateSourceName)
1549
- const stateField = props.state
1550
- const lense = this.withCalculated(this.createSubComponentLense(stateField, 'Switchable sub-component'))
1551
-
1552
- const switchableComponents = props.of
1553
- const keys = Object.keys(switchableComponents)
1554
- keys.forEach(key => {
1555
- const current = switchableComponents[key]
1556
- if (!current.isSygnalComponent) {
1557
- const name = current.componentName || current.label || current.name || 'FUNCTION_COMPONENT'
1558
- const view = current
1559
- const { model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = current
1560
- const options = { name, view, model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
1561
- switchableComponents[key] = component(options)
1562
- }
1563
- })
1564
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
1565
-
1566
- const sink$ = isolate(switchable(switchableComponents, props$.map((props: any) => props.current), ''), { [this.stateSourceName]: lense })(sources)
1567
-
1568
- if (!isObj(sink$)) {
1569
- fail('SYG903', this, 'Switchable factory returned invalid sinks', 'Return a sinks object')
1570
- }
1571
-
1572
- return sink$
1573
- }
1574
-
1575
- instantiateCustomComponent(el: any, props$: any, children$: any): any {
1576
- const componentName = el.sel
1577
- const data = el.data
1578
- const props = data.props || {}
1579
-
1580
- const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
1581
- .map((state: any) => {
1582
- return isObj(state) ? this.addCalculated(state) : state
1583
- })
1584
-
1585
- let stateSource = new StateSource(state$, this.stateSourceName)
1586
- const stateField = props.state
1587
-
1588
- if (typeof props.sygnalFactory !== 'function' && isObj(props.sygnalOptions)) {
1589
- props.sygnalFactory = component(props.sygnalOptions)
1590
- }
1591
-
1592
- const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory)
1593
- if (!factory) {
1594
- if (componentName === 'sygnal-factory') fail('SYG413', this, 'Unnamed component has no factory', 'Name the function, or set Comp.componentName')
1595
- fail('SYG414', this, `Component '${componentName}' not found`, 'Import it, or add it to .components')
1596
- }
1597
-
1598
- // Guard against sub-components accidentally overwriting parent state with .initialState
1599
- const subInitialState = props.sygnalOptions?.initialState
1600
- const subIsolatedState = props.sygnalOptions?.isolatedState
1601
- if (subInitialState && !subIsolatedState) {
1602
- const subName = props.sygnalOptions?.name || componentName
1603
- fail('SYG405', subName, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
1604
- }
1605
-
1606
- const subInitState = subIsolatedState ? subInitialState : undefined
1607
- let lense = this.createSubComponentLense(stateField, 'Sub-component', subInitState)
1608
- // B-008: isolatedState without a `state` prop = state local to this instance; the
1609
- // parent's state is never replaced. It's kept off the parent's state stream (1H-9): the
1610
- // child's reducers are applied below and feed its own state source, so a child write
1611
- // doesn't produce a new parent state. Until the child first writes (INITIALIZE; never,
1612
- // without a model) it reads the parent's state, as before.
1613
- let local: any, local$: any
1614
- if (subIsolatedState && typeof stateField === 'undefined') {
1615
- local$ = xs.create()
1616
- stateSource = new StateSource(xs.merge(state$.filter(() => local === undefined), local$), this.stateSourceName)
1617
- }
1618
-
1619
- const sources: Record<string, any> = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
1620
- lense = local$ ? null : this.withCalculated(lense)
1621
-
1622
- // Detect Command objects in props and expose as commands$ source
1623
- for (const key of Object.keys(props)) {
1624
- const val = props[key]
1625
- if (val && val.__sygnalCommand) {
1626
- val._targetComponentName = componentName
1627
- sources.commands$ = makeCommandSource(val as Command)
1628
- break
1629
- }
1630
- }
1631
-
1632
- const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources)
1633
-
1634
- if (!isObj(sink$)) {
1635
- const name = componentName === 'sygnal-factory' ? 'custom element' : componentName
1636
- fail('SYG903', this, `Factory for ${name} returned invalid sinks`, 'Return a sinks object')
1637
- }
1638
- if (local$ && sink$[this.stateSourceName]) {
1639
- sink$[this.stateSourceName] = sink$[this.stateSourceName]
1640
- .map((reducer: any) => local$.shamefullySendNext(local = reducer(local === undefined ? this.addCalculated(this.currentState) : local)))
1641
- .filter(() => false)
1642
- }
1643
-
1644
- return sink$
1645
- }
1646
-
1647
- renderVdom(componentInstances$: any): any {
1648
- // Build the component name Set once — avoids rebuilding per render cycle
1649
- const componentNameSet = new Set(['collection', 'switchable', 'sygnal-factory', ...Object.keys(this.components)])
1650
-
1651
- return xs.combine(this.subComponentsRendered$, componentInstances$, this._readyChanged$.startWith(null))
1652
- .compose(debounce(1))
1653
- .map(([_, components]: [any, any]) => {
1654
- const root = components['::ROOT::']
1655
- const entries = Object.entries(components).filter(([id]) => id !== '::ROOT::')
1656
-
1657
- if (entries.length === 0) {
1658
- return xs.of(processSuspensePost(root))
1659
- }
1660
-
1661
- const ids: string[] = []
1662
- const vdom$ = entries
1663
- .map(([id, val]: [string, any]) => {
1664
- ids.push(id)
1665
- return val.sink$[this.DOMSourceName].startWith(undefined)
1666
- })
1667
-
1668
- if (vdom$.length === 0) return xs.of(root)
1669
-
1670
- // Track READY state on the component instance (persists across folds)
1671
- for (const [id, val] of entries as [string, any]) {
1672
- if (this._childReadyState[id] !== undefined) continue // already tracking
1673
- const readySink = val.sink$[READY_SINK_NAME]
1674
- if (readySink) {
1675
- const isExplicit = readySink.__explicitReady
1676
- this._childReadyState[id] = isExplicit ? false : true
1677
- readySink.addListener({
1678
- next: (ready: any) => {
1679
- const wasReady = this._childReadyState[id]
1680
- this._childReadyState[id] = !!ready
1681
- // When READY state changes, trigger a re-render
1682
- if (wasReady !== !!ready) {
1683
- if (this._readyChangedListener) {
1684
- setTimeout(() => {
1685
- this._readyChangedListener?.next(null)
1686
- }, 0)
1687
- }
1688
- if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
1689
- (window as any).__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready)
1690
- }
1691
- }
1692
- },
1693
- error: () => {},
1694
- complete: () => {},
1695
- })
1696
- } else {
1697
- this._childReadyState[id] = true
1698
- }
1699
- }
1700
-
1701
- return xs.combine(...vdom$)
1702
- .filter((vdoms: any) => vdoms.every((v: any) => v !== undefined))
1703
- .map((vdoms: any) => {
1704
- const withIds = vdoms.reduce((acc: Record<string, any>, vdom: any, index: any) => {
1705
- acc[ids[index]] = vdom
1706
- return acc
1707
- }, {} as Record<string, any>)
1708
- const injected = injectComponents(root, withIds, componentNameSet, 'r', undefined, this._childReadyState)
1709
- return processSuspensePost(injected)
1710
- })
1711
- })
1712
- .flatten()
1713
- .filter((val: any) => !!val)
1714
- // [diagnostics hook]
1715
- .map((vdom: any) => { diag.onRender(this, vdom); return vdom })
1716
- .remember()
1717
- }
1718
-
1719
- }
1720
-
1721
-
1722
-
1723
-
1724
-
1725
-
1726
-
1727
- /**
1728
- * factory to create a logging function meant to be used inside of an xstream .compose()
1729
- *
1730
- * @param {String} context name of the component or file to be prepended to any messages
1731
- * @return {Function}
1732
- *
1733
- * returned function accepts either a `String` of `Function`
1734
- * `String` values will be logged to `console` as is
1735
- * `Function` values will be called with the current `stream` value and the result will be logged to `console`
1736
- * all output will be prepended with the `context` (ex. "[CONTEXT] My output")
1737
- * ONLY outputs if the global `DEBUG` variable is set to `true`
1738
- */
1739
- function makeLog(context: string): any {
1740
- return function (this: Component, msg: any, immediate: boolean = false) {
1741
- const fixedMsg = (typeof msg === 'function') ? msg : (_: any) => msg
1742
- if (immediate) {
1743
- if (this.debug) {
1744
- const text = `[${context}] ${fixedMsg(msg)}`
1745
- console.log(text)
1746
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
1747
- window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
1748
- }
1749
- }
1750
- return
1751
- } else {
1752
- return (stream: any) => {
1753
- return stream.debug((msg: any) => {
1754
- if (this.debug) {
1755
- const text = `[${context}] ${fixedMsg(msg)}`
1756
- console.log(text)
1757
- if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
1758
- window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
1759
- }
1760
- }
1761
- })
1762
- }
1763
- }
1764
- }
1765
- }
1766
-
1767
-
1768
-
1769
- function getComponents(currentElement: any, componentNameSet: Set<string>, path: string = 'r', parentId?: string, found?: Record<string, any>): Record<string, any> {
1770
- if (!currentElement) return found || {}
1771
-
1772
- if (currentElement.data?.componentsProcessed) return found || {}
1773
- if (path === 'r') currentElement.data.componentsProcessed = true
1774
-
1775
- // Allocate the result object only at the root call
1776
- if (!found) found = {}
1777
-
1778
- const sel = currentElement.sel
1779
- const isCollection = sel && sel.toLowerCase() === 'collection'
1780
- const isSwitchable = sel && sel.toLowerCase() === 'switchable'
1781
- const isComponent = (sel && componentNameSet.has(sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
1782
- const props = (currentElement.data && currentElement.data.props) || {}
1783
- const children = currentElement.children || []
1784
-
1785
- let id = parentId
1786
- if (isComponent) {
1787
- id = getComponentIdFromElement(currentElement, path, parentId)
1788
- if (isCollection) {
1789
- if (!props.of) fail('SYG411', undefined, "Collection is missing 'of'", 'Use of={ItemComponent}')
1790
- if (typeof props.of !== 'string' && typeof props.of !== 'function') fail('SYG411', undefined, `Collection 'of' is a ${typeof props.of}`, 'Use of={ItemComponent}')
1791
- if (typeof props.of !== 'function' && !componentNameSet.has(props.of)) fail('SYG411', undefined, `Collection 'of' component not found: ${props.of}`, 'Use of={ItemComponent}')
1792
- // an invalid 'from' is reported once, with the component name, by instantiateCollection (G-026)
1793
- currentElement.data.isCollection = true
1794
- currentElement.data.props ||= {}
1795
- } else if (isSwitchable) {
1796
- if (!props.of) fail('SYG415', undefined, "Switchable is missing 'of'", 'Use of={{ name: Component }}')
1797
- if (!isObj(props.of)) fail('SYG415', undefined, `Switchable 'of' is a ${typeof props.of}`, 'Use of={{ name: Component }}')
1798
- const switchableComponents = Object.values(props.of)
1799
- if (!switchableComponents.every(comp => typeof comp === 'function')) fail('SYG415', undefined, "Switchable 'of' has a value that is not a component", 'Use of={{ name: Component }}')
1800
- if (!props.current || (typeof props.current !== 'string' && typeof props.current !== 'function')) fail('SYG416', undefined, `Switchable 'current' is missing or a ${typeof props.current}`, "Set current to a key of 'of'")
1801
- const switchableComponentNames = Object.keys(props.of)
1802
- if (!switchableComponentNames.includes(props.current)) fail('SYG416', undefined, `Switchable 'current' '${props.current}' is not a key of 'of'`, "Set current to a key of 'of'")
1803
- currentElement.data.isSwitchable = true
1804
- }
1805
- if (typeof props.key === 'undefined') (currentElement.data.props ||= {}).key = id
1806
- found[id] = currentElement
1807
- }
1808
-
1809
- for (let i = 0; i < children.length; i++) {
1810
- getComponents(children[i], componentNameSet, `${path}.${i}`, id, found)
1811
- }
1812
-
1813
- return found
1814
- }
1815
-
1816
- function injectComponents(currentElement: any, components: Record<string, any>, componentNameSet: Set<string>, path: string = 'r', parentId?: string, readyMap?: Record<string, boolean>): any {
1817
- if (!currentElement) return undefined
1818
-
1819
- const sel = currentElement.sel || 'NO SELECTOR'
1820
- const isComponent = componentNameSet.has(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
1821
- const isCollection = currentElement?.data?.isCollection
1822
- const children = currentElement.children || []
1823
-
1824
- let id = parentId
1825
- if (isComponent) {
1826
- id = getComponentIdFromElement(currentElement, path, parentId)
1827
- let component = components[id]
1828
- // Mark an injected VNode that is NOT ready (non-mutating) so Suspense can find it.
1829
- // Ready children get no attribute (G-018): moving markup into a child component
1830
- // must not change the DOM.
1831
- if (readyMap && id && readyMap[id] === false && component && typeof component === 'object' && component.sel) {
1832
- component = {
1833
- ...component,
1834
- data: {
1835
- ...(component.data || {}),
1836
- attrs: {
1837
- ...(component.data?.attrs || {}),
1838
- 'data-sygnal-ready': 'false'
1839
- }
1840
- }
1841
- }
1842
- }
1843
- if (isCollection) {
1844
- return {
1845
- ...currentElement,
1846
- sel: 'div',
1847
- children: Array.isArray(component) ? component : [component]
1848
- }
1849
- } else {
1850
- return component
1851
- }
1852
- } else if (children.length > 0) {
1853
- const newChildren = children.map((child: any, i: any) => injectComponents(child, components, componentNameSet, `${path}.${i}`, id, readyMap)).flat()
1854
- return { ...currentElement, children: newChildren }
1855
- } else {
1856
- return currentElement
1857
- }
1858
- }
1859
-
1860
- function getComponentIdFromElement(el: any, path: string, parentId?: string): string {
1861
- const sel = el.sel
1862
- const name = typeof sel === 'string' ? sel : 'functionComponent'
1863
- const props = el.data?.props || {}
1864
- const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path
1865
- const parentString = parentId ? `${parentId}|` : ''
1866
- const fullId = `${parentString}${name}::${id}`
1867
- return fullId
1868
- }
1869
-
1870
-
1871
- function hasNotReadyChild(vnode: any): boolean {
1872
- if (!vnode || !vnode.sel) return false
1873
- // Check for data-sygnal-ready="false" on injected sub-components
1874
- if (vnode.data?.attrs?.['data-sygnal-ready'] === 'false') return true
1875
- // Check for lazy-loading placeholder (not yet instantiated as a component)
1876
- if (vnode.data?.attrs?.['data-sygnal-lazy'] === 'loading') return true
1877
- // Stop at inner Suspense boundaries — they handle their own children
1878
- if (vnode.sel === 'suspense') return false
1879
- if (Array.isArray(vnode.children)) {
1880
- for (const child of vnode.children) {
1881
- if (hasNotReadyChild(child)) return true
1882
- }
1883
- }
1884
- return false
1885
- }
1886
-
1887
- function processSuspensePost(vnode: any): any {
1888
- if (!vnode || !vnode.sel) return vnode
1889
- if (vnode.sel === 'suspense') {
1890
- const props = vnode.data?.props || {}
1891
- const fallback = props.fallback
1892
- const children = vnode.children || []
1893
-
1894
- // Check if any child within this boundary is not ready
1895
- const isPending = children.some(hasNotReadyChild)
1896
-
1897
- if (isPending && fallback) {
1898
- // Render fallback
1899
- if (typeof fallback === 'string') {
1900
- return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'pending' } }, children: [{ text: fallback }], text: undefined, elm: undefined, key: undefined }
1901
- }
1902
- return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'pending' } }, children: [fallback], text: undefined, elm: undefined, key: undefined }
1903
- }
1904
-
1905
- // All children ready or no fallback — render children directly
1906
- if (children.length === 1) return processSuspensePost(children[0])
1907
- return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'resolved' } }, children: children.map((c: any) => processSuspensePost(c)), text: undefined, elm: undefined, key: undefined }
1908
- }
1909
- if (vnode.children && vnode.children.length > 0) {
1910
- const newChildren = vnode.children.map((c: any) => processSuspensePost(c))
1911
- return { ...vnode, children: newChildren }
1912
- }
1913
- return vnode
1914
- }
1915
-
1916
- const portalPatch = snabbdomInit(defaultModules);
1917
-
1918
- function applyTransitionHooks(vnode: any, name: string, duration?: number): any {
1919
- const existingInsert = vnode.data?.hook?.insert
1920
- const existingRemove = vnode.data?.hook?.remove
1921
-
1922
- vnode.data = vnode.data || {}
1923
- vnode.data.hook = vnode.data.hook || {}
1924
-
1925
- vnode.data.hook.insert = (vn: any) => {
1926
- if (existingInsert) existingInsert(vn)
1927
- const el = vn.elm
1928
- if (!el || !el.classList) return
1929
- el.classList.add(`${name}-enter-from`, `${name}-enter-active`)
1930
- requestAnimationFrame(() => {
1931
- requestAnimationFrame(() => {
1932
- el.classList.remove(`${name}-enter-from`)
1933
- el.classList.add(`${name}-enter-to`)
1934
- onTransitionEnd(el, duration, () => {
1935
- el.classList.remove(`${name}-enter-active`, `${name}-enter-to`)
1936
- })
1937
- })
1938
- })
1939
- }
1940
-
1941
- vnode.data.hook.remove = (vn: any, rm: () => void) => {
1942
- if (existingRemove) existingRemove(vn, () => {})
1943
- const el = vn.elm
1944
- if (!el || !el.classList) { rm(); return }
1945
- el.classList.add(`${name}-leave-from`, `${name}-leave-active`)
1946
- requestAnimationFrame(() => {
1947
- requestAnimationFrame(() => {
1948
- el.classList.remove(`${name}-leave-from`)
1949
- el.classList.add(`${name}-leave-to`)
1950
- onTransitionEnd(el, duration, () => {
1951
- el.classList.remove(`${name}-leave-active`, `${name}-leave-to`)
1952
- rm()
1953
- })
1954
- })
1955
- })
1956
- }
1957
-
1958
- return vnode
1959
- }
1960
-
1961
- /**
1962
- * Single-pass VNode preprocessor that handles lazy, portal, transition, and clientonly
1963
- * nodes in one recursive walk instead of four separate passes.
1964
- */
1965
- function preprocessVdom(vnode: any, componentInstance: any): any {
1966
- if (!vnode || !vnode.sel) return vnode
1967
-
1968
- // Handle lazy components
1969
- const view = vnode.data?.props?.sygnalOptions?.view
1970
- if (view && view.__sygnalLazy) {
1971
- if (view.__sygnalLazyLoaded()) {
1972
- const loaded = view.__sygnalLazyLoadedComponent
1973
- if (loaded) {
1974
- const props = vnode.data?.props || {}
1975
- const name = loaded.componentName || loaded.label || loaded.name || 'LazyLoaded'
1976
- const { model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = loaded
1977
- const options = { name, view: loaded, model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
1978
- const cleanProps = { ...props }
1979
- delete cleanProps.sygnalOptions
1980
- return {
1981
- sel: name,
1982
- data: { props: { ...cleanProps, sygnalOptions: options } },
1983
- children: vnode.children || [],
1984
- text: undefined, elm: undefined, key: undefined,
1985
- }
1986
- }
1987
- } else {
1988
- if (!view.__sygnalLazyReRenderScheduled && view.__sygnalLazyPromise && componentInstance) {
1989
- view.__sygnalLazyReRenderScheduled = true
1990
- view.__sygnalLazyPromise.then(() => {
1991
- setTimeout(() => {
1992
- const stateSource = componentInstance.sources?.[componentInstance.stateSourceName]
1993
- if (stateSource && stateSource.stream) {
1994
- const stateCopy = { ...componentInstance.currentState, __sygnalLazyTick: Date.now() }
1995
- stateSource.stream.shamefullySendNext(stateCopy)
1996
- }
1997
- }, 0)
1998
- })
1999
- }
2000
- }
2001
- }
2002
-
2003
- // Handle portals
2004
- if (vnode.sel === 'portal') {
2005
- const target = vnode.data?.props?.target
2006
- const children = vnode.children || []
2007
- return createPortalPlaceholder(target, children)
2008
- }
2009
-
2010
- // Handle transitions — recursively preprocess the child before applying hooks
2011
- if (vnode.sel === 'transition') {
2012
- const props = vnode.data?.props || {}
2013
- const name = props.name || 'v'
2014
- const duration = props.duration
2015
- const children = vnode.children || []
2016
- const child = children[0]
2017
- if (!child || !child.sel) return child || vnode
2018
- return applyTransitionHooks(preprocessVdom(child, componentInstance), name, duration)
2019
- }
2020
-
2021
- // Handle clientonly — unwrap to children on client
2022
- if (vnode.sel === 'clientonly') {
2023
- const children = vnode.children || []
2024
- if (children.length === 0) return { sel: 'div', data: {}, children: [] }
2025
- if (children.length === 1) return preprocessVdom(children[0], componentInstance)
2026
- return {
2027
- sel: 'div',
2028
- data: {},
2029
- children: children.map((c: any) => preprocessVdom(c, componentInstance)),
2030
- text: undefined,
2031
- elm: undefined,
2032
- key: undefined,
2033
- }
2034
- }
2035
-
2036
- // Recurse into children
2037
- if (vnode.children && vnode.children.length > 0) {
2038
- vnode.children = vnode.children.map((child: any) => preprocessVdom(child, componentInstance))
2039
- }
2040
- return vnode
2041
- }
2042
-
2043
-
2044
- function onTransitionEnd(el: any, duration: number | undefined, cb: () => void): void {
2045
- if (typeof duration === 'number') {
2046
- setTimeout(cb, duration)
2047
- } else {
2048
- const handler = () => {
2049
- el.removeEventListener('transitionend', handler)
2050
- cb()
2051
- }
2052
- el.addEventListener('transitionend', handler)
2053
- }
2054
- }
2055
-
2056
-
2057
- function portalMount(vnode: any, target: string, children: any[]): void {
2058
- const container = document.querySelector(target)
2059
- if (!container) {
2060
- warn('SYG417', 'Portal', `Target '${target}' not found; content not rendered`, 'Render the target first')
2061
- return
2062
- }
2063
- const anchor = document.createElement('div')
2064
- container.appendChild(anchor)
2065
- vnode.data._portalVnode = portalPatch(anchor, {
2066
- sel: 'div', data: {}, children,
2067
- text: undefined, elm: undefined, key: undefined,
2068
- })
2069
- vnode.data._portalContainer = container
2070
- }
2071
-
2072
- function createPortalPlaceholder(target: string, children: any[]): any {
2073
- const portalChildren = children || []
2074
-
2075
- return {
2076
- sel: 'div',
2077
- data: {
2078
- style: { display: 'none' },
2079
- attrs: { 'data-sygnal-portal': target },
2080
- portalChildren,
2081
- hook: {
2082
- insert: (vnode: any) => {
2083
- // Try synchronously first (target outside component tree or
2084
- // plain element in the same patch cycle).
2085
- const container = document.querySelector(target)
2086
- if (container) {
2087
- portalMount(vnode, target, portalChildren)
2088
- return
2089
- }
2090
- // Target may be rendered by a sub-component whose streams
2091
- // haven't settled yet (debounced). Retry a few times to
2092
- // allow sub-component rendering to complete.
2093
- let attempts = 0
2094
- const tryMount = () => {
2095
- if (vnode.data._portalVnode) return // already mounted
2096
- if (document.querySelector(target)) {
2097
- portalMount(vnode, target, portalChildren)
2098
- } else if (++attempts < 10) {
2099
- setTimeout(tryMount, 5)
2100
- } else {
2101
- warn('SYG417', 'Portal', `Target '${target}' not found; content not rendered`, 'Render the target first')
2102
- }
2103
- }
2104
- setTimeout(tryMount, 5)
2105
- },
2106
- postpatch: (oldVnode: any, newVnode: any) => {
2107
- const prevPortalVnode = oldVnode.data?._portalVnode
2108
- const container = oldVnode.data?._portalContainer
2109
- const newChildren = newVnode.data?.portalChildren || []
2110
- if (!prevPortalVnode || !container) {
2111
- // insert was deferred and hasn't fired yet, or target
2112
- // wasn't found. Try mounting now with updated children.
2113
- if (!newVnode.data._portalVnode) {
2114
- portalMount(newVnode, target, newChildren)
2115
- }
2116
- return
2117
- }
2118
- newVnode.data._portalVnode = portalPatch(prevPortalVnode, {
2119
- sel: 'div', data: {}, children: newChildren,
2120
- text: undefined, elm: undefined, key: undefined,
2121
- })
2122
- newVnode.data._portalContainer = container
2123
- },
2124
- destroy: (vnode: any) => {
2125
- const pv = vnode.data?._portalVnode
2126
- if (pv && pv.elm && pv.elm.parentNode) {
2127
- pv.elm.parentNode.removeChild(pv.elm)
2128
- }
2129
- },
2130
- },
2131
- },
2132
- children: [],
2133
- text: undefined,
2134
- elm: undefined,
2135
- key: undefined,
2136
- }
2137
- }
2138
-
2139
-
2140
- function propsIsEqual(obj1: any, obj2: any): boolean {
2141
- return objIsEqual(sanitizeObject(obj1), sanitizeObject(obj2))
2142
- }
2143
-
2144
- function objIsEqual(obj1: any, obj2?: any, maxDepth: number = 5, depth: number = 0): boolean {
2145
- // Base case: if the current depth exceeds maxDepth, return true
2146
- if (depth > maxDepth) {
2147
- return false;
2148
- }
2149
-
2150
- // If both are the same object or are both exactly null or undefined
2151
- if (obj1 === obj2) {
2152
- return true;
2153
- }
2154
-
2155
- // If either is not an object (null, undefined, or primitive), directly compare
2156
- if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
2157
- return false;
2158
- }
2159
-
2160
- // Special handling for arrays
2161
- if (Array.isArray(obj1) && Array.isArray(obj2)) {
2162
- if (obj1.length !== obj2.length) {
2163
- return false;
2164
- }
2165
- for (let i = 0; i < obj1.length; i++) {
2166
- if (!objIsEqual(obj1[i], obj2[i], maxDepth, depth + 1)) {
2167
- return false;
2168
- }
2169
- }
2170
- return true;
2171
- }
2172
-
2173
- // Get keys of both objects
2174
- const keys1 = Object.keys(obj1);
2175
-
2176
- // Check if the number of properties is different
2177
- if (keys1.length !== Object.keys(obj2).length) {
2178
- return false;
2179
- }
2180
-
2181
- // Recursively check each property
2182
- for (const key of keys1) {
2183
- if (!(key in obj2)) {
2184
- return false;
2185
- }
2186
- if (!objIsEqual(obj1[key], obj2[key], maxDepth, depth + 1)) {
2187
- return false;
2188
- }
2189
- }
2190
-
2191
- return true;
2192
- }
2193
-
2194
- function sanitizeObject(obj: any): any {
2195
- if (!isObj(obj)) return obj
2196
- const {state, of, from, filter, ...sanitized} = obj
2197
- return sanitized
2198
- }
2199
-
2200
- function isObj(obj: any): obj is Record<string, any> {
2201
- return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
2202
- }
2203
-
2204
- const SORT_FIX = "Use a field name, { field: 'asc'|'desc'|1|-1 }, or a function"
2205
-
2206
- function __baseSort(a: any, b: any, ascending: boolean = true): number {
2207
- const direction = ascending ? 1 : -1
2208
- switch(true) {
2209
- case a > b: return 1 * direction
2210
- case a < b: return -1 * direction
2211
- default: return 0
2212
- }
2213
- }
2214
-
2215
- function __sortFunctionFromObj(item: Record<string, any>): ((a: any, b: any) => number) | undefined {
2216
- const entries = Object.entries(item)
2217
- if (entries.length > 1) {
2218
- logError('SYG418', 'Collection', 'sort object must have one key; ignored', SORT_FIX, item)
2219
- return undefined
2220
- }
2221
- const entry = entries[0]
2222
- const [field, directionRaw] = entry
2223
- if (!['string', 'number'].includes(typeof directionRaw)) {
2224
- logError('SYG418', 'Collection', 'sort direction must be a string or number; ignored', SORT_FIX, item)
2225
- return undefined
2226
- }
2227
- let ascending = true
2228
- if (typeof directionRaw === 'string') {
2229
- if (!['asc', 'desc'].includes(directionRaw.toLowerCase())) {
2230
- logError('SYG418', 'Collection', "sort direction must be 'asc' or 'desc'; ignored", SORT_FIX, item)
2231
- return undefined
2232
- }
2233
- ascending = directionRaw.toLowerCase() !== 'desc'
2234
- }
2235
- if (typeof directionRaw === 'number') {
2236
- if (directionRaw !== 1 && directionRaw !== -1) {
2237
- logError('SYG418', 'Collection', 'sort direction must be 1 or -1; ignored', SORT_FIX, item)
2238
- return undefined
2239
- }
2240
- ascending = directionRaw === 1
2241
- }
2242
- return (a, b) => __baseSort(a[field], b[field], ascending)
2243
- }
2244
-
2245
- function sortFunctionFromProp(sortProp: any): ((a: any, b: any) => number) | undefined {
2246
- if (!sortProp) return undefined
2247
- const propType = typeof sortProp
2248
- // if function do nothing
2249
- if (propType === 'function') return sortProp
2250
- if (propType === 'string') {
2251
- // if passed either 'asc' or 'desc' sort on the entire item
2252
- if (sortProp.toLowerCase() === 'asc' || sortProp.toLowerCase() === 'desc') {
2253
- const ascending = sortProp.toLowerCase() !== 'desc'
2254
- return (a, b) => __baseSort(a, b, ascending)
2255
- }
2256
- // assume it's a field/property name, and sort it ascending
2257
- const field = sortProp
2258
- return (a, b) => __baseSort(a[field], b[field], true)
2259
- } else if (Array.isArray(sortProp)) {
2260
- const sorters = sortProp.map(item => {
2261
- if (typeof item === 'function') return item
2262
- if (typeof item === 'string' && !['asc', 'desc'].includes(item.toLowerCase())) return (a: any, b: any) => __baseSort(a[item], b[item], true)
2263
- if (isObj(item)) {
2264
- return __sortFunctionFromObj(item)
2265
- }
2266
- })
2267
-
2268
- return (a, b) => sorters.filter(sorter => typeof sorter === 'function').reduce((comparisonSoFar, currentSorter) => {
2269
- if (comparisonSoFar !== 0) return comparisonSoFar
2270
- return currentSorter(a, b)
2271
- }, 0)
2272
- } else if (isObj(sortProp)) {
2273
- return __sortFunctionFromObj(sortProp)
2274
- } else {
2275
- logError('SYG418', 'Collection', 'Invalid sort prop; ignored', SORT_FIX, sortProp)
2276
- return undefined
2277
- }
2278
- }
2279
-
2280
- function extractSlots(children: any[]): { slots: Record<string, any[]>, defaultChildren: any[] } {
2281
- const slots: Record<string, any[]> = {}
2282
- const defaultChildren: any[] = []
2283
-
2284
- for (const child of children) {
2285
- if (child && child.sel === 'slot') {
2286
- const name = (child.data?.props?.name) || 'default'
2287
- if (!slots[name]) slots[name] = []
2288
- const slotChildren = Array.isArray(child.children) ? child.children : (child.children ? [child.children] : [])
2289
- slots[name].push(...slotChildren)
2290
- } else {
2291
- defaultChildren.push(child)
2292
- }
2293
- }
2294
-
2295
- if (defaultChildren.length > 0) {
2296
- if (!slots['default']) slots['default'] = []
2297
- slots['default'].push(...defaultChildren)
2298
- }
2299
-
2300
- return { slots, defaultChildren: slots['default'] || [] }
2301
- }