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,286 @@
1
+ /**
2
+ * PLAN-4 3-E (GS-10): "Copy as test". Turns a recorded session (./devtoolsActions getSession())
3
+ * into a Vitest + renderComponent test that replays it:
4
+ *
5
+ * renderComponent(C, { initialState?, drivers? }) the session's initial state (left out when
6
+ * it is the component's own initialState)
7
+ * await t.ready()
8
+ * t.simulateAction(type, data) each action caused by 'intent' | 'simulateAction' | 'behavior'
9
+ * await t.respond(sink, body, OK) a reply action from a makeFetchDriver source (renderComponent's
10
+ * await t.fail(sink, status, ...) HTTP fake answers the request the replayed actions send again)
11
+ * await t.settle()
12
+ * expect(t.state).toEqual(final)
13
+ *
14
+ * 'next' and 'built-in' actions are consequences (the replay produces them again), never replayed;
15
+ * nor is an intent action whose data is the component's own state (an intent over STATE.stream).
16
+ * A t.respond / t.fail that follows simulateAction calls waits for the request they send (G-218).
17
+ * Data is written as JS literals: JSON values, Date, Map, Set, NaN / Infinity / -0, bigint. A DOM
18
+ * event or element becomes a stub ({ type, key, target: { dataset, value, checked, id } }), with a
19
+ * comment. Anything else (functions, class instances, cycles) is left out with a comment.
20
+ *
21
+ * The final-state assertion is written only when the replay is complete: every state-changing
22
+ * action could be replayed with its data, no descendant instance changed the state on its own
23
+ * (a child's intent: copy the test from that instance instead), the session wasn't truncated,
24
+ * and the final state is JSON-safe. Otherwise a comment says why. A test written with the
25
+ * assertion passes unchanged (for drivers the session's app had, pass `drivers`).
26
+ */
27
+ import type {SessionRecording, SessionAction} from './devtoolsActions'
28
+ import {isElement, isEvent, deepEqual} from './devtoolsActions'
29
+
30
+ export interface CopyAsTestOptions {
31
+ /** the import line(s) for the component, e.g. "import App from './App.jsx'" (default: `import <Name> from './<Name>.js'`) */
32
+ componentImport?: string
33
+ /** the identifier the component has in the test (default: the recorded component name) */
34
+ componentName?: string
35
+ /** more import lines (drivers, helpers) */
36
+ imports?: string[]
37
+ /** drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
38
+ drivers?: Record<string, string>
39
+ /** more renderComponent options, as source code: "strict: true" */
40
+ renderOptions?: string
41
+ /** the test's name (default: '<Component> session (copied from sygnal/devtools)') */
42
+ testName?: string
43
+ /** adds a `// @vitest-environment <env>` first line (e.g. 'jsdom') */
44
+ environment?: string
45
+ }
46
+
47
+ export interface CopyAsTestResult {
48
+ /** the test file */
49
+ code: string
50
+ /** true when the test asserts the final state */
51
+ complete: boolean
52
+ /** what was left out, and why (also comments in the code) */
53
+ warnings: string[]
54
+ /** actions written as simulateAction / respond / fail */
55
+ replayed: number
56
+ }
57
+
58
+ const REPLAY = new Set(['intent', 'simulateAction', 'behavior'])
59
+ const IDENT = /^[A-Za-z_$][\w$]*$/
60
+
61
+ class Unserializable extends Error {}
62
+
63
+ /** a single-quoted JS string literal */
64
+ const str = (s: string) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`
65
+
66
+ interface Lit { code: string; stubbed: boolean }
67
+
68
+ /** `v` as a JS literal. mode 'data' turns DOM events / elements into stubs; 'state' refuses them. */
69
+ export function literal(v: any, mode: 'data' | 'state' = 'data', indent = ''): Lit {
70
+ const flag = {stubbed: false}
71
+ const code = lit(v, mode, indent, new Set(), flag)
72
+ return {code, stubbed: flag.stubbed}
73
+ }
74
+
75
+ function elementStub(el: any): any {
76
+ const out: any = {}
77
+ const ds = el.dataset && typeof el.dataset == 'object' ? {...el.dataset} : {}
78
+ if (Object.keys(ds).length) out.dataset = ds
79
+ if (el.id) out.id = el.id
80
+ const tag = String(el.tagName).toUpperCase()
81
+ if (/^(INPUT|SELECT|TEXTAREA)$/.test(tag) && typeof el.value == 'string') out.value = el.value
82
+ if (tag == 'INPUT' && /^(checkbox|radio)$/i.test(el.type || '')) out.checked = !!el.checked
83
+ return out
84
+ }
85
+
86
+ function eventStub(e: any): any {
87
+ const out: any = {type: e.type}
88
+ if (typeof e.key == 'string') out.key = e.key
89
+ if (isElement(e.target)) {
90
+ const t = elementStub(e.target)
91
+ if (Object.keys(t).length) out.target = t
92
+ }
93
+ return out
94
+ }
95
+
96
+ function lit(v: any, mode: 'data' | 'state', ind: string, seen: Set<any>, flag: {stubbed: boolean}): string {
97
+ if (v === null) return 'null'
98
+ switch (typeof v) {
99
+ case 'undefined': return 'undefined'
100
+ case 'boolean': return String(v)
101
+ case 'string': return str(v)
102
+ case 'number': return Object.is(v, -0) ? '-0' : String(v)
103
+ case 'bigint': return `${v}n`
104
+ case 'function': throw new Unserializable(`a function (${v.name || 'anonymous'})`)
105
+ case 'symbol': throw new Unserializable(`a symbol (${String(v)})`)
106
+ }
107
+ if (seen.has(v)) throw new Unserializable('a circular reference')
108
+ if (isEvent(v) || isElement(v)) {
109
+ if (mode == 'state') throw new Unserializable(isEvent(v) ? `a DOM ${v.type} event` : 'a DOM element')
110
+ flag.stubbed = true
111
+ return lit(isEvent(v) ? eventStub(v) : elementStub(v), mode, ind, seen, flag)
112
+ }
113
+ seen.add(v)
114
+ try {
115
+ const inner = ind + ' '
116
+ const list = (items: string[], open: string, close: string) => {
117
+ if (!items.length) return open + close
118
+ const one = open + (open == '{' ? ' ' : '') + items.join(', ') + (open == '{' ? ' ' : '') + close
119
+ if (one.length + ind.length <= 100 && !one.includes('\n')) return one
120
+ return open + '\n' + items.map(s => inner + s).join(',\n') + ',\n' + ind + close
121
+ }
122
+ if (Array.isArray(v)) return list(Array.from(v, x => lit(x, mode, inner, seen, flag)), '[', ']')
123
+ if (v instanceof Date) return `new Date(${str(isNaN(+v) ? 'Invalid Date' : v.toISOString())})`
124
+ if (v instanceof Map) return `new Map(${list([...v].map(([k, x]) => list([lit(k, mode, inner + ' ', seen, flag), lit(x, mode, inner + ' ', seen, flag)], '[', ']')), '[', ']')})`
125
+ if (v instanceof Set) return `new Set(${list([...v].map(x => lit(x, mode, inner, seen, flag)), '[', ']')})`
126
+ const proto = Object.getPrototypeOf(v)
127
+ // G-265: a vnode of the JSX pragma (its own constructor, flagged $p) is a plain object
128
+ if (proto !== Object.prototype && proto !== null && !v.$p) throw new Unserializable(`a ${(v.constructor && v.constructor.name) || 'class instance'}`)
129
+ const items: string[] = []
130
+ for (const k of Object.keys(v)) {
131
+ if (v[k] === undefined) continue
132
+ items.push(`${IDENT.test(k) ? k : str(k)}: ${lit(v[k], mode, inner, seen, flag)}`)
133
+ }
134
+ return list(items, '{', '}')
135
+ } finally {
136
+ seen.delete(v)
137
+ }
138
+ }
139
+
140
+ const q = str
141
+ const tryLit = (v: any, mode: 'data' | 'state', ind: string): Lit | {error: string} => {
142
+ try { return literal(v, mode, ind) } catch (e: any) {
143
+ if (e instanceof Unserializable) return {error: e.message}
144
+ throw e
145
+ }
146
+ }
147
+
148
+ /** an error reply from makeFetchDriver: { error, request, status?, body? } for the request's `error` action */
149
+ const isErrorReply = (a: SessionAction) =>
150
+ a.data && typeof a.data == 'object' && 'error' in a.data && a.data.request && typeof a.data.request == 'object' && a.data.request.error === a.type
151
+
152
+ /** The test for a recorded session (see the module comment), with what it left out. */
153
+ export function sessionToTest(rec: SessionRecording, options: CopyAsTestOptions = {}): CopyAsTestResult {
154
+ const name = options.componentName || (IDENT.test(rec.component) ? rec.component : 'Component')
155
+ const warnings: string[] = []
156
+ const incomplete: string[] = []
157
+ const body: string[] = []
158
+ const I = ' '
159
+ const drivers = options.drivers || {}
160
+ let replayed = 0
161
+
162
+ if (rec.truncated) incomplete.push('the session lost its oldest actions (clear the log, then record a shorter session)')
163
+
164
+ // render options
165
+ const opts: string[] = []
166
+ if (rec.initialState !== undefined && !deepEqual(rec.initialState, rec.definitionInitialState)) {
167
+ const l = tryLit(rec.initialState, 'state', I)
168
+ if ('error' in l) {
169
+ warnings.push(`the initial state holds ${l.error}: rendering with the component's own initialState`)
170
+ incomplete.push('the initial state could not be written')
171
+ } else opts.push(`initialState: ${l.code}`)
172
+ }
173
+ const fakeable = new Set(rec.fakeable)
174
+ const driverNames = [...new Set([...rec.drivers.filter(n => !fakeable.has(n)), ...Object.keys(drivers)])]
175
+ const given = driverNames.filter(n => drivers[n] !== undefined)
176
+ const missing = driverNames.filter(n => drivers[n] === undefined)
177
+ if (given.length) opts.push(`drivers: { ${given.map(n => `${IDENT.test(n) ? n : q(n)}: ${drivers[n]}`).join(', ')} }`)
178
+ if (missing.length) warnings.push(`the app had ${missing.length == 1 ? 'a driver' : 'drivers'} for ${missing.join(', ')}: pass ${missing.length == 1 ? 'it' : 'them'} with copyAsTest(..., { drivers: { ${missing[0]}: '...' } }) if the component reads ${missing.length == 1 ? 'it' : 'them'}`)
179
+ if (options.renderOptions) opts.push(options.renderOptions)
180
+
181
+ const names = rec.actionNames && new Set(rec.actionNames)
182
+ // (G-218: t.respond / t.fail right after queued simulateAction calls wait for the request
183
+ // those calls send, so no settle() is needed before them)
184
+ const answer = (line: string) => body.push(line)
185
+ for (const a of rec.actions) {
186
+ const changes = a.sinks.includes('STATE') || a.sinks.includes('state')
187
+ if (REPLAY.has(a.cause)) {
188
+ // an intent over the component's own STATE stream: the replay produces it again
189
+ if (a.echo) continue
190
+ // an intent action with no model entry does nothing (renderComponent can't send it)
191
+ if (names && !names.has(a.type) && a.cause != 'behavior') continue
192
+ replayed++
193
+ if (a.data === undefined) { body.push(`${I}t.simulateAction(${q(a.type)})`); continue }
194
+ const l = tryLit(a.data, 'data', I)
195
+ if ('error' in l) {
196
+ body.push(`${I}t.simulateAction(${q(a.type)}) // data left out: it held ${l.error}`)
197
+ if (changes) incomplete.push(`${a.type}'s data could not be written (${l.error})`)
198
+ continue
199
+ }
200
+ if (l.stubbed) body.push(`${I}// ${a.type}: DOM event/element data as a stub (type, key, target dataset/value/checked/id)`)
201
+ body.push(`${I}t.simulateAction(${q(a.type)}, ${l.code})`)
202
+ } else if (a.cause == 'reply') {
203
+ const sink = a.replySink
204
+ if (!sink || a.replyKind != 'fetch' || drivers[sink] !== undefined) {
205
+ body.push(`${I}// reply ${a.type}${sink ? ` from ${sink}` : ''}: not replayed (renderComponent fakes makeFetchDriver sources only)`)
206
+ if (changes) incomplete.push(`the ${a.type} reply could not be replayed`)
207
+ continue
208
+ }
209
+ if (isErrorReply(a)) {
210
+ const err = a.data.error, status = a.data.status ?? (err && err.status), errBody = a.data.body ?? (err && err.body)
211
+ if (typeof status == 'number') {
212
+ if (errBody === undefined) answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)})`)
213
+ else {
214
+ const l = tryLit(errBody, 'state', I)
215
+ if ('error' in l) { answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)}) // body left out: it held ${l.error}`); if (changes) incomplete.push(`the ${a.type} error body could not be written`) }
216
+ else answer(`${I}await t.fail(${q(sink)}, ${status}, { request: (r) => r.error === ${q(a.type)}, body: ${l.code} })`)
217
+ }
218
+ } else if (a.data.issues) {
219
+ body.push(`${I}// reply ${a.type}: a validation failure, not replayed`)
220
+ if (changes) incomplete.push(`the ${a.type} validation failure could not be replayed`)
221
+ continue
222
+ } else {
223
+ answer(`${I}await t.fail(${q(sink)}, new Error(${str(String(err && err.message || err))}), ${q(a.type)})`)
224
+ }
225
+ replayed++
226
+ continue
227
+ }
228
+ const l = tryLit(a.data, 'state', I)
229
+ if ('error' in l) {
230
+ body.push(`${I}// reply ${a.type} from ${sink}: its data held ${l.error}, not replayed`)
231
+ if (changes) incomplete.push(`the ${a.type} reply's data could not be written`)
232
+ continue
233
+ }
234
+ answer(`${I}await t.respond(${q(sink)}, ${l.code}, ${q(a.type)})`)
235
+ replayed++
236
+ } else if (a.type == 'RESOURCE' && a.data && (a.data.status == 'success' || a.data.status == 'error')) {
237
+ body.push(`${I}// RESOURCE ${a.data.name}: ${a.data.status}, not replayed (answer it with t.respond / t.fail)`)
238
+ if (changes) incomplete.push(`resource '${a.data.name}' was answered`)
239
+ }
240
+ // 'next' and 'built-in': consequences, produced again by the replay
241
+ }
242
+
243
+ if (rec.foreign.length) {
244
+ const list = rec.foreign.slice(0, 5).map(f => `${f.component}#${f.instance} ${f.type}`).join(', ') + (rec.foreign.length > 5 ? ', ...' : '')
245
+ incomplete.push(`child components changed the state through their own actions (${list}): copy the test from that component instance instead`)
246
+ }
247
+
248
+ let assertion: string[]
249
+ if (!incomplete.length) {
250
+ const l = tryLit(rec.finalState, 'state', I)
251
+ if ('error' in l) incomplete.push(`the final state holds ${l.error}`)
252
+ else assertion = [`${I}expect(t.state).toEqual(${l.code})`]
253
+ }
254
+ if (incomplete.length) {
255
+ warnings.push(...incomplete.map(s => 'no final-state assertion: ' + s))
256
+ assertion = [`${I}// No final-state assertion: ${incomplete.join('; ')}`]
257
+ }
258
+
259
+ const head: string[] = []
260
+ if (options.environment) head.push(`// @vitest-environment ${options.environment}`)
261
+ head.push(`// Copied from a sygnal/devtools session of ${rec.component} (${rec.actions.length} recorded actions, ${replayed} replayed)`)
262
+ for (const w of warnings) head.push(`// NOTE: ${w}`)
263
+ head.push(`import { it, expect } from 'vitest'`, `import { renderComponent } from 'sygnal'`)
264
+ head.push(...(options.imports || []))
265
+ head.push(options.componentImport || `import ${name} from './${name}.js'`)
266
+
267
+ const testName = options.testName || `${rec.component} session (copied from sygnal/devtools)`
268
+ const render = opts.length ? `renderComponent(${name}, {\n${opts.map(o => `${I}${o},`).join('\n')}\n })` : `renderComponent(${name})`
269
+ const code = [
270
+ ...head,
271
+ '',
272
+ `it(${q(testName)}, async () => {`,
273
+ ` const t = ${render}`,
274
+ ' try {',
275
+ `${I}await t.ready()`,
276
+ ...body,
277
+ `${I}await t.settle()`,
278
+ ...assertion!,
279
+ ' } finally {',
280
+ `${I}t.dispose()`,
281
+ ' }',
282
+ '})',
283
+ '',
284
+ ].join('\n')
285
+ return {code, complete: !incomplete.length, warnings, replayed}
286
+ }
@@ -1,5 +1,11 @@
1
1
  import {getDiagnostics} from './diagnostics/index';
2
2
  import type {Diagnostic} from './diagnostics/index';
3
+ import {recordActions, clearActions, getActions, onAction, getSession, preview} from './devtoolsActions';
4
+ import type {DevtoolsAction, SessionRecording} from './devtoolsActions';
5
+ import {sessionToTest} from './copyAsTest';
6
+ import type {CopyAsTestOptions, CopyAsTestResult} from './copyAsTest';
7
+
8
+ import {addLayer, devtoolsHooks} from './devtoolsNext';
3
9
 
4
10
  const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
5
11
  const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
@@ -9,13 +15,10 @@ declare global {
9
15
  interface Window {
10
16
  __SYGNAL_DEVTOOLS__?: SygnalDevTools;
11
17
  __SYGNAL_DEVTOOLS_APP__?: {
12
- sinks?: { STATE?: { shamefullySendNext: (fn: any) => void } };
18
+ __runtime?: { setState: (id: any, fn: (s: any) => any) => void };
13
19
  };
14
20
  __SYGNAL_DEVTOOLS_PAGE__?: boolean;
15
21
  SYGNAL_DEBUG?: string | false;
16
- __SYGNAL_HMR_UPDATING?: boolean;
17
- __SYGNAL_HMR_STATE?: unknown;
18
- __SYGNAL_HMR_PERSISTED_STATE?: unknown;
19
22
  __SYGNAL_HMR_LAST_CAPTURED_STATE?: unknown;
20
23
  Cyclejs?: { sinks?: any };
21
24
  CyclejsDevTool_startGraphSerializer?: (sinks: any) => void;
@@ -58,11 +61,16 @@ interface ExtensionMessage {
58
61
  source?: string;
59
62
  }
60
63
 
61
- class SygnalDevTools {
64
+ export class SygnalDevTools {
62
65
  _connected: boolean;
66
+ _initialized = false;
63
67
  _components: Map<number, ComponentMeta>;
64
68
  _stateHistory: StateHistoryEntry[];
65
69
  _maxHistory: number;
70
+ /** PLAN-4 3-E: actions changed since the last ACTIONS post (flushed on a timeout) */
71
+ _dirty = new Set<DevtoolsAction>();
72
+ _flush: any = null;
73
+ _copyOptions: CopyAsTestOptions = {};
66
74
 
67
75
  constructor() {
68
76
  this._connected = false;
@@ -80,11 +88,24 @@ class SygnalDevTools {
80
88
  return this._connected && typeof window !== 'undefined';
81
89
  }
82
90
 
91
+ /** Publish the bridge on window and listen for the extension. Idempotent (D77: called once, by 'sygnal/devtools'). */
83
92
  init(): void {
84
- if (typeof window === 'undefined') return;
93
+ if (typeof window === 'undefined' || this._initialized) return;
94
+ this._initialized = true;
85
95
 
86
96
  window.__SYGNAL_DEVTOOLS__ = this;
87
97
 
98
+ // PLAN-4 3-E (GS-10): the action log (recorded from now on, connected or not)
99
+ recordActions();
100
+ // PLAN-4.6 R4: the apps read this bridge's hooks (devtoolsNext.ts)
101
+ addLayer((api: any) => devtoolsHooks(this, api));
102
+ onAction((a, kind) => {
103
+ if (!this.connected) return;
104
+ if (kind == 'reset') { this._dirty.clear(); this._post('ACTIONS_RESET', {actions: []}); return; }
105
+ this._dirty.add(a!);
106
+ if (this._flush === null) this._flush = setTimeout(() => this._flushActions(), 0);
107
+ });
108
+
88
109
  window.addEventListener('message', (event: MessageEvent) => {
89
110
  if (event.source !== window) return;
90
111
  if (event.data?.source === EXTENSION_SOURCE) {
@@ -99,6 +120,13 @@ class SygnalDevTools {
99
120
  this._connected = true;
100
121
  if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
101
122
  this._sendFullTree();
123
+ this._post('ACTIONS_RESET', {actions: getActions().slice(-500).map(a => this._serializeAction(a))});
124
+ break;
125
+ case 'CLEAR_ACTIONS':
126
+ clearActions();
127
+ break;
128
+ case 'COPY_AS_TEST':
129
+ this._post('COPY_AS_TEST_RESULT', this._copyAsTest(msg.payload?.instance));
102
130
  break;
103
131
  case 'DISCONNECT':
104
132
  this._connected = false;
@@ -130,7 +158,7 @@ class SygnalDevTools {
130
158
  hasIntent: !!instance.intent,
131
159
  hasContext: !!instance.context,
132
160
  hasCalculated: !!instance.calculated,
133
- components: Object.keys(instance.components || {}),
161
+ components: [],
134
162
  parentId: null,
135
163
  children: [],
136
164
  debug: instance._debug,
@@ -283,6 +311,42 @@ class SygnalDevTools {
283
311
  });
284
312
  }
285
313
 
314
+ /** PLAN-4 3-E: post the actions added or changed since the last flush */
315
+ _flushActions(): void {
316
+ this._flush = null;
317
+ if (!this.connected || !this._dirty.size) { this._dirty.clear(); return; }
318
+ const list = [...this._dirty].sort((a, b) => a.seq - b.seq).map(a => this._serializeAction(a));
319
+ this._dirty.clear();
320
+ this._post('ACTIONS', {actions: list});
321
+ }
322
+
323
+ _serializeAction(a: DevtoolsAction): any {
324
+ return {
325
+ seq: a.seq, type: a.type, data: preview(a.data), component: a.component, instance: a.instance, parent: a.parent,
326
+ sinks: [...a.sinks], cause: a.cause, at: a.at,
327
+ ...('after' in a ? {before: this._safeClone(a.before), after: this._safeClone(a.after)} : {}),
328
+ };
329
+ }
330
+
331
+ /** "Copy as test" for the panel: the session of `instance` (default: the root) */
332
+ _copyAsTest(instance?: string | number): CopyAsTestResult | {error: string} {
333
+ try {
334
+ return sessionToTest(getSession(instance ?? undefined), this._copyOptions);
335
+ } catch (e: any) {
336
+ return {error: String(e?.message || e)};
337
+ }
338
+ }
339
+
340
+ /** Defaults for "Copy as test" from the panel (componentImport, drivers, imports, ...) */
341
+ configureCopyAsTest(options: CopyAsTestOptions): void {
342
+ this._copyOptions = {...this._copyOptions, ...options};
343
+ }
344
+
345
+ /** The recorded session of an instance (see sygnal/devtools getSession) */
346
+ getSession(target?: any): SessionRecording {
347
+ return getSession(target);
348
+ }
349
+
286
350
  _setDebug({componentId, enabled}: {componentId?: number; enabled: boolean}): void {
287
351
  if (typeof componentId === 'undefined' || componentId === null) {
288
352
  if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
@@ -294,7 +358,8 @@ class SygnalDevTools {
294
358
  if (meta && meta._instanceRef) {
295
359
  const instance = meta._instanceRef.deref();
296
360
  if (instance) {
297
- instance._debug = enabled;
361
+ // (through the app's runtime API)
362
+ instance.__api?.setDebug(componentId, enabled);
298
363
  meta.debug = enabled;
299
364
  this._post('DEBUG_TOGGLED', {componentId, enabled});
300
365
  }
@@ -311,40 +376,27 @@ class SygnalDevTools {
311
376
 
312
377
  const newState = this._safeClone(state);
313
378
 
314
- // Try per-component time-travel via the component's STATE sink (reducer stream)
379
+ // Per-component time travel through the app's runtime API
315
380
  const meta = this._components.get(componentId);
316
381
  if (meta) {
317
382
  const instance = meta._instanceRef?.deref();
318
383
  if (!instance) {
319
384
  console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
320
- } else {
321
- // sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
322
- const stateSinkName = instance.stateSourceName || 'STATE';
323
- const stateSink = instance.sinks?.[stateSinkName];
324
- if (stateSink?.shamefullySendNext) {
325
- stateSink.shamefullySendNext(() => ({...newState}));
326
- this._post('TIME_TRAVEL_APPLIED', {
327
- componentId,
328
- componentName,
329
- state: newState,
330
- });
331
- return;
332
- }
333
- console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
385
+ } else if (instance.__api) {
386
+ // the runtime's setState (a queued action, one patch)
387
+ instance.__api.setState(componentId, () => ({...newState}));
388
+ this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
389
+ return;
334
390
  }
335
391
  } else {
336
392
  console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
337
393
  }
338
394
 
339
- // Fall back to root STATE sink for root-level components
395
+ // Fall back to the root's state
340
396
  const app = window.__SYGNAL_DEVTOOLS_APP__;
341
- if (app?.sinks?.STATE?.shamefullySendNext) {
342
- app.sinks.STATE.shamefullySendNext(() => ({...newState}));
343
- this._post('TIME_TRAVEL_APPLIED', {
344
- componentId,
345
- componentName,
346
- state: newState,
347
- });
397
+ if (app?.__runtime) {
398
+ app.__runtime.setState('root', () => ({...newState}));
399
+ this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
348
400
  } else {
349
401
  console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
350
402
  }
@@ -506,9 +558,27 @@ class SygnalDevTools {
506
558
 
507
559
  let instance: SygnalDevTools | null = null;
508
560
 
561
+ /**
562
+ * The bridge singleton of the 'sygnal/devtools' entry. Not part of the core
563
+ * (D77): the core only calls the hooks of window.__SYGNAL_DEVTOOLS__ when it exists.
564
+ */
509
565
  export function getDevTools(): SygnalDevTools {
510
566
  if (!instance) instance = new SygnalDevTools();
511
567
  return instance;
512
568
  }
513
569
 
570
+ /**
571
+ * Install the bridge (what run() did before 6.0): window.__SYGNAL_DEVTOOLS__ is
572
+ * set and the extension can connect. A bridge already on window (e.g. a second
573
+ * copy of this entry) is kept. Nothing happens outside a browser.
574
+ */
575
+ export function installDevTools(): SygnalDevTools | undefined {
576
+ if (typeof window === 'undefined') return undefined;
577
+ const existing = window.__SYGNAL_DEVTOOLS__;
578
+ if (existing) return existing;
579
+ const dt = getDevTools();
580
+ dt.init();
581
+ return dt;
582
+ }
583
+
514
584
  export default getDevTools;