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
@@ -40,6 +40,407 @@ const Stream = Stream$1 ||
40
40
  (xs$1 && xs$1.Stream) ||
41
41
  (xs && xs.Stream);
42
42
 
43
+ const DOCS_BASE_URL = 'https://sygnal.js.org';
44
+ const CODE_SEVERITY = {
45
+ SYG101: 'warn',
46
+ SYG102: 'info',
47
+ SYG103: 'info',
48
+ SYG104: 'warn',
49
+ SYG105: 'info',
50
+ SYG106: 'warn',
51
+ SYG110: 'warn',
52
+ SYG111: 'warn',
53
+ SYG201: 'warn',
54
+ SYG202: 'warn',
55
+ SYG301: 'error',
56
+ SYG401: 'warn',
57
+ SYG900: 'warn',
58
+ // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
59
+ SYG206: 'error',
60
+ SYG207: 'warn',
61
+ SYG208: 'warn',
62
+ SYG209: 'error',
63
+ SYG210: 'warn',
64
+ SYG211: 'error',
65
+ SYG212: 'error',
66
+ SYG213: 'warn',
67
+ SYG214: 'error',
68
+ SYG215: 'error',
69
+ SYG216: 'error',
70
+ SYG217: 'warn',
71
+ SYG218: 'error',
72
+ SYG219: 'warn',
73
+ SYG220: 'warn',
74
+ SYG402: 'error',
75
+ SYG403: 'error',
76
+ SYG404: 'error',
77
+ SYG405: 'error',
78
+ SYG406: 'error',
79
+ SYG407: 'error',
80
+ SYG408: 'error',
81
+ SYG409: 'warn',
82
+ SYG410: 'error',
83
+ SYG411: 'error',
84
+ SYG412: 'error',
85
+ SYG413: 'error',
86
+ SYG414: 'error',
87
+ SYG415: 'error',
88
+ SYG416: 'error',
89
+ SYG417: 'warn',
90
+ SYG418: 'error',
91
+ SYG419: 'error',
92
+ SYG420: 'error',
93
+ SYG601: 'error',
94
+ SYG602: 'error',
95
+ SYG603: 'error',
96
+ SYG604: 'error',
97
+ SYG605: 'error',
98
+ SYG606: 'error',
99
+ SYG607: 'error',
100
+ SYG608: 'warn',
101
+ SYG609: 'warn',
102
+ SYG610: 'error',
103
+ SYG901: 'error',
104
+ SYG902: 'error',
105
+ SYG903: 'error',
106
+ // ---- end 1E retrofit codes ----
107
+ };
108
+ const CODE_TITLES = {
109
+ // SYG1xx wiring (1A)
110
+ SYG101: 'Intent action has no model entry',
111
+ SYG102: 'Model entry is unreachable',
112
+ SYG103: 'Intent selector matched no rendered element',
113
+ SYG104: 'Intent selector crosses an isolation boundary',
114
+ SYG105: 'EVENTS type selected but never emitted, or emitted but never selected',
115
+ SYG106: 'Parent prop is overwritten by a reserved view argument',
116
+ SYG110: 'Intent selector not present in the component view',
117
+ SYG111: 'Controlled input has no input listener',
118
+ SYG115: 'Unknown DOM event shorthand',
119
+ SYG116: 'EVENTS value has no string type',
120
+ SYG124: 'Component used as a control or selector',
121
+ SYG125: 'Control given .intent, .model or .initialState',
122
+ SYG126: 'Control rendered but never listened to',
123
+ SYG127: 'Behavior collision or unresolvable uses entry',
124
+ SYG128: 'Duplicate control key',
125
+ SYG129: "CHILD.select() of a component this one doesn't render",
126
+ SYG112: 'Reply action has no model entry',
127
+ SYG130: 'href() names no route or leaves out a param',
128
+ SYG131: 'Route params the pattern does not use',
129
+ SYG132: 'Declaration static that is never sent',
130
+ SYG133: 'SPA router inside a Vike app',
131
+ // PLAN-5 W-1: widgets (defineWidget; checks/widgets.ts, checks/elementCommands.ts; SYG141 static only)
132
+ SYG140: 'Widget dispatched an undeclared event',
133
+ SYG141: 'Listener for an event the widget does not declare',
134
+ SYG142: 'Widget command not declared',
135
+ SYG143: 'Widget tag used as a selector',
136
+ SYG144: 'Widget event the host element also fires natively',
137
+ // PLAN-5 B-1 (3-D): the sortable behavior (checks/sortable.ts; sortable.ts calls the bridge's sortable hook)
138
+ SYG145: 'Sortable item without its id attribute',
139
+ SYG146: 'Sortable item or handle selector matches nothing',
140
+ SYG147: 'Sortable list key is not an array in the host state',
141
+ // PLAN-5 3-F G-419: a Collection's viewTransitionName (checks/next.ts)
142
+ SYG148: 'Collection viewTransitionName is not a CSS identifier',
143
+ SYG149: 'Two items with the same view-transition name',
144
+ // PLAN-5 V-1: VirtualCollection (checks/virtual.ts)
145
+ SYG430: 'VirtualCollection has no bounded height',
146
+ SYG431: 'VirtualCollection items without ids',
147
+ SYG432: 'VirtualCollection item does not render one element',
148
+ SYG433: 'VirtualCollection scroll target not in the list',
149
+ SYG434: 'Invalid VirtualCollection estimateSize or overscan',
150
+ // PLAN-5 3-H (G-449): sortable over a list shown in another order (checks/sortable.ts)
151
+ SYG435: 'Sortable list shown filtered or in another order',
152
+ // SYG2xx state & reducers (1A)
153
+ SYG201: 'STATE reducer dropped keys from the previous state',
154
+ SYG202: 'STATE reducer returned undefined',
155
+ // SYG3xx streams & intent construction (1A)
156
+ SYG301: 'RxJS operator used on an xstream stream',
157
+ // SYG4xx collections / switchable / sub-components / context (1A)
158
+ SYG401: "Collection 'from' field is missing or not an array",
159
+ // SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
160
+ SYG501: "Removed in 6.0: view with positional arguments",
161
+ SYG502: 'Retired in 6.0: STATE reducer signals no change without ABORT',
162
+ SYG503: 'Side effect in a STATE reducer that returns ABORT',
163
+ SYG504: "Removed in 6.0: 'ACTION | SINK' model key",
164
+ SYG505: 'Non-canonical EVENTS emit',
165
+ SYG506: "Removed in 6.0: CHILD.select() with a component name",
166
+ SYG507: 'Prop drilled through more than 2 component levels',
167
+ SYG508: 'select()/errors() round trip where reply actions would do',
168
+ // SYG9xx internal (0B)
169
+ SYG900: 'A diagnostics check threw',
170
+ // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
171
+ SYG206: 'Invalid calculated field definition',
172
+ SYG207: 'Calculated field shadows an initialState key',
173
+ SYG208: 'Calculated field depends on an unknown key',
174
+ SYG209: 'Circular calculated field dependency',
175
+ SYG210: 'INITIALIZE used with a sink other than STATE',
176
+ SYG211: "Retired in 6.0: Invalid model shorthand entry",
177
+ SYG212: 'Model entry is not a function or an object of sinks',
178
+ SYG213: "Retired in 6.0: Duplicate model entry for the same action and sink",
179
+ SYG214: 'EFFECT handler threw',
180
+ SYG215: 'next() delay is not a number',
181
+ SYG216: 'Reducer threw',
182
+ SYG217: 'Driver-sink reducer returned undefined',
183
+ SYG218: 'Reducer returned an unsupported type',
184
+ SYG219: 'EFFECT handler returned a value',
185
+ SYG220: 'Calculated field threw',
186
+ SYG221: 'set() called with a string',
187
+ SYG222: 'STATE reducer returned the same object after it was mutated in place',
188
+ SYG223: 'persist pick or omit names a key that is not in initialState',
189
+ SYG224: 'persist on a component that is not the root',
190
+ SYG226: 'Undo track, resetOn or coalesce names an unknown action',
191
+ // PLAN-5 F-1: the form behavior (checks/forms.ts)
192
+ SYG230: 'Form field name not in values',
193
+ SYG231: 'form() schema is not a Standard Schema',
194
+ SYG232: 'Form submit dropped while one is in progress',
195
+ SYG233: 'Form value missing from the schema output',
196
+ SYG234: 'Form submit action has no model entry',
197
+ SYG235: 'Form check names an unknown field or sets reply fields',
198
+ SYG236: 'Form array rows without an id',
199
+ SYG237: 'Two forms in one component listen on the same form selector',
200
+ // PLAN-5 2-R (G-393; checks/state.ts; dev entry)
201
+ SYG238: 'Root component with a model but no initial state',
202
+ SYG402: 'Context is not an object',
203
+ SYG403: 'Invalid context entry',
204
+ SYG404: 'Context stream errored',
205
+ SYG405: 'Sub-component has initialState without isolatedState',
206
+ SYG406: 'View threw',
207
+ SYG407: 'onError handler threw',
208
+ SYG408: 'Sub-component failed to instantiate',
209
+ SYG409: 'Child tried to update a calculated field',
210
+ SYG410: "Invalid sub-component 'state' prop",
211
+ SYG411: "Collection 'of' is missing or not a component",
212
+ SYG412: "Invalid Collection 'from' prop",
213
+ SYG413: "Retired in 6.0: Unnamed component factory not found",
214
+ SYG414: "Retired in 6.0: Component not found",
215
+ SYG415: "Switchable 'of' is missing or invalid",
216
+ SYG416: "Switchable 'current' is missing or invalid",
217
+ SYG417: 'Portal target not found',
218
+ SYG418: 'Invalid Collection sort prop',
219
+ SYG419: "Retired in 6.0: Invalid switchable() 'name$' parameter",
220
+ SYG420: 'JSX tag is undefined',
221
+ SYG421: 'Invalid data (dataset) key',
222
+ SYG422: 'Invalid timer spec',
223
+ // PLAN-4.6 R4: the core's dev checks (checks/next.ts; dev entry)
224
+ SYG423: 'Context change skipped a view whose output it changes',
225
+ SYG424: 'Duplicate Collection item id',
226
+ SYG425: 'isolatedState slice lacks initialState keys',
227
+ SYG601: "Retired in 6.0: Missing or invalid sources",
228
+ SYG602: 'Intent is not a function',
229
+ SYG603: 'Intent returned an invalid value',
230
+ SYG604: "Retired in 6.0: Invalid hmrActions",
231
+ SYG605: "Retired in 6.0: Intent action name contains '|'",
232
+ SYG606: 'calculated is not an object',
233
+ SYG607: "Retired in 6.0: State source not found",
234
+ SYG608: 'Strict mode requested without the diagnostics entry',
235
+ SYG609: 'Sink or source has no driver',
236
+ SYG610: "Request has a 'then' or 'catch' key",
237
+ SYG611: 'Socket message not sent or connection not opened',
238
+ // PLAN-4.6 R4 (D173): a form 6.0 removed, met at runtime (checks/next.ts; dev entry)
239
+ SYG612: 'Removed in 6.0',
240
+ SYG620: 'Router command not performed',
241
+ // PLAN-3 5-3: makeFetchDriver's cache, invalidation and validation (dev entry; SYG634 static only)
242
+ SYG630: 'Cached request is not idempotent',
243
+ SYG631: 'validate is not a Standard Schema',
244
+ SYG632: 'invalidate matched nothing',
245
+ SYG633: 'abort names a lane the requests do not use',
246
+ SYG634: 'latest: true with a computed key',
247
+ // PLAN-3 5-5 (D88)
248
+ SYG635: 'Caching asked for without a queryCache',
249
+ // PLAN-4 GS-2: element commands (reported by the dev entry, checks/elementCommands.ts)
250
+ SYG640: 'Element command target not found',
251
+ SYG641: 'Unknown element command',
252
+ // PLAN-4 GS-5: persist() (src/extra/persist.ts reports SYG642 with an explicit severity, like SYG611)
253
+ SYG642: 'Persisted state could not be restored or saved',
254
+ // PLAN-4 GS-7: timers (checks/timers.ts; dev entry)
255
+ SYG643: 'Declaration static with no driver to take it',
256
+ // PLAN-4 D131: sygnal/element (reported through the core bridge when diagnostics are on)
257
+ SYG644: 'Custom element prop hides an HTMLElement member',
258
+ // PLAN-4 GS-12 (D129): View Transitions (checks/viewTransitions.ts; dev entry)
259
+ SYG645: "viewTransitions that can't run",
260
+ // PLAN-5 W-1: a widget's mount / update / unmount threw (checks/widgets.ts; dev entry)
261
+ SYG660: 'Widget mount threw',
262
+ SYG661: 'Widget update threw or reported an error',
263
+ SYG662: 'Widget unmount threw',
264
+ // PLAN-5 B-3: browser sources (makeBrowserDriver; checks/browserSources.ts; dev entry)
265
+ SYG663: 'Invalid browser-source spec or command',
266
+ SYG664: 'Browser source not in the driver',
267
+ SYG665: 'Browser source failed with no error action',
268
+ // PLAN-5 W-2 (D203): the adapter entries (sygnal/react, sygnal/zag, sygnal/ui/menu|select|combobox): SYG666 from
269
+ // sygnal/vite (an entry imported without its optional peer dependencies), SYG667 thrown by
270
+ // fromZag / fromReact
271
+ SYG666: 'Adapter peer dependency missing',
272
+ SYG667: 'Invalid adapter argument',
273
+ // PLAN-5 2-R (G-383, G-387)
274
+ SYG668: 'Browser source has no element to observe',
275
+ // PLAN-5 2-T (G-410): Sygnal components / widget tags / special JSX inside a fromZag render
276
+ SYG669: 'Sygnal component inside a fromZag render',
277
+ // PLAN-4 GS-3: a11y lane (static only, sygnal-check)
278
+ SYG701: 'Click listener on a non-interactive element',
279
+ SYG702: 'Form field without an accessible label',
280
+ SYG703: 'Image without alt text',
281
+ SYG704: 'Link without href used as a button',
282
+ SYG705: 'Button without an accessible name',
283
+ SYG706: 'Positive tabIndex',
284
+ SYG707: 'Unknown ARIA attribute or invalid role',
285
+ SYG708: 'Label or ARIA reference to an id that is not rendered',
286
+ // PLAN-5 2-T (D211): Zag UI parts (sygnal-check rules/a11y/syg722-ui-name.js)
287
+ SYG722: 'Menu, Select or Combobox without an accessible name',
288
+ // PLAN-5 B-1 (3-D): static only (sygnal-check)
289
+ SYG724: 'Sortable handle not keyboard-accessible, or no live region',
290
+ SYG901: "Retired in 6.0: Sub-component sink stream errored",
291
+ SYG902: "Retired in 6.0: EFFECT stream errored",
292
+ SYG903: "Retired in 6.0: Component factory returned invalid sinks",
293
+ // ---- end 1E retrofit codes ----
294
+ };
295
+ // 2A: strict-mode (SYG5xx) default severities. Kept OUT of CODE_SEVERITY so the
296
+ // main bundle doesn't carry them (D29: zero bytes); the 'sygnal/diagnostics'
297
+ // entry registers them (registerCodes) and its reports pass the severity
298
+ // explicitly. sygnal-check keeps the same table in sygnal-check/src/codes.js.
299
+ const STRICT_CODE_SEVERITY = {
300
+ SYG501: 'error',
301
+ SYG502: 'warn',
302
+ SYG503: 'warn',
303
+ SYG504: 'error',
304
+ SYG505: 'warn',
305
+ SYG506: 'error',
306
+ SYG507: 'info',
307
+ SYG508: 'warn',
308
+ };
309
+ // PLAN-3 1-G (G-143): codes only the 'sygnal/diagnostics' dev entry reports. Like the strict
310
+ // table, kept OUT of CODE_SEVERITY (zero bytes in the main bundle); the entry registers them
311
+ // and passes the severity explicitly.
312
+ // PLAN-3 1-D: SYG112 (a reply action has no model entry) too.
313
+ // PLAN-3 2-A: SYG611 too, though makeSocketDriver reports it (legacy error(), severity passed
314
+ // explicitly): kept out of CODE_SEVERITY so the core bundle doesn't pay for an opt-in driver.
315
+ const DEV_CODE_SEVERITY = {
316
+ SYG112: 'error',
317
+ SYG115: 'warn',
318
+ SYG116: 'error',
319
+ // PLAN-4 1-D: controls, static (sygnal-check)
320
+ SYG124: 'error',
321
+ SYG125: 'error',
322
+ SYG126: 'info',
323
+ // PLAN-4 GS-1: behaviors (checks/behaviors.ts)
324
+ SYG127: 'error',
325
+ SYG128: 'error',
326
+ // PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
327
+ SYG129: 'warn',
328
+ SYG221: 'error',
329
+ // PLAN-4 GS-4 (state.ts)
330
+ SYG222: 'warn',
331
+ // PLAN-4 GS-5 (checks/persist.ts; sygnal-check statically)
332
+ SYG223: 'warn',
333
+ SYG224: 'error',
334
+ // PLAN-4 GS-8 (undo.ts reports it when diagnostics are on; sygnal-check statically)
335
+ SYG226: 'warn',
336
+ // PLAN-5 F-1: the form behavior (checks/forms.ts; form.ts calls the bridge's form hook)
337
+ SYG230: 'warn',
338
+ SYG231: 'error',
339
+ SYG232: 'info',
340
+ SYG233: 'warn',
341
+ SYG234: 'warn',
342
+ SYG235: 'warn',
343
+ SYG236: 'warn',
344
+ SYG237: 'warn',
345
+ SYG238: 'warn',
346
+ SYG421: 'error',
347
+ SYG611: 'error',
348
+ // PLAN-3 5-4b: router (SYG620 is reported by the router itself, like SYG611) and G-167
349
+ SYG130: 'error',
350
+ SYG131: 'warn',
351
+ SYG132: 'warn',
352
+ SYG133: 'warn',
353
+ SYG620: 'error',
354
+ // PLAN-3 5-3 (fetch.ts)
355
+ SYG630: 'warn',
356
+ SYG631: 'error',
357
+ SYG632: 'info',
358
+ SYG633: 'warn',
359
+ // static only (sygnal-check); listed so the entry knows its title
360
+ SYG634: 'info',
361
+ // PLAN-3 5-5 (fetch.ts)
362
+ SYG635: 'warn',
363
+ // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
364
+ SYG640: 'warn',
365
+ SYG641: 'error',
366
+ // PLAN-4 GS-5 (persist.ts, through legacy warn(): printed in production too)
367
+ SYG642: 'warn',
368
+ // PLAN-4 GS-7 (timers.ts): an invalid timer spec (not started); timers / connections /
369
+ // resources declared with no driver registered
370
+ SYG422: 'error',
371
+ SYG643: 'warn',
372
+ // PLAN-4.6 R4 (checks/next.ts): D168 context-tracking safety net, D169 duplicate ids, D174 kept
373
+ // slice, D173 removed forms
374
+ SYG423: 'warn',
375
+ SYG424: 'warn',
376
+ SYG425: 'warn',
377
+ SYG612: 'error',
378
+ // PLAN-4 D131 (element.ts, through the core bridge; a console warning in dev with diagnostics off)
379
+ SYG644: 'warn',
380
+ // PLAN-4 GS-12 (viewTransitions.ts): the static set, but the app's DOM driver isn't makeViewTransitionDOMDriver()
381
+ SYG645: 'warn',
382
+ // PLAN-5 W-1: widgets (checks/widgets.ts, checks/elementCommands.ts; SYG141 static only, listed
383
+ // so the entry knows its title)
384
+ SYG140: 'warn',
385
+ SYG141: 'warn',
386
+ SYG142: 'error',
387
+ SYG143: 'warn',
388
+ SYG144: 'info',
389
+ // PLAN-5 B-1 (3-D): the sortable behavior (checks/sortable.ts)
390
+ SYG145: 'warn',
391
+ SYG146: 'warn',
392
+ SYG147: 'warn',
393
+ // PLAN-5 3-F G-419 (checks/next.ts: onHostProps, onPatch)
394
+ SYG148: 'warn',
395
+ SYG149: 'warn',
396
+ SYG660: 'error',
397
+ SYG661: 'error',
398
+ SYG662: 'error',
399
+ SYG663: 'error',
400
+ SYG664: 'error',
401
+ SYG665: 'warn',
402
+ SYG666: 'error',
403
+ SYG667: 'error',
404
+ SYG668: 'warn',
405
+ SYG669: 'warn',
406
+ // PLAN-5 V-1: VirtualCollection (checks/virtual.ts, through the core bridge)
407
+ SYG430: 'warn',
408
+ SYG431: 'warn',
409
+ SYG432: 'warn',
410
+ SYG433: 'warn',
411
+ SYG434: 'warn',
412
+ // PLAN-5 3-H (G-449): sortable (checks/sortable.ts)
413
+ SYG435: 'warn',
414
+ // PLAN-4 GS-3: a11y lane, static only (sygnal-check; warn, also under --strict; error with --a11y=error, D144); listed so the entry knows its title
415
+ SYG701: 'warn',
416
+ SYG702: 'warn',
417
+ SYG703: 'warn',
418
+ SYG704: 'warn',
419
+ SYG705: 'warn',
420
+ SYG706: 'warn',
421
+ SYG707: 'warn',
422
+ SYG708: 'warn',
423
+ SYG722: 'warn',
424
+ // PLAN-5 B-1 (3-D): static only (sygnal-check)
425
+ SYG724: 'warn',
426
+ };
427
+ function getCodeInfo(code) {
428
+ const severity = CODE_SEVERITY[code];
429
+ return severity && { code, severity, title: CODE_TITLES[code] || '', docsSlug: code.toLowerCase() };
430
+ }
431
+ function listCodes() {
432
+ return Object.keys(CODE_SEVERITY).map(c => getCodeInfo(c));
433
+ }
434
+ function registerCodes(entries) {
435
+ for (const [code, severity, title] of entries) {
436
+ CODE_SEVERITY[code] = severity;
437
+ CODE_TITLES[code] = title;
438
+ }
439
+ }
440
+ function docsUrlFor(code) {
441
+ return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
442
+ }
443
+
43
444
  /**
44
445
  * Shared plumbing for the runtime checks (PLAN-1 workstream 1A).
45
446
  *
@@ -68,6 +469,12 @@ const reportSafely = (code, details) => {
68
469
  return undefined;
69
470
  }
70
471
  };
472
+ registerCodes(Object.keys(DEV_CODE_SEVERITY).map(code => [code, DEV_CODE_SEVERITY[code], CODE_TITLES[code]]));
473
+ /**
474
+ * reportSafely() for the dev-entry-only codes (DEV_CODE_SEVERITY, G-143): the main bundle's
475
+ * core doesn't know their severity, so it is passed explicitly.
476
+ */
477
+ const devReport = (code, details) => reportSafely(code, { severity: DEV_CODE_SEVERITY[code], ...details });
71
478
  /** Tunables (test seam: configureChecks()). */
72
479
  const timing = {
73
480
  /** delay after a render before DOM checks run (lets nested renders patch) */
@@ -77,13 +484,98 @@ const timing = {
77
484
  /** ... and only once the component has rendered at least this often */
78
485
  minRenders: 3,
79
486
  };
80
- /** Built-in actions Sygnal dispatches itself; never "missing" or "unreachable". */
81
- const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'HYDRATE', 'DISPOSE', 'READY']);
487
+ /**
488
+ * Built-in actions Sygnal dispatches itself; never "missing" or "unreachable".
489
+ * (HYDRATE is not one since 6.0, D66: nothing dispatches it, so it is an ordinary action.)
490
+ */
491
+ const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'DISPOSE', 'READY', 'RESOURCE']);
492
+ /** Sinks the core handles itself: their values never go to a reply-action driver. */
493
+ // (PLAN-4 GS-2: ELEMENT too; `{ scrollIntoView: Row, block: 'center' }` names no action)
494
+ const NON_REPLY_SINK = /^(STATE|EFFECT|EVENTS|PARENT|READY|DOM|CHILD|ELEMENT|PERSIST)$/;
495
+ // `ok: 'X'` / `"error": "X"` in function source (minified code keeps string literals and keys)
496
+ const keyedNames = (keys) => new RegExp(`(?:^|[{,\\s])["']?(?:${keys})["']?\\s*:\\s*(["'\`])([\\w$.:/-]+)\\1`, 'g');
497
+ // (PLAN-3 5-4b: and a router `{ block: 'ACTION' }`)
498
+ const REPLY_IN_SOURCE = keyedNames('ok|error|block');
499
+ const CONNECTION_IN_SOURCE = keyedNames('message|open|close|error');
500
+ // G-224: a `timers` static's `action: 'TICK'` / `frame: 'FRAME'`
501
+ const TIMER_IN_SOURCE = keyedNames('action|frame');
502
+ // PLAN-5 B-3: a `browser` static's `action: 'SEEN'` / `error: 'GEO_FAILED'`
503
+ const BROWSER_IN_SOURCE = keyedNames('action|error');
504
+ const namesIn = (fn, re, out) => {
505
+ if (typeof fn !== 'function')
506
+ return;
507
+ let src = '';
508
+ try {
509
+ src = Function.prototype.toString.call(fn);
510
+ }
511
+ catch (_) {
512
+ return;
513
+ }
514
+ re.lastIndex = 0;
515
+ for (let m = re.exec(src); m; m = re.exec(src))
516
+ out.add(m[2]);
517
+ };
518
+ /**
519
+ * Reply actions a component's requests and `connections` static can name, read from the
520
+ * source of its non-STATE sink functions (`HTTP: (s) => ({ url, ok: 'LOADED', error: 'FAILED' })`)
521
+ * and of `connections` (message/open/close/error). A heuristic for SYG102 (PLAN-3): a name
522
+ * built at run time is missed (SYG102 stays info, so that only costs a false hint).
523
+ */
524
+ function replyNamesOf(component) {
525
+ const out = new Set();
526
+ const model = component && component.model;
527
+ if (model && typeof model === 'object') {
528
+ for (const key of Object.keys(model)) {
529
+ const value = model[key];
530
+ const bar = key.indexOf('|');
531
+ if (bar >= 0) {
532
+ if (!NON_REPLY_SINK.test(key.slice(bar + 1).trim()))
533
+ namesIn(value, REPLY_IN_SOURCE, out);
534
+ }
535
+ else if (value && typeof value === 'object') {
536
+ for (const sink of Object.keys(value)) {
537
+ if (!NON_REPLY_SINK.test(sink))
538
+ namesIn(value[sink], REPLY_IN_SOURCE, out);
539
+ }
540
+ }
541
+ }
542
+ }
543
+ namesIn(component?.view?.connections, CONNECTION_IN_SOURCE, out);
544
+ // G-224: makeTimerDriver() replies the actions the `timers` static names (a function of the
545
+ // state, or an object of them)
546
+ const timers = component?.view?.timers;
547
+ if (timers && typeof timers === 'object')
548
+ for (const k in timers)
549
+ namesIn(timers[k], TIMER_IN_SOURCE, out);
550
+ else
551
+ namesIn(timers, TIMER_IN_SOURCE, out);
552
+ // PLAN-5 B-3: the browser driver replies the actions the `browser` static names
553
+ const browser = component?.view?.browser;
554
+ if (browser && typeof browser === 'object')
555
+ for (const k in browser)
556
+ namesIn(browser[k], BROWSER_IN_SOURCE, out);
557
+ else
558
+ namesIn(browser, BROWSER_IN_SOURCE, out);
559
+ // PLAN-3 5-4b: the router replies the action a `route` static names
560
+ const route = component?.view?.route;
561
+ if (typeof route === 'string')
562
+ out.add(route);
563
+ // PLAN-4 GS-5: persist() adds RESTORE (sent after hydration and by sync)
564
+ if (component?.view?.persist)
565
+ out.add('RESTORE');
566
+ return out;
567
+ }
568
+ /** Reply actions a component instance's requests were seen naming (replies.ts → inspect). */
569
+ const replySeen = new WeakMap();
82
570
  /** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
83
571
  const isInternalAction = (action) => typeof action !== 'string' || action.startsWith('__') || BUILTIN_ACTIONS.has(action);
84
572
  let seen = new Set();
85
573
  /** true the first time `key` is seen (dedupes reports, e.g. per component name). */
86
574
  const once = (key) => (seen.has(key) ? false : (seen.add(key), true));
575
+ /** undo once(key): the report it guarded wasn't made (G-470: diagnostics off) */
576
+ const unsee = (key) => void seen.delete(key);
577
+ /** whether once(`key`) has reported already (a check can skip its work: SYG435) */
578
+ const onced = (key) => seen.has(key);
87
579
  /**
88
580
  * Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
89
581
  * when an outermost instance starts, so findings are reported again in every test). Unlike
@@ -106,7 +598,7 @@ const nameOf = (component) => (component && component.name) || 'component';
106
598
  const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value) &&
107
599
  (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
108
600
  /** Levenshtein distance (small strings only). */
109
- function distance(a, b) {
601
+ function distance$1(a, b) {
110
602
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
111
603
  for (let i = 1; i <= a.length; i++) {
112
604
  let prev = row[0];
@@ -126,7 +618,7 @@ function suggest(name, candidates) {
126
618
  for (const c of candidates) {
127
619
  if (c === name)
128
620
  continue;
129
- const score = c.toLowerCase() === name.toLowerCase() ? 0 : distance(c, name);
621
+ const score = c.toLowerCase() === name.toLowerCase() ? 0 : distance$1(c, name);
130
622
  if (score < bestScore) {
131
623
  best = c;
132
624
  bestScore = score;
@@ -140,13 +632,64 @@ const didYouMean = (name, candidates) => {
140
632
  };
141
633
 
142
634
  const intentActions = new WeakMap();
635
+ // SYG609 (G-110, PLAN-2 E2): sinks/sources that no driver provides. Sinks the core handles
636
+ // itself never need one. renderComponent (mock DOM source) records such sinks and fakes such
637
+ // sources (t.requests / t.respond), so its components are skipped.
638
+ // PLAN-4 GS-2: ELEMENT (element commands) is built in; GS-5: PERSIST is persist()'s (an EFFECT)
639
+ const NO_DRIVER_NEEDED = /^(STATE|EFFECT|PARENT|READY|DOM|CHILD|ELEMENT|PERSIST)$/;
640
+ const DRIVER_NAME = /^[A-Z][A-Z0-9_]*$/;
641
+ // (`in`, not a read: the DOM source's shorthand Proxy returns a function for any property)
642
+ const underTest = (c) => {
643
+ const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
644
+ return !!dom && typeof dom == 'object' && '_hub' in dom;
645
+ };
646
+ const driverFix = (sink) => `Pass a driver named ${sink} to run(): run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver. ` +
647
+ `Check the spelling against the drivers you pass`;
143
648
  const wiringCheck = {
144
649
  id: 'wiring',
145
650
  onIntent(component, actionNames) {
146
651
  intentActions.set(component, actionNames || []);
147
652
  },
653
+ // SYG609: an intent that reads a source no driver provides (`HTTP.select(...)` with no HTTP
654
+ // driver) would crash with "Cannot read properties of undefined": say why first
655
+ sources(component, sources) {
656
+ if (underTest(component) || typeof Proxy != 'function')
657
+ return;
658
+ const name = nameOf(component);
659
+ return new Proxy(sources, {
660
+ get(t, k) {
661
+ if (typeof k == 'string' && !(k in t) && DRIVER_NAME.test(k) && once(`SYG609:${name}:source:${k}`)) {
662
+ report('SYG609', {
663
+ component,
664
+ message: `${name}.intent reads the ${k} source, but no driver named ${k} was passed to run(), so it is undefined`,
665
+ fix: driverFix(k),
666
+ data: { name: k, kind: 'source', drivers: Object.keys(t).filter(n => DRIVER_NAME.test(n)) },
667
+ });
668
+ }
669
+ return t[k];
670
+ },
671
+ });
672
+ },
148
673
  onModel(component, modelMap) {
149
674
  const name = nameOf(component);
675
+ // SYG609: a sink no driver receives is dropped silently by run()
676
+ if (!underTest(component)) {
677
+ const own = new Set(component?.sourceNames || []);
678
+ for (const action of Object.keys(modelMap || {})) {
679
+ for (const sink of modelMap[action] || []) {
680
+ if (own.has(sink) || NO_DRIVER_NEEDED.test(sink) || sink === component?.stateSourceName)
681
+ continue;
682
+ if (!once(`SYG609:${name}:sink:${sink}`))
683
+ continue;
684
+ report('SYG609', {
685
+ component,
686
+ message: `Model entry '${action}' sends to the ${sink} sink, but no driver named ${sink} was passed to run(), so every value sent there is dropped`,
687
+ fix: driverFix(sink),
688
+ data: { name: sink, kind: 'sink', action, drivers: [...own].filter(n => DRIVER_NAME.test(n)) },
689
+ });
690
+ }
691
+ }
692
+ }
150
693
  const intent$ = component && component.intent$;
151
694
  const injected = new Set((intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || []);
152
695
  const actions = (intentActions.get(component) || []).filter(a => !injected.has(a));
@@ -164,44 +707,113 @@ const wiringCheck = {
164
707
  data: { action, modelActions },
165
708
  });
166
709
  }
167
- // A single-stream intent (intent returns one action$ stream) has no
168
- // action names to compare against.
169
- if (intent$ && typeof intent$.addListener === 'function')
170
- return;
171
- const hmr = [].concat(component?.hmrActions || []);
710
+ const replies = replyNamesOf(component);
711
+ // D199: a behavior's own actions ('tip.SHOW') are its business: a timer, next() or a reply
712
+ // may trigger them (the behavior-owned map, behaviors.ts)
713
+ const owned = component?._behaviorActions || {};
172
714
  for (const action of modelActions) {
173
- if (isInternalAction(action) || actions.includes(action) || hmr.includes(action) || injected.has(action))
715
+ if (isInternalAction(action) || actions.includes(action) || injected.has(action) || replies.has(action) || action in owned)
174
716
  continue;
175
717
  if (!once(`SYG102:${name}:${action}`))
176
718
  continue;
177
719
  report('SYG102', {
178
720
  component,
179
721
  message: `Model entry '${action}' has no intent action with that name${didYouMean(action, actions)}`,
180
- fix: `Add '${action}' to ${name}.intent, or remove the model entry. If it is only dispatched with next('${action}'), ignore this`,
722
+ fix: `Add '${action}' to ${name}.intent, name it in a request (ok: '${action}'), or remove the model entry. If it is only dispatched with next('${action}'), ignore this`,
181
723
  data: { action, intentActions: actions },
182
724
  });
183
725
  }
184
726
  },
185
727
  };
186
728
 
187
- const SKIP = new Set(['INITIALIZE', 'HYDRATE']);
729
+ /*
730
+ * Tiny helpers shared by the core and the helper modules (behaviors, undo, the dev entry), kept
731
+ * out of the core so importing them never pulls it in.
732
+ */
733
+ /** The "no change / send nothing" sentinel (a registered symbol: the same in every copy) */
734
+ /**
735
+ * Whether a value is ABORT. Compares the description, not the identity, in case bundlers (e.g.
736
+ * Vite) create duplicate module instances with separate Symbol.for() registries. (G-214: the
737
+ * one copy, for the core, behaviors, undo and the action log)
738
+ */
739
+ const isAbort = (v) => typeof v == 'symbol' && v.description == 'sygnal.ABORT';
740
+ /**
741
+ * Set on a dev-only wrapper of a model function (SYG222's) to the function it wraps, so the
742
+ * action log and t.explain() show the user's reducer (G-214 follow-up)
743
+ */
744
+ const ORIGINAL = Symbol.for('sygnal.original');
745
+
746
+ const SKIP = new Set(['INITIALIZE']);
747
+ // SYG222: the instance's STATE reducers, wrapped (see the header)
748
+ const WRAPPED$1 = Symbol('SYG222');
749
+ function watchMutation(component, action, fn) {
750
+ if (typeof fn !== 'function' || fn[WRAPPED$1])
751
+ return fn;
752
+ const wrapped = (state, ...rest) => {
753
+ if (!isPlainObject(state))
754
+ return fn(state, ...rest);
755
+ // the state's top level, and (G-214) each behavior slice's: a behavior reducer that mutates
756
+ // its slice gets the host state back unchanged (behaviors.ts), so state[key] looks the same
757
+ const snaps = [[state, ''], ...(component._uses || []).map(([k]) => [state[k], k + '.'])]
758
+ .filter(([o]) => isPlainObject(o)).map(([o, p]) => [o, p, Object.keys(o), Object.values(o)]);
759
+ const out = fn(state, ...rest);
760
+ if (out === state) {
761
+ const changed = snaps.flatMap(([o, p, keys, values]) => keys.filter((k, i) => !(k in o) || o[k] !== values[i])
762
+ .concat(Object.keys(o).filter(k => !keys.includes(k))).map((k) => p + k));
763
+ const name = nameOf(component);
764
+ if (changed.length && once(`SYG222:${name}:${action}`)) {
765
+ const list = changed.map(k => `'${k}'`).join(', ');
766
+ devReport('SYG222', {
767
+ component,
768
+ message: `The STATE reducer for '${action}' changed ${list} in place and returned the same object. Returning the object a reducer got means "no change" (as ABORT), so the change is ignored and nothing re-renders`,
769
+ fix: `Return a new object: ${changed[0].includes('.') ? `(slice, data) => ({ ...slice, ${changed[0].split('.')[1]}: … })` : `(state, data) => ({ ...state, ${changed[0]}: … })`}. To write updates as mutations, wrap the reducer in immer's produce()`,
770
+ data: { action, keys: changed },
771
+ });
772
+ }
773
+ }
774
+ return out;
775
+ };
776
+ wrapped[WRAPPED$1] = true;
777
+ // checks that read reducer source (reply actions) see the original; so do the action log
778
+ // and t.explain() (ORIGINAL)
779
+ wrapped.toString = () => fn.toString();
780
+ wrapped[ORIGINAL] = fn;
781
+ return wrapped;
782
+ }
188
783
  const stateCheck = {
189
784
  id: 'state',
785
+ // SYG222: checks/next.ts wraps the STATE reducers (watchMutation) through wrapHandler
786
+ onModel(component) {
787
+ // SYG238 (G-393): the initial state is set at construction (D165), before onModel
788
+ const n = component?.__next;
789
+ if (!n?.isRoot || !n.def?.model || n.state !== undefined)
790
+ return;
791
+ const name = nameOf(component);
792
+ if (!once(`SYG238:${name}`))
793
+ return;
794
+ devReport('SYG238', {
795
+ component,
796
+ message: `${name} is a root with a model but no initialState: its state is undefined, so it renders nothing until an action sets one`,
797
+ fix: `Add ${name}.initialState = { … } (the root's start state; a child gets its state from its parent instead)`,
798
+ data: {},
799
+ });
800
+ },
190
801
  onReducer(component, action, prevState, nextState) {
191
802
  if (typeof action !== 'string' || action.startsWith('__') || SKIP.has(action))
192
803
  return;
193
804
  const name = nameOf(component);
194
805
  if (nextState === undefined) {
195
- if (!once(`SYG202:${name}:${action}`))
806
+ // G-357: returning undefined from a Collection item's reducer is the documented way to
807
+ // remove the item: not reported. In another sub-component it is info, in the root (where
808
+ // it wipes the whole app state) a warning.
809
+ if (component?.__next?.kind === 'item' || !once(`SYG202:${name}:${action}`))
196
810
  return;
197
- // Returning undefined from a Collection item's reducer is the documented way to remove
198
- // the item, so only the root (where it wipes the whole app state) is a warning.
199
811
  const sub = !!component?.isSubComponent;
200
812
  report('SYG202', {
201
813
  component,
202
814
  severity: sub ? 'info' : 'warn',
203
815
  message: `The STATE reducer for '${action}' returned undefined` +
204
- (sub ? ' (this removes the item if the component is a Collection item)' : ''),
816
+ (sub ? ' (only a Collection item removes itself this way)' : ''),
205
817
  fix: `Return the new state, e.g. (state, data) => ({ ...state, ... }), or return ABORT to leave the state unchanged`,
206
818
  data: { action },
207
819
  });
@@ -209,6 +821,27 @@ const stateCheck = {
209
821
  }
210
822
  if (!isPlainObject(prevState) || !isPlainObject(nextState))
211
823
  return;
824
+ // SYG221 (G-143): set('city') spreads the string into the state: { ...state, 0: 'c', 1: 'i', ... }
825
+ const chars = [];
826
+ while (typeof nextState[chars.length] === 'string' && nextState[chars.length].length === 1 && !(chars.length in prevState)) {
827
+ chars.push(nextState[chars.length]);
828
+ }
829
+ if (chars.length > 0 && !(chars.length in nextState)) {
830
+ if (!once(`SYG221:${name}:${action}`))
831
+ return;
832
+ const field = chars.join('');
833
+ const update = /^[A-Za-z_$][\w$]*$/.test(field)
834
+ ? `set((state, ${field}) => ({ ${field} }))`
835
+ : `set((state, value) => ({ ['${field}']: value }))`;
836
+ devReport('SYG221', {
837
+ component,
838
+ message: `The STATE reducer for '${action}' added the keys ${chars.map((_, i) => `'${i}'`).slice(0, 4).join(', ')}${chars.length > 4 ? ', …' : ''} ` +
839
+ `(one per character of '${field}'): set() was called with a string, set('${field}'). set() takes an object or a function`,
840
+ fix: `To store the action data in '${field}', pass a function: ${update}. For a fixed value pass an object: set({ ${field}: … })`,
841
+ data: { action, field },
842
+ });
843
+ return;
844
+ }
212
845
  const calculated = component?._calculatedFieldNames || null;
213
846
  const dropped = Object.keys(prevState).filter(key => !(key in nextState) && prevState[key] !== undefined && !(calculated && calculated.has(key)));
214
847
  if (dropped.length === 0 || !once(`SYG201:${name}:${action}`))
@@ -223,10 +856,10 @@ const stateCheck = {
223
856
  },
224
857
  };
225
858
 
226
- let selected = new Set();
859
+ let selected$1 = new Set();
227
860
  let emitted = new Set();
228
861
  let selectsAll = false;
229
- onReset(() => { selected = new Set(); emitted = new Set(); selectsAll = false; });
862
+ onReset(() => { selected$1 = new Set(); emitted = new Set(); selectsAll = false; });
230
863
  const eventsCheck = {
231
864
  id: 'events',
232
865
  onBusSelect(type) {
@@ -236,17 +869,29 @@ const eventsCheck = {
236
869
  }
237
870
  for (const t of [].concat(type))
238
871
  if (typeof t === 'string')
239
- selected.add(t);
872
+ selected$1.add(t);
240
873
  },
241
874
  onBusEmit(type, emitterName) {
242
- if (typeof type !== 'string')
875
+ if (typeof type !== 'string') {
876
+ // SYG116 (G-143): no EVENTS.select() can ever receive it. The core copies each emitted
877
+ // value into a new object (devtools stamp), so a function arrives here as {}.
878
+ if (once(`SYG116:${emitterName}`)) {
879
+ devReport('SYG116', {
880
+ component: emitterName,
881
+ message: `${emitterName ? emitterName + "'s model" : 'A model'} sent a value with no string 'type' to the EVENTS sink, so nothing receives it. ` +
882
+ `The value was probably a function (a model entry that returns event(...) instead of being event(...)), or undefined/null`,
883
+ fix: `Make event() the sink entry itself: ACTION: { EVENTS: event('TYPE', (state, data) => payload) }; return ABORT to send nothing`,
884
+ data: { type, emitter: emitterName },
885
+ });
886
+ }
243
887
  return;
888
+ }
244
889
  emitted.add(type);
245
- if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`))
890
+ if (selectsAll || selected$1.has(type) || !once(`SYG105:emit:${type}`))
246
891
  return;
247
892
  report('SYG105', {
248
893
  component: emitterName,
249
- message: `EVENTS event '${type}' was emitted, but nothing selects it, so it was dropped${didYouMean(type, selected)}`,
894
+ message: `EVENTS event '${type}' was emitted, but nothing selects it, so it was dropped${didYouMean(type, selected$1)}`,
250
895
  fix: `Subscribe with EVENTS.select('${type}') in the component that should react, or check the event name`,
251
896
  data: { type, direction: 'emitted-not-selected', emitter: emitterName },
252
897
  });
@@ -258,8 +903,8 @@ const eventsCheck = {
258
903
  * tests, at a point where the session is "done".
259
904
  */
260
905
  function checkEventBus() {
261
- const selectedNeverEmitted = [...selected].filter(t => !emitted.has(t));
262
- const emittedNeverSelected = selectsAll ? [] : [...emitted].filter(t => !selected.has(t));
906
+ const selectedNeverEmitted = [...selected$1].filter(t => !emitted.has(t));
907
+ const emittedNeverSelected = selectsAll ? [] : [...emitted].filter(t => !selected$1.has(t));
263
908
  for (const type of selectedNeverEmitted) {
264
909
  if (!once(`SYG105:select:${type}`))
265
910
  continue;
@@ -269,10 +914,32 @@ function checkEventBus() {
269
914
  data: { type, direction: 'selected-not-emitted' },
270
915
  });
271
916
  }
272
- return { selected: [...selected], emitted: [...emitted], selectedNeverEmitted, emittedNeverSelected };
917
+ return { selected: [...selected$1], emitted: [...emitted], selectedNeverEmitted, emittedNeverSelected };
918
+ }
919
+
920
+ registerCodes(Object.keys(STRICT_CODE_SEVERITY).map(code => [code, STRICT_CODE_SEVERITY[code], CODE_TITLES[code]]));
921
+ /**
922
+ * Turn runtime strict checks on (true), off (false), or back to the default
923
+ * (undefined → globalThis.__SYGNAL_STRICT__ === true).
924
+ */
925
+ function configureStrict(on) {
926
+ const core = bridge();
927
+ if (core)
928
+ core.strict = on;
929
+ }
930
+ /** Whether runtime strict checks are on. */
931
+ function isStrictEnabled() {
932
+ const flag = bridge()?.strict;
933
+ return flag === undefined ? globalThis.__SYGNAL_STRICT__ === true : flag === true;
273
934
  }
935
+ /** The strict check's runtime half is SYG508 (replies.ts); this entry keeps the flag. */
936
+ const strictCheck = {
937
+ id: 'strict',
938
+ };
274
939
 
275
- const RESERVED = ['children', 'slots', 'context', 'peers'];
940
+ // PLAN-4 GS-9: uid (the view's uid(name?) function)
941
+ // (R5: 6.0 passes no `peers`, D164: a prop of that name reaches the view)
942
+ const RESERVED$1 = ['children', 'slots', 'context', 'uid'];
276
943
  const propsCheck = {
277
944
  id: 'props',
278
945
  onRender(component) {
@@ -280,12 +947,13 @@ const propsCheck = {
280
947
  if (!props || typeof props !== 'object')
281
948
  return;
282
949
  const name = nameOf(component);
283
- for (const key of RESERVED) {
950
+ const severity = isStrictEnabled() ? 'error' : 'warn';
951
+ for (const key of RESERVED$1) {
284
952
  if (!(key in props) || !once(`SYG106:${name}:${key}`))
285
953
  continue;
286
954
  report('SYG106', {
287
955
  component,
288
- severity: 'warn',
956
+ severity,
289
957
  message: `The prop '${key}' passed to ${name} is overwritten: '${key}' is a reserved view argument, so the child never sees the parent's value`,
290
958
  fix: `Rename the prop (for example '${key}Value'), in the parent and in ${name}`,
291
959
  data: { prop: key },
@@ -297,7 +965,7 @@ const propsCheck = {
297
965
  if (!isLens && value !== undefined && once(`SYG106:${name}:state`)) {
298
966
  report('SYG106', {
299
967
  component,
300
- severity: 'warn',
968
+ severity,
301
969
  message: `The prop 'state' passed to ${name} is not data: Sygnal reads it as the child's state lens (a state field name or a { get, set } object), and the view's 'state' argument is the child's own state`,
302
970
  fix: `To give ${name} a slice of the parent state use state="fieldName"; to pass data, use another prop name (for example 'item')`,
303
971
  data: { prop: 'state', valueType: typeof value },
@@ -307,7 +975,7 @@ const propsCheck = {
307
975
  },
308
976
  };
309
977
 
310
- const describe = (v) => v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
978
+ const describe$1 = (v) => v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
311
979
  function checkCollection(component, el) {
312
980
  const props = (el && el.data && el.data.props) || {};
313
981
  const from = props.from;
@@ -328,7 +996,7 @@ function checkCollection(component, el) {
328
996
  if (from !== undefined)
329
997
  return;
330
998
  if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
331
- problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe(state)}, not an array`;
999
+ problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array`;
332
1000
  }
333
1001
  if (!problem || !once(`SYG401:${name}:undefined`))
334
1002
  return;
@@ -394,8 +1062,122 @@ class ScopeChecker {
394
1062
  }
395
1063
  }
396
1064
 
1065
+ /** the key of a control, from its selector `[data-control="<Key>"]` */
1066
+ const controlKey = (control) => String(control).slice(15, -2);
1067
+ /** a control selector `[data-control="<Key>"]` as its key, else undefined */
1068
+ const keyOfSelector = (selector) => {
1069
+ const m = /^\[data-control="([^"]+)"\]$/.exec(selector);
1070
+ return m ? m[1] : undefined;
1071
+ };
1072
+ const isControl$1 = (x) => typeof x == 'function' && !!x.__sygnalControl;
1073
+ const componentName = (fn) => fn.componentName || fn.label || fn.name || 'the component';
1074
+ let vnodeControls = new WeakMap();
1075
+ let staticsSeen = new WeakSet();
1076
+ onReset(() => {
1077
+ vnodeControls = new WeakMap();
1078
+ staticsSeen = new WeakSet();
1079
+ });
1080
+ /** the control that rendered a vnode (only while the dev entry is installed) */
1081
+ const controlOfVnode = (vnode) => vnode && typeof vnode == 'object' ? vnodeControls.get(vnode) : undefined;
1082
+ const STATICS$1 = ['intent', 'model', 'initialState'];
1083
+ const describe = (v) => v === undefined || v === null ? String(v)
1084
+ : Array.isArray(v) ? `an array of ${v.length}`
1085
+ : typeof v != 'object' ? `a ${typeof v} (${JSON.stringify(v)})`
1086
+ : typeof v.sel != 'string' ? (v.text !== undefined ? 'a text vnode' : 'a fragment')
1087
+ // the pragma's component placeholder (the component function in data.c)
1088
+ : v.data && typeof v.data.c == 'function' ? `the component <${v.sel}>`
1089
+ : 'an element vnode';
1090
+ function onControl(control, vnode) {
1091
+ const key = controlKey(control);
1092
+ if (vnode && typeof vnode == 'object')
1093
+ vnodeControls.set(vnode, control);
1094
+ if (!staticsSeen.has(control)) {
1095
+ staticsSeen.add(control);
1096
+ const given = STATICS$1.filter(s => control[s] != null);
1097
+ if (given.length) {
1098
+ devReport('SYG125', {
1099
+ message: `The control ${key} has ${given.map(s => '.' + s).join(', ')}, but controls are elements, not components: ${given.length > 1 ? 'these statics are' : 'it is'} never used`,
1100
+ fix: `Put the intent and model on the component that renders <${key}>, and listen to the control in its intent: DOM.click(${key}). For something with its own state and actions, write a component instead`,
1101
+ data: { control: key, statics: given },
1102
+ });
1103
+ }
1104
+ }
1105
+ const what = describe(vnode);
1106
+ if (what == 'an element vnode')
1107
+ return;
1108
+ const kind = control.kind;
1109
+ // report() (not devReport): in 'error' mode its DiagnosticError is thrown into the render
1110
+ const d = report('SYG125', {
1111
+ severity: DEV_CODE_SEVERITY.SYG125,
1112
+ message: `The control ${key}'s spec${kind ? ` (kind '${kind}')` : ''} vnode() returned ${what}, but it must return one element vnode`,
1113
+ fix: `Return a single element built with the h argument: vnode(props, children, h) { return h('div', props, ...children) }`,
1114
+ data: { control: key, kind, returned: what },
1115
+ });
1116
+ if (d)
1117
+ throw new Error(d.text);
1118
+ }
1119
+ /** Publish the control hook on the core bridge. Returns an uninstall function. */
1120
+ function installControlHooks() {
1121
+ const core = bridge();
1122
+ if (!core)
1123
+ return () => { };
1124
+ core.control = onControl;
1125
+ return () => { if (core.control === onControl)
1126
+ core.control = undefined; };
1127
+ }
1128
+ const wrapped$1 = new WeakMap();
1129
+ /** a DOM source whose select() and event shorthands report SYG124 for a component argument */
1130
+ function checkedDOM$1(component, dom) {
1131
+ if (!dom || typeof dom != 'object')
1132
+ return dom;
1133
+ let proxy = wrapped$1.get(dom);
1134
+ if (proxy)
1135
+ return proxy;
1136
+ proxy = new Proxy(dom, {
1137
+ get(t, k) {
1138
+ const v = t[k];
1139
+ if (typeof k != 'string' || typeof v != 'function' || (k != 'select' && k in t))
1140
+ return v;
1141
+ return function (arg, ...rest) {
1142
+ if (typeof arg == 'function' && !isControl$1(arg)) {
1143
+ const name = nameOf(component), child = componentName(arg);
1144
+ if (once(`SYG124:${name}:${child}:${k}`)) {
1145
+ const call = k == 'select' ? `DOM.select(${child})` : `DOM.${k}(${child})`;
1146
+ devReport('SYG124', {
1147
+ component,
1148
+ message: `${name}.intent passes the component ${child} to ${call}, but a component is not an element: it has no element of its own to listen on, and its elements are isolated from ${name}`,
1149
+ fix: `Handle the event in ${child} and send it up with PARENT, then read it here with CHILD.select(${child}); or render a control ${name} owns around it: const { ${child}Box } = controls({ ${child}Box: 'div' }), <${child}Box><${child} /></${child}Box>, DOM.click(${child}Box)`,
1150
+ data: { component: child, method: k },
1151
+ });
1152
+ }
1153
+ }
1154
+ const out = v.call(t, arg, ...rest);
1155
+ return k == 'select' ? checkedDOM$1(component, out) : out;
1156
+ };
1157
+ },
1158
+ });
1159
+ wrapped$1.set(dom, proxy);
1160
+ return proxy;
1161
+ }
1162
+ const controlsCheck = {
1163
+ id: 'controls',
1164
+ sources(component, sources) {
1165
+ if (!sources || typeof Proxy != 'function')
1166
+ return;
1167
+ const domName = component?.DOMSourceName || 'DOM';
1168
+ if (!sources[domName])
1169
+ return;
1170
+ return new Proxy(sources, {
1171
+ get(t, k) {
1172
+ const v = t[k];
1173
+ return k === domName ? checkedDOM$1(component, v) : v;
1174
+ },
1175
+ });
1176
+ },
1177
+ };
1178
+
397
1179
  const scopesOf = (ns) => ns.filter(s => s.type !== 'selector');
398
- const keyOf = (scopes) => scopes.map(s => `${s.type[0]}:${s.scope}`).join('/');
1180
+ const keyOf$1 = (scopes) => scopes.map(s => `${s.type[0]}:${s.scope}`).join('/');
399
1181
  let pending = [];
400
1182
  let tracked = new WeakMap();
401
1183
  /** isolation-scope key -> the component instance that owns it (latest wins) */
@@ -428,7 +1210,9 @@ function viewSource(component) {
428
1210
  return src;
429
1211
  }
430
1212
  /** class / id / tag tokens of a selector: '.a .b-c > #d' -> ['a', 'b-c', 'd'] */
431
- const tokensOf = (selector) => (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || []);
1213
+ const tokensOf = (selector) => (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [])
1214
+ // CT-1: a control's key (`<Add>` / `h(Add)` in the view source)
1215
+ .concat(Array.from(selector.matchAll(/\[data-control="([^"]+)"\]/g), m => m[1]));
432
1216
  function appearsInView(component, selector) {
433
1217
  const src = viewSource(component);
434
1218
  if (!src)
@@ -438,13 +1222,14 @@ function appearsInView(component, selector) {
438
1222
  return false;
439
1223
  return tokens.every(t => new RegExp(`(^|[^\\w-])${t.replace(/[-]/g, '\\-')}($|[^\\w-])`).test(src));
440
1224
  }
441
- function rootElementOf(t) {
1225
+ // G-144: a fragment-rooted component has several root elements
1226
+ function rootElementsOf(t) {
442
1227
  try {
443
1228
  const im = t.source._isolateModule;
444
- return im && im.getElement(scopesOf(t.source.namespace));
1229
+ return (im && im.getElements(scopesOf(t.source.namespace))) || [];
445
1230
  }
446
1231
  catch (_) {
447
- return undefined;
1232
+ return [];
448
1233
  }
449
1234
  }
450
1235
  function childNameFor(t, elements) {
@@ -463,7 +1248,7 @@ function childNameFor(t, elements) {
463
1248
  for (let i = depth; i < ns.length; i++) {
464
1249
  if (ns[i].type !== 'total')
465
1250
  continue;
466
- const owner = owners.get(keyOf(ns.slice(0, i + 1)));
1251
+ const owner = owners.get(keyOf$1(ns.slice(0, i + 1)));
467
1252
  if (owner)
468
1253
  return nameOf(owner);
469
1254
  break;
@@ -486,8 +1271,8 @@ function report103(component, t, selector, severity) {
486
1271
  function sweep(component, t, escalate) {
487
1272
  if (t.disposed)
488
1273
  return;
489
- const root = rootElementOf(t);
490
- if (!root || typeof root.querySelectorAll !== 'function')
1274
+ const roots = rootElementsOf(t).filter(r => typeof r.querySelectorAll === 'function');
1275
+ if (!roots.length)
491
1276
  return;
492
1277
  const im = t.source._isolateModule;
493
1278
  const checker = new ScopeChecker(t.source.namespace, im);
@@ -498,9 +1283,12 @@ function sweep(component, t, escalate) {
498
1283
  continue;
499
1284
  let all;
500
1285
  try {
501
- all = Array.prototype.slice.call(root.querySelectorAll(selector));
502
- if (root.matches(selector))
503
- all.push(root);
1286
+ all = [];
1287
+ for (const root of roots) {
1288
+ all.push(...Array.prototype.slice.call(root.querySelectorAll(selector)));
1289
+ if (root.matches(selector))
1290
+ all.push(root);
1291
+ }
504
1292
  }
505
1293
  catch (_) {
506
1294
  continue; // invalid selector: the DOM driver reports that itself
@@ -520,11 +1308,13 @@ function sweep(component, t, escalate) {
520
1308
  if (!once(`SYG104:${t.name}:${selector}`))
521
1309
  continue;
522
1310
  const child = childNameFor(t, all) || 'a child component';
1311
+ // CT-1: a control is named by its identifier
1312
+ const control = keyOfSelector(selector);
523
1313
  reportSafely('SYG104', {
524
1314
  component,
525
- message: `DOM.select('${selector}') in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
526
- fix: `Handle the event in ${child} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
527
- data: { selector, child },
1315
+ message: `${control ? `The control ${control}` : `DOM.select('${selector}')`} in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
1316
+ fix: `Handle the event in ${child}${control ? ` (DOM.<event>(${control}) in its intent)` : ''} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
1317
+ data: control ? { selector, child, control } : { selector, child },
528
1318
  });
529
1319
  continue;
530
1320
  }
@@ -561,7 +1351,7 @@ const domCheck = {
561
1351
  if (!Array.isArray(ns) || typeof selector !== 'string' || !selector)
562
1352
  return;
563
1353
  const prefix = ns.filter(s => s.type === 'selector').map(s => s.scope).join(' ');
564
- pending.push({ key: keyOf(scopesOf(ns)), selector: prefix ? `${prefix} ${selector}` : selector });
1354
+ pending.push({ key: keyOf$1(scopesOf(ns)), selector: prefix ? `${prefix} ${selector}` : selector });
565
1355
  if (pending.length > 1000)
566
1356
  pending.shift();
567
1357
  },
@@ -571,7 +1361,7 @@ const domCheck = {
571
1361
  const source = component && component.sources && component.sources[component.DOMSourceName || 'DOM'];
572
1362
  if (!source || !source._isolateModule || !Array.isArray(source.namespace))
573
1363
  return;
574
- const key = keyOf(scopesOf(source.namespace));
1364
+ const key = keyOf$1(scopesOf(source.namespace));
575
1365
  const name = nameOf(component);
576
1366
  owners.set(key, component);
577
1367
  ownerKeys.set(component, key);
@@ -607,254 +1397,1466 @@ const domCheck = {
607
1397
  },
608
1398
  };
609
1399
 
610
- const DOCS_BASE_URL = 'https://sygnal.js.org';
611
- const CODE_SEVERITY = {
612
- SYG101: 'warn',
613
- SYG102: 'info',
614
- SYG103: 'info',
615
- SYG104: 'warn',
616
- SYG105: 'info',
617
- SYG106: 'warn',
618
- SYG110: 'warn',
619
- SYG111: 'warn',
620
- SYG201: 'warn',
621
- SYG202: 'warn',
622
- SYG301: 'error',
623
- SYG401: 'warn',
624
- SYG900: 'warn',
625
- // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
626
- SYG206: 'error',
627
- SYG207: 'warn',
628
- SYG208: 'warn',
629
- SYG209: 'error',
630
- SYG210: 'warn',
631
- SYG211: 'error',
632
- SYG212: 'error',
633
- SYG213: 'warn',
634
- SYG214: 'error',
635
- SYG215: 'error',
636
- SYG216: 'error',
637
- SYG217: 'warn',
638
- SYG218: 'error',
639
- SYG219: 'warn',
640
- SYG220: 'warn',
641
- SYG402: 'error',
642
- SYG403: 'error',
643
- SYG404: 'error',
644
- SYG405: 'warn',
645
- SYG406: 'error',
646
- SYG407: 'error',
647
- SYG408: 'error',
648
- SYG409: 'warn',
649
- SYG410: 'error',
650
- SYG411: 'error',
651
- SYG412: 'error',
652
- SYG413: 'error',
653
- SYG414: 'error',
654
- SYG415: 'error',
655
- SYG416: 'error',
656
- SYG417: 'warn',
657
- SYG418: 'error',
658
- SYG419: 'error',
659
- SYG420: 'error',
660
- SYG601: 'error',
661
- SYG602: 'error',
662
- SYG603: 'error',
663
- SYG604: 'error',
664
- SYG605: 'error',
665
- SYG606: 'error',
666
- SYG607: 'error',
667
- SYG901: 'error',
668
- SYG902: 'error',
669
- SYG903: 'error',
670
- // ---- end 1E retrofit codes ----
1400
+ // Standard DOM / UI event names (lower case). In a browser, `on<name>` on an element or the
1401
+ // window also counts, so newer events are accepted there too.
1402
+ const KNOWN = new Set(('abort afterprint animationcancel animationend animationiteration animationstart auxclick ' +
1403
+ 'beforeinput beforeprint beforetoggle beforeunload blur cancel canplay canplaythrough change click close ' +
1404
+ 'compositionend compositionstart compositionupdate contextlost contextmenu contextrestored copy cuechange cut ' +
1405
+ 'dblclick drag dragend dragenter dragleave dragover dragstart drop durationchange emptied ended error ' +
1406
+ 'focus focusin focusout formdata fullscreenchange fullscreenerror gotpointercapture hashchange input invalid ' +
1407
+ 'keydown keypress keyup languagechange load loadeddata loadedmetadata loadstart lostpointercapture message ' +
1408
+ 'messageerror mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup offline online pagehide ' +
1409
+ 'pageshow paste pause play playing pointercancel pointerdown pointerenter pointerleave pointermove pointerout ' +
1410
+ 'pointerover pointerrawupdate pointerup popstate progress ratechange rejectionhandled reset resize scroll ' +
1411
+ 'scrollend search securitypolicyviolation seeked seeking select selectionchange selectstart slotchange stalled ' +
1412
+ 'storage submit suspend timeupdate toggle touchcancel touchend touchmove touchstart transitioncancel ' +
1413
+ 'transitionend transitionrun transitionstart unhandledrejection unload visibilitychange volumechange waiting wheel').split(' '));
1414
+ const isKnown = (name) => {
1415
+ // PLAN-5 0-S6: a hyphenated name is a custom element's event (DOM['wa-hover']('.food')): no
1416
+ // standard DOM event has a hyphen, and a bracketed name is no typo for one
1417
+ if (KNOWN.has(name) || name.includes('-'))
1418
+ return true;
1419
+ const on = 'on' + name;
1420
+ const g = globalThis;
1421
+ try {
1422
+ return !!((g.HTMLElement && on in g.HTMLElement.prototype) || (g.window && on in g.window) ||
1423
+ (g.Document && on in g.Document.prototype));
1424
+ }
1425
+ catch (_) {
1426
+ return false;
1427
+ }
671
1428
  };
672
- const CODE_TITLES = {
673
- // SYG1xx wiring (1A)
674
- SYG101: 'Intent action has no model entry',
675
- SYG102: 'Model entry is unreachable',
676
- SYG103: 'Intent selector matched no rendered element',
677
- SYG104: 'Intent selector crosses an isolation boundary',
678
- SYG105: 'EVENTS type selected but never emitted, or emitted but never selected',
679
- SYG106: 'Parent prop is overwritten by a reserved view argument',
680
- SYG110: 'Intent selector not present in the component view',
681
- SYG111: 'Controlled input has no input listener',
682
- // SYG2xx state & reducers (1A)
683
- SYG201: 'STATE reducer dropped keys from the previous state',
684
- SYG202: 'STATE reducer returned undefined',
685
- // SYG3xx streams & intent construction (1A)
686
- SYG301: 'RxJS operator used on an xstream stream',
687
- // SYG4xx collections / switchable / sub-components / context (1A)
688
- SYG401: "Collection 'from' field is missing or not an array",
689
- // SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
690
- SYG501: 'View uses positional arguments',
691
- SYG502: 'STATE reducer signals no change without ABORT',
692
- SYG503: 'Side effect in a STATE reducer that returns ABORT',
693
- SYG504: "'ACTION | SINK' shorthand model key",
694
- SYG505: 'Non-canonical EVENTS emit',
695
- SYG506: 'CHILD.select() with a string component name',
696
- SYG507: 'Prop drilled through more than 2 component levels',
697
- // SYG9xx internal (0B)
698
- SYG900: 'A diagnostics check threw',
699
- // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
700
- SYG206: 'Invalid calculated field definition',
701
- SYG207: 'Calculated field shadows an initialState key',
702
- SYG208: 'Calculated field depends on an unknown key',
703
- SYG209: 'Circular calculated field dependency',
704
- SYG210: 'INITIALIZE used with a sink other than STATE',
705
- SYG211: 'Invalid model shorthand entry',
706
- SYG212: 'Model entry is not a function or an object of sinks',
707
- SYG213: 'Duplicate model entry for the same action and sink',
708
- SYG214: 'EFFECT handler threw',
709
- SYG215: 'next() delay is not a number',
710
- SYG216: 'Reducer threw',
711
- SYG217: 'Driver-sink reducer returned undefined',
712
- SYG218: 'Reducer returned an unsupported type',
713
- SYG219: 'EFFECT handler returned a value',
714
- SYG220: 'Calculated field threw',
715
- SYG402: 'Context is not an object',
716
- SYG403: 'Invalid context entry',
717
- SYG404: 'Context stream errored',
718
- SYG405: 'Sub-component has initialState without isolatedState',
719
- SYG406: 'View threw',
720
- SYG407: 'onError handler threw',
721
- SYG408: 'Sub-component failed to instantiate',
722
- SYG409: 'Child tried to update a calculated field',
723
- SYG410: "Invalid sub-component 'state' prop",
724
- SYG411: "Collection 'of' is missing or not a component",
725
- SYG412: "Invalid Collection 'from' prop",
726
- SYG413: 'Unnamed component factory not found',
727
- SYG414: 'Component not found',
728
- SYG415: "Switchable 'of' is missing or invalid",
729
- SYG416: "Switchable 'current' is missing or invalid",
730
- SYG417: 'Portal target not found',
731
- SYG418: 'Invalid Collection sort prop',
732
- SYG419: "Invalid switchable() 'name$' parameter",
733
- SYG420: 'JSX tag is undefined',
734
- SYG601: 'Missing or invalid sources',
735
- SYG602: 'Intent is not a function',
736
- SYG603: 'Intent returned an invalid value',
737
- SYG604: 'Invalid hmrActions',
738
- SYG605: "Intent action name contains '|'",
739
- SYG606: 'calculated is not an object',
740
- SYG607: 'State source not found',
741
- SYG901: 'Sub-component sink stream errored',
742
- SYG902: 'EFFECT stream errored',
743
- SYG903: 'Component factory returned invalid sinks',
744
- // ---- end 1E retrofit codes ----
1429
+ // Names agents reach for that are enriched-stream methods or key names, not events
1430
+ const KEY_FIX = (sel, key) => `Listen for the real event and read the key: DOM.keydown(${sel}).key()` +
1431
+ (key ? `.filter(k => k === '${key}')` : '');
1432
+ const SPECIAL = {
1433
+ key: sel => KEY_FIX(sel),
1434
+ keys: sel => KEY_FIX(sel),
1435
+ enter: sel => KEY_FIX(sel, 'Enter'),
1436
+ escape: sel => KEY_FIX(sel, 'Escape'),
1437
+ esc: sel => KEY_FIX(sel, 'Escape'),
1438
+ value: sel => `Listen for the real event and read the value: DOM.input(${sel}).value()`,
1439
+ checked: sel => `Listen for the real event and read the box: DOM.change(${sel}).checked()`,
1440
+ data: sel => `Listen for the real event and read the attribute: DOM.click(${sel}).data('name')`,
1441
+ doubleclick: sel => `Use DOM.dblclick(${sel})`,
745
1442
  };
746
- // 2A: strict-mode (SYG5xx) default severities. Kept OUT of CODE_SEVERITY so the
747
- // main bundle doesn't carry them (D29: zero bytes); the 'sygnal/diagnostics'
748
- // entry registers them (registerCodes) and its reports pass the severity
749
- // explicitly. sygnal-check keeps the same table in sygnal-check/src/codes.js.
750
- const STRICT_CODE_SEVERITY = {
751
- SYG501: 'warn',
752
- SYG502: 'warn',
753
- SYG503: 'warn',
754
- SYG504: 'warn',
755
- SYG505: 'warn',
756
- SYG506: 'warn',
757
- SYG507: 'info',
1443
+ const fixFor = (name, selector) => {
1444
+ const sel = typeof selector === 'string' ? `'${selector}'` : 'sel';
1445
+ const special = SPECIAL[name.toLowerCase()];
1446
+ if (special)
1447
+ return special(sel);
1448
+ const s = suggest(name, KNOWN);
1449
+ return (s ? `Did you mean DOM.${s}(${sel})? ` : '') +
1450
+ `If '${name}' is a custom event you dispatch yourself, use the explicit form DOM.select(${sel}).events('${name}')`;
758
1451
  };
759
- function getCodeInfo(code) {
760
- const severity = CODE_SEVERITY[code];
761
- return severity && { code, severity, title: CODE_TITLES[code] || '', docsSlug: code.toLowerCase() };
1452
+ const wrapped = new WeakMap();
1453
+ function checkedDOM(component, dom) {
1454
+ if (!dom || typeof dom != 'object')
1455
+ return dom;
1456
+ let proxy = wrapped.get(dom);
1457
+ if (proxy)
1458
+ return proxy;
1459
+ const name = nameOf(component);
1460
+ proxy = new Proxy(dom, {
1461
+ get(t, k) {
1462
+ const v = t[k];
1463
+ if (typeof k != 'string' || k in t || typeof v != 'function' || isKnown(k))
1464
+ return v;
1465
+ return function (selector, ...rest) {
1466
+ if (once(`SYG115:${name}:${k}`)) {
1467
+ devReport('SYG115', {
1468
+ component,
1469
+ message: `${name}.intent uses DOM.${k}(${typeof selector == 'string' ? `'${selector}'` : '…'}), but '${k}' is not a DOM event, so it never fires`,
1470
+ fix: fixFor(k, selector),
1471
+ data: { event: k, selector },
1472
+ });
1473
+ }
1474
+ return v.call(this, selector, ...rest);
1475
+ };
1476
+ },
1477
+ });
1478
+ wrapped.set(dom, proxy);
1479
+ return proxy;
762
1480
  }
763
- function listCodes() {
764
- return Object.keys(CODE_SEVERITY).map(c => getCodeInfo(c));
1481
+ const shorthandCheck = {
1482
+ id: 'shorthand',
1483
+ sources(component, sources) {
1484
+ if (!sources || typeof Proxy != 'function')
1485
+ return;
1486
+ const domName = component?.DOMSourceName || 'DOM';
1487
+ if (!sources[domName])
1488
+ return;
1489
+ return new Proxy(sources, {
1490
+ get(t, k) {
1491
+ const v = t[k];
1492
+ return k === domName ? checkedDOM(component, v) : v;
1493
+ },
1494
+ });
1495
+ },
1496
+ };
1497
+
1498
+ /** The element commands Sygnal documents and types (the core runs any method of the element) */
1499
+ const NATIVE_COMMAND_NAMES = ['focus', 'blur', 'select', 'click', 'scrollIntoView', 'showModal', 'show', 'close', 'showPopover', 'hidePopover', 'togglePopover'];
1500
+ const core = () => globalThis.__SYGNAL_DIAGNOSTICS__;
1501
+ const send = (code, details) => {
1502
+ try {
1503
+ core()?.report(code, { severity: DEV_CODE_SEVERITY[code], ...details });
1504
+ }
1505
+ catch (err) {
1506
+ // 'error' mode: rethrown asynchronously (never into the caller)
1507
+ setTimeout(() => { throw err; });
1508
+ }
1509
+ };
1510
+ const isControl = (x) => typeof x == 'function' && !!x.__sygnalControl;
1511
+ const keyOf = (control) => String(control).slice(15, -2);
1512
+ /** a command as written: `{ focus: Email, preventScroll: true }` */
1513
+ const describeCommand = (cmd) => {
1514
+ if (!cmd || typeof cmd != 'object')
1515
+ return JSON.stringify(cmd) ?? String(cmd);
1516
+ const parts = Object.keys(cmd).map(k => {
1517
+ const v = cmd[k];
1518
+ let text;
1519
+ if (isControl(v))
1520
+ text = keyOf(v);
1521
+ else
1522
+ try {
1523
+ text = JSON.stringify(v) ?? String(v);
1524
+ }
1525
+ catch (_) {
1526
+ text = String(v);
1527
+ }
1528
+ return `${k}: ${text}`;
1529
+ });
1530
+ return `{ ${parts.join(', ')} }`;
1531
+ };
1532
+ const distance = (a, b) => {
1533
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
1534
+ for (let i = 1; i <= a.length; i++) {
1535
+ let prev = row[0];
1536
+ row[0] = i;
1537
+ for (let j = 1; j <= b.length; j++) {
1538
+ const cur = row[j];
1539
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] == b[j - 1] ? 0 : 1));
1540
+ prev = cur;
1541
+ }
1542
+ }
1543
+ return row[b.length];
1544
+ };
1545
+ const closest = (name, names) => {
1546
+ let best, d = 3;
1547
+ for (const n of names) {
1548
+ const x = n.toLowerCase() == name.toLowerCase() ? 0 : distance(name, n);
1549
+ if (x < d) {
1550
+ d = x;
1551
+ best = n;
1552
+ }
1553
+ }
1554
+ return best;
1555
+ };
1556
+ const tagOf = (el) => el && typeof el.tagName == 'string' ? `<${el.tagName.toLowerCase()}>` : 'element';
1557
+ const notACommand = (component, name, cmd) => send('SYG641', {
1558
+ component,
1559
+ message: `${name} sent ${describeCommand(cmd)} to the ELEMENT sink, which is not a command`,
1560
+ fix: `Send { <method>: control or selector, ...options }, e.g. { focus: Email }, or an array of them`,
1561
+ data: { command: cmd },
1562
+ });
1563
+ /** SYG640 (no `el`) or SYG641 (an element, but no command of that name) for `cmd` sent by `component` */
1564
+ function reportElementCommand(component, cmd, el) {
1565
+ // a disposed instance's pending commands are dropped silently
1566
+ if (component && component._disposed)
1567
+ return;
1568
+ const name = (component && component.name) || 'component';
1569
+ const method = cmd && typeof cmd == 'object' ? Object.keys(cmd)[0] : undefined;
1570
+ if (method === undefined)
1571
+ return notACommand(component, name, cmd);
1572
+ const target = cmd[method];
1573
+ const what = `ELEMENT ${describeCommand(cmd)}`;
1574
+ const control = isControl(target) ? keyOf(target) : undefined;
1575
+ const data = { method, command: cmd, ...(control ? { control } : { selector: target == null ? target : String(target) }) };
1576
+ if (!el) {
1577
+ const missing = target == null || target === '';
1578
+ return send('SYG640', {
1579
+ component,
1580
+ message: missing
1581
+ ? `${what} in ${name} has no target (${String(target)}), so the command was dropped`
1582
+ : `${what} in ${name} matched no element: nothing in ${name}'s own view matches ${control ? `the control ${control}` : `'${String(target)}'`} (waited 1 s), so the command was dropped`,
1583
+ fix: missing
1584
+ ? `Give the method a control or a selector: { ${method}: Email }`
1585
+ : `Render the element in ${name}'s view, or send the command from the component that renders it: a child component's elements are isolated from its parent, and a command reaches only its sender's own elements (in a Collection, each item sends its own)`,
1586
+ data,
1587
+ });
1588
+ }
1589
+ // PLAN-5 W-1: the matched element is a widget's host (selector or control target): SYG142 names
1590
+ // the widget's declared commands
1591
+ const widget = el.__sw?.w;
1592
+ if (widget) {
1593
+ const declared = Object.keys(widget.commands);
1594
+ const hint = closest(method, [...declared, ...NATIVE_COMMAND_NAMES]);
1595
+ return send('SYG142', {
1596
+ component,
1597
+ message: `${what} in ${name}: the matched ${tagOf(el)} is a widget's host, and the widget declares no '${method}' command` +
1598
+ (hint ? ` (did you mean '${hint}'?)` : '') + (declared.length ? `; it declares: ${declared.join(', ')}` : '; it declares none'),
1599
+ fix: `Add '${method}' to the widget's commands (defineWidget({ commands: { ${method}: (instance, options) => … } })), or send one it declares`,
1600
+ data: { ...data, element: tagOf(el), commands: declared },
1601
+ });
1602
+ }
1603
+ const commands = control && target.spec && typeof target.spec == 'object' && target.spec.commands
1604
+ ? Object.keys(target.spec.commands) : [];
1605
+ data.element = tagOf(el);
1606
+ if (commands.length)
1607
+ data.commands = commands;
1608
+ let message, fix;
1609
+ if (NATIVE_COMMAND_NAMES.includes(method)) {
1610
+ message = `${what} in ${name}: the matched ${tagOf(el)} has no ${method}() method${commands.length ? `, and the control ${control}'s spec declares no '${method}' command (it declares: ${commands.join(', ')})` : ''}`;
1611
+ fix = /^(show|showModal|close)$/.test(method)
1612
+ ? `${method}() works on a <dialog> element: render the target as a <dialog>`
1613
+ : /Popover$/.test(method)
1614
+ ? `The popover methods need an element with the popover attribute (attrs: { popover: 'auto' }) in a browser with the Popover API`
1615
+ : `Target an element that has ${method}()`;
1616
+ }
1617
+ else {
1618
+ const hint = closest(method, [...commands, ...NATIVE_COMMAND_NAMES]);
1619
+ message = `${what} in ${name}: '${method}' is not an element command` + (hint ? ` (did you mean '${hint}'?)` : '') +
1620
+ (el.tagName ? `: the matched ${tagOf(el)} has no such method` : '') +
1621
+ (commands.length ? `, and the control ${control}'s spec declares only: ${commands.join(', ')}` : '');
1622
+ fix = `Use ${commands.length ? `one of its commands (${commands.join(', ')}) or ` : ''}one of: ${NATIVE_COMMAND_NAMES.join(', ')} (or another method of the element, like play). The first key of a command is the method, the others are its options: { scrollIntoView: Row, block: 'nearest' }`;
1623
+ }
1624
+ send('SYG641', { component, message, fix, data });
765
1625
  }
766
- function registerCodes(entries) {
767
- for (const [code, severity, title] of entries) {
768
- CODE_SEVERITY[code] = severity;
769
- CODE_TITLES[code] = title;
1626
+ // methods that change the DOM snabbdom owns (the next patch undoes them or trips over them)
1627
+ const DOM_MUTATORS = /^(remove|append|prepend|before|after|replaceWith|replaceChildren|appendChild|removeChild|insertBefore|replaceChild|insertAdjacent(Element|HTML|Text)|(set|remove|toggle)Attribute(NS|Node)?|attachShadow|setHTML(Unsafe)?|normalize)$/;
1628
+ /** SYG641 when it is sent: not a command object, or a method that changes the DOM Sygnal renders */
1629
+ function checkSentCommand(component, cmd) {
1630
+ if (cmd == null)
1631
+ return;
1632
+ const name = (component && component.name) || 'component';
1633
+ if (typeof cmd != 'object' || Array.isArray(cmd) || !Object.keys(cmd).length)
1634
+ return notACommand(component, name, cmd);
1635
+ const method = Object.keys(cmd)[0], target = cmd[method];
1636
+ if (!DOM_MUTATORS.test(method) || (isControl(target) && target.spec?.commands?.[method]))
1637
+ return;
1638
+ send('SYG641', {
1639
+ component,
1640
+ message: `ELEMENT ${describeCommand(cmd)} in ${name}: ${method}() changes the DOM that ${name}'s view renders, so the next render undoes it or fails`,
1641
+ fix: `Change the state and render the result instead; element commands are for what a view can't express (focus, scrolling, dialogs, popovers, media)`,
1642
+ data: { method, command: cmd },
1643
+ });
1644
+ }
1645
+ /** The dev entry check: checkSentCommand runs on every ELEMENT value through the core's onElementCommand hook (checks/next.ts) */
1646
+ const elementCommandsCheck = {
1647
+ id: 'elementCommands',
1648
+ };
1649
+ /** Publish the element command hook on the core bridge. Returns an uninstall function. */
1650
+ function installElementCommandHooks() {
1651
+ const c = core();
1652
+ if (!c)
1653
+ return () => { };
1654
+ c.elementCommand = reportElementCommand;
1655
+ return () => { if (c.elementCommand === reportElementCommand)
1656
+ c.elementCommand = undefined; };
1657
+ }
1658
+
1659
+ /**
1660
+ * PLAN-5 W-1: widget diagnostics (dev entry; `defineWidget` in src/extra/widget.ts carries no text).
1661
+ *
1662
+ * The widget module calls `__SYGNAL_DIAGNOSTICS__.widget(code, widgetTag, owner, extra)`:
1663
+ * 140 dispatch(name) (mount's third parameter) — SYG140 (warn) when `name` is not in the widget's `events`: sygnal-check and
1664
+ * the docs read `events`, so an undeclared name is a typo or a missing declaration
1665
+ * 143 the widget tag used as a selector (DOM.select(DatePicker), `${DatePicker}`) — SYG143
1666
+ * (warn, once per widget): it matches nothing
1667
+ * 144 a host mounted — SYG144 (info, once per widget): a declared event name the host element
1668
+ * also fires natively (`'change'` on an `<input>` host), so the intent sees both; not
1669
+ * for the first-party Zag parts (`def.$own`: Menu's `select`, D211)
1670
+ * 660/661/662 mount / update / unmount threw (`extra` is the error) — SYG660–662 (error);
1671
+ * 661 with a fifth argument (G-580): the widget reported the error through mount's
1672
+ * error(e) (its own timer or callback), worded as such (data.phase 'error')
1673
+ * 669 fromZag (G-410): its render returned Sygnal components / widget tags / special JSX
1674
+ * (`extra`: their tags, e.g. ['<Badge>', '<Transition>']) — SYG669 (warn, once per widget)
1675
+ * It returns the reported diagnostic (the widget module logs a bare `[Sygnal SYG66x]` otherwise).
1676
+ *
1677
+ * SYG141 (listening to an event a widget doesn't declare) is static only (sygnal-check): the
1678
+ * runtime doesn't see which listener a selector serves. SYG142 for an undeclared command sent to a
1679
+ * widget's host is reported by the element command check (checks/elementCommands.ts).
1680
+ */
1681
+ /** a widget's name for messages: `name` from its definition, else its host tag */
1682
+ const widgetName = (w) => w?.def?.name ? `widget ${w.def.name}` : `widget <${w?.def?.tag || 'div'}>`;
1683
+ const ids = new WeakMap();
1684
+ let next = 0;
1685
+ const idOf = (w) => {
1686
+ let id = ids.get(w);
1687
+ if (id === undefined)
1688
+ ids.set(w, id = ++next);
1689
+ return id;
1690
+ };
1691
+ const PHASE = { 660: 'mount', 661: 'update', 662: 'unmount' };
1692
+ function reportWidget(code, w, owner, x, reported) {
1693
+ const name = widgetName(w), component = owner ? nameOf(owner) : name;
1694
+ if (code == 140) {
1695
+ if (w.events.includes(x) || !once(`SYG140:${idOf(w)}:${x}`))
1696
+ return;
1697
+ return devReport('SYG140', {
1698
+ component,
1699
+ message: `${name} dispatched '${x}', which is not one of its declared events (${w.events.length ? w.events.join(', ') : 'it declares none'})`,
1700
+ fix: `Add '${x}' to the widget's events (defineWidget({ events: [${[...w.events, x].map((e) => `'${e}'`).join(', ')}] })), or dispatch a declared name`,
1701
+ data: { event: x, events: w.events },
1702
+ });
1703
+ }
1704
+ if (code == 143) {
1705
+ if (!once(`SYG143:${idOf(w)}`))
1706
+ return;
1707
+ return devReport('SYG143', {
1708
+ component: name,
1709
+ message: `${name} was used as a selector (DOM.select(${w.def.name || 'Widget'}) or in a template string); a widget tag matches nothing`,
1710
+ fix: `Give the widget a className and select that (<${w.def.name || 'Widget'} className="due" />, DOM.select('.due')), or make it a control: controls({ Due: ${w.def.name || 'Widget'} })`,
1711
+ });
1712
+ }
1713
+ if (code == 144) {
1714
+ // D211: not for Sygnal's own parts (Menu's `select` is its documented event)
1715
+ if (!x || w.def.$own || !once(`SYG144:${idOf(w)}`))
1716
+ return;
1717
+ const native = w.events.filter((e) => 'on' + e in x);
1718
+ if (!native.length)
1719
+ return;
1720
+ return devReport('SYG144', {
1721
+ component: name,
1722
+ message: `${name} declares ${native.map((e) => `'${e}'`).join(', ')}, which its <${x.localName}> host also fires natively, so a listener gets both the widget's events and the element's`,
1723
+ fix: `Name the widget's event differently (e.g. 'pick' for a date picker's change), or read only what both carry`,
1724
+ data: { events: native, element: x.localName },
1725
+ });
1726
+ }
1727
+ if (code == 669) {
1728
+ // G-470: once per widget tag, counted only when reported (not with diagnostics off)
1729
+ const k = `SYG669:${idOf(w)}`;
1730
+ if (!once(k))
1731
+ return;
1732
+ const d = devReport('SYG669', {
1733
+ component: name,
1734
+ message: `${name}'s fromZag render returned ${x.join(', ')}; the adapter patches its render outside the component tree, so they don't run there (a component isn't instantiated, a widget doesn't mount, special JSX renders as an unknown element)`,
1735
+ fix: `Render plain elements in the fromZag render (with Zag's prop getters spread on them), pass data in through the widget's props, and put components, widgets and special JSX around the widget tag in the view`,
1736
+ data: { found: x },
1737
+ });
1738
+ if (!d)
1739
+ unsee(k);
1740
+ return d;
1741
+ }
1742
+ const phase = PHASE[code];
1743
+ if (!phase)
1744
+ return;
1745
+ const sev = DEV_CODE_SEVERITY[`SYG${code}`];
1746
+ // G-580: a 661 from error(e) is not a throwing update()
1747
+ const by = code == 661 && reported;
1748
+ const d = devReport(`SYG${code}`, {
1749
+ component,
1750
+ message: (by ? `${name} reported an error through error(e) (mount's fourth parameter)` : `${name}'s ${phase}() threw`) + `: ${x?.message ?? x}` +
1751
+ (code < 662 ? `; ${owner ? `${nameOf(owner)}'s onError fallback renders in its place` : 'it is not mounted'}` : ''),
1752
+ fix: code < 662
1753
+ ? `Fix ${by ? 'the failure the widget reports through error(e) (its own timer, callback or redraw)' : `the widget's ${phase}()`}, or give ${owner ? nameOf(owner) : 'the component'} an .onError for a custom fallback`
1754
+ : `Fix the widget's unmount()`,
1755
+ data: { phase: by ? 'error' : phase, error: x },
1756
+ severity: sev,
1757
+ });
1758
+ // the error with its stack (the text is printed by report() in 'warn' mode, collected otherwise)
1759
+ if (d)
1760
+ console.error(x);
1761
+ return d;
1762
+ }
1763
+ /** Publish the widget hook on the core bridge. Returns an uninstall function. */
1764
+ function installWidgetHooks() {
1765
+ const c = globalThis.__SYGNAL_DIAGNOSTICS__;
1766
+ if (!c)
1767
+ return () => { };
1768
+ c.widget = reportWidget;
1769
+ return () => { if (c.widget === reportWidget)
1770
+ c.widget = undefined; };
1771
+ }
1772
+
1773
+ /**
1774
+ * PLAN-5 V-1: `<VirtualCollection>` diagnostics (dev entry; src/extra/virtual.ts carries no text).
1775
+ *
1776
+ * The VirtualCollection host calls `__SYGNAL_DIAGNOSTICS__.virtual(code, owner, extra)`:
1777
+ * 430 the container has no bounded height: `extra.reason` 'height' (laid out 0 px tall) or
1778
+ * 'grows' (as tall as all its rows and taller than the viewport, so it would render every
1779
+ * row); the window is clamped to the viewport (once per host and reason)
1780
+ * 431 items without `id` (index keys; once per host)
1781
+ * 432 an item renders a fragment or text, not one element (once per host)
1782
+ * 433 scrollToIndex / scrollToId for an index or id not in the (filtered) list
1783
+ * 434 estimateSize / overscan not a positive number (or a function, for estimateSize)
1784
+ */
1785
+ const tag = (owner) => `<VirtualCollection> in ${nameOf(owner)}`;
1786
+ function reportVirtual(code, owner, x = {}) {
1787
+ const component = owner ? nameOf(owner) : 'VirtualCollection';
1788
+ if (code == 430) {
1789
+ const grows = x.reason == 'grows';
1790
+ return devReport('SYG430', {
1791
+ component,
1792
+ message: grows
1793
+ ? `${tag(owner)} grows with its rows (it is ${Math.round(x.height)} px tall, as tall as all of them), so it is not a scroll container; only the rows within the viewport's height render`
1794
+ : `${tag(owner)} is 0 px tall, so no row is in view`,
1795
+ fix: 'Give its className a bounded height (height or max-height, e.g. .rows { height: 400px }, or flex: 1 with min-height: 0 in a flex column); it scrolls itself (overflow-y: auto)',
1796
+ data: x,
1797
+ });
1798
+ }
1799
+ if (code == 431) {
1800
+ return devReport('SYG431', {
1801
+ component,
1802
+ message: `${tag(owner)} has items without an id (${x.count} items): they are keyed by their index, so a row's instance and measured height follow its position, not its item, when items are added, removed or reordered`,
1803
+ fix: "Give every item a unique id (e.g. rows: data.map((r, i) => ({ id: r.key ?? i, ...r })))",
1804
+ data: x,
1805
+ });
770
1806
  }
1807
+ if (code == 432) {
1808
+ return devReport('SYG432', {
1809
+ component,
1810
+ message: `${tag(owner)}: its item component ${x.component} renders a fragment or text, so its rows can't be measured, positioned or given aria-posinset`,
1811
+ fix: `Make ${x.component}'s view return one element (wrap its content in a <div> or <li>)`,
1812
+ data: x,
1813
+ });
1814
+ }
1815
+ if (code == 433) {
1816
+ const what = 'id' in x ? `id ${JSON.stringify(x.id) ?? String(x.id)}` : `index ${x.index}`;
1817
+ return devReport('SYG433', {
1818
+ component,
1819
+ message: `${tag(owner)}: ${'id' in x ? 'scrollToId' : 'scrollToIndex'} for ${what}, which is not in its list (${x.count} rows after filter); it didn't scroll`,
1820
+ fix: 'id' in x
1821
+ ? 'Send the id of an item in the array the filter keeps: { scrollToId: \'.rows\', id }'
1822
+ : `Send an integer index from 0 to ${Math.max(0, x.count - 1)} into the shown (filtered, sorted) rows: { scrollToIndex: '.rows', index }`,
1823
+ data: x,
1824
+ });
1825
+ }
1826
+ if (code == 434) {
1827
+ return devReport('SYG434', {
1828
+ component,
1829
+ message: `${tag(owner)}: ${x.prop}={${typeof x.value == 'string' ? JSON.stringify(x.value) : String(x.value)}} is not ${x.prop == 'overscan' ? 'a number ≥ 0; 5 is used' : 'a positive number or a function; 32 is used'}`,
1830
+ fix: x.prop == 'overscan' ? 'Pass overscan={5} (rows rendered beyond each edge)' : 'Pass estimateSize={32} (about a row\'s height in px), or (item, index) => px',
1831
+ data: x,
1832
+ });
1833
+ }
1834
+ }
1835
+ /** Publish the VirtualCollection hook on the core bridge. Returns an uninstall function. */
1836
+ function installVirtualHooks() {
1837
+ const c = globalThis.__SYGNAL_DIAGNOSTICS__;
1838
+ if (!c)
1839
+ return () => { };
1840
+ c.virtual = reportVirtual;
1841
+ return () => { if (c.virtual === reportVirtual)
1842
+ c.virtual = undefined; };
1843
+ }
1844
+
1845
+ const what$1 = (v) => v === null || v === undefined ? String(v)
1846
+ : typeof v == 'function' ? 'a function (a defineBehavior factory not called?)'
1847
+ : typeof v == 'object' ? 'a plain object'
1848
+ : `a ${typeof v}`;
1849
+ const behaviorsCheck = {
1850
+ id: 'behaviors',
1851
+ onModel(component) {
1852
+ const view = component?.view, uses = view?.uses;
1853
+ if (!uses || typeof uses != 'object' || !once('SYG127:' + nameOf(component)))
1854
+ return;
1855
+ const init = view.initialState;
1856
+ for (const key of Object.keys(uses)) {
1857
+ const b = uses[key];
1858
+ if (typeof b?.merge != 'function') {
1859
+ devReport('SYG127', {
1860
+ component,
1861
+ message: `uses entry '${key}' is ${what$1(b)}, not a defineBehavior() result; it was skipped, so state.${key} and its '${key}.*' actions don't exist`,
1862
+ fix: `Use a defineBehavior() factory's result: uses = { ${key}: behavior(options) }`,
1863
+ data: { key, reason: 'unresolvable' },
1864
+ });
1865
+ }
1866
+ else if (init && typeof init == 'object' && key in init) {
1867
+ devReport('SYG127', {
1868
+ component,
1869
+ message: `uses key '${key}' is also a key of ${nameOf(component)}'s initialState; the behavior's slice replaces that value`,
1870
+ fix: `Remove '${key}' from initialState (pass its values as the behavior's options), or rename the uses key`,
1871
+ data: { key, reason: 'initialState' },
1872
+ });
1873
+ }
1874
+ }
1875
+ },
1876
+ };
1877
+
1878
+ const isRow = (r) => r?.id != null;
1879
+ // the key of segment `s` in `v`: an array's row with that id, else the index unless the element
1880
+ // there is a row (G-380: a row is named by its id only; -1, no element), an object's key
1881
+ const key = (v, s) => {
1882
+ if (!Array.isArray(v))
1883
+ return s;
1884
+ const i = v.findIndex(r => isRow(r) && r.id + '' == s);
1885
+ return i < 0 && !isRow(v[+s]) ? +s : i;
1886
+ };
1887
+ /** whether `name` is a field of `values`: its path exists (G-379: also when the value is undefined) */
1888
+ const hasField = (values, name) => {
1889
+ let v = values;
1890
+ for (const s of name.split('.')) {
1891
+ const k = v && typeof v == 'object' ? key(v, s) : -1;
1892
+ if (!(k in Object(v)))
1893
+ return false;
1894
+ v = v[k];
1895
+ }
1896
+ return true;
1897
+ };
1898
+
1899
+ // the form's own action names (G-577: read from the form's model, so CHECKED_<field> counts)
1900
+ const ownActions = (b) => Object.keys(b?.model || {});
1901
+ const SUBMIT_FIX = (key, a, own) => `Name the submit after a host action the form doesn't define (not ${own.join(', ')}): submit: 'SAVE', and rename the model entry '${a}' to SAVE: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) }`;
1902
+ const RESERVED = ['ok', 'error', 'latest'];
1903
+ const what = (v) => v === null || v === undefined ? String(v)
1904
+ : typeof v == 'function' ? 'a function'
1905
+ : Array.isArray(v) ? 'an array'
1906
+ : typeof v == 'object' ? `an object without ~standard.validate${typeof v.parse == 'function' ? ' (a validator from before Standard Schema?)' : ''}`
1907
+ : `a ${typeof v}`;
1908
+ function reportForm(code, a, b, c) {
1909
+ if (code == 231) {
1910
+ const err = new TypeError(`[Sygnal SYG231] form(schema, options): the schema is ${what(a)}, not a Standard Schema. ` +
1911
+ `Pass a Standard Schema object: a zod, valibot or arktype schema, or an object with '~standard': { version: 1, vendor, validate }. ${docsUrlFor('SYG231')}`);
1912
+ err.code = 'SYG231';
1913
+ throw err;
1914
+ }
1915
+ if (code == 230) {
1916
+ if (!once(`SYG230:${a}`))
1917
+ return;
1918
+ devReport('SYG230', {
1919
+ component: 'form',
1920
+ message: `form: a field named '${a}' changed, but '${a}' isn't a field of the form's values (${Object.keys(b || {}).join(', ') || 'none'}); the change is ignored`,
1921
+ fix: `Name the input after a path in values (name="email", name="addresses.<row id>.city"), add '${a}' to values, or move the input out of the form element`,
1922
+ data: { name: a },
1923
+ });
1924
+ }
1925
+ else if (code == 232) {
1926
+ devReport('SYG232', {
1927
+ component: 'form',
1928
+ message: `form: a submit was dropped: ${a?.submitting ? 'the previous submit is still being sent (no form.DONE / form.ERRORS yet)' : 'a submit is already queued, waiting for an async check or an async schema'}`,
1929
+ fix: `Disable the submit button while state.form.submitting (or queued) is true; answer the submit with ok: 'form.DONE', error: 'form.ERRORS'`,
1930
+ });
1931
+ }
1932
+ else if (code == 233) {
1933
+ if (!b || typeof b != 'object' || Object.keys(b).some(k => !(k in (a || {}))))
1934
+ return;
1935
+ for (const k of Object.keys(a || {})) {
1936
+ if (!(k in b) && once(`SYG233:${k}`))
1937
+ devReport('SYG233', {
1938
+ component: 'form',
1939
+ message: `form: values.${k} is not in the schema's output (stripped or renamed), so the submit action doesn't get it under that name (a schema strips keys it doesn't declare)`,
1940
+ fix: `Declare '${k}' in the schema (or make the schema keep unknown keys), or remove it from values; if the schema renames it on purpose, ignore this`,
1941
+ data: { key: k },
1942
+ });
1943
+ }
1944
+ }
1945
+ else if (code == 234) {
1946
+ if (!once(`SYG234:run:${b}:${a}`))
1947
+ return;
1948
+ devReport('SYG234', {
1949
+ component: 'form',
1950
+ message: `form '${b}': a valid submit dispatched '${b}.${a}' (the form's own ${a} action), not the host's '${a}': submit: '${a}' names one of the form's own actions, which next() prefers`,
1951
+ fix: SUBMIT_FIX(b, a, ownActions(c)),
1952
+ data: { key: b, submit: a },
1953
+ });
1954
+ }
1955
+ else if (code == 235) {
1956
+ const set = RESERVED.filter(k => b && k in b);
1957
+ if (set.length && once(`SYG235:req:${a}`))
1958
+ devReport('SYG235', {
1959
+ component: 'form',
1960
+ message: `form: the check for '${a}' returns a request with ${set.map(k => `'${k}'`).join(', ')}; the form sets them itself (its reply action and latest: true), so they are replaced`,
1961
+ fix: `Return only the request: check: { ${a}: { request: (v) => ({ url, query }), error: (body) => message } }`,
1962
+ data: { field: a, keys: set },
1963
+ });
1964
+ }
1965
+ }
1966
+ /** row ids missing in `values`: the names of the arrays of objects that have a row without an id */
1967
+ const missingIds = (v, at = '', out = []) => {
1968
+ if (v && typeof v == 'object') {
1969
+ if (Array.isArray(v) && v.some(r => r && typeof r == 'object' && !Array.isArray(r) && r.id == null))
1970
+ out.push(at);
1971
+ for (const k in v)
1972
+ missingIds(v[k], at ? `${at}.${k}` : k, out);
1973
+ }
1974
+ return out;
1975
+ };
1976
+ const formsCheck = {
1977
+ id: 'forms',
1978
+ onModel(component) {
1979
+ const view = component?.view, uses = view?.uses;
1980
+ if (!uses || typeof uses != 'object')
1981
+ return;
1982
+ const name = nameOf(component), sels = {};
1983
+ for (const key of Object.keys(uses)) {
1984
+ const b = uses[key];
1985
+ if (!b?.form)
1986
+ continue;
1987
+ // G-374: each form listens on its selector inside the host, so two with one selector hear
1988
+ // each other's fields and submits
1989
+ const sel = b.options?.form || 'form';
1990
+ if (sel in sels) {
1991
+ if (once(`SYG237:${name}:${sel}`))
1992
+ devReport('SYG237', {
1993
+ component,
1994
+ message: `${name}'s forms '${sels[sel]}' and '${key}' both listen on '${sel}', so each hears the other's fields and submits`,
1995
+ fix: `Give each form element its own class and pass it: ${sels[sel]}: form(schema, { ..., form: '.${sels[sel]}' }), ${key}: form(schema, { ..., form: '.${key}' })`,
1996
+ data: { keys: [sels[sel], key], form: sel },
1997
+ });
1998
+ }
1999
+ else
2000
+ sels[sel] = key;
2001
+ if (!once(`SYG23x:${name}:${key}`))
2002
+ continue;
2003
+ const o = b.options || {}, values = o.values || {};
2004
+ const model = Object.keys(view.model || {}).map(a => a.split('|')[0].trim()), own = ownActions(b);
2005
+ const clash = own.includes(o.submit);
2006
+ if (typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
2007
+ devReport('SYG234', {
2008
+ component,
2009
+ message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action, so a valid submit dispatches nothing`
2010
+ : clash ? `${name}'s form '${key}' names submit: '${o.submit}', one of the form's own actions; a valid submit dispatches the form's '${key}.${o.submit}'${model.includes(o.submit) ? `, never ${name}'s '${o.submit}' entry` : ''}`
2011
+ : `${name}'s form '${key}' dispatches '${o.submit}' on a valid submit, but ${name} has no model entry '${o.submit}'`,
2012
+ fix: clash ? SUBMIT_FIX(key, o.submit, own)
2013
+ : `Name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
2014
+ data: { key, submit: o.submit },
2015
+ });
2016
+ }
2017
+ for (const f of Object.keys(o.check || {})) {
2018
+ if (!hasField(values, f))
2019
+ devReport('SYG235', {
2020
+ component,
2021
+ message: `${name}'s form '${key}' has a check for '${f}', which isn't a field of its values; it never runs`,
2022
+ fix: `Use a field name from values (${Object.keys(values).join(', ')})`,
2023
+ data: { key, field: f },
2024
+ });
2025
+ }
2026
+ for (const at of missingIds(values))
2027
+ devReport('SYG236', {
2028
+ component,
2029
+ message: `${name}'s form '${key}': values.${at} has rows without an id, so its fields are named by index and errors, touched state and focus don't follow a row when another is removed`,
2030
+ fix: `Give each row a unique id: ${at}: [{ id: 1, ... }] (form.ADD numbers new rows itself)`,
2031
+ data: { key, field: at },
2032
+ });
2033
+ }
2034
+ },
2035
+ };
2036
+ /** Install the `form` hook on the core bridge (form.ts calls it). */
2037
+ function installFormHooks() {
2038
+ const c = globalThis.__SYGNAL_DIAGNOSTICS__;
2039
+ if (!c)
2040
+ return () => { };
2041
+ c.form = reportForm;
2042
+ return () => { if (c.form === reportForm)
2043
+ c.form = undefined; };
2044
+ }
2045
+
2046
+ /**
2047
+ * PLAN-5 B-1 (3-D): `sortable` behavior diagnostics (dev entry; src/extra/sortable.ts carries no
2048
+ * text). The behavior calls `__SYGNAL_DIAGNOSTICS__.sortable(code, ...args)`:
2049
+ * 145 (item element, item selector, attr) a press or key on an item whose element has no
2050
+ * `attr`: SYG145 (warn, once per selector); the event is ignored
2051
+ * 146 (host root, item, handle, attr) the host's first pointerdown / keydown: SYG146 (warn,
2052
+ * once per selector) when elements carry `attr` under the root but none matches `item`,
2053
+ * or items exist but none contains `handle`. Returns true once it could decide (an empty
2054
+ * list can't tell: the check runs again at the next interaction)
2055
+ * 147 (from key, host state, uses key) when the host starts: a `from` key whose value isn't
2056
+ * an array: SYG147 (warn, once per key)
2057
+ * 435 (3-H G-449) (fn, uses key, from keys, idOf): the host's own items' ids as shown, per
2058
+ * container (fn() returns [the item elements, the root]; the behavior only asks in dev), or nothing when
2059
+ * every list has been reported (3-L G-478: no scan). A row's place is its `data-index`
2060
+ * when it has one (VirtualCollection's pinned focused row is out of DOM order). (shown
2061
+ * groups, host state, from keys, idField, uses key) at a keyboard pick-up or a pointer
2062
+ * press: SYG435 (warn, once per uses key and list) when a list's shown items (those in
2063
+ * the container holding most of them: an element elsewhere with a matching id is no list
2064
+ * item; a row's container is its nearest ancestor holding another row, 3-O G-505) aren't
2065
+ * one run of the array in its order (a Collection's sort, or a filter hiding entries
2066
+ * between shown ones)
2067
+ */
2068
+ const all = (root, sel) => {
2069
+ try {
2070
+ return [...root.querySelectorAll(sel)];
2071
+ }
2072
+ catch (_) {
2073
+ return [];
2074
+ }
2075
+ };
2076
+ function reportSortable(code, a, b, c, d, e) {
2077
+ if (code == 145) {
2078
+ if (once(`SYG145:${b}:${c}`))
2079
+ devReport('SYG145', {
2080
+ component: 'sortable',
2081
+ message: `sortable: an item ('${b}') has no ${c} attribute, so it can't be moved (the behavior finds each item in the list by that id)`,
2082
+ fix: `Render the item's id on its element: <li className="${String(b).replace(/^\./, '')}" ${c}={item.id}>, or name the attribute: sortable({ ..., attr: 'data-key' })`,
2083
+ data: { item: b, attr: c },
2084
+ });
2085
+ return;
2086
+ }
2087
+ if (code == 146) {
2088
+ const root = a, item = b, handle = c, attr = d;
2089
+ if (!root?.querySelectorAll)
2090
+ return true;
2091
+ const items = all(root, item).filter(el => el !== root);
2092
+ if (!items.length) {
2093
+ // nothing carries the id attribute either: an empty list (decide later)
2094
+ if (!all(root, `[${attr}]`).length)
2095
+ return false;
2096
+ report146('item', item, `no element under the host matches it, though elements with ${attr} are rendered`, `Use the selector of the items' elements (the element with ${attr}), e.g. item: '.task'`);
2097
+ return true;
2098
+ }
2099
+ if (handle && !items.some(el => all(el, handle).length)) {
2100
+ report146('handle', handle, `no item ('${item}') contains an element that matches it`, `Use the selector of the drag handle inside each item, e.g. handle: '.grip', or leave handle out to drag the whole item`);
2101
+ }
2102
+ return true;
2103
+ }
2104
+ if (code == 435) {
2105
+ // (fn, uses key, from keys, idOf) the shown ids per container: computed only here (dev)
2106
+ if (typeof a == 'function') {
2107
+ if ((c || []).every((l) => onced(`SYG435:${b}:${l}`)))
2108
+ return;
2109
+ try {
2110
+ const by = new Map(), [els, root] = a(), n = new Map();
2111
+ // a row's place: its data-index (a VirtualCollection row below the host's root; its
2112
+ // pinned row isn't in DOM order)
2113
+ const rows = els.map((el) => {
2114
+ const r0 = el.closest?.('[data-index]'), row = r0 && r0 !== root && root.contains?.(r0) ? r0 : el;
2115
+ return [el, row, Number(row === el && !r0 ? NaN : row.getAttribute('data-index'))];
2116
+ });
2117
+ // 3-O G-505: a row's container is its nearest ancestor holding another row (items each in
2118
+ // their own wrapper, <li><div data-id>, a table cell, a Collection item's root, are still
2119
+ // one list); `n` counts the rows below each ancestor
2120
+ const up = (row, f) => { for (let p = row.parentElement; p; p = p === root ? null : p.parentElement)
2121
+ if (f(p))
2122
+ return p; };
2123
+ for (const [, row] of rows)
2124
+ up(row, p => void n.set(p, (n.get(p) || 0) + 1));
2125
+ for (const [el, row, at] of rows) {
2126
+ const p = up(row, p => n.get(p) > 1) || row.parentElement;
2127
+ by.has(p) || by.set(p, []);
2128
+ by.get(p).push([isNaN(at) ? by.get(p).length : at, String(d(el))]);
2129
+ }
2130
+ return [...by.values()].map(g => g.sort((x, y) => x[0] - y[0]).map(x => x[1]));
2131
+ }
2132
+ catch (_) {
2133
+ return;
2134
+ }
2135
+ }
2136
+ // (shown groups, host state, from keys, idField, uses key) at a keyboard pick-up or a press
2137
+ const groups = Array.isArray(a?.[0]) ? a : [a || []];
2138
+ for (const l of c || []) {
2139
+ if (onced(`SYG435:${e}:${l}`))
2140
+ continue;
2141
+ const at = new Map();
2142
+ if (Array.isArray(b?.[l]))
2143
+ b[l].forEach((x, i) => at.set(String(x?.[d]), i));
2144
+ // the list's container: the one holding most of its entries (others: stray matches)
2145
+ let dom = [];
2146
+ for (const g of groups) {
2147
+ const ix = g.filter(i => at.has(i)).map(i => at.get(i));
2148
+ if (ix.length > dom.length)
2149
+ dom = ix;
2150
+ }
2151
+ // a contiguous run in the array's order is fine (all of it, or a window of it)
2152
+ if (!dom.some((v, j) => v !== dom[0] + j))
2153
+ continue;
2154
+ if (once(`SYG435:${e}:${l}`))
2155
+ devReport('SYG435', {
2156
+ component: 'sortable',
2157
+ message: `sortable '${e}': state.${l} is shown ${dom.length < at.size ? 'with entries hidden between the shown ones' : 'in another order'} (a Collection with ${dom.length < at.size ? 'filter' : 'sort'}?), but moves and the announced positions follow the array: a keyboard step can pass entries nobody sees, or change nothing visible`,
2158
+ fix: `Reorder a list that is shown as it is stored: drop the Collection's sort (sort state.${l} itself), and hide the drag handles while a filter hides entries`,
2159
+ data: { from: l, key: e },
2160
+ });
2161
+ }
2162
+ return;
2163
+ }
2164
+ if (code == 147) {
2165
+ const st = b || {}, arrays = Object.keys(st).filter(k => Array.isArray(st[k]));
2166
+ const near = suggest(a, arrays);
2167
+ if (once(`SYG147:${c}:${a}`))
2168
+ devReport('SYG147', {
2169
+ component: 'sortable',
2170
+ message: `sortable '${c}': from names '${a}', but state.${a} is ${st[a] === undefined ? 'missing' : 'not an array'} when the host starts${near ? ` (did you mean '${near}'?)` : ''}; nothing in it can be moved`,
2171
+ fix: `Name the host state's list: sortable({ from: '${near || arrays[0] || 'items'}', ... })${arrays.length ? ` (arrays in the state: ${arrays.join(', ')})` : `, and start it as an array (initialState: { ${a}: [] })`}`,
2172
+ data: { from: a, key: c },
2173
+ });
2174
+ }
2175
+ }
2176
+ const report146 = (option, selector, why, fix) => {
2177
+ if (once(`SYG146:${option}:${selector}`))
2178
+ devReport('SYG146', {
2179
+ component: 'sortable',
2180
+ message: `sortable: ${option} '${selector}' matches nothing: ${why}, so no item can be dragged`,
2181
+ fix,
2182
+ data: { option, selector },
2183
+ });
2184
+ };
2185
+ /** Install the `sortable` hook on the core bridge (sortable.ts calls it). */
2186
+ function installSortableHooks() {
2187
+ const c = globalThis.__SYGNAL_DIAGNOSTICS__;
2188
+ if (!c)
2189
+ return () => { };
2190
+ c.sortable = reportSortable;
2191
+ return () => { if (c.sortable === reportSortable)
2192
+ c.sortable = undefined; };
2193
+ }
2194
+
2195
+ const camel = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2196
+ const BAD_CHAR = /[\s"'<>\/=`]/;
2197
+ function problem(key) {
2198
+ if (/-[a-z]/.test(key))
2199
+ return `has a hyphen followed by a lower-case letter, which a dataset name can't have`;
2200
+ if (BAD_CHAR.test(key))
2201
+ return `contains a character an HTML attribute name can't have`;
2202
+ return undefined;
2203
+ }
2204
+ function walk(vnode, visit, depth = 0) {
2205
+ if (!vnode || typeof vnode != 'object' || depth > 500)
2206
+ return;
2207
+ const ds = vnode.data && vnode.data.dataset;
2208
+ if (ds && typeof ds == 'object')
2209
+ visit(ds, vnode.sel);
2210
+ const children = vnode.children;
2211
+ if (Array.isArray(children))
2212
+ for (const c of children)
2213
+ walk(c, visit, depth + 1);
2214
+ }
2215
+ const datasetCheck = {
2216
+ id: 'dataset',
2217
+ onRender(component, rootVnode) {
2218
+ walk(rootVnode, (dataset, sel) => {
2219
+ for (const key of Object.keys(dataset)) {
2220
+ const why = problem(key);
2221
+ if (!why || !once(`SYG421:${key}`))
2222
+ continue;
2223
+ const good = camel(key).replace(new RegExp(BAD_CHAR.source, 'g'), '');
2224
+ const attr = good.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
2225
+ const name = good || 'name';
2226
+ devReport('SYG421', {
2227
+ component,
2228
+ message: `The view of ${nameOf(component)} renders the data key '${key}'${sel ? ` on <${String(sel).split(/[.#]/)[0]}>` : ''} ` +
2229
+ `(from data={{ '${key}': … }}), but '${key}' ${why}. ` +
2230
+ `Writing it throws a DOMException, which can stop rendering`,
2231
+ fix: `Use a camelCase key in the data prop: data={{ ${name}: … }} renders the attribute data-${attr || 'name'}; read it with .data('${name}')`,
2232
+ data: { key, suggested: good, attribute: `data-${attr}` },
2233
+ });
2234
+ }
2235
+ });
2236
+ },
2237
+ };
2238
+
2239
+ const REPLY_KEYS = ['ok', 'error'];
2240
+ // PLAN-3 5-4b: on the router's sink, `{ route }` (the route static) and `{ block }` name reply actions
2241
+ const ROUTER_KEYS = ['route', 'block'];
2242
+ const CONNECTION_KEYS = ['message', 'open', 'close', 'error'];
2243
+ /** [action, key, connection name?] for every reply action name a sink value names */
2244
+ function replyNames(req, router) {
2245
+ const out = [];
2246
+ for (const key of router ? REPLY_KEYS.concat(ROUTER_KEYS) : REPLY_KEYS)
2247
+ if (typeof req[key] === 'string')
2248
+ out.push([req[key], key]);
2249
+ const conns = req.connections;
2250
+ if (conns && typeof conns === 'object') {
2251
+ for (const name of Object.keys(conns)) {
2252
+ const spec = conns[name];
2253
+ if (!spec || typeof spec !== 'object')
2254
+ continue;
2255
+ for (const key of CONNECTION_KEYS)
2256
+ if (typeof spec[key] === 'string')
2257
+ out.push([spec[key], key, name]);
2258
+ const events = spec.events;
2259
+ if (events && typeof events === 'object') {
2260
+ for (const ev of Object.keys(events))
2261
+ if (typeof events[ev] === 'string')
2262
+ out.push([events[ev], `events.${ev}`, name]);
2263
+ }
2264
+ }
2265
+ }
2266
+ return out;
2267
+ }
2268
+ /** instance → 'SINK\0category' → the select/errors call that read it */
2269
+ const selected = new WeakMap();
2270
+ function readingSource(component, sink, source) {
2271
+ const wrapped = Object.create(source);
2272
+ for (const method of ['select', 'errors']) {
2273
+ if (typeof source[method] !== 'function')
2274
+ continue;
2275
+ wrapped[method] = (category, ...rest) => {
2276
+ if (typeof category === 'string') {
2277
+ let reads = selected.get(component);
2278
+ if (!reads)
2279
+ selected.set(component, reads = new Map());
2280
+ reads.set(`${sink}\0${category}`, `${sink}.${method}('${category}')`);
2281
+ }
2282
+ return source[method](category, ...rest);
2283
+ };
2284
+ }
2285
+ return wrapped;
2286
+ }
2287
+ function checkRequest(component, sink, req, modelActions) {
2288
+ if (!req || typeof req !== 'object')
2289
+ return;
2290
+ const name = nameOf(component);
2291
+ let replies = false;
2292
+ for (const [action, key, connection] of replyNames(req, !!component?.sources?.[sink]?.__sygnalRouter)) {
2293
+ replies = true;
2294
+ let seen = replySeen.get(component);
2295
+ if (!seen)
2296
+ replySeen.set(component, seen = new Set());
2297
+ seen.add(action);
2298
+ if (modelActions.includes(action) || !once(`SYG112:${name}:${action}`))
2299
+ continue;
2300
+ const near = suggest(action, modelActions.filter(a => !a.startsWith('__')));
2301
+ devReport('SYG112', {
2302
+ component,
2303
+ message: (connection
2304
+ ? `${sink} connection '${connection}' names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so those events are dropped`
2305
+ : `A ${sink} request names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so the reply is dropped`) +
2306
+ (near ? ` (did you mean '${near}'?)` : ''),
2307
+ fix: near
2308
+ ? `Use the existing entry: ${key.startsWith('events.') ? `events: { '${key.slice(7)}': '${near}' }` : `${key}: '${near}'`}`
2309
+ : `Add '${action}' to ${name}.model, e.g. ${action}: (state, data) => ({ ...state, ... }), or fix the name`,
2310
+ data: { action, key, sink, ...(connection ? { connection } : {}), ...(near ? { suggestion: near } : {}), modelActions },
2311
+ });
2312
+ }
2313
+ // SYG508 (strict): a request without reply actions whose replies this instance reads back with select()/errors()
2314
+ if (replies || 'abort' in req || typeof req.category !== 'string' || !isStrictEnabled())
2315
+ return;
2316
+ const read = selected.get(component)?.get(`${sink}\0${req.category}`);
2317
+ if (!read || !once(`SYG508:${name}:${sink}:${req.category}`))
2318
+ return;
2319
+ // (inside a sink stream: reportSafely, so 'error' mode can't break the stream)
2320
+ reportSafely('SYG508', {
2321
+ severity: STRICT_CODE_SEVERITY.SYG508,
2322
+ component,
2323
+ message: `${name} sends a ${sink} request with category: '${req.category}' and reads the reply back with ${read}`,
2324
+ fix: `Use reply actions instead: name them in the request, ${sink}: (state) => ({ url, ok: 'LOADED', error: 'FAILED' }), ` +
2325
+ `handle LOADED (the parsed body) and FAILED ({ error, status, body, request }) in the model, and remove ${read} from the intent`,
2326
+ data: { sink, category: req.category, read },
2327
+ });
2328
+ }
2329
+ const repliesCheck = {
2330
+ id: 'replies',
2331
+ sources(component, sources) {
2332
+ if (!isStrictEnabled() || typeof Proxy != 'function')
2333
+ return;
2334
+ const cache = new Map();
2335
+ return new Proxy(sources, {
2336
+ get(t, k) {
2337
+ const v = t[k];
2338
+ if (typeof k != 'string' || !v || v.__sygnalReplies !== true)
2339
+ return v;
2340
+ if (!cache.has(k))
2341
+ cache.set(k, readingSource(component, k, v));
2342
+ return cache.get(k);
2343
+ },
2344
+ });
2345
+ },
2346
+ };
2347
+
2348
+ const PARAM = /:(\w+)/g;
2349
+ const paramsOf = (pat) => (pat.match(PARAM) || []).map(p => p.slice(1));
2350
+ function checkHref(routes, name, params) {
2351
+ const names = Object.keys(routes).filter(k => routes[k] != '*');
2352
+ const pat = routes[name];
2353
+ if (pat == null || pat == '*') {
2354
+ if (!once(`SYG130:${name}`))
2355
+ return;
2356
+ const near = suggest(String(name), names);
2357
+ devReport('SYG130', {
2358
+ message: `href('${name}') names no route, so the link points to '/'` + (near ? ` (did you mean '${near}'?)` : ''),
2359
+ fix: `Use one of the route names: ${names.join(', ')}`,
2360
+ data: { name, routes: names, ...(near ? { suggestion: near } : {}) },
2361
+ });
2362
+ return;
2363
+ }
2364
+ const wanted = paramsOf(pat);
2365
+ const given = params && typeof params == 'object' ? params : {};
2366
+ const missing = wanted.filter(k => given[k] == null);
2367
+ const extra = Object.keys(given).filter(k => !wanted.includes(k));
2368
+ if (missing.length && once(`SYG130:${name}:${missing}`)) {
2369
+ devReport('SYG130', {
2370
+ message: `href('${name}') leaves out ${missing.map(k => `'${k}'`).join(', ')} (${pat}), so that segment is empty`,
2371
+ fix: `href('${name}', { ${wanted.join(', ')} })`,
2372
+ data: { name, pattern: pat, missing },
2373
+ });
2374
+ }
2375
+ if (extra.length && once(`SYG131:${name}:${extra}`)) {
2376
+ devReport('SYG131', {
2377
+ message: `Route '${name}' (${pat}) has no param ${extra.map(k => `'${k}'`).join(', ')}; it is ignored`,
2378
+ fix: wanted.length ? `Pass only ${wanted.join(', ')}; put other values in the query: href('${name}', params, { ${extra[0]} })` : `Put it in the query: href('${name}', {}, { ${extra[0]} })`,
2379
+ data: { name, pattern: pat, extra },
2380
+ });
2381
+ }
2382
+ }
2383
+ /** install the hook the router's href() calls (removed by the returned function) */
2384
+ function installRouterHooks() {
2385
+ const core = bridge();
2386
+ if (!core)
2387
+ return () => { };
2388
+ core.routerHref = checkHref;
2389
+ return () => { if (core.routerHref === checkHref)
2390
+ core.routerHref = undefined; };
2391
+ }
2392
+ const routerCheck = {
2393
+ id: 'router',
2394
+ onModel(component) {
2395
+ const sources = component?.sources;
2396
+ if (!sources || typeof sources != 'object')
2397
+ return;
2398
+ const name = nameOf(component);
2399
+ for (const n of component.sourceNames || Object.keys(sources)) {
2400
+ const src = sources[n];
2401
+ const k = src?.__sygnalStatic;
2402
+ if (typeof k == 'string' && component.view?.[k] != null) {
2403
+ // statics are computed from state (G-167: with or without a model): a root whose state
2404
+ // never emits (a model but no initialState) never sends one
2405
+ if (!component.isSubComponent && component.initialState === undefined && once(`SYG132:${name}:${k}`)) {
2406
+ devReport('SYG132', {
2407
+ component,
2408
+ message: `${name} declares ${name}.${k}, but a root without initialState has no state, so ${k} is never sent to ${n}`,
2409
+ fix: `Add ${name}.initialState (e.g. { }${k == 'route' ? ', or { route: router.current() }' : ''})`,
2410
+ data: { static: k, source: n, reason: 'state' },
2411
+ });
2412
+ }
2413
+ }
2414
+ const opts = src?.__sygnalRouter;
2415
+ const g = globalThis;
2416
+ if (opts && typeof opts == 'object' && !opts.navigate && g.document?.getElementById?.('vike_pageContext') && once('SYG133')) {
2417
+ devReport('SYG133', {
2418
+ component,
2419
+ message: `The SPA router (${n}) runs inside a Vike app: Vike owns links and history, so both would handle a click`,
2420
+ fix: "Pass Vike's navigate: makeRouter({ routes, navigate }) with import { navigate } from 'vike/client/router', or use Vike's routing",
2421
+ data: { source: n },
2422
+ });
2423
+ }
2424
+ }
2425
+ },
2426
+ };
2427
+
2428
+ /*
2429
+ * Standard Schema (https://standardschema.dev) without a dependency: any validator that exposes
2430
+ * `schema['~standard'].validate(value)` (zod, valibot, arktype, ...), sync or async.
2431
+ * Used by makeFetchDriver's `validate` (PLAN-3 §1.6, D80); PLAN-4's forms and server functions
2432
+ * reuse it.
2433
+ */
2434
+ /** true for a Standard Schema object (it has `~standard.validate`) */
2435
+ const isStandardSchema = (schema) => !!schema && (typeof schema == 'object' || typeof schema == 'function') && typeof schema['~standard']?.validate == 'function';
2436
+
2437
+ const IDEMPOTENT = /^(GET|HEAD)$/i;
2438
+ /** instance → reply key lanes it has sent, and ok name → the other key it was sent under */
2439
+ const lanes = new WeakMap();
2440
+ const fetchSink = (component, name) => component?.sources?.[name]?.__sygnalStatic === 'resources';
2441
+ /** 5-5: the sink's driver has no queryCache() */
2442
+ const noCache = (component, sink) => {
2443
+ const i = component?.sources?.[sink]?.__inspect?.();
2444
+ return !!i && i.cache === undefined;
2445
+ };
2446
+ const reportNoCache = (component, sink, what, url) => {
2447
+ if (!noCache(component, sink) || !once(`SYG635:${nameOf(component)}:${what}:${url}`))
2448
+ return;
2449
+ devReport('SYG635', {
2450
+ component,
2451
+ message: `${sink} ${url ? url + ' ' : ''}uses ${what}, but the ${sink} driver has no queryCache(), so nothing is cached`,
2452
+ fix: `Give the driver a cache: makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) }) (import { queryCache } from 'sygnal'); in tests renderComponent(C, { http: { cache: queryCache() } }). Or remove ${what}`,
2453
+ data: { sink, url, what },
2454
+ });
2455
+ };
2456
+ function checkOne(component, sink, req, where) {
2457
+ if (!req || typeof req !== 'object')
2458
+ return;
2459
+ const name = nameOf(component);
2460
+ const url = typeof req.url === 'string' ? req.url : '';
2461
+ const method = String(req.method || (req.json !== undefined || req.body !== undefined ? 'POST' : 'GET')).toUpperCase();
2462
+ if ((req.cache === true || req.staleTime != null) && !IDEMPOTENT.test(method) && once(`SYG630:${name}:${method}:${url}`)) {
2463
+ devReport('SYG630', {
2464
+ component,
2465
+ message: `${where} ${method} ${url} sets ${req.cache === true ? 'cache: true' : 'staleTime'}, so a cached reply answers it instead of sending it again`,
2466
+ fix: `Cache reads only: remove ${req.cache === true ? 'cache' : 'staleTime'} from the ${method}, and refresh the cached reads after it succeeds with invalidates: ['tag']`,
2467
+ data: { sink, method, url },
2468
+ });
2469
+ }
2470
+ if (req.cache === true || req.staleTime != null)
2471
+ reportNoCache(component, sink, req.cache === true ? 'cache: true' : 'staleTime', url);
2472
+ if ('validate' in req && !isStandardSchema(req.validate) && once(`SYG631:${name}:${url}`)) {
2473
+ devReport('SYG631', {
2474
+ component,
2475
+ message: `${where} ${url} has validate: ${typeof req.validate}, which is not a Standard Schema, so the request fails`,
2476
+ fix: "Pass a Standard Schema object (zod, valibot, arktype, ...: anything with a '~standard'.validate function), e.g. validate: QuoteSchema",
2477
+ data: { sink, url },
2478
+ });
2479
+ }
2480
+ }
2481
+ function check(component, sink, req) {
2482
+ if (!req || typeof req !== 'object')
2483
+ return;
2484
+ const name = nameOf(component);
2485
+ const res = req.resources;
2486
+ if (res && typeof res === 'object') {
2487
+ for (const k of Object.keys(res))
2488
+ checkOne(component, sink, typeof res[k] === 'string' ? { url: res[k] } : res[k], `${name}.resources.${k}:`);
2489
+ return;
2490
+ }
2491
+ if (req.prefetch) {
2492
+ const p = req.prefetch;
2493
+ reportNoCache(component, sink, '{ prefetch }', typeof p === 'string' ? p : String(p?.url ?? ''));
2494
+ return;
2495
+ }
2496
+ if ('invalidate' in req) {
2497
+ const count = component.sources[sink]?.__matches?.(req.invalidate);
2498
+ const what = typeof req.invalidate === 'function' ? 'the predicate' : JSON.stringify(req.invalidate);
2499
+ if (count === 0 && once(`SYG632:${name}:${what}`)) {
2500
+ devReport('SYG632', {
2501
+ component,
2502
+ message: `${sink} { invalidate: ${what} } matched no cache entry and no mounted resource`,
2503
+ fix: "Give the resources (or cached requests) to refresh tags: ['quotes'] and invalidate that tag, or a URL prefix starting with '/' ('/api/quotes')",
2504
+ data: { sink, invalidate: what },
2505
+ });
2506
+ }
2507
+ return;
2508
+ }
2509
+ let l = lanes.get(component);
2510
+ if (!l)
2511
+ lanes.set(component, (l = { used: new Set(), keyed: new Map() }));
2512
+ const { ok, error, key, abort } = req;
2513
+ if (typeof abort === 'string' && key === undefined) {
2514
+ const other = l.keyed.get(abort);
2515
+ if (other !== undefined && !l.used.has(abort) && once(`SYG633:${name}:${abort}`)) {
2516
+ devReport('SYG633', {
2517
+ component,
2518
+ message: `${sink} { abort: '${abort}' } cancels the requests with key '${abort}', but ${name} sends its ok: '${abort}' requests with key: '${other}', so nothing is cancelled`,
2519
+ fix: `Cancel by that key: { abort: true, key: '${other}' } (or drop the key, so the lane is the ok action)`,
2520
+ data: { sink, abort, key: other },
2521
+ });
2522
+ }
2523
+ return;
2524
+ }
2525
+ if (typeof ok === 'string' || typeof error === 'string') {
2526
+ const lane = key ?? ok ?? error;
2527
+ l.used.add(lane);
2528
+ if (typeof ok === 'string' && key !== undefined && key !== ok)
2529
+ l.keyed.set(ok, String(key));
2530
+ }
2531
+ checkOne(component, sink, req, `A ${sink} request`);
2532
+ }
2533
+ const fetchCheck = {
2534
+ id: 'fetch',
2535
+ onModel(component) {
2536
+ const model$ = component?.model$;
2537
+ if (!model$ || typeof model$ !== 'object')
2538
+ return;
2539
+ for (const sink of Object.keys(model$)) {
2540
+ const s$ = model$[sink];
2541
+ if (!fetchSink(component, sink) || !s$ || typeof s$.map !== 'function')
2542
+ continue;
2543
+ model$[sink] = s$.map((req) => {
2544
+ try {
2545
+ check(component, sink, req);
2546
+ }
2547
+ catch (_) { }
2548
+ return req;
2549
+ });
2550
+ }
2551
+ },
2552
+ };
2553
+
2554
+ const NEEDS = {
2555
+ timers: 'makeTimerDriver()',
2556
+ connections: 'makeSocketDriver()',
2557
+ resources: 'makeFetchDriver()',
2558
+ browser: 'makeBrowserDriver()',
2559
+ };
2560
+ const KEY = { timers: 'TIMER', connections: 'WS', resources: 'HTTP', browser: 'BROWSER' };
2561
+ const brief$1 = (v) => { try {
2562
+ return JSON.stringify(v);
2563
+ }
2564
+ catch (_) {
2565
+ return String(v);
2566
+ } };
2567
+ /** why a timer spec can't run ('' when it can; mirrors timers.ts's valid()) */
2568
+ function timerSpecProblem(s) {
2569
+ if (!s || typeof s != 'object')
2570
+ return 'is not an object ({ every, action }, { after, action } or { frame: action })';
2571
+ if ('frame' in s && s.frame)
2572
+ return typeof s.frame == 'string' ? '' : 'frame must be the action name (a string)';
2573
+ if (!s.action || typeof s.action != 'string')
2574
+ return 'has no action (a string)';
2575
+ if (s.every == null && s.after == null)
2576
+ return 'has neither every nor after';
2577
+ if (s.every != null && s.after != null)
2578
+ return 'has both every and after';
2579
+ if (s.every != null)
2580
+ return s.every > 0 && s.every < 1 / 0 ? '' : `every must be a positive number of ms (got ${brief$1(s.every)})`;
2581
+ return s.after >= 0 && s.after < 1 / 0 ? '' : `after must be a number of ms, 0 or more (got ${brief$1(s.after)})`;
2582
+ }
2583
+ function checkSpec(name, spec, component) {
2584
+ const problem = timerSpecProblem(spec) || 'is invalid';
2585
+ if (!once(`SYG422:${component}:${name}:${brief$1(spec)}`))
2586
+ return;
2587
+ devReport('SYG422', {
2588
+ component,
2589
+ message: `Timer '${name}' ${problem}; it is not started`,
2590
+ fix: `Declare ${name}: { every: 100, action: 'TICK' }, { after: 5000, action: 'EXPIRE' } or { frame: 'FRAME' } (falsy to stop it)`,
2591
+ data: { name, spec },
2592
+ });
771
2593
  }
772
- function docsUrlFor(code) {
773
- return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
2594
+ /** install the hook makeTimerDriver() calls for an invalid spec (removed by the returned function) */
2595
+ function installTimerHooks() {
2596
+ const core = bridge();
2597
+ if (!core)
2598
+ return () => { };
2599
+ core.timerSpec = checkSpec;
2600
+ return () => { if (core.timerSpec === checkSpec)
2601
+ core.timerSpec = undefined; };
774
2602
  }
2603
+ const timersCheck = {
2604
+ id: 'timers',
2605
+ onModel(component) {
2606
+ const sources = component?.sources, view = component?.view;
2607
+ if (!sources || typeof sources != 'object' || !view)
2608
+ return;
2609
+ const names = component.sourceNames || Object.keys(sources);
2610
+ for (const k in NEEDS) {
2611
+ if (view[k] == null)
2612
+ continue;
2613
+ if (names.some((n) => sources[n]?.__sygnalStatic === k))
2614
+ continue;
2615
+ const name = nameOf(component);
2616
+ if (!once(`SYG643:${name}:${k}`))
2617
+ continue;
2618
+ devReport('SYG643', {
2619
+ component,
2620
+ message: `${name} declares ${name}.${k}, but no ${NEEDS[k]} driver is registered, so ${k == 'timers' ? 'its timers never run' : k == 'connections' ? 'its connections never open' : k == 'browser' ? 'its browser sources never start' : 'its resources are never fetched'}`,
2621
+ fix: `Register the driver: run(App, { ${KEY[k]}: ${NEEDS[k]} })`,
2622
+ data: { static: k, driver: NEEDS[k] },
2623
+ });
2624
+ }
2625
+ },
2626
+ };
775
2627
 
776
- registerCodes(Object.keys(STRICT_CODE_SEVERITY).map(code => [code, STRICT_CODE_SEVERITY[code], CODE_TITLES[code]]));
777
2628
  /**
778
- * Turn runtime strict checks on (true), off (false), or back to the default
779
- * (undefined → globalThis.__SYGNAL_STRICT__ === true).
2629
+ * Browser sources (PLAN-5 B-3, src/extra/browserSources.ts). Dev-entry-only codes
2630
+ * (DEV_CODE_SEVERITY), reported when the driver calls
2631
+ * `__SYGNAL_DIAGNOSTICS__.browserSource(code, name, spec, componentName, extra)`:
2632
+ *
2633
+ * SYG663 — a `browser` entry the driver can't start (no known kind, no `action`, an intersection
2634
+ * / resize target that is neither a selector nor `true`), or a BROWSER sink command
2635
+ * with an unknown method. It isn't started / run.
2636
+ * SYG664 — a kind the driver wasn't made with (`makeBrowserDriverWith(...)` without its source).
2637
+ * SYG665 — a source or command failed (permission denied, API missing, storage blocked) and its
2638
+ * spec names no `error` action, so nothing else would say so.
2639
+ * SYG668 — an intersection / resize declaration with nothing to observe: no DOM source reached
2640
+ * the driver for the instance (extra 'dom'), or, in renderComponent's fake with
2641
+ * `dom: 'real'`, its selector matches no element of the component (extra 'none').
2642
+ *
2643
+ * A component declaring `browser` with no browser driver is SYG643 (checks/timers.ts).
780
2644
  */
781
- function configureStrict(on) {
782
- const core = bridge();
783
- if (core)
784
- core.strict = on;
2645
+ const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation'];
2646
+ const SOURCE = {
2647
+ intersection: 'intersectionSource', resize: 'resizeSource', media: 'mediaSource', storage: 'storageSource',
2648
+ visibility: 'visibilitySource', online: 'onlineSource', geolocation: 'geolocationSource',
2649
+ copy: 'clipboardSource', paste: 'clipboardSource', setItem: 'storageSource', removeItem: 'storageSource',
2650
+ };
2651
+ const brief = (v) => { try {
2652
+ return JSON.stringify(v);
785
2653
  }
786
- /** Whether runtime strict checks are on. */
787
- function isStrictEnabled() {
788
- const flag = bridge()?.strict;
789
- return flag === undefined ? globalThis.__SYGNAL_STRICT__ === true : flag === true;
2654
+ catch (_) {
2655
+ return String(v);
2656
+ } };
2657
+ /** why a declared spec can't start ('' when it can; mirrors browserDriver's checks) */
2658
+ function browserSpecProblem(s) {
2659
+ if (!s || typeof s != 'object')
2660
+ return `is not an object ({ ${KINDS.join(' | ')}: …, action })`;
2661
+ const k = KINDS.find(k => k in s);
2662
+ if (!k)
2663
+ return `names no source (one of ${KINDS.join(', ')})`;
2664
+ if (!s.action || typeof s.action != 'string')
2665
+ return 'has no action (a string)';
2666
+ if ((k == 'intersection' || k == 'resize') && s[k] !== true && (typeof s[k] != 'string' || !s[k]))
2667
+ return `${k} must be a selector in the component or true (its root element), got ${brief(s[k])}`;
2668
+ return '';
790
2669
  }
791
- const strictReport = (code, details) => report(code, { severity: STRICT_CODE_SEVERITY[code], ...details });
792
- const strictCheck = {
793
- id: 'strict',
2670
+ function onBrowserSource(code, name, spec, component, extra, commands) {
2671
+ if (code == 'SYG663' && extra === 0) {
2672
+ // an unknown command (extra 0; `commands`: the driver's)
2673
+ if (!once(`SYG663:${component}:cmd:${name}`))
2674
+ return;
2675
+ devReport('SYG663', {
2676
+ component,
2677
+ message: `BROWSER command '${name}' is unknown; it is not run`,
2678
+ fix: `Name the method as a key of the command: ${(commands || []).map(c => `{ ${c}: … }`).join(', ') || 'this driver takes no commands'}`,
2679
+ data: { name, command: spec },
2680
+ });
2681
+ return;
2682
+ }
2683
+ if (code == 'SYG663') {
2684
+ if (!once(`SYG663:${component}:${name}:${brief(spec)}`))
2685
+ return;
2686
+ devReport('SYG663', {
2687
+ component,
2688
+ message: `browser entry '${name}' ${browserSpecProblem(spec) || 'is invalid'}; it is not started`,
2689
+ fix: `Declare ${name}: { media: '(prefers-color-scheme: dark)', action: 'DARK' }, { intersection: '.card', action: 'SEEN' }, … (falsy to stop it)`,
2690
+ data: { name, spec },
2691
+ });
2692
+ }
2693
+ else if (code == 'SYG664') {
2694
+ if (!once(`SYG664:${component}:${name}`))
2695
+ return;
2696
+ devReport('SYG664', {
2697
+ component,
2698
+ message: `browser entry '${name}' uses the ${extra} source, which this browser driver wasn't made with; it is not started`,
2699
+ fix: `Add it: makeBrowserDriverWith(${SOURCE[extra]}, …), or use makeBrowserDriver() (every source)`,
2700
+ data: { name, spec, kind: extra },
2701
+ });
2702
+ }
2703
+ else if (code == 'SYG668') {
2704
+ if (!once(`SYG668:${component}:${name}:${extra}`))
2705
+ return;
2706
+ const k = KINDS.find(k => spec && k in spec) || 'intersection', target = spec?.[k];
2707
+ devReport('SYG668', {
2708
+ component,
2709
+ message: extra == 'none'
2710
+ ? `browser entry '${name}' observes ${brief(target)}, which matches no element of the component; it reports nothing`
2711
+ : `browser entry '${name}' (${k}) has no DOM to observe: the component's DOM source didn't reach the browser driver; it reports nothing`,
2712
+ fix: extra == 'none'
2713
+ ? `Use a selector the component's own view renders (or true for its root element), and declare the entry only while that element exists`
2714
+ : `Run the app with a DOM driver (run() with a mount point) and declare intersection / resize in a component's own \`browser\` static`,
2715
+ data: { name, spec, reason: extra },
2716
+ });
2717
+ }
2718
+ else if (code == 'SYG665') {
2719
+ if (!once(`SYG665:${component}:${name}:${extra?.name ?? extra?.code}`))
2720
+ return;
2721
+ devReport('SYG665', {
2722
+ component,
2723
+ message: `browser ${spec && 'action' in spec ? `entry '${name}'` : `command '${name}'`} failed (${extra?.message || extra?.name || 'error'}) and names no error action`,
2724
+ fix: `Name one to handle it, e.g. { ..., error: 'FAILED' }, and a model entry FAILED: (state, { message }) => ...`,
2725
+ data: { name, spec, failure: extra },
2726
+ });
2727
+ }
2728
+ }
2729
+ /** install the hook the browser driver calls (removed by the returned function) */
2730
+ function installBrowserSourceHooks() {
2731
+ const core = bridge();
2732
+ if (!core)
2733
+ return () => { };
2734
+ core.browserSource = onBrowserSource;
2735
+ return () => { if (core.browserSource === onBrowserSource)
2736
+ core.browserSource = undefined; };
2737
+ }
2738
+
2739
+ const persistCheck = {
2740
+ id: 'persist',
794
2741
  onModel(component) {
795
- if (!isStrictEnabled() || !component)
2742
+ const view = component?.view, p = view?.persist;
2743
+ if (!p)
796
2744
  return;
797
2745
  const name = nameOf(component);
798
- // SYG501 — view(props, state, context, peers): positional use of the 2nd+ args.
799
- // (A default value or a rest parameter stops Function.length, so this can
800
- // miss `(props, state = {})`, but it never flags a single-argument view.)
801
- const view = component.view;
802
- if (typeof view === 'function' && view.length > 1 && !view.__sygnalLazy && once(`SYG501:${name}`)) {
803
- const extra = ['state', 'context', 'peers'].slice(0, view.length - 1);
804
- strictReport('SYG501', {
805
- component,
806
- message: `The view takes ${view.length} positional arguments (props, ${extra.join(', ')})`,
807
- fix: `Destructure the first argument instead: function ${name}({ ${extra.join(', ')}, ...props })`,
808
- data: { arity: view.length },
809
- });
2746
+ if (component.isSubComponent) {
2747
+ // 3-B2: under Vike's Layout/Wrapper shell (onRenderClient's `__vike` source) the Page,
2748
+ // Layouts and Wrappers are all sub-components of the shell root
2749
+ const vike = component.sources?.__vike;
2750
+ if (once('SYG224:' + name))
2751
+ devReport('SYG224', {
2752
+ component,
2753
+ message: vike
2754
+ ? `${name}.persist is set, but ${name} renders in Vike's Layout/Wrapper shell (the shell is the app's root), so its state is never saved or restored: persist() isn't supported on Vike pages, layouts or wrappers yet`
2755
+ : `${name}.persist is set, but ${name} is not the root component, so its state is never saved or restored (persist() works on the root only)`,
2756
+ fix: vike
2757
+ ? `Remove persist from ${name}. Save the state yourself (STATE.watch writing to localStorage, read back in BOOTSTRAP), or use persist() on a component you run() yourself, or on a Vike page with no Layout or Wrapper (it is its own root)`
2758
+ : `Move persist to the root component (the one passed to run()) and pick the keys it holds: persist({ key, pick: ['${name.toLowerCase()}'] })`,
2759
+ data: { key: p.options?.key, ...(vike && { vike: true }) },
2760
+ });
2761
+ return;
810
2762
  }
811
- // SYG504 — 'ACTION | SINK' shorthand keys
812
- const model = component.model;
813
- if (!model || typeof model !== 'object')
2763
+ const o = p.options, init = view.initialState;
2764
+ if (!o || !init || typeof init != 'object')
814
2765
  return;
815
- for (const key of Object.keys(model)) {
816
- if (!key.includes('|'))
817
- continue;
818
- const [action, sink] = key.split('|').map(s => s.trim());
819
- if (!action || !sink || !once(`SYG504:${name}:${key}`))
820
- continue;
821
- const rewrite = sink === 'EVENTS'
822
- ? `${action}: { EVENTS: event('TYPE', (state, data) => payload) }`
823
- : `${action}: { ${sink}: (state, data, next) => ... }`;
824
- strictReport('SYG504', {
825
- component,
826
- message: `Model key '${key}' uses the 'ACTION | SINK' shorthand`,
827
- fix: `Use the object form: ${rewrite} (merge it into an existing '${action}' entry if there is one)`,
828
- data: { key, action, sink },
829
- });
2766
+ const keys = Object.keys(init);
2767
+ for (const opt of ['pick', 'omit']) {
2768
+ for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
2769
+ if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
2770
+ continue;
2771
+ devReport('SYG223', {
2772
+ component,
2773
+ message: `persist ${opt} names '${k}', which is not a key of ${name}.initialState, so it is ${opt == 'pick' ? 'never saved' : 'never left out'}${didYouMean(k, keys)}`,
2774
+ fix: `Use top-level keys of initialState in ${opt} (${keys.join(', ')}), or add '${k}' to initialState`,
2775
+ data: { option: opt, key: k, stateKeys: keys },
2776
+ });
2777
+ }
830
2778
  }
831
2779
  },
832
- onReducer(component, action, prevState, nextState) {
833
- if (!isStrictEnabled() || isInternalAction(action))
834
- return; // incl. INITIALIZE/HYDRATE/DISPOSE
835
- // undefined is SYG202 (state check, every mode); only the identical object here.
836
- if (!prevState || typeof prevState !== 'object' || nextState === undefined)
2780
+ };
2781
+
2782
+ const viewTransitionsCheck = {
2783
+ id: 'viewTransitions',
2784
+ onModel(component) {
2785
+ const list = component?.view?.viewTransitions;
2786
+ if (list == null)
2787
+ return;
2788
+ if (!Array.isArray(list)) {
2789
+ const name = nameOf(component);
2790
+ if (!once(`SYG645:${name}`))
2791
+ return;
2792
+ const actions = Object.keys(component.model || {}).filter(a => !a.includes('|'));
2793
+ devReport('SYG645', {
2794
+ component,
2795
+ message: `${name}.viewTransitions is not an array (it is ${typeof list == 'string' ? `'${list}'` : typeof list}), so no action is listed for a View Transition`,
2796
+ fix: `List the action names in an array: ${name}.viewTransitions = [${(typeof list == 'string' ? [list] : actions.slice(0, 2)).map(a => `'${a}'`).join(', ') || "'ACTION'"}] (an array of action names)`,
2797
+ data: { value: list },
2798
+ });
837
2799
  return;
838
- let same = nextState === prevState;
839
- if (!same && typeof component?.addCalculated === 'function') {
840
- // with calculated fields the reducer saw addCalculated(prev) (memoized: same object)
841
- try {
842
- same = component.addCalculated(prevState) === nextState;
843
- }
844
- catch (_) { }
845
2800
  }
2801
+ if (!list.length)
2802
+ return;
2803
+ const im = component.sources?.[component.DOMSourceName || 'DOM']?._isolateModule;
2804
+ // the core's DOM source proxy answers any unknown key with an event shorthand (a function)
2805
+ if (typeof im != 'object' || !im || im.vtDriver)
2806
+ return;
2807
+ if (typeof document != 'undefined' && document.querySelector?.('.sygnal-test'))
2808
+ return;
846
2809
  const name = nameOf(component);
847
- if (!same || !once(`SYG502:${name}:${action}`))
2810
+ if (!once(`SYG645:${name}`))
848
2811
  return;
849
- strictReport('SYG502', {
2812
+ devReport('SYG645', {
850
2813
  component,
851
- message: `The STATE reducer for '${action}' returned the unchanged state object`,
852
- fix: `Return ABORT for "no change": ${action}: (state, data) => cond ? { ...state, ... } : ABORT`,
853
- data: { action },
2814
+ message: `${name} declares ${name}.viewTransitions (${list.map((a) => `'${a}'`).join(', ')}), but the app's DOM driver can't run View Transitions, so those actions patch without one`,
2815
+ fix: `Give run() the View Transition DOM driver: run(App, { DOM: makeViewTransitionDOMDriver('#root') })`,
2816
+ data: { actions: list },
854
2817
  });
855
2818
  },
856
2819
  };
857
2820
 
2821
+ /**
2822
+ * PLAN-4.5 G-275: marks an initial state that a wrapper built from its caller's values (an
2823
+ * element's host props, Vike's pageContext.data or hydrated state, renderComponent's
2824
+ * initialState option). The dev statics freeze (D152, checks/statics.ts) leaves a marked object
2825
+ * and everything in it alone: the caller may still change those values. A non-enumerable symbol
2826
+ * key: spreads, JSON and Object.keys don't see it.
2827
+ */
2828
+ const OWNED = Symbol.for('sygnal.owned');
2829
+
2830
+ const plain = (v) => {
2831
+ if (!v || typeof v != 'object')
2832
+ return false;
2833
+ const p = Object.getPrototypeOf(v);
2834
+ return Array.isArray(v) || p === Object.prototype || p === null;
2835
+ };
2836
+ const deepFreeze = (v) => {
2837
+ if (!plain(v) || Object.isFrozen(v) || v[OWNED])
2838
+ return;
2839
+ Object.freeze(v);
2840
+ for (const k of Object.keys(v))
2841
+ deepFreeze(v[k]);
2842
+ };
2843
+ const freeze = (o) => {
2844
+ if (!o || (typeof o != 'object' && typeof o != 'function'))
2845
+ return;
2846
+ deepFreeze(o.initialState);
2847
+ for (const k of ['model', 'context', 'calculated'])
2848
+ if (plain(o[k]))
2849
+ Object.freeze(o[k]);
2850
+ };
2851
+ const staticsCheck = {
2852
+ id: 'statics',
2853
+ onIntent(component) {
2854
+ if (typeof component?.view == 'function')
2855
+ freeze(component.view);
2856
+ freeze(component?._o);
2857
+ },
2858
+ };
2859
+
858
2860
  /**
859
2861
  * SYG301 — an RxJS operator was called on an xstream stream (error).
860
2862
  *
@@ -866,7 +2868,7 @@ const strictCheck = {
866
2868
  * xstream equivalent and the SYG301 docs link. The error is also reported
867
2869
  * (collected / printed per the diagnostics mode; in 'error' mode report()
868
2870
  * throws the DiagnosticError itself). It works anywhere a stream is used
869
- * (intent, peers, drivers), with no hook in component.ts.
2871
+ * (intent, peers, drivers), with no hook in the core.
870
2872
  *
871
2873
  * Caveat: `typeof stream.pipe === 'function'` is true while this entry is
872
2874
  * loaded (dev only).
@@ -941,32 +2943,134 @@ function installRxjsHints(proto) {
941
2943
  delete proto[op]; };
942
2944
  }
943
2945
 
2946
+ /**
2947
+ * PLAN-4 2-C (GS-10): the action log. Records every action a component instance runs as
2948
+ * { type, data, component, instance, sinks, cause, time }
2949
+ * for renderComponent's t.actions / t.explain() (src/extra/testing.ts), inspect({ actions }) in
2950
+ * the 'sygnal/diagnostics' dev entry (./inspect.ts) and the DevTools action log
2951
+ * (src/extra/devtoolsActions.ts).
2952
+ *
2953
+ * 0 B in apps: nothing in the core calls this. actionHooks() is a hook layer of the core
2954
+ * (04-hooks-contract §3.3): onAction opens the record (the core passes the cause: 'intent',
2955
+ * 'next', 'reply', 'built-in', 'simulateAction'; an intent action a behavior owns is
2956
+ * 'behavior'), wrapHandler sees which sinks produced a value. (The old core's version patched
2957
+ * each instance; R5 removed it.)
2958
+ *
2959
+ * Sinks: a function reducer counts when it returns a value: not ABORT (or another symbol, which
2960
+ * the core drops), and for STATE not the state it was given (GS-4: no change); EFFECT counts when
2961
+ * the handler ran without ABORT. A constant / `true` entry always sends. A throwing reducer
2962
+ * sends nothing.
2963
+ */
2964
+ const BUILT_IN = /^(BOOTSTRAP|INITIALIZE|DISPOSE|RESOURCE)$/;
2965
+ /** now on the test's fake clock when there is one (vi.useFakeTimers()), else Date.now() */
2966
+ const clockNow = () => {
2967
+ const c = setTimeout.clock;
2968
+ return c ? c.now : Date.now();
2969
+ };
2970
+ /**
2971
+ * PLAN-4.6 R4: the same log on the core, from its hooks (04 §3.3) instead of instance
2972
+ * patches: onAction opens the record (the core passes the cause: 'intent', 'next', 'reply',
2973
+ * 'built-in', 'simulateAction'), wrapHandler sees which sinks produced a value. Same records,
2974
+ * same cause rules: a built-in type is 'built-in' unless simulated; an intent action a behavior
2975
+ * owns is 'behavior'. `only(inst)`: the instances to record (renderComponent: its own tree).
2976
+ */
2977
+ function actionHooks(listener, only) {
2978
+ const cur = new WeakMap();
2979
+ // INITIALIZE: the core writes the initial state at creation (no action, unless the model
2980
+ // has an INITIALIZE entry); today's log has an INITIALIZE record with STATE, so it is opened here
2981
+ const init = new WeakMap();
2982
+ return {
2983
+ onCreate(inst) {
2984
+ const d = inst.def;
2985
+ if ((only && !only(inst)) || d.initialState === undefined || !(inst.isRoot || d.isolated))
2986
+ return;
2987
+ const rec = { type: 'INITIALIZE', data: d.initialState, component: inst.name, instance: String(inst.id), sinks: [], cause: 'built-in', time: clockNow() };
2988
+ init.set(inst, rec);
2989
+ try {
2990
+ listener.action(rec, inst);
2991
+ }
2992
+ catch (_) { /* ignore */ }
2993
+ addSink(listener, rec, ['STATE'], 'STATE', undefined);
2994
+ },
2995
+ onAction(inst, a) {
2996
+ if (only && !only(inst))
2997
+ return;
2998
+ const type = String(a.type), owned = inst.def.behaviorActions;
2999
+ const hs = inst.def.handlers.get(type) || [], order = hs.map((h) => h[0]);
3000
+ const opened = type == 'INITIALIZE' && init.get(inst);
3001
+ if (opened) {
3002
+ init.delete(inst);
3003
+ cur.set(inst, [opened, order]);
3004
+ return;
3005
+ }
3006
+ const cause = a.cause == 'simulateAction' ? a.cause : BUILT_IN.test(type) ? 'built-in'
3007
+ : a.cause == 'next' || a.cause == 'reply' ? a.cause
3008
+ : owned && Object.prototype.hasOwnProperty.call(owned, type) ? 'behavior' : 'intent';
3009
+ const rec = { type, data: a.data, component: inst.name, instance: String(inst.id), sinks: [], cause, time: clockNow(), ...(a.source !== undefined && { source: a.source }) };
3010
+ cur.set(inst, [rec, order]);
3011
+ try {
3012
+ listener.action(rec, inst);
3013
+ }
3014
+ catch (_) { /* a listener never breaks the app */ }
3015
+ // a constant / `true` entry always sends (EFFECT runs functions only)
3016
+ for (const [sink, h] of hs)
3017
+ if (typeof h != 'function' && sink != 'EFFECT')
3018
+ addSink(listener, rec, order, sink, undefined);
3019
+ },
3020
+ wrapHandler(inst, type, sink, fn) {
3021
+ const c = cur.get(inst);
3022
+ if (!c || c[0].type !== type || typeof fn != 'function')
3023
+ return;
3024
+ const [rec, order] = c, isState = sink == 'STATE', isEffect = sink == 'EFFECT';
3025
+ return function (state) {
3026
+ const r = fn.apply(this, arguments);
3027
+ if (!isAbort(r) && (isEffect || typeof r != 'symbol') && !(isState && r === state))
3028
+ addSink(listener, rec, order, sink, fn[ORIGINAL] || fn);
3029
+ return r;
3030
+ };
3031
+ },
3032
+ };
3033
+ }
3034
+ function addSink(l, rec, order, sink, reducer) {
3035
+ if (!rec.sinks.includes(sink)) {
3036
+ rec.sinks.push(sink);
3037
+ rec.sinks.sort((a, b) => order.indexOf(a) - order.indexOf(b));
3038
+ }
3039
+ try {
3040
+ l.sink?.(rec, sink, reducer);
3041
+ }
3042
+ catch (_) { /* ignore */ }
3043
+ }
3044
+
944
3045
  let records = new Map();
945
3046
  let pendingSelects = [];
946
3047
  /** every EVENTS type seen on the bus */
947
3048
  let busTypes = new Set();
3049
+ /** PLAN-4 2-C (GS-10): the most recent actions of any instance, oldest first */
3050
+ const RECENT_MAX = 200;
3051
+ let recent = [];
3052
+ let epoch = clockNow();
3053
+ const actionListener = {
3054
+ action(r) { if (recent.push(r) > RECENT_MAX)
3055
+ recent.shift(); },
3056
+ };
948
3057
  onReset(() => {
949
3058
  records = new Map();
950
3059
  pendingSelects = [];
951
3060
  busTypes = new Set();
3061
+ recent = [];
3062
+ epoch = clockNow();
952
3063
  });
953
3064
  const uniq = (list) => [...new Set(list)];
3065
+ const NEXT_KIND = { root: 'root', child: 'child', item: 'collection-item', page: 'switchable' };
954
3066
  function kindOf(c) {
955
- const s = (c && c.sources) || {};
956
- if (typeof s.__parentComponentNumber !== 'number')
957
- return 'root';
958
- // Collection items get PARENT: null (src/component.ts instantiateCollection)
959
- if ('PARENT' in s && s.PARENT === null)
960
- return 'collection-item';
961
- // switchable() adds a lower-case `state` source next to the component's own (src/switchable.ts)
962
- if (c.stateSourceName !== 'state' && 'state' in s && s.state !== s[c.stateSourceName])
963
- return 'switchable';
964
- return 'child';
3067
+ // PLAN-4.6 R4: the core's instance view says it (checks/next.ts)
3068
+ return (c && c.__next && NEXT_KIND[c.__next.kind]) || 'child';
965
3069
  }
966
3070
  function ensure(c) {
967
3071
  let r = records.get(c);
968
3072
  if (!r) {
969
- const parent = c && c.sources && c.sources.__parentComponentNumber;
3073
+ const parent = c && c.__next && c.__next.parentId;
970
3074
  r = {
971
3075
  instance: c,
972
3076
  id: String(c && c._componentNumber),
@@ -979,6 +3083,7 @@ function ensure(c) {
979
3083
  fired: new Set(),
980
3084
  eventsSelected: new Set(),
981
3085
  eventsEmitted: new Set(),
3086
+ controls: new Map(),
982
3087
  };
983
3088
  records.set(c, r);
984
3089
  }
@@ -1013,7 +3118,7 @@ const inspectCheck = {
1013
3118
  attachDevtools();
1014
3119
  const r = ensure(component);
1015
3120
  const intent$ = component && component.intent$;
1016
- r.intentActions = intent$ && typeof intent$.addListener === 'function' ? null : (actionNames || []);
3121
+ r.intentActions = actionNames || [];
1017
3122
  r.injected = new Set((intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || []);
1018
3123
  for (const t of pendingSelects)
1019
3124
  r.eventsSelected.add(t);
@@ -1022,17 +3127,11 @@ const inspectCheck = {
1022
3127
  onModel(component, modelMap) {
1023
3128
  const r = ensure(component);
1024
3129
  r.modelMap = modelMap || {};
1025
- const model$ = component && component.model$;
1026
- const events$ = model$ && model$.EVENTS;
1027
- if (events$ && typeof events$.map === 'function' && !events$.__sygnalInspect) {
1028
- const tapped = events$.map((ev) => {
1029
- if (ev && typeof ev.type === 'string')
1030
- r.eventsEmitted.add(ev.type);
1031
- return ev;
1032
- });
1033
- tapped.__sygnalInspect = true;
1034
- model$.EVENTS = tapped;
1035
- }
3130
+ },
3131
+ onRender(component, root) {
3132
+ const r = records.get(component);
3133
+ if (r)
3134
+ collectControls(root, r.controls, true);
1036
3135
  },
1037
3136
  onReducer(component, action) {
1038
3137
  const r = records.get(component);
@@ -1047,6 +3146,48 @@ const inspectCheck = {
1047
3146
  records.delete(component);
1048
3147
  },
1049
3148
  };
3149
+ /** PLAN-4.6 R4 (onSink): an EVENTS value the instance sent */
3150
+ function eventEmitted(component, ev) {
3151
+ const r = records.get(component);
3152
+ if (r && ev && typeof ev.type === 'string')
3153
+ r.eventsEmitted.add(ev.type);
3154
+ }
3155
+ /**
3156
+ * CT-1: record the controls in a component's rendered tree (data-control="<Key>"), stopping at
3157
+ * isolated child components (their root carries data.isolate on the real DOM, a '.___<scope>'
3158
+ * class in the mock DOM): the controls inside belong to the child.
3159
+ */
3160
+ function collectControls(v, out, root) {
3161
+ if (!v || typeof v != 'object')
3162
+ return;
3163
+ if (!root && typeof v.sel == 'string' && ((v.data && v.data.isolate) || v.sel.includes('.___')))
3164
+ return;
3165
+ const key = v.data && v.data.attrs && v.data.attrs['data-control'];
3166
+ if (typeof key == 'string' && !out.has(key)) {
3167
+ const control = controlOfVnode(v);
3168
+ const spec = control && control.spec;
3169
+ out.set(key, spec && typeof spec == 'object'
3170
+ ? { element: null, kind: String(spec.kind) }
3171
+ : { element: typeof spec == 'string' ? spec : typeof v.sel == 'string' ? v.sel.split(/[.#]/)[0] : null });
3172
+ }
3173
+ for (const k of [].concat(v.children || [], (v.data && v.data.portalChildren) || []))
3174
+ collectControls(k, out, false);
3175
+ }
3176
+ /** CT-1: the controls a component rendered, `listened` from its intent's selectors */
3177
+ function controlsOf(r, selectors) {
3178
+ if (!r.controls.size)
3179
+ return undefined;
3180
+ const listened = new Set();
3181
+ for (const s of selectors)
3182
+ for (const m of s.selector.matchAll(/\[data-control="([^"]+)"\]/g))
3183
+ listened.add(m[1]);
3184
+ return [...r.controls].map(([name, { element, kind }]) => ({ name, element, ...(kind ? { kind } : {}), listened: listened.has(name) }));
3185
+ }
3186
+ /** CT-1: mark a selector that is a control ([data-control="<Key>"]) with its key */
3187
+ const withControl = (s) => {
3188
+ const key = keyOfSelector(s.selector);
3189
+ return key ? { ...s, control: key } : s;
3190
+ };
1050
3191
  const slim = (d) => {
1051
3192
  const out = { code: d.code, severity: d.severity, message: d.message };
1052
3193
  if (d.component)
@@ -1067,12 +3208,14 @@ const slim = (d) => {
1067
3208
  function actionsOf(r) {
1068
3209
  const intent = (r.intentActions || []).filter(a => !r.injected.has(a));
1069
3210
  const names = uniq([...intent, ...Object.keys(r.modelMap)]).filter(a => typeof a === 'string' && !a.startsWith('__'));
3211
+ const replies = replySeen.get(r.instance);
1070
3212
  return names.map(name => ({
1071
3213
  name,
1072
3214
  trigger: BUILTIN_ACTIONS.has(name) ? 'builtin'
1073
3215
  : intent.includes(name) ? 'intent'
1074
- : !r.injected.has(name) && r.fired.has(name) ? 'next'
1075
- : 'unknown',
3216
+ : replies?.has(name) ? 'reply'
3217
+ : !r.injected.has(name) && r.fired.has(name) ? 'next'
3218
+ : 'unknown',
1076
3219
  sinks: [...(r.modelMap[name] || [])],
1077
3220
  }));
1078
3221
  }
@@ -1080,6 +3223,38 @@ function stateKeysOf(c, calculated) {
1080
3223
  const s = c && c.currentState;
1081
3224
  return isPlainObject(s) ? Object.keys(s).filter(k => !calculated.includes(k)) : [];
1082
3225
  }
3226
+ /** PLAN-3 5-3: the instance's resources (`resources` static) and their state slots */
3227
+ function resourcesOf(c) {
3228
+ const res = c && c.view && c.view.resources;
3229
+ if (!res || typeof res !== 'object')
3230
+ return undefined;
3231
+ const s = c.currentState || {};
3232
+ return Object.keys(res).map(name => {
3233
+ const r = s[name] || {};
3234
+ return { name, status: r.status ?? 'idle', refreshing: !!r.refreshing, hasData: r.data !== undefined, error: r.error ? String(r.error.message ?? r.error) : undefined };
3235
+ });
3236
+ }
3237
+ /** PLAN-3 5-3: the cache entries of every makeFetchDriver source the instances use, by sink name (sinks with an empty cache left out) */
3238
+ function cacheOf(recs) {
3239
+ const out = {};
3240
+ for (const r of recs) {
3241
+ const sources = r.instance && r.instance.sources;
3242
+ if (!sources || typeof sources !== 'object')
3243
+ continue;
3244
+ for (const k of Object.keys(sources)) {
3245
+ const src = sources[k];
3246
+ if (out[k] || !src || src.__sygnalStatic !== 'resources' || typeof src.__inspect !== 'function')
3247
+ continue;
3248
+ try {
3249
+ const list = src.__inspect().cache;
3250
+ if (list.length)
3251
+ out[k] = list;
3252
+ }
3253
+ catch (_) { /* a driver without the cache listing */ }
3254
+ }
3255
+ }
3256
+ return out;
3257
+ }
1083
3258
  const isScope = (s) => s.startsWith('.___');
1084
3259
  function mockSelectors(mock) {
1085
3260
  const out = {};
@@ -1103,16 +3278,16 @@ function mockSelectors(mock) {
1103
3278
  function selectorsOf(r, options, diags) {
1104
3279
  const given = options.selectors && options.selectors[r.id];
1105
3280
  if (given)
1106
- return given.map(s => ({ ...s }));
3281
+ return given.map(s => withControl({ ...s }));
1107
3282
  return (selectorStatus(r.instance) || []).map(({ selector, matched, crossed }) => {
1108
3283
  const hit = diags.find(d => d.code === 'SYG104' && d.component === r.name && d.data && d.data.selector === selector);
1109
3284
  const missed = diags.some(d => d.code === 'SYG103' && d.component === r.name && d.data && d.data.selector === selector);
1110
- return {
3285
+ return withControl({
1111
3286
  selector,
1112
3287
  events: null,
1113
3288
  matched: matched === null && missed ? false : matched,
1114
3289
  isolationHit: crossed ? ((hit && hit.data.child) || 'a child component') : null,
1115
- };
3290
+ });
1116
3291
  });
1117
3292
  }
1118
3293
  function childrenOf(r, all) {
@@ -1157,6 +3332,8 @@ function inspect(options = {}) {
1157
3332
  const components = recs.map(r => {
1158
3333
  const c = r.instance;
1159
3334
  const calculated = c && c.calculated && typeof c.calculated === 'object' ? Object.keys(c.calculated) : [];
3335
+ const selectors = selectorsOf(r, options, diags);
3336
+ const controls = controlsOf(r, selectors);
1160
3337
  return {
1161
3338
  name: r.name,
1162
3339
  id: r.id,
@@ -1170,10 +3347,13 @@ function inspect(options = {}) {
1170
3347
  eventsEmitted: [...r.eventsEmitted],
1171
3348
  eventsSelected: [...r.eventsSelected],
1172
3349
  children: childrenOf(r, all),
1173
- selectors: selectorsOf(r, options, diags),
3350
+ selectors,
3351
+ ...(controls ? { controls } : {}),
1174
3352
  diagnostics: diags.filter(d => d && d.component === r.name).map(slim),
3353
+ ...(resourcesOf(c) ? { resources: resourcesOf(c) } : {}),
1175
3354
  };
1176
3355
  });
3356
+ const cache = cacheOf(recs) || {};
1177
3357
  const events = {};
1178
3358
  const ev = (type) => events[type] || (events[type] = { emitters: [], selectors: [] });
1179
3359
  const add = (list, name) => { if (!list.includes(name))
@@ -1194,8 +3374,36 @@ function inspect(options = {}) {
1194
3374
  components,
1195
3375
  events,
1196
3376
  diagnostics: diags.filter(d => d && (!d.component || !names.has(d.component))).map(slim),
3377
+ ...(Object.keys(cache).length ? { cache } : {}),
3378
+ ...(options.actions ? { recentActions: recentActions(options.actions, ids) } : {}),
1197
3379
  };
1198
3380
  }
3381
+ /** a JSON-safe copy of an action's data (dropped when it doesn't serialize, or is large: a DOM event) */
3382
+ function jsonData(d) {
3383
+ if (d === undefined)
3384
+ return {};
3385
+ try {
3386
+ const text = JSON.stringify(d);
3387
+ return text === undefined || text.length > 1000 ? {} : { data: JSON.parse(text) };
3388
+ }
3389
+ catch (_) {
3390
+ return {};
3391
+ }
3392
+ }
3393
+ /** PLAN-4 2-C: the last `limit` actions (true: all kept) of the listed instances */
3394
+ function recentActions(limit, ids) {
3395
+ const list = ids ? recent.filter(r => ids.has(r.instance)) : recent;
3396
+ const n = limit === true ? list.length : Math.max(0, Math.floor(limit));
3397
+ return list.slice(list.length - n).map(r => ({
3398
+ type: r.type,
3399
+ ...jsonData(r.data),
3400
+ component: r.component,
3401
+ instance: r.instance,
3402
+ sinks: [...r.sinks],
3403
+ cause: r.cause,
3404
+ at: r.time - epoch,
3405
+ }));
3406
+ }
1199
3407
  /** Publish inspect() on the core bridge and the devtools object. Returns an uninstall function. */
1200
3408
  function installInspect() {
1201
3409
  const core = bridge();
@@ -1215,6 +3423,501 @@ function installInspect() {
1215
3423
  };
1216
3424
  }
1217
3425
 
3426
+ /**
3427
+ * PLAN-4.6 R4: the dev entry's checks on the next component core (src/core/).
3428
+ *
3429
+ * The core has no instance to patch and no diagnostics calls of its own: it reads this
3430
+ * entry's hooks once per app (`__SYGNAL_DIAGNOSTICS__.nextHooks(api)`, 04-hooks-contract §2.2)
3431
+ * and calls them with read-only InstanceViews. This module maps them onto the existing checks:
3432
+ *
3433
+ * - each instance gets a facade with the fields the checks read from today's instances (name,
3434
+ * view, model, sources, currentState, ...), so the DiagnosticCheck hooks (onIntent, onModel,
3435
+ * onRender, onReducer, onDispose, sources) run unchanged through the core's dispatchers;
3436
+ * - what the checks did by patching an instance becomes a hook: SYG222's reducer wrapping
3437
+ * (wrapHandler), the reply / fetch / EVENTS taps of a sink (onSink), the ELEMENT check
3438
+ * (onElementCommand), the Collection check (onHostProps), the action log (actionLog.ts);
3439
+ * - the core's own dev codes: SYG423 (D168: a view context tracking skipped would have
3440
+ * rendered differently), SYG424 (D169/D177: duplicate Collection ids), SYG425 (D174: an
3441
+ * isolatedState child kept a slice that lacks its initialState keys), SYG148 / SYG149 (G-419:
3442
+ * a Collection's viewTransitionName that isn't an identifier, the same name twice on the
3443
+ * page after a patch, G-460: on rendered elements), SYG612 (D173: a form
3444
+ * 6.0 removed, met at runtime; once per form and component, with a link to the migration
3445
+ * guide).
3446
+ *
3447
+ * Only while diagnostics are on (the checks' own rule); production builds never load this.
3448
+ */
3449
+ const MIGRATE = 'https://sygnal.js.org/guide/migrating-to-6';
3450
+ /** the removed forms (D162-D164) by migration-guide anchor: what it was, how to write it now */
3451
+ const REMOVED = {
3452
+ 'string-tags': ['a component named by a string tag (<Name /> as a string, looked up in .components)', 'Import the component and use it as a JSX tag: <Badge />'],
3453
+ 'components': ['the .components registry', 'Import each component and use it as a JSX tag; remove .components'],
3454
+ 'collection-of-name': ['<Collection of="Name"> (a name)', 'Pass the component itself: <Collection of={Item} from="items" />'],
3455
+ 'collection-wrapper': ["the Collection's wrapper element (className, style, class, attrs, data-*, on, ref or hook on <Collection>)", 'Collection renders its items directly into its parent: put them on your own wrapping element, <ul className="x"><Collection of={Item} from="items" /></ul>'],
3456
+ 'child-select-name': ["CHILD.select('Name') (a name)", 'Pass the component itself: CHILD.select(Item)'],
3457
+ 'pipe-keys': ["'ACTION | SINK' model keys", "Use the object form: ACTION: { SINK: (state, data) => ... }"],
3458
+ 'peers': ['.peers', 'Render the peer as a sibling in the parent view'],
3459
+ 'hmractions': ['hmrActions', 'Remove hmrActions; sygnal/vite keeps the state across a hot update'],
3460
+ 'positional-views': ['a view with positional parameters (props, state, context, peers)', 'Destructure the one argument: function C({ state, context, ...props })'],
3461
+ 'source-names': ['DOMSourceName / stateSourceName', "Remove them: the sources are always DOM and STATE"],
3462
+ 'storecalculatedinstate': ['storeCalculatedInState', 'Remove it: calculated fields are always part of the state the view and reducers get'],
3463
+ 'component-factory': ['the component({ ... }) factory (with sources / isolateOpts)', 'Write a function component with statics (C.model, C.intent, C.initialState), or use defineComponent({ ... })'],
3464
+ };
3465
+ /** 4-H: the marker's props / data buckets the removed Collection wrapper element took */
3466
+ const WRAPPER = ['className', 'style', 'class', 'attrs', 'dataset', 'on', 'hook'];
3467
+ const STATICS = [['components', 'components'], ['peers', 'peers'], ['hmrActions', 'hmractions'], ['DOMSourceName', 'source-names'], ['stateSourceName', 'source-names'], ['storeCalculatedInState', 'storecalculatedinstate']];
3468
+ function removed(component, name, anchor, detail) {
3469
+ if (!once(`SYG612:${name}:${anchor}`))
3470
+ return;
3471
+ const [what, fix] = REMOVED[anchor];
3472
+ devReport('SYG612', {
3473
+ component: name,
3474
+ message: `${name} uses ${what}${detail ? ` (${detail})` : ''}, which Sygnal 6.0 removed, so it is ignored or fails`,
3475
+ fix: `${fix}. See ${MIGRATE}#${anchor}`,
3476
+ data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3477
+ });
3478
+ }
3479
+ /** the removed forms a definition shows (statics, view arity, model keys), once per function */
3480
+ let seenDefs = new WeakSet();
3481
+ // G-332: resetChecks() forgets them (as the once() dedupe)
3482
+ onReset(() => { seenDefs = new WeakSet(); });
3483
+ function removedStatics(f) {
3484
+ const view = f.view;
3485
+ if (!view || seenDefs.has(view))
3486
+ return;
3487
+ seenDefs.add(view);
3488
+ const name = f.name;
3489
+ if (view.isSygnalComponent)
3490
+ removed(f, name, 'component-factory');
3491
+ for (const [k, anchor] of STATICS)
3492
+ if (view[k] !== undefined && !(k == 'components' && isPlainObject(view[k]) && !Object.keys(view[k]).length))
3493
+ removed(f, name, anchor, k);
3494
+ // G-343: a defineComponent() wrapper's own arity is 1; the view it calls is __sygnalView
3495
+ const arity = typeof view == 'function' ? (view.__sygnalView || view).length : 0;
3496
+ if (arity > 1 && !view.__sygnalLazy && !view.isSygnalComponent)
3497
+ removed(f, name, 'positional-views', `${arity} parameters`);
3498
+ const model = view.model;
3499
+ if (isPlainObject(model))
3500
+ for (const k of Object.keys(model))
3501
+ if (k.includes('|'))
3502
+ removed(f, name, 'pipe-keys', `'${k}'`);
3503
+ }
3504
+ /** string tags left in a rendered vnode (the core renders them as elements) */
3505
+ function removedTags(f, v, depth = 0) {
3506
+ if (!v || typeof v != 'object' || depth > 300)
3507
+ return;
3508
+ const sel = v.sel;
3509
+ // G-329: a component name is PascalCase; an all-caps tag (h('SPAN'), h(el.tagName)) is an element
3510
+ const tag = typeof sel == 'string' ? sel.split(/[.#]/)[0] : '';
3511
+ if (/^[A-Z]/.test(tag) && /[a-z]/.test(tag) && !(v.data && v.data.c))
3512
+ removed(f, f.name, 'string-tags', `<${tag}>`);
3513
+ const kids = v.children;
3514
+ if (Array.isArray(kids))
3515
+ for (const k of kids)
3516
+ removedTags(f, k, depth + 1);
3517
+ }
3518
+ /** the instance fields the checks read from today's Component instances */
3519
+ class Facade {
3520
+ constructor(__next) {
3521
+ this.__next = __next;
3522
+ }
3523
+ get name() { return this.__next.name; }
3524
+ get view() { return this.__next.def.view; }
3525
+ get model() {
3526
+ const m = {};
3527
+ this.__next.def.handlers.forEach((hs, a) => { const e = m[a] = {}; for (const [s, fn] of hs)
3528
+ e[s] = fn; });
3529
+ return m;
3530
+ }
3531
+ get modelMap() {
3532
+ const m = {};
3533
+ this.__next.def.handlers.forEach((hs, a) => { m[a] = hs.map(h => h[0]); });
3534
+ // the built-in INITIALIZE (today's model map ends with it for every component)
3535
+ if (!m.INITIALIZE)
3536
+ m.INITIALIZE = ['STATE'];
3537
+ return m;
3538
+ }
3539
+ get sources() { return this._so || this.__next.sources; }
3540
+ get sourceNames() { return Object.keys(this.__next.sources); }
3541
+ get stateSourceName() { return 'STATE'; }
3542
+ get DOMSourceName() { return 'DOM'; }
3543
+ get isSubComponent() { return !this.__next.isRoot; }
3544
+ get initialState() { return this.__next.def.initialState; }
3545
+ get currentState() { return this.__next.state; }
3546
+ get currentProps() { return this.__next.props; }
3547
+ get currentContext() { return this.__next.context; }
3548
+ get _componentNumber() { return this.__next.id; }
3549
+ get calculated() { return this.__next.def.view.calculated; }
3550
+ get _calculatedFieldNames() { return this.__next.def.calcNames; }
3551
+ get context() { return this.__next.def.view.context; }
3552
+ get _uses() { return Object.entries(this.__next.def.view.uses || {}); }
3553
+ get _behaviorActions() { return this.__next.def.behaviorActions; }
3554
+ get intent$() { return { __sygnalTestActions: this.__next.def.testActions || [] }; }
3555
+ get _disposed() { return this.__next.disposed; }
3556
+ /** the state already has its calculated fields */
3557
+ addCalculated(s) { return s; }
3558
+ }
3559
+ const facades = new WeakMap();
3560
+ const fac = (iv) => {
3561
+ let f = facades.get(iv);
3562
+ if (!f)
3563
+ facades.set(iv, f = new Facade(iv));
3564
+ return f;
3565
+ };
3566
+ /** structural vnode equality (functions equal: a view may make new closures every call) */
3567
+ function same(a, b, d = 0) {
3568
+ if (a === b || (typeof a == 'function' && typeof b == 'function'))
3569
+ return true;
3570
+ if (!a || !b || typeof a != 'object' || typeof b != 'object')
3571
+ return a !== a && b !== b;
3572
+ if (d > 300)
3573
+ return true;
3574
+ if (Array.isArray(a) != Array.isArray(b))
3575
+ return false;
3576
+ const ka = Object.keys(a).filter(k => k != 'elm' && a[k] !== undefined), kb = Object.keys(b).filter(k => k != 'elm' && b[k] !== undefined);
3577
+ if (ka.length != kb.length)
3578
+ return false;
3579
+ for (const k of ka)
3580
+ if (!same(a[k], b[k], d + 1))
3581
+ return false;
3582
+ return true;
3583
+ }
3584
+ const SAMPLE = 16, CAP = 20;
3585
+ /** a CSS identifier's start, then identifier characters (`<prefix>-<id>` is one when the prefix starts one) */
3586
+ const IDENT = /^(--|-?[A-Za-z_\u0080-\uffff])[\w\u0080-\uffff-]*$/;
3587
+ /** G-419 SYG148: a Collection's viewTransitionName that is not a CSS identifier (true: a valid one) */
3588
+ function vtPrefix(name, p) {
3589
+ if (typeof p == 'string' && IDENT.test(p))
3590
+ return true;
3591
+ const shown = typeof p == 'string' ? JSON.stringify(p) : `{${typeof p}}`;
3592
+ if (once(`SYG148:${name}:${String(p)}`))
3593
+ devReport('SYG148', {
3594
+ component: name,
3595
+ message: `A Collection in ${name} has viewTransitionName=${shown}, which is not a CSS identifier, so every item's view-transition-name (${typeof p == 'string' ? p : '<prefix>'}-<id>) is invalid CSS: the browser ignores it and nothing animates`,
3596
+ fix: 'Use a CSS identifier as the prefix: letters, digits, - and _, not starting with a digit (viewTransitionName="card")',
3597
+ data: { value: p },
3598
+ });
3599
+ return false;
3600
+ }
3601
+ /**
3602
+ * G-460: an element the browser renders (a View Transition captures only those): not in a
3603
+ * display: none subtree (a hidden tab panel, a list shown only at another width). checkVisibility()
3604
+ * where there is one; else (jsdom) no display: none on it or an ancestor
3605
+ */
3606
+ function rendered(el) {
3607
+ if (!el?.isConnected)
3608
+ return false;
3609
+ if (typeof el.checkVisibility == 'function')
3610
+ return el.checkVisibility();
3611
+ const cs = el.ownerDocument?.defaultView?.getComputedStyle;
3612
+ for (let n = el; n && n.nodeType == 1; n = n.parentNode)
3613
+ if (n.hidden || cs?.(n).display == 'none')
3614
+ return false;
3615
+ return true;
3616
+ }
3617
+ /**
3618
+ * G-419 SYG149: two elements of the patched page with the same view-transition-name from
3619
+ * Collections with the same prefix (the same id shown in two of them): the browser skips the
3620
+ * whole View Transition. Walks the vnode tree (dev only, while a named Collection is mounted);
3621
+ * G-460: a name found twice is checked on the DOM after the patch (a microtask later; when a View
3622
+ * Transition defers the patch, again a little later): only rendered elements count. Returns the
3623
+ * groups of vnodes with one name (none: an empty list)
3624
+ */
3625
+ function vtDuplicates(root, prefixes) {
3626
+ const seen = new Map(), dup = [];
3627
+ const stack = [root];
3628
+ while (stack.length) {
3629
+ const v = stack.pop();
3630
+ if (!v || typeof v != 'object')
3631
+ continue;
3632
+ const st = v.data?.style, n = st?.viewTransitionName;
3633
+ if (typeof n == 'string' && prefixes.has(st.viewTransitionClass)) {
3634
+ const g = seen.get(n);
3635
+ if (!g)
3636
+ seen.set(n, [v]);
3637
+ else if (g.push(v) == 2)
3638
+ dup.push(g);
3639
+ }
3640
+ if (Array.isArray(v.children))
3641
+ for (let i = v.children.length; i--;)
3642
+ stack.push(v.children[i]);
3643
+ }
3644
+ return dup;
3645
+ }
3646
+ // G-538: a fragment root never gets an element (the DOM driver flattens it into the container):
3647
+ // it has run when each of its children has one (a kept child has its old one: a View Transition
3648
+ // patch of a fragment root whose children are all kept is checked before it runs)
3649
+ const patched = (v) => v.elm || !v.sel && v.children?.every((c) => !c || patched(c));
3650
+ /**
3651
+ * G-527: the patches waiting for their SYG149 check ([root vnode, duplicate groups], newest last,
3652
+ * at most 8) and one chain of checks over them: a microtask after the first, then every 50 ms
3653
+ * while some wait. The patch of a root has run when it has its element (a View Transition patches
3654
+ * in its update callback, and replaces a held vnode it hasn't patched yet; kept vnodes have their
3655
+ * old elements before that): each check takes the newest patch that ran and drops it and the older
3656
+ * ones. A newer patch joins the queue and doesn't restart the chain, so patches closer than 50 ms
3657
+ * (View Transitions back to back) can't starve it. A patch without duplicates joins only a queue
3658
+ * that waits (once it has run, the older ones are moot). G-501: the chain stops once the app (or
3659
+ * every named Collection) is gone, and after 10 tries without a patched root (the mock DOM; a root
3660
+ * that never gets its element isn't checked on its children's old elements)
3661
+ */
3662
+ function vtChecker(live) {
3663
+ let q = [], busy = false, tries = 0;
3664
+ const vtRendered = () => {
3665
+ if (!live()) {
3666
+ q = [];
3667
+ busy = false;
3668
+ return;
3669
+ }
3670
+ let i = q.length;
3671
+ while (i-- && !patched(q[i][0]))
3672
+ ;
3673
+ if (i >= 0) {
3674
+ vtReport(q[i][1]);
3675
+ q = q.slice(i + 1);
3676
+ tries = 0;
3677
+ }
3678
+ else
3679
+ tries++;
3680
+ if (!q.length || tries > 10) {
3681
+ q = [];
3682
+ busy = false;
3683
+ return;
3684
+ }
3685
+ setTimeout(vtRendered, 50);
3686
+ };
3687
+ return (root, dup) => {
3688
+ if (!dup.length && !q.length)
3689
+ return;
3690
+ q.push([root, dup]);
3691
+ if (q.length > 8)
3692
+ q.shift();
3693
+ if (!busy) {
3694
+ busy = true;
3695
+ tries = 0;
3696
+ queueMicrotask(vtRendered);
3697
+ }
3698
+ };
3699
+ }
3700
+ function vtReport(dup) {
3701
+ for (const g of dup) {
3702
+ const v = g[0], st = v.data.style, n = st.viewTransitionName;
3703
+ if (g.filter(x => rendered(x.elm)).length < 2 || !once(`SYG149:${n}`))
3704
+ continue;
3705
+ devReport('SYG149', {
3706
+ component: 'Collection',
3707
+ message: `Two elements on the page have view-transition-name '${n}': Collections with viewTransitionName="${st.viewTransitionClass}" show the item with that id at the same time. Names must be unique when a View Transition starts, so the browser skips every transition while both are shown`,
3708
+ fix: `Give Collections that can show the same item at once different prefixes (viewTransitionName="${st.viewTransitionClass}" and another), or name the item yourself with its own style`,
3709
+ data: { name: n, prefix: st.viewTransitionClass },
3710
+ });
3711
+ }
3712
+ }
3713
+ /** The hooks one app gets (installChecks publishes this on the bridge). */
3714
+ function nextHooks(_api) {
3715
+ const core = bridge(), H = core.hooks;
3716
+ const on = () => H.on();
3717
+ const modelOf = (f) => f.modelMap;
3718
+ const wired = new WeakSet();
3719
+ const wire = (iv, names) => {
3720
+ const f = fac(iv);
3721
+ if (wired.has(f))
3722
+ return;
3723
+ wired.add(f);
3724
+ H.onIntent(f, names);
3725
+ H.onModel(f, modelOf(f));
3726
+ };
3727
+ // D168: the views context tracking skipped, checked on a sample (each instance's first two
3728
+ // skips, then 1 in 16; at most CAP per tick)
3729
+ const skips = new WeakMap();
3730
+ let budget = 0;
3731
+ const log = actionHooks(actionListener, () => on());
3732
+ // G-419: the viewTransitionName prefixes this app's Collections use; G-460: per instance that
3733
+ // renders them, so the SYG149 walk stops once every one of them is disposed
3734
+ const vtp = new Map();
3735
+ // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
3736
+ // that renders a named Collection is disposed, the app's dispose included)
3737
+ const vtCheck = vtChecker(() => vtp.size && on());
3738
+ return {
3739
+ onCreate(iv) {
3740
+ if (!on())
3741
+ return;
3742
+ log.onCreate(iv);
3743
+ const f = fac(iv);
3744
+ removedStatics(f);
3745
+ if (!iv.def.intent)
3746
+ wire(iv, []);
3747
+ },
3748
+ wrapSources(iv, so) {
3749
+ const f = fac(iv);
3750
+ f._so = so;
3751
+ let out;
3752
+ try {
3753
+ out = H.sourcesFor(f);
3754
+ }
3755
+ finally {
3756
+ f._so = undefined;
3757
+ }
3758
+ if (!on() || typeof Proxy != 'function')
3759
+ return out;
3760
+ // D173: CHILD.select('Name')
3761
+ const s = out || so;
3762
+ return new Proxy(s, {
3763
+ get(t, k) {
3764
+ const v = t[k];
3765
+ if (k !== 'CHILD' || !v || typeof v.select != 'function')
3766
+ return v;
3767
+ return { ...v, select: (x, ...r) => { if (typeof x == 'string')
3768
+ removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`); return v.select(x, ...r); } };
3769
+ },
3770
+ });
3771
+ },
3772
+ onIntent(iv, names) {
3773
+ wire(iv, names);
3774
+ },
3775
+ onRender(iv, vnode) {
3776
+ const f = fac(iv);
3777
+ H.onRender(f, vnode);
3778
+ if (on())
3779
+ removedTags(f, vnode);
3780
+ },
3781
+ onReducer(iv, type, prev, next) {
3782
+ H.onReducer(fac(iv), type, prev, next, 'STATE');
3783
+ },
3784
+ onDispose(iv) {
3785
+ vtp.delete(iv);
3786
+ H.onDispose(fac(iv));
3787
+ },
3788
+ onAction(iv, a) { log.onAction(iv, a); },
3789
+ wrapHandler(iv, type, sink, fn) {
3790
+ let h = log.wrapHandler(iv, type, sink, fn) || fn;
3791
+ // SYG222: the STATE reducers, wrapped (today the check rewrote the instance's model)
3792
+ if (on() && sink == 'STATE' && typeof fn == 'function')
3793
+ h = watchMutation(fac(iv), type, h);
3794
+ return h === fn ? undefined : h;
3795
+ },
3796
+ onSink(iv, type, sink, v) {
3797
+ if (!on())
3798
+ return;
3799
+ const f = fac(iv), src = iv.sources[sink];
3800
+ try {
3801
+ if (sink == 'EVENTS')
3802
+ eventEmitted(f, v);
3803
+ if (src && src.__sygnalReplies === true)
3804
+ checkRequest(f, sink, v, Object.keys(f.modelMap));
3805
+ if (src && src.__sygnalStatic === 'resources')
3806
+ check(f, sink, v);
3807
+ }
3808
+ catch (_) { /* a check never breaks the app */ }
3809
+ },
3810
+ onElementCommand(iv, cmd) {
3811
+ if (!on())
3812
+ return;
3813
+ const f = fac(iv);
3814
+ for (const c of [].concat(cmd))
3815
+ checkSentCommand(f, c);
3816
+ },
3817
+ onHostProps(owner, sel, props, data) {
3818
+ if (!on() || (sel != 'collection' && sel != 'virtual-collection'))
3819
+ return;
3820
+ const f = fac(owner);
3821
+ if (typeof props.of == 'string')
3822
+ removed(f, f.name, 'collection-of-name', `of="${props.of}"`);
3823
+ // 4-H (D229): what went only to the removed wrapper div (VirtualCollection keeps its container)
3824
+ if (sel == 'collection') {
3825
+ const w = WRAPPER.filter(k => k == 'className' ? props.className != null : data?.[k] != null);
3826
+ if (w.length)
3827
+ removed(f, f.name, 'collection-wrapper', w.map(k => k == 'dataset' ? 'data-*' : k == 'hook' ? 'ref / hook' : k).join(', '));
3828
+ }
3829
+ try {
3830
+ checkCollection(f, { data: { props } });
3831
+ }
3832
+ catch (_) { /* ignore */ }
3833
+ // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
3834
+ // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
3835
+ const p = props.viewTransitionName;
3836
+ if (p && vtPrefix(f.name, p)) {
3837
+ let s = vtp.get(owner);
3838
+ if (!s)
3839
+ vtp.set(owner, s = new Set());
3840
+ s.add(p);
3841
+ }
3842
+ },
3843
+ onPatch(vnode) {
3844
+ if (!vtp.size || !on())
3845
+ return;
3846
+ const all = new Set();
3847
+ for (const s of vtp.values())
3848
+ for (const p of s)
3849
+ all.add(p);
3850
+ try {
3851
+ vtCheck(vnode, vtDuplicates(vnode, all));
3852
+ }
3853
+ catch (_) { /* a check never breaks the app */ }
3854
+ },
3855
+ onDuplicateKey(owner, key) {
3856
+ if (!on())
3857
+ return;
3858
+ const name = owner.name;
3859
+ if (!once(`SYG424:${name}:${String(key)}`))
3860
+ return;
3861
+ devReport('SYG424', {
3862
+ component: name,
3863
+ message: `A Collection in ${name} has more than one item with the id ${JSON.stringify(key) ?? String(key)} (duplicate ids); only the first of them renders, and their writes go to it`,
3864
+ fix: 'Give every item a unique id (or no id: an item without one is keyed by its index)',
3865
+ data: { id: key },
3866
+ });
3867
+ },
3868
+ onStateSeed(iv, slice, init) {
3869
+ if (!on() || !isPlainObject(init) || !isPlainObject(slice))
3870
+ return;
3871
+ // G-333: a key initialState sets to undefined is no data the slice lacks
3872
+ const missing = Object.keys(init).filter(k => init[k] !== undefined && !(k in slice));
3873
+ const name = iv.name;
3874
+ if (!missing.length || !once(`SYG425:${name}:${missing.join(',')}`))
3875
+ return;
3876
+ const list = missing.map(k => `'${k}'`).join(', ');
3877
+ devReport('SYG425', {
3878
+ component: name,
3879
+ message: `${name} has isolatedState and is bound to a slice of its parent's state that already exists, so it keeps that slice instead of its initialState; the slice has no ${list}, which ${name}.initialState defines`,
3880
+ fix: `To start ${name} from its initialState instead, add resetState to the tag (<${name} state="..." resetState />). To keep the parent's data, initialize ${list} in the parent's state`,
3881
+ data: { missing, slice: Object.keys(slice) },
3882
+ });
3883
+ },
3884
+ onContextSkip(iv, render, prev, next, keys) {
3885
+ if (!on() || !prev || !next)
3886
+ return;
3887
+ const n = (skips.get(iv) || 0) + 1;
3888
+ skips.set(iv, n);
3889
+ if ((n > 2 && n % SAMPLE) || budget >= CAP)
3890
+ return;
3891
+ if (!budget++)
3892
+ queueMicrotask(() => { budget = 0; });
3893
+ let a, b, c;
3894
+ try {
3895
+ a = render(prev);
3896
+ b = render(next);
3897
+ c = render(prev);
3898
+ }
3899
+ catch (_) {
3900
+ return;
3901
+ }
3902
+ // G-327: a control render: a view whose output differs between two calls with the same
3903
+ // context (a render counter, Date.now(), Math.random(), generated ids) proves nothing
3904
+ if (same(a, b) || !same(a, c))
3905
+ return;
3906
+ const name = iv.name;
3907
+ const changed = Object.keys(next).filter(k => next[k] !== prev[k]);
3908
+ if (!once(`SYG423:${name}`))
3909
+ return;
3910
+ reportSafely('SYG423', {
3911
+ severity: DEV_CODE_SEVERITY.SYG423,
3912
+ component: name,
3913
+ message: `The context keys ${changed.map(k => `'${k}'`).join(', ')} changed and ${name}'s view was not re-rendered, because the view read none of them during its last call (it read: ${keys.length ? keys.map(k => `'${k}'`).join(', ') : 'nothing'}). Calling it with the new context renders something else, so the page shows a stale render`,
3914
+ fix: `Read context entries in the view itself (context.theme), not through a value kept from an earlier render or a function that runs later; Object.getOwnPropertyDescriptor and similar reflection aren't tracked`,
3915
+ data: { keysRead: keys, changed },
3916
+ });
3917
+ },
3918
+ };
3919
+ }
3920
+
1218
3921
  /** Every runtime check in this entry. */
1219
3922
  const checks = [
1220
3923
  wiringCheck,
@@ -1223,7 +3926,20 @@ const checks = [
1223
3926
  propsCheck,
1224
3927
  collectionsCheck,
1225
3928
  domCheck,
3929
+ shorthandCheck,
3930
+ controlsCheck,
3931
+ elementCommandsCheck,
3932
+ behaviorsCheck,
3933
+ formsCheck,
3934
+ datasetCheck,
1226
3935
  strictCheck,
3936
+ repliesCheck,
3937
+ routerCheck,
3938
+ fetchCheck,
3939
+ timersCheck,
3940
+ persistCheck,
3941
+ viewTransitionsCheck,
3942
+ staticsCheck,
1227
3943
  inspectCheck,
1228
3944
  ];
1229
3945
  /**
@@ -1240,10 +3956,30 @@ function installChecks() {
1240
3956
  const unregister = checks.map(check => core.registerCheck(check));
1241
3957
  const uninstallHints = installRxjsHints(Stream && Stream.prototype);
1242
3958
  const uninstallInspect = installInspect();
3959
+ const uninstallRouter = installRouterHooks();
3960
+ const uninstallControls = installControlHooks();
3961
+ const uninstallTimers = installTimerHooks();
3962
+ const uninstallBrowser = installBrowserSourceHooks();
3963
+ const uninstallElementCommands = installElementCommandHooks();
3964
+ const uninstallWidgets = installWidgetHooks();
3965
+ const uninstallVirtual = installVirtualHooks();
3966
+ const uninstallForms = installFormHooks();
3967
+ const uninstallSortable = installSortableHooks();
3968
+ (core.layers || (core.layers = new Set())).add(nextHooks);
1243
3969
  const uninstall = () => {
3970
+ core.layers?.delete(nextHooks);
1244
3971
  unregister.forEach(fn => fn());
1245
3972
  uninstallHints();
1246
3973
  uninstallInspect();
3974
+ uninstallRouter();
3975
+ uninstallControls();
3976
+ uninstallTimers();
3977
+ uninstallBrowser();
3978
+ uninstallElementCommands();
3979
+ uninstallWidgets();
3980
+ uninstallVirtual();
3981
+ uninstallForms();
3982
+ uninstallSortable();
1247
3983
  if (core.__uninstallChecks === uninstall)
1248
3984
  core.__uninstallChecks = undefined;
1249
3985
  };