sygnal 5.4.0 → 6.0.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 (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,323 @@
1
+ import {Stream} from 'xstream';
2
+ import {senderOf, makeReplies} from './replies';
3
+ import {SET} from '../core/teardown';
4
+
5
+ /*
6
+ * makeRouter({ routes, base?, mode?, scroll?, focus?, prefetch?, navigate?, window?, ... })
7
+ * (PLAN-3 §1.7, D81, D84). Public docs: src/index.d.ts and docs guide/router.md. Summary:
8
+ *
9
+ * - `routes`: { name: '/path/:param' | '*' }. First match wins; '*' is the not-found route.
10
+ * Matching ignores a trailing slash and empty segments; the reported `path` is normalised
11
+ * (no trailing slash, base removed). `href()` never adds a trailing slash.
12
+ * - The returned object: `href(name, params?, query?, hash?)` and `match(url)` are pure (SSR);
13
+ * `current(url?)` reads the location (or `url`: SSR passes the request URL); `driver` is the
14
+ * driver for `run(App, { ROUTER: router.driver })`; `options` is what the 5-4c fake reuses.
15
+ * - Reading: a component declares `App.route = 'ROUTE'` (a string static, `__sygnalStatic:
16
+ * 'route'`); the driver replies ROUTE with `{ name, params, query, hash, path }` on
17
+ * declaration and on every change. `{ route }` is the declaration: commands never use it.
18
+ * - Redirect order (G-168): the FIRST declarer (the outermost one: declarations are kept in
19
+ * mount order) is the guard owner. It gets each new route at once; the other declarers get it
20
+ * once its ROUTE ran (in the same flush, G-565), and only if no newer navigation (a redirect
21
+ * from the guard's ROUTE entry, e.g. `ROUTER: { to: 'login', replace: true }`) happened.
22
+ * G-571: past 32 navigations before a microtask (a redirect loop) each next one waits a task.
23
+ * - Commands: `{ to: name, params?, query?, hash?, replace?, scroll? }`, `{ url, replace? }`,
24
+ * `{ back: true }`, `{ forward: true }`, `{ go: n }`, `{ block: 'ACTION' | false }`,
25
+ * `{ prefetch: name | url, params?, query? }` (calls options.prefetch(route, url); else a
26
+ * no-op hook until the fetch cache, 5-5). `force: true` skips a block. `block` may come with a
27
+ * navigation: `{ ...proceed, block: false }` clears the block and makes it.
28
+ * - Block: while a component has `{ block: 'ACTION' }`, an attempted navigation (link click,
29
+ * command, back/forward) is not made; the last blocker gets ACTION with `{ to, route,
30
+ * proceed }` (`proceed` is the command that makes it anyway). A back/forward is undone with
31
+ * history.go(-delta) first. beforeunload is prevented while any block is set.
32
+ * - Links: document-level click interception of same-origin `<a href>` (HTML and SVG,
33
+ * inside shadow roots via composedPath(), resolved against <base>), except modified or
34
+ * non-left clicks, `target` other than _self, `download`, `rel="external"`,
35
+ * `data-router-ignore`, defaultPrevented, outside `base`, and (history mode) a hash-only link
36
+ * to the current page. Forms are left alone.
37
+ * - Scroll (option `scroll`, default true): `history.state` carries `{ key, i }`; positions are
38
+ * kept per key in memory. Push: top (or the hash target after render); back/forward: the saved
39
+ * position after render; replace: unchanged (or the hash target). Reload: the browser's own
40
+ * (scrollRestoration goes back to 'auto' on pagehide).
41
+ * - Focus (option `focus`, default '[data-router-focus],main h1,h1'; false disables): after a push or a
42
+ * back/forward, once the DOM has been quiet for `settleMs` (MutationObserver; capped at 1 s),
43
+ * the first match of the comma-separated selectors, tried in order, gets focus (tabindex=-1
44
+ * added when needed, preventScroll). Content that renders later (data still loading) is
45
+ * missed; the scroll restore has the same limit.
46
+ * - No window (SSR): no listeners, no history, no replies; href/match/current(url) still work.
47
+ * - Vike (`navigate` option, Vike's navigate()): no link interception (Vike owns links),
48
+ * navigation calls navigate(url, { overwriteLastHistoryEntry }), scroll is left to Vike, and
49
+ * the route is re-read on popstate and on the 'sygnal:navigate' event the Vike client hook
50
+ * dispatches before each client-side render.
51
+ */
52
+
53
+ type Routes = Record<string, string>;
54
+
55
+ const enc = encodeURIComponent;
56
+ const dec = (s: string) => { try { return decodeURIComponent(s); } catch (_) { return s; } };
57
+ const g: any = globalThis;
58
+ const PARAM = /:(\w+)/g;
59
+ // G-571: synchronous navigations (redirects) before a microtask, after which each waits a task
60
+ const HOPS = 32;
61
+
62
+ /** the names of the params a pattern requires */
63
+ export const paramsOf = (pat: string) => (pat.match(PARAM) || []).map(p => p.slice(1));
64
+
65
+ // the core's coded error() (published on the diagnostics bridge): no import, so the router
66
+ // costs nothing more standalone; prints in production too (like SYG611)
67
+ const bad = (v: any, msg: string, fix: string) => (g.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
68
+
69
+ export function makeRouter(options: any = {}) {
70
+ const {mode, prefetch, navigate} = options, routes: Routes = options.routes || {};
71
+ const base = (options.base || '').replace(/\/$/, '');
72
+ const hashMode = mode == 'hash';
73
+ const focusSel = options.focus ?? '[data-router-focus],main h1,h1';
74
+ const doScroll = options.scroll ?? !navigate;
75
+
76
+ const href = (name: string, params: any = {}, query?: any, hash?: string) => {
77
+ // dev entry only (SYG130: unknown name, missing or extra params)
78
+ g.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
79
+ const pat = routes[name];
80
+ const path = pat == null || pat == '*' ? '/' : pat.replace(PARAM, (_, k) => enc(params[k] ?? ''));
81
+ const q = query && new URLSearchParams(Object.entries(query).filter(([, v]) => v != null) as any) + '';
82
+ return base + (hashMode ? (base ? '/#' : '#') : '') + path + (q ? '?' + q : '') + (hash ? '#' + hash : '');
83
+ };
84
+
85
+ const matchPath = (path: string) => {
86
+ const segs = path.split('/').filter(Boolean);
87
+ let notFound: any = null;
88
+ for (const name in routes) {
89
+ const pat = routes[name];
90
+ if (pat == '*') { notFound ??= name; continue; }
91
+ const ps = pat.split('/').filter(Boolean), params: any = {};
92
+ if (ps.length == segs.length && ps.every((p, i) => p[0] == ':' ? (params[p.slice(1)] = dec(segs[i]), 1) : p == segs[i])) return {name, params};
93
+ }
94
+ return {name: notFound, params: {}};
95
+ };
96
+
97
+ /** `{ name, params, query, hash, path }` for a URL (a path, or absolute) */
98
+ const match = (url: string) => {
99
+ let u = new URL(url, 'http://x');
100
+ if (hashMode) u = new URL(u.hash.slice(1) || '/', 'http://x');
101
+ let p = u.pathname;
102
+ if (!hashMode && base && (p == base || p.startsWith(base + '/'))) p = p.slice(base.length);
103
+ const path = '/' + p.split('/').filter(Boolean).join('/');
104
+ return {...matchPath(path), query: Object.fromEntries(new URLSearchParams(u.search)), hash: dec(u.hash.slice(1)), path};
105
+ };
106
+
107
+ const win = () => options.window || (typeof window != 'undefined' ? window : null);
108
+ const current = (url?: string) => {
109
+ const w = win();
110
+ return match(url ?? (w ? (options.location || w.location).href : '/'));
111
+ };
112
+
113
+ const driver = (sink$: Stream<any>) => {
114
+ const w: any = win();
115
+ const L: any = w && (options.location || w.location);
116
+ const H: any = w && (options.history || w.history);
117
+ const D: any = w && (options.document || w.document);
118
+ // sender → reply action; insertion (mount) order: the first is the guard owner (G-168)
119
+ const declared = new Map<any, string>();
120
+ // sender → block action; the last one set gets the attempted navigation
121
+ const blocks = new Map<any, string>();
122
+ const pos = new Map<string, number[]>();
123
+ let ver = 0, idx = 0, key = '', last = '', force = 0, undo = 0, n = 0, hops = 0, warned = 0;
124
+ let cur: any = null, cancel: any = null, pendingKind = '';
125
+
126
+ const newKey = () => Date.now().toString(36) + (n++);
127
+ const stamp = (i: number) => ({key: (key = newKey()), i: (idx = i)});
128
+
129
+ const settle = (kind: string) => {
130
+ if (!D) return;
131
+ pendingKind = cancel && kind == 'replace' ? pendingKind : kind;
132
+ cancel?.();
133
+ let t: any, mo: any;
134
+ const done = () => {
135
+ cancel?.();
136
+ const k = pendingKind, h = cur.hash && D.getElementById(cur.hash), p = k == 'pop' && pos.get(key);
137
+ if (doScroll) p ? w.scrollTo(p[0], p[1]) : h?.scrollIntoView?.();
138
+ if (focusSel && k != 'replace') for (const sel of focusSel.split(',')) {
139
+ const el = D.querySelector(sel);
140
+ if (el) {
141
+ if (el.tabIndex < 0 && !el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');
142
+ el.focus({preventScroll: true});
143
+ break;
144
+ }
145
+ }
146
+ };
147
+ const quiet = () => { clearTimeout(t); t = setTimeout(done, options.settleMs ?? 30); };
148
+ const cap = setTimeout(done, 1000);
149
+ if (w.MutationObserver) (mo = new w.MutationObserver(quiet)).observe(D.body || D.documentElement, {childList: true, subtree: true, characterData: true});
150
+ cancel = () => { clearTimeout(t); clearTimeout(cap); mo?.disconnect(); cancel = null; };
151
+ quiet();
152
+ };
153
+
154
+ // G-565: `f` runs once the actions queued so far ran: a state write that changes nothing,
155
+ // queued behind them through the replies of the instances `ss` (one listening, else a task
156
+ // later). The first declarer's ROUTE (and any redirect it makes) runs first; the others get
157
+ // theirs in the same flush, before the patch, as long as no redirect happened (G-168).
158
+ // G-570: queued on every listening one and run by the first that is still there (the queue
159
+ // skips a disposed instance: the first declarer navigating from its DISPOSE / dispose$)
160
+ const after = (ss: any[], f: any) => {
161
+ let d = 0;
162
+ const l = ss.filter(listening), g = (x: any) => (d++ || f(), x);
163
+ l.length ? l.forEach(s => reply(s, SET, g)) : setTimeout(f);
164
+ };
165
+
166
+ const emit = (kind: string) => {
167
+ last = L.href;
168
+ const r = (cur = current()), list = [...declared], v = ++ver;
169
+ const send = () => {
170
+ if (ver != v || !list.length) return;
171
+ reply(list[0][0], list[0][1], r);
172
+ after(list.map(x => x[0]), () => { if (ver == v) for (const [s, a] of list.slice(1)) if (declared.get(s) == a) reply(s, a, r); });
173
+ };
174
+ // G-571: a redirect made from a ROUTE entry is delivered at once (a chain /secret → /login
175
+ // → /other is one flush, one patch); past HOPS of them before a microtask (a redirect
176
+ // loop) each next one waits a task, so the loop can't hang the page (a warning, once, when the
177
+ // diagnostics are on: in development)
178
+ if (!hops++) queueMicrotask(() => { hops = 0; });
179
+ if (hops > HOPS) {
180
+ if (g.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++) console.warn(`[Sygnal] router: more than ${HOPS} navigations in one flush (a redirect loop?); each next one waits a task (reported once)`, r.path);
181
+ setTimeout(send);
182
+ } else send();
183
+ if (kind != 'start') settle(kind);
184
+ };
185
+
186
+ const blocked = (data: any) => {
187
+ const b = [...blocks].pop();
188
+ return b && (reply(b[0], b[1], data), 1);
189
+ };
190
+
191
+ const go = (url: string, replace?: boolean, scroll?: boolean, f?: any) => {
192
+ if (!f && blocked({to: url, route: match(url), proceed: {url, replace, force: true}})) return;
193
+ if (navigate) return navigate(url, {overwriteLastHistoryEntry: !!replace});
194
+ pos.set(key, [w.scrollX, w.scrollY]);
195
+ H[replace ? 'replaceState' : 'pushState'](stamp(replace ? idx : idx + 1), '', url);
196
+ if (doScroll && !replace && scroll !== false) w.scrollTo(0, 0);
197
+ emit(replace ? 'replace' : 'push');
198
+ };
199
+
200
+ const onPop = () => {
201
+ const st = H.state, i = st?.i ?? idx + 1;
202
+ if (undo) return void (undo = 0);
203
+ if (!force && blocked({to: L.href, route: current(), proceed: {go: i - idx, force: true}})) {
204
+ undo = 1;
205
+ return H.go(idx - i);
206
+ }
207
+ force = 0;
208
+ pos.set(key, [w.scrollX, w.scrollY]);
209
+ if (st?.key) { key = st.key; idx = i; }
210
+ else H.replaceState({...st, ...stamp(i)}, '');
211
+ if (L.href != last) emit('pop');
212
+ };
213
+
214
+ const onClick = (e: any) => {
215
+ if (e.defaultPrevented || e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
216
+ const a = (e.composedPath?.() || []).find((x: any) => x.localName == 'a') || e.target?.closest?.('a');
217
+ const at = (k: string) => a.getAttribute(k);
218
+ const raw = a && (at('href') ?? a.getAttributeNS?.('http://www.w3.org/1999/xlink', 'href'));
219
+ if (raw == null || (at('target') || '_self') != '_self' || a.hasAttribute('download') ||
220
+ a.hasAttribute('data-router-ignore') || /(^|\s)external(\s|$)/i.test(at('rel') || '')) return;
221
+ const u = new URL(raw, D.baseURI || L.href), same = u.pathname == L.pathname && u.search == L.search;
222
+ if (u.origin != L.origin) return;
223
+ if (hashMode ? !(same && u.hash.startsWith('#/')) : (base && u.pathname != base && !u.pathname.startsWith(base + '/')) || (u.hash && same)) return;
224
+ e.preventDefault();
225
+ go(u.pathname + u.search + u.hash);
226
+ };
227
+
228
+ const onUnload = (e: any) => { e.preventDefault(); e.returnValue = ''; };
229
+ const onHide = () => { H.scrollRestoration = 'auto'; };
230
+ const listen = (on: boolean) => {
231
+ const m = on ? 'addEventListener' : 'removeEventListener';
232
+ w[m]('popstate', onPop);
233
+ if (hashMode) w[m]('hashchange', onPop);
234
+ if (navigate) w[m]('sygnal:navigate', onNav);
235
+ else D[m]('click', onClick);
236
+ if (doScroll) w[m]('pagehide', onHide);
237
+ };
238
+ const syncUnload = () => w?.[blocks.size ? 'addEventListener' : 'removeEventListener']('beforeunload', onUnload);
239
+ // Vike: the client hook says a navigation happened (Vike already updated the URL)
240
+ // (once the hook's microtasks ran, and again a task later in case the URL changes after it)
241
+ const onNav = () => { const f = () => L.href != last && emit('push'); queueMicrotask(f); setTimeout(f); };
242
+
243
+ const {replies, reply, listening} = makeReplies(s => {
244
+ declared.delete(s);
245
+ if (blocks.delete(s)) syncUnload();
246
+ });
247
+
248
+ const stop = () => {
249
+ if (!w) return;
250
+ listen(false);
251
+ blocks.clear();
252
+ syncUnload();
253
+ cancel?.();
254
+ onHide();
255
+ };
256
+
257
+ if (w) {
258
+ const st = H.state;
259
+ if (st?.key) { key = st.key; idx = st.i; }
260
+ else H.replaceState({...st, ...stamp(0)}, '');
261
+ if (doScroll && 'scrollRestoration' in H) H.scrollRestoration = 'manual';
262
+ listen(true);
263
+ emit('start');
264
+ }
265
+
266
+ const target = (v: any, name: any) => {
267
+ if (!(name in routes) || routes[name] == '*') return bad(v, `no route named '${name}'`, `Use one of: ${Object.keys(routes).filter(k => routes[k] != '*').join(', ')}`);
268
+ const missing = paramsOf(routes[name]).filter(k => v.params?.[k] == null);
269
+ if (missing.length) return bad(v, `route '${name}' (${routes[name]}) needs params: ${missing.join(', ')}`, `{ to: '${name}', params: { ${missing.join(', ')} } }`);
270
+ return href(name, v.params, v.query, v.hash);
271
+ };
272
+
273
+ sink$.addListener({
274
+ next: (v: any) => {
275
+ if (!v || typeof v != 'object') return;
276
+ const s = senderOf(v);
277
+ if ('route' in v) {
278
+ // the core sends { route: 'ACTION' } (or a falsy value) for a component's `route` static
279
+ if (Object.keys(v).length > 1 || v.route in routes) return bad(v, '`route` is the declaration key', "Navigate with { to: 'name', params }");
280
+ if (!v.route || s === undefined) return void declared.delete(s);
281
+ declared.set(s, v.route);
282
+ // G-555: the guard owner (first declarer) gets it at once when it listens to its replies
283
+ // (the core subscribes them at creation, before its statics), so its ROUTE is part of
284
+ // the flush that declared it: a task later came after the first render and undid what
285
+ // happened in between (a field typed into). G-565: the others too, once the actions
286
+ // queued before (the guard owner's ROUTE) ran, as for a navigation; a redirect in
287
+ // between supersedes the route (G-168). A declarer not listening yet (a Vike Layout):
288
+ // a task later
289
+ const v0 = ver, f = () => declared.get(s) == v.route && ver == v0 && reply(s, v.route, cur);
290
+ if (cur) declared.keys().next().value === s && listening(s) ? f() : after([s], f);
291
+ return;
292
+ }
293
+ // `block` combines with a navigation: { ...proceed, block: false } clears it and goes
294
+ const nav = v.back || v.forward || v.go || v.to != null || v.url != null;
295
+ if ('block' in v) {
296
+ v.block ? blocks.set(s, v.block) : blocks.delete(s);
297
+ syncUnload();
298
+ if (!nav) return;
299
+ }
300
+ if ('prefetch' in v) {
301
+ const url = v.prefetch in routes ? target({...v, to: v.prefetch}, v.prefetch) : v.prefetch;
302
+ if (url) prefetch?.(match(url), url);
303
+ } else if (!w) return;
304
+ else if (v.back || v.forward || v.go) {
305
+ force = v.force ? 1 : 0;
306
+ H.go(v.back ? -1 : v.forward ? 1 : v.go);
307
+ } else if (nav) {
308
+ const url = v.url ?? target(v, v.to);
309
+ if (url) go(url, v.replace, v.scroll, v.force);
310
+ } else bad(v, 'unknown command', "{ to: 'name', params?, query?, hash?, replace? }, { back: true }, { forward: true }, { block: 'ACTION' } or { prefetch: 'name' }");
311
+ },
312
+ error: (e: any) => console.error('[Sygnal] router', e),
313
+ complete: stop,
314
+ });
315
+
316
+ return {...replies, __sygnalStatic: 'route', __sygnalRouter: options, current: () => cur, href, dispose: stop};
317
+ };
318
+
319
+ return {routes, href, match, current, driver, options};
320
+ }
321
+
322
+ /** `makeRouter(options).driver`: for a router whose href() the app doesn't need */
323
+ export const makeRouterDriver = (options: any) => makeRouter(options).driver;
package/src/extra/run.ts CHANGED
@@ -1,249 +1,122 @@
1
- import {setup} from '../cycle/run/index';
2
- import {withState} from '../cycle/state/index';
3
- import {makeDOMDriver} from '../cycle/dom/index';
4
- import eventBusDriver from './eventDriver';
5
- import logDriver from './logDriver';
6
- import component, {ABORT} from '../component';
7
- import {getDevTools} from './devtools';
8
- import {configureDiagnostics} from './diagnostics/index';
1
+ import {configureDiagnostics, isDiagnosticsEnabled} from './diagnostics/index';
2
+ import {warn} from './diagnostics/legacy';
9
3
  import type {DiagnosticsMode, DiagnosticsOptions} from './diagnostics/index';
4
+ import {start} from '../core/runtime';
5
+
6
+ interface RunDiagnosticsOptions extends DiagnosticsOptions {
7
+ /** Strict (canonical-form) runtime checks; needs the 'sygnal/diagnostics' dev entry (G-036). */
8
+ strict?: boolean;
9
+ }
10
10
 
11
11
  interface RunOptions {
12
12
  mountPoint?: string;
13
+ /** @deprecated No effect since 6.0 (3-Q): fragments always work (the DOM driver flattens them) */
13
14
  fragments?: boolean;
14
15
  useDefaultDrivers?: boolean;
15
- /** Diagnostics mode (or mode + ignore list). Overrides globalThis.__SYGNAL_DEV__. */
16
- diagnostics?: DiagnosticsMode | DiagnosticsOptions;
16
+ /** Diagnostics mode (or mode + ignore list + strict). Overrides globalThis.__SYGNAL_DEV__. */
17
+ diagnostics?: DiagnosticsMode | RunDiagnosticsOptions;
18
+ /** PLAN-4 GS-11: app-level error hook, reporting only (after the component's onError boundary) */
19
+ onError?: (error: any, info: {componentName?: string; action?: string; phase: string; driver?: string}) => void;
20
+ /** PLAN-4 G-206: the root of this app's uid() strings (default 'u'); give each app on a page its own */
21
+ uid?: string;
17
22
  }
18
23
 
24
+ let warnedStrict = false;
25
+ // G-212: apps running now (an HMR swap disposes, then starts the new one)
26
+ let liveApps = 0;
27
+
19
28
  interface SygnalRunResult {
20
29
  sources: any;
21
30
  sinks: any;
22
31
  dispose: () => void;
23
32
  hmr?: (newComponent: any, explicitState?: any) => void;
33
+ /** internal: the app's runtime API (devtools, HMR, sygnal/element, Vike) */
34
+ __runtime?: any;
35
+ }
36
+
37
+ function resolveHotModule(incoming: any): any {
38
+ if (!incoming) return null;
39
+ if (Array.isArray(incoming)) return resolveHotModule(incoming.find(Boolean));
40
+ if (incoming.default && typeof incoming.default === 'function') return incoming;
41
+ if (typeof incoming === 'function') return {default: incoming};
42
+ return null;
24
43
  }
25
44
 
26
45
  export default function run(
27
46
  app: any,
28
47
  drivers: Record<string, any> = {},
29
- options: RunOptions = {}
48
+ options: RunOptions = {},
49
+ // G-216 (internal): a hot swap's { u: swapping, s: state to keep }, this app's `__hmr` source
50
+ hmrSwap?: {u: boolean; s: any}
30
51
  ): SygnalRunResult {
31
- // Initialize DevTools instrumentation bridge early (before component creation)
32
- if (typeof window !== 'undefined') {
33
- const dt = getDevTools();
34
- dt.init();
35
- }
52
+ // D77: the DevTools bridge is not installed here; 'sygnal/devtools' (injected by
53
+ // sygnal/vite in dev) installs window.__SYGNAL_DEVTOOLS__ before run() is called.
36
54
 
37
55
  // Resolve diagnostics mode: explicit option > globalThis.__SYGNAL_DEV__ > 'off'.
38
- // Each run() is authoritative: without the option, mode and ignore list
39
- // reset to defaults (no leakage from an earlier run()/configureDiagnostics()).
56
+ // Each run() with the option is authoritative. Without it, mode and ignore list reset to
57
+ // the defaults (no leakage from an earlier run()/configureDiagnostics()), unless another
58
+ // app is still running (G-212: a second app or a custom element keeps the host's mode).
40
59
  const {diagnostics} = options;
41
- const diagOptions: DiagnosticsOptions = typeof diagnostics === 'string' ? {mode: diagnostics} : diagnostics || {};
42
- configureDiagnostics({mode: diagOptions.mode, ignore: diagOptions.ignore || []});
43
-
44
- const {mountPoint = '#root', fragments = true, useDefaultDrivers = true} = options;
45
- if (!app.isSygnalComponent) {
46
- const name = app.name || app.componentName || app.label || 'FUNCTIONAL_COMPONENT';
47
- const view = app;
48
- const {
49
- model,
50
- intent,
51
- hmrActions,
52
- context,
53
- peers,
54
- components,
55
- initialState,
56
- calculated,
57
- storeCalculatedInState,
58
- DOMSourceName,
59
- stateSourceName,
60
- onError,
61
- debug,
62
- } = app;
63
- const componentOptions = {
64
- name,
65
- view,
66
- model,
67
- intent,
68
- hmrActions,
69
- context,
70
- peers,
71
- components,
72
- initialState,
73
- calculated,
74
- storeCalculatedInState,
75
- DOMSourceName,
76
- stateSourceName,
77
- onError,
78
- debug,
79
- };
80
-
81
- app = component(componentOptions);
82
- }
83
-
84
- if (
85
- typeof window !== 'undefined' &&
86
- window.__SYGNAL_HMR_UPDATING === true &&
87
- typeof window.__SYGNAL_HMR_PERSISTED_STATE !== 'undefined'
88
- ) {
89
- app.initialState = window.__SYGNAL_HMR_PERSISTED_STATE;
90
- }
91
-
92
- const wrapped = withState(app, 'STATE');
93
-
94
- const baseDrivers = useDefaultDrivers
95
- ? {
96
- EVENTS: eventBusDriver,
97
- DOM: makeDOMDriver(mountPoint, {snabbdomOptions: {experimental: {fragments}}} as any),
98
- LOG: logDriver,
60
+ const diagOptions: RunDiagnosticsOptions = typeof diagnostics === 'string' ? {mode: diagnostics} : diagnostics || {};
61
+ const {mode, strict} = diagOptions;
62
+ if (diagnostics || !liveApps) configureDiagnostics({mode, ignore: diagOptions.ignore || []});
63
+ // G-036: strict is applied only when given (an earlier configureStrict() is kept). It sets
64
+ // the flag the dev entry's strict checks read; strict without a mode turns diagnostics on.
65
+ // G-093: dispose() restores the previous value (an HMR swap disposes, then re-applies it).
66
+ const core = (globalThis as any).__SYGNAL_DIAGNOSTICS__;
67
+ const prevStrict = core.strict;
68
+ if (strict !== undefined) {
69
+ if ((core.strict = strict)) {
70
+ if (!mode && !isDiagnosticsEnabled()) configureDiagnostics({mode: 'warn'});
71
+ // SYG608 (once): the long explanation is in the docs / sygnal-check explain
72
+ if (!core.__uninstallChecks && !warnedStrict) {
73
+ warnedStrict = true;
74
+ warn('SYG608', 'run', "strict needs 'sygnal/diagnostics'");
99
75
  }
100
- : {};
101
-
102
- const combinedDrivers = {...baseDrivers, ...drivers};
103
-
104
- const {sources, sinks, run: _run} = setup(wrapped, combinedDrivers as any);
105
- const rawDispose = _run();
106
- let persistListener: any = null;
107
-
108
- if (
109
- typeof window !== 'undefined' &&
110
- (sources as any)?.STATE?.stream &&
111
- typeof (sources as any).STATE.stream.addListener === 'function'
112
- ) {
113
- persistListener = {
114
- next: (state: any) => {
115
- window.__SYGNAL_HMR_PERSISTED_STATE = state;
116
- },
117
- error: () => {},
118
- complete: () => {},
119
- };
120
- (sources as any).STATE.stream.addListener(persistListener);
121
- }
122
-
123
- const dispose = () => {
124
- if (
125
- persistListener &&
126
- (sources as any)?.STATE?.stream &&
127
- typeof (sources as any).STATE.stream.removeListener === 'function'
128
- ) {
129
- (sources as any).STATE.stream.removeListener(persistListener);
130
- persistListener = null;
131
- }
132
- // Trigger the component's dispose() which fires the DISPOSE action and dispose$ stream
133
- if (typeof (sinks as any).__dispose === 'function') {
134
- try { (sinks as any).__dispose(); } catch (_) {}
135
76
  }
136
- rawDispose();
137
- };
138
-
139
- const exposed: SygnalRunResult = {sources, sinks, dispose};
140
-
141
- // Store app reference for time-travel
142
- if (typeof window !== 'undefined') {
143
- window.__SYGNAL_DEVTOOLS_APP__ = exposed;
144
77
  }
145
78
 
146
- const swapToComponent = (newComponent: any, state?: any) => {
147
- const persistedState =
148
- typeof window !== 'undefined' ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
149
- const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
150
- const resolvedState = typeof state === 'undefined' ? fallbackState : state;
151
- if (typeof window !== 'undefined') {
152
- window.__SYGNAL_HMR_UPDATING = true;
153
- window.__SYGNAL_HMR_STATE = resolvedState;
154
- window.__SYGNAL_HMR_PERSISTED_STATE = resolvedState;
155
- }
79
+ const {uid} = options;
80
+ // (R4) an HMR swap: the kept state is the new root's first state, and the instances made at
81
+ // its start get no BOOTSTRAP (as the 5.x core's `__hmr` source); no 0/20 ms re-sends
82
+ const started = start(app, drivers, {...options, __hooks: (options as any).__hooks, __state: hmrSwap?.s, __swap: !!hmrSwap} as any);
83
+ liveApps++;
84
+ let off = false;
85
+ const exposed: SygnalRunResult = {
86
+ sources: started.sources,
87
+ sinks: started.sinks,
88
+ dispose: () => {
89
+ if (off) return;
90
+ off = true;
91
+ liveApps--;
92
+ // G-212: unregister from DevTools (a later app, or this app's hot-swapped successor, registers)
93
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__ === exposed) window.__SYGNAL_DEVTOOLS_APP__ = undefined;
94
+ started.dispose();
95
+ if (strict !== undefined) core.strict = prevStrict;
96
+ },
97
+ };
98
+ (exposed as any).__runtime = started.api;
99
+ // G-214: the uid option, as the 5.x core's __uid source
100
+ if (uid !== undefined) Object.defineProperty(exposed.sources, '__uid', {value: uid.replace(/[^\w-]+/g, '_'), enumerable: false, configurable: true});
101
+ if (typeof window !== 'undefined') window.__SYGNAL_DEVTOOLS_APP__ ||= exposed;
102
+ // (R4, 04 §3.12) hmr(): this app's current state through the runtime API (runtime.getState(),
103
+ // no STATE.stream._v), then the new component started with it
104
+ let current = app;
105
+ exposed.hmr = (newComponent: any, explicitState?: any) => {
106
+ const mod = resolveHotModule(newComponent) || {default: current};
107
+ const state = explicitState !== undefined ? explicitState : (exposed as any).__runtime.getState();
108
+ if (typeof window !== 'undefined') window.__SYGNAL_HMR_LAST_CAPTURED_STATE = state;
109
+ const wasRegistered = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__ === exposed;
156
110
  exposed.dispose();
157
- const App = newComponent.default || newComponent;
158
- App.initialState = resolvedState;
159
- const updated = run(App, drivers, options);
111
+ current = mod.default;
112
+ const updated: any = run(current, drivers, options, state === undefined ? undefined : {u: true, s: state});
160
113
  exposed.sources = updated.sources;
161
114
  exposed.sinks = updated.sinks;
162
- exposed.dispose = updated.dispose;
163
-
164
- if (
165
- typeof resolvedState !== 'undefined' &&
166
- updated?.sinks?.STATE &&
167
- typeof updated.sinks.STATE.shamefullySendNext === 'function'
168
- ) {
169
- const restore = () => updated.sinks.STATE.shamefullySendNext(() => ({...resolvedState}));
170
- setTimeout(restore, 0);
171
- setTimeout(restore, 20);
172
- }
173
-
174
- if (
175
- typeof window !== 'undefined' &&
176
- updated?.sources?.STATE?.stream &&
177
- typeof updated.sources.STATE.stream.setDebugListener === 'function'
178
- ) {
179
- updated.sources.STATE.stream.setDebugListener({
180
- next: () => {
181
- updated.sources.STATE.stream.setDebugListener(null);
182
- window.__SYGNAL_HMR_STATE = undefined;
183
- setTimeout(() => {
184
- window.__SYGNAL_HMR_UPDATING = false;
185
- }, 100);
186
- },
187
- });
188
- } else if (typeof window !== 'undefined') {
189
- window.__SYGNAL_HMR_STATE = undefined;
190
- window.__SYGNAL_HMR_UPDATING = false;
191
- }
115
+ (exposed as any).__runtime = updated.__runtime;
116
+ // the registration follows the swap (the successor is this same object)
117
+ if (typeof window !== 'undefined' && (window.__SYGNAL_DEVTOOLS_APP__ === updated || wasRegistered)) window.__SYGNAL_DEVTOOLS_APP__ = exposed;
118
+ const d = updated.dispose;
119
+ exposed.dispose = () => { if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__ === exposed) window.__SYGNAL_DEVTOOLS_APP__ = undefined; d(); };
192
120
  };
193
-
194
- const resolveHotModule = (incoming: any): any => {
195
- if (!incoming) return null;
196
- if (Array.isArray(incoming)) return resolveHotModule(incoming.find(Boolean));
197
- if (incoming.default && typeof incoming.default === 'function') return incoming;
198
- if (typeof incoming === 'function') return {default: incoming};
199
- return null;
200
- };
201
-
202
- const hmr = (newComponent: any, explicitState?: any) => {
203
- const normalizedModule = resolveHotModule(newComponent);
204
- const moduleToUse = normalizedModule || {default: app};
205
-
206
- if (typeof explicitState !== 'undefined') {
207
- if (typeof window !== 'undefined')
208
- window.__SYGNAL_HMR_LAST_CAPTURED_STATE = explicitState;
209
- swapToComponent(moduleToUse, explicitState);
210
- return;
211
- }
212
-
213
- const persistedState =
214
- typeof window !== 'undefined' ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
215
- if (typeof persistedState !== 'undefined') {
216
- if (typeof window !== 'undefined')
217
- window.__SYGNAL_HMR_LAST_CAPTURED_STATE = persistedState;
218
- swapToComponent(moduleToUse, persistedState);
219
- return;
220
- }
221
-
222
- const sourceState = exposed?.sources?.STATE?.stream?._v;
223
- if (typeof sourceState !== 'undefined') {
224
- if (typeof window !== 'undefined')
225
- window.__SYGNAL_HMR_LAST_CAPTURED_STATE = sourceState;
226
- swapToComponent(moduleToUse, sourceState);
227
- return;
228
- }
229
-
230
- if (
231
- exposed?.sinks?.STATE &&
232
- typeof exposed.sinks.STATE.shamefullySendNext === 'function'
233
- ) {
234
- exposed.sinks.STATE.shamefullySendNext((state: any) => {
235
- if (typeof window !== 'undefined')
236
- window.__SYGNAL_HMR_LAST_CAPTURED_STATE = state;
237
- swapToComponent(moduleToUse, state);
238
- return ABORT;
239
- });
240
- return;
241
- }
242
-
243
- swapToComponent(moduleToUse);
244
- };
245
-
246
- exposed.hmr = hmr;
247
-
248
121
  return exposed;
249
122
  }