sygnal 5.4.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Browser sources (PLAN-5 B-3, src/extra/browserSources.ts). Dev-entry-only codes
3
+ * (DEV_CODE_SEVERITY), reported when the driver calls
4
+ * `__SYGNAL_DIAGNOSTICS__.browserSource(code, name, spec, componentName, extra)`:
5
+ *
6
+ * SYG663 — a `browser` entry the driver can't start (no known kind, no `action`, an intersection
7
+ * / resize target that is neither a selector nor `true`), or a BROWSER sink command
8
+ * with an unknown method. It isn't started / run.
9
+ * SYG664 — a kind the driver wasn't made with (`makeBrowserDriverWith(...)` without its source).
10
+ * SYG665 — a source or command failed (permission denied, API missing, storage blocked) and its
11
+ * spec names no `error` action, so nothing else would say so.
12
+ * SYG668 — an intersection / resize declaration with nothing to observe: no DOM source reached
13
+ * the driver for the instance (extra 'dom'), or, in renderComponent's fake with
14
+ * `dom: 'real'`, its selector matches no element of the component (extra 'none').
15
+ *
16
+ * A component declaring `browser` with no browser driver is SYG643 (checks/timers.ts).
17
+ */
18
+ import {bridge, devReport, once} from './shared'
19
+
20
+ const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation']
21
+ const SOURCE: Record<string, string> = {
22
+ intersection: 'intersectionSource', resize: 'resizeSource', media: 'mediaSource', storage: 'storageSource',
23
+ visibility: 'visibilitySource', online: 'onlineSource', geolocation: 'geolocationSource',
24
+ copy: 'clipboardSource', paste: 'clipboardSource', setItem: 'storageSource', removeItem: 'storageSource',
25
+ }
26
+ const brief = (v: any) => { try { return JSON.stringify(v) } catch (_) { return String(v) } }
27
+
28
+ /** why a declared spec can't start ('' when it can; mirrors browserDriver's checks) */
29
+ export function browserSpecProblem(s: any): string {
30
+ if (!s || typeof s != 'object') return `is not an object ({ ${KINDS.join(' | ')}: …, action })`
31
+ const k = KINDS.find(k => k in s)
32
+ if (!k) return `names no source (one of ${KINDS.join(', ')})`
33
+ if (!s.action || typeof s.action != 'string') return 'has no action (a string)'
34
+ if ((k == 'intersection' || k == 'resize') && s[k] !== true && (typeof s[k] != 'string' || !s[k])) return `${k} must be a selector in the component or true (its root element), got ${brief(s[k])}`
35
+ return ''
36
+ }
37
+
38
+ function onBrowserSource(code: string, name: string, spec: any, component?: string, extra?: any, commands?: string[]) {
39
+ if (code == 'SYG663' && extra === 0) {
40
+ // an unknown command (extra 0; `commands`: the driver's)
41
+ if (!once(`SYG663:${component}:cmd:${name}`)) return
42
+ devReport('SYG663', {
43
+ component,
44
+ message: `BROWSER command '${name}' is unknown; it is not run`,
45
+ fix: `Name the method as a key of the command: ${(commands || []).map(c => `{ ${c}: … }`).join(', ') || 'this driver takes no commands'}`,
46
+ data: {name, command: spec},
47
+ })
48
+ return
49
+ }
50
+ if (code == 'SYG663') {
51
+ if (!once(`SYG663:${component}:${name}:${brief(spec)}`)) return
52
+ devReport('SYG663', {
53
+ component,
54
+ message: `browser entry '${name}' ${browserSpecProblem(spec) || 'is invalid'}; it is not started`,
55
+ fix: `Declare ${name}: { media: '(prefers-color-scheme: dark)', action: 'DARK' }, { intersection: '.card', action: 'SEEN' }, … (falsy to stop it)`,
56
+ data: {name, spec},
57
+ })
58
+ } else if (code == 'SYG664') {
59
+ if (!once(`SYG664:${component}:${name}`)) return
60
+ devReport('SYG664', {
61
+ component,
62
+ message: `browser entry '${name}' uses the ${extra} source, which this browser driver wasn't made with; it is not started`,
63
+ fix: `Add it: makeBrowserDriverWith(${SOURCE[extra]}, …), or use makeBrowserDriver() (every source)`,
64
+ data: {name, spec, kind: extra},
65
+ })
66
+ } else if (code == 'SYG668') {
67
+ if (!once(`SYG668:${component}:${name}:${extra}`)) return
68
+ const k = KINDS.find(k => spec && k in spec) || 'intersection', target = spec?.[k]
69
+ devReport('SYG668', {
70
+ component,
71
+ message: extra == 'none'
72
+ ? `browser entry '${name}' observes ${brief(target)}, which matches no element of the component; it reports nothing`
73
+ : `browser entry '${name}' (${k}) has no DOM to observe: the component's DOM source didn't reach the browser driver; it reports nothing`,
74
+ fix: extra == 'none'
75
+ ? `Use a selector the component's own view renders (or true for its root element), and declare the entry only while that element exists`
76
+ : `Run the app with a DOM driver (run() with a mount point) and declare intersection / resize in a component's own \`browser\` static`,
77
+ data: {name, spec, reason: extra},
78
+ })
79
+ } else if (code == 'SYG665') {
80
+ if (!once(`SYG665:${component}:${name}:${extra?.name ?? extra?.code}`)) return
81
+ devReport('SYG665', {
82
+ component,
83
+ message: `browser ${spec && 'action' in spec ? `entry '${name}'` : `command '${name}'`} failed (${extra?.message || extra?.name || 'error'}) and names no error action`,
84
+ fix: `Name one to handle it, e.g. { ..., error: 'FAILED' }, and a model entry FAILED: (state, { message }) => ...`,
85
+ data: {name, spec, failure: extra},
86
+ })
87
+ }
88
+ }
89
+
90
+ /** install the hook the browser driver calls (removed by the returned function) */
91
+ export function installBrowserSourceHooks(): () => void {
92
+ const core = bridge()
93
+ if (!core) return () => {}
94
+ core.browserSource = onBrowserSource
95
+ return () => { if (core.browserSource === onBrowserSource) core.browserSource = undefined }
96
+ }
@@ -20,7 +20,7 @@ import {reportSafely, once, nameOf, isPlainObject} from './shared'
20
20
  const describe = (v: any): string =>
21
21
  v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`
22
22
 
23
- function checkCollection(component: any, el: any): void {
23
+ export function checkCollection(component: any, el: any): void {
24
24
  const props = (el && el.data && el.data.props) || {}
25
25
  const from = props.from
26
26
  const of = props.of
@@ -0,0 +1,148 @@
1
+ /**
2
+ * PLAN-4 CT-1, runtime control checks (dev entry only; the static ones are in sygnal-check).
3
+ *
4
+ * SYG124 (error): a component passed where a control or selector is expected, as in
5
+ * `DOM.click(TodoItem)` or `DOM.select(TodoItem)`. Mechanism: the `sources` hook wraps the
6
+ * component's DOM source in a Proxy; `select()` (also chained: `DOM.select('.x').select(C)`)
7
+ * and every event shorthand check their first argument. A function that isn't a control is a
8
+ * component. Reported once per component and argument; the call then goes on to the DOM
9
+ * source, whose select() still throws ("expects the argument to be a string").
10
+ *
11
+ * SYG125 (error): controls are elements, not components.
12
+ * - a control given `.intent`, `.model` or `.initialState` (reported when it first renders,
13
+ * the statics are set after controls() returns);
14
+ * - a spec object whose `vnode()` returns anything but one element vnode (text, nothing, an
15
+ * array, a component). With diagnostics on, the render then throws an Error with the
16
+ * SYG125 text, so the component's error boundary names the control instead of failing on
17
+ * the stamp.
18
+ * Mechanism: controls() (src/extra/controls.ts) calls `__SYGNAL_DIAGNOSTICS__.control(control,
19
+ * vnode)` on every render; installControlHooks() publishes it on the core bridge.
20
+ *
21
+ * The same hook remembers which control rendered each vnode (controlOfVnode), so inspect() can
22
+ * list a component's controls with their element or kind.
23
+ */
24
+ import type {DiagnosticCheck} from '../index'
25
+ import {bridge, devReport, once, onReset, nameOf, report} from './shared'
26
+ import {DEV_CODE_SEVERITY} from '../codes'
27
+
28
+ /** the key of a control, from its selector `[data-control="<Key>"]` */
29
+ export const controlKey = (control: any): string => String(control).slice(15, -2)
30
+
31
+ /** a control selector `[data-control="<Key>"]` as its key, else undefined */
32
+ export const keyOfSelector = (selector: string): string | undefined => {
33
+ const m = /^\[data-control="([^"]+)"\]$/.exec(selector)
34
+ return m ? m[1] : undefined
35
+ }
36
+
37
+ const isControl = (x: any): boolean => typeof x == 'function' && !!x.__sygnalControl
38
+
39
+ const componentName = (fn: any): string =>
40
+ fn.componentName || fn.label || fn.name || 'the component'
41
+
42
+ let vnodeControls = new WeakMap<object, any>()
43
+ let staticsSeen = new WeakSet<object>()
44
+ onReset(() => {
45
+ vnodeControls = new WeakMap()
46
+ staticsSeen = new WeakSet()
47
+ })
48
+
49
+ /** the control that rendered a vnode (only while the dev entry is installed) */
50
+ export const controlOfVnode = (vnode: any): any =>
51
+ vnode && typeof vnode == 'object' ? vnodeControls.get(vnode) : undefined
52
+
53
+ const STATICS = ['intent', 'model', 'initialState']
54
+
55
+ const describe = (v: any): string =>
56
+ v === undefined || v === null ? String(v)
57
+ : Array.isArray(v) ? `an array of ${v.length}`
58
+ : typeof v != 'object' ? `a ${typeof v} (${JSON.stringify(v)})`
59
+ : typeof v.sel != 'string' ? (v.text !== undefined ? 'a text vnode' : 'a fragment')
60
+ // the pragma's component placeholder (the component function in data.c)
61
+ : v.data && typeof v.data.c == 'function' ? `the component <${v.sel}>`
62
+ : 'an element vnode'
63
+
64
+ function onControl(control: any, vnode: any): void {
65
+ const key = controlKey(control)
66
+ if (vnode && typeof vnode == 'object') vnodeControls.set(vnode, control)
67
+
68
+ if (!staticsSeen.has(control)) {
69
+ staticsSeen.add(control)
70
+ const given = STATICS.filter(s => control[s] != null)
71
+ if (given.length) {
72
+ devReport('SYG125', {
73
+ 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`,
74
+ 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`,
75
+ data: {control: key, statics: given},
76
+ })
77
+ }
78
+ }
79
+
80
+ const what = describe(vnode)
81
+ if (what == 'an element vnode') return
82
+ const kind = control.kind
83
+ // report() (not devReport): in 'error' mode its DiagnosticError is thrown into the render
84
+ const d = report('SYG125', {
85
+ severity: DEV_CODE_SEVERITY.SYG125,
86
+ message: `The control ${key}'s spec${kind ? ` (kind '${kind}')` : ''} vnode() returned ${what}, but it must return one element vnode`,
87
+ fix: `Return a single element built with the h argument: vnode(props, children, h) { return h('div', props, ...children) }`,
88
+ data: {control: key, kind, returned: what},
89
+ })
90
+ if (d) throw new Error(d.text)
91
+ }
92
+
93
+ /** Publish the control hook on the core bridge. Returns an uninstall function. */
94
+ export function installControlHooks(): () => void {
95
+ const core = bridge()
96
+ if (!core) return () => {}
97
+ core.control = onControl
98
+ return () => { if (core.control === onControl) core.control = undefined }
99
+ }
100
+
101
+ const wrapped = new WeakMap<object, any>()
102
+
103
+ /** a DOM source whose select() and event shorthands report SYG124 for a component argument */
104
+ function checkedDOM(component: any, dom: any): any {
105
+ if (!dom || typeof dom != 'object') return dom
106
+ let proxy = wrapped.get(dom)
107
+ if (proxy) return proxy
108
+ proxy = new Proxy(dom, {
109
+ get(t: any, k: any) {
110
+ const v = t[k]
111
+ if (typeof k != 'string' || typeof v != 'function' || (k != 'select' && k in t)) return v
112
+ return function (this: any, arg: any, ...rest: any[]) {
113
+ if (typeof arg == 'function' && !isControl(arg)) {
114
+ const name = nameOf(component), child = componentName(arg)
115
+ if (once(`SYG124:${name}:${child}:${k}`)) {
116
+ const call = k == 'select' ? `DOM.select(${child})` : `DOM.${k}(${child})`
117
+ devReport('SYG124', {
118
+ component,
119
+ 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}`,
120
+ 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)`,
121
+ data: {component: child, method: k},
122
+ })
123
+ }
124
+ }
125
+ const out = v.call(t, arg, ...rest)
126
+ return k == 'select' ? checkedDOM(component, out) : out
127
+ }
128
+ },
129
+ })
130
+ wrapped.set(dom, proxy)
131
+ return proxy
132
+ }
133
+
134
+ export const controlsCheck: DiagnosticCheck = {
135
+ id: 'controls',
136
+
137
+ sources(component, sources) {
138
+ if (!sources || typeof Proxy != 'function') return
139
+ const domName = component?.DOMSourceName || 'DOM'
140
+ if (!sources[domName]) return
141
+ return new Proxy(sources, {
142
+ get(t: any, k: any) {
143
+ const v = t[k]
144
+ return k === domName ? checkedDOM(component, v) : v
145
+ },
146
+ })
147
+ },
148
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SYG421 — invalid `data` (dataset) key in a view (error, G-143).
3
+ *
4
+ * JSX `data={{ ... }}` becomes the vnode's `data.dataset` (a `data-x-y="..."` attribute is
5
+ * camelCased to 'xY' by the pragma, G-152), which the DOM driver writes with
6
+ * `element.dataset[key] = value`. The browser throws a SyntaxError DOMException for a key with
7
+ * a hyphen followed by a lower-case letter (`'task-id'`), and setAttribute() throws for a name
8
+ * with characters an attribute can't have (spaces, quotes, `=`, ...). The patch fails, so the
9
+ * app stops rendering, and the console shows only a bare "DOMException {}".
10
+ *
11
+ * Mechanism: onRender (before the DOM driver patches) walks the rendered vnode tree and checks
12
+ * every dataset key. Reported once per key; the mock DOM of renderComponent never throws, so
13
+ * this is also the only signal there.
14
+ */
15
+ import type {DiagnosticCheck} from '../index'
16
+ import {devReport, once, nameOf} from './shared'
17
+
18
+ const camel = (key: string): string => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
19
+ const BAD_CHAR = /[\s"'<>\/=`]/
20
+
21
+ function problem(key: string): string | undefined {
22
+ if (/-[a-z]/.test(key)) return `has a hyphen followed by a lower-case letter, which a dataset name can't have`
23
+ if (BAD_CHAR.test(key)) return `contains a character an HTML attribute name can't have`
24
+ return undefined
25
+ }
26
+
27
+ function walk(vnode: any, visit: (dataset: Record<string, any>, sel: string) => void, depth = 0): void {
28
+ if (!vnode || typeof vnode != 'object' || depth > 500) return
29
+ const ds = vnode.data && vnode.data.dataset
30
+ if (ds && typeof ds == 'object') visit(ds, vnode.sel)
31
+ const children = vnode.children
32
+ if (Array.isArray(children)) for (const c of children) walk(c, visit, depth + 1)
33
+ }
34
+
35
+ export const datasetCheck: DiagnosticCheck = {
36
+ id: 'dataset',
37
+
38
+ onRender(component, rootVnode) {
39
+ walk(rootVnode, (dataset, sel) => {
40
+ for (const key of Object.keys(dataset)) {
41
+ const why = problem(key)
42
+ if (!why || !once(`SYG421:${key}`)) continue
43
+ const good = camel(key).replace(new RegExp(BAD_CHAR.source, 'g'), '')
44
+ const attr = good.replace(/[A-Z]/g, c => '-' + c.toLowerCase())
45
+ const name = good || 'name'
46
+ devReport('SYG421', {
47
+ component,
48
+ message: `The view of ${nameOf(component)} renders the data key '${key}'${sel ? ` on <${String(sel).split(/[.#]/)[0]}>` : ''} ` +
49
+ `(from data={{ '${key}': … }}), but '${key}' ${why}. ` +
50
+ `Writing it throws a DOMException, which can stop rendering`,
51
+ fix: `Use a camelCase key in the data prop: data={{ ${name}: … }} renders the attribute data-${attr || 'name'}; read it with .data('${name}')`,
52
+ data: {key, suggested: good, attribute: `data-${attr}`},
53
+ })
54
+ }
55
+ })
56
+ },
57
+ }
@@ -37,6 +37,7 @@ import type {DiagnosticCheck} from '../index'
37
37
  import {ScopeChecker} from '../../../cycle/dom/ScopeChecker'
38
38
  import type {Scope} from '../../../cycle/dom/isolate'
39
39
  import {reportSafely, once, onReset, timing, nameOf} from './shared'
40
+ import {keyOfSelector} from './controls'
40
41
 
41
42
  interface Tracked {
42
43
  name: string
@@ -83,7 +84,9 @@ function viewSource(component: any): string {
83
84
 
84
85
  /** class / id / tag tokens of a selector: '.a .b-c > #d' -> ['a', 'b-c', 'd'] */
85
86
  const tokensOf = (selector: string): string[] =>
86
- (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [])
87
+ (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [] as string[])
88
+ // CT-1: a control's key (`<Add>` / `h(Add)` in the view source)
89
+ .concat(Array.from(selector.matchAll(/\[data-control="([^"]+)"\]/g), m => m[1]))
87
90
 
88
91
  function appearsInView(component: any, selector: string): boolean {
89
92
  const src = viewSource(component)
@@ -93,12 +96,13 @@ function appearsInView(component: any, selector: string): boolean {
93
96
  return tokens.every(t => new RegExp(`(^|[^\\w-])${t.replace(/[-]/g, '\\-')}($|[^\\w-])`).test(src))
94
97
  }
95
98
 
96
- function rootElementOf(t: Tracked): Element | undefined {
99
+ // G-144: a fragment-rooted component has several root elements
100
+ function rootElementsOf(t: Tracked): Element[] {
97
101
  try {
98
102
  const im = t.source._isolateModule
99
- return im && im.getElement(scopesOf(t.source.namespace))
103
+ return (im && im.getElements(scopesOf(t.source.namespace))) || []
100
104
  } catch (_) {
101
- return undefined
105
+ return []
102
106
  }
103
107
  }
104
108
 
@@ -133,8 +137,8 @@ function report103(component: any, t: Tracked, selector: string, severity: 'info
133
137
 
134
138
  function sweep(component: any, t: Tracked, escalate: boolean): void {
135
139
  if (t.disposed) return
136
- const root = rootElementOf(t)
137
- if (!root || typeof root.querySelectorAll !== 'function') return
140
+ const roots = rootElementsOf(t).filter(r => typeof r.querySelectorAll === 'function')
141
+ if (!roots.length) return
138
142
  const im = t.source._isolateModule
139
143
  const checker = new ScopeChecker(t.source.namespace, im)
140
144
  let unmatched = false
@@ -144,8 +148,11 @@ function sweep(component: any, t: Tracked, escalate: boolean): void {
144
148
  if (matched.has(matchKey) || t.crossed.has(selector)) continue
145
149
  let all: Element[]
146
150
  try {
147
- all = Array.prototype.slice.call(root.querySelectorAll(selector))
148
- if (root.matches(selector)) all.push(root)
151
+ all = []
152
+ for (const root of roots) {
153
+ all.push(...Array.prototype.slice.call(root.querySelectorAll(selector)))
154
+ if (root.matches(selector)) all.push(root)
155
+ }
149
156
  } catch (_) {
150
157
  continue // invalid selector: the DOM driver reports that itself
151
158
  }
@@ -156,11 +163,13 @@ function sweep(component: any, t: Tracked, escalate: boolean): void {
156
163
  t.crossed.add(selector)
157
164
  if (!once(`SYG104:${t.name}:${selector}`)) continue
158
165
  const child = childNameFor(t, all) || 'a child component'
166
+ // CT-1: a control is named by its identifier
167
+ const control = keyOfSelector(selector)
159
168
  reportSafely('SYG104', {
160
169
  component,
161
- message: `DOM.select('${selector}') in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
162
- fix: `Handle the event in ${child} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
163
- data: {selector, child},
170
+ message: `${control ? `The control ${control}` : `DOM.select('${selector}')`} in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
171
+ 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`,
172
+ data: control ? {selector, child, control} : {selector, child},
164
173
  })
165
174
  continue
166
175
  }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * PLAN-4 GS-2: element command diagnostics (dev entry; the core carries no text).
3
+ *
4
+ * The core (src/extra/elementCommands.ts) calls `__SYGNAL_DIAGNOSTICS__.elementCommand(component,
5
+ * command, element?)` when a command can't run:
6
+ * - no element (`element` falsy): SYG640 (warn): nothing in the sending instance's DOM scope
7
+ * matched the target within about 1 s;
8
+ * - an element, but neither the control's spec command nor a method of the element: SYG641
9
+ * (error). It names the control's declared commands. When the element is a widget's host
10
+ * (PLAN-5 W-1, `__sw`): SYG142 (error), naming the widget's declared commands.
11
+ * installElementCommandHooks() publishes it on the core bridge.
12
+ *
13
+ * When a command is sent (checkSentCommand, from the elementCommands check's onModel, which wraps
14
+ * the instance's ELEMENT stream before the core subscribes it): SYG641 for a value that is not a
15
+ * command object, and for a method that changes the DOM Sygnal renders (remove, append,
16
+ * setAttribute...): the core runs any method the element has, and those break the next patch.
17
+ *
18
+ * renderComponent uses these functions too (mock DOM, and the real DOM without the dev entry), so
19
+ * this module has no side effects and doesn't import ./shared (which registers codes when loaded).
20
+ */
21
+ import type {DiagnosticCheck} from '../index'
22
+ import {DEV_CODE_SEVERITY} from '../codes'
23
+
24
+ /** The element commands Sygnal documents and types (the core runs any method of the element) */
25
+ export const NATIVE_COMMAND_NAMES = ['focus', 'blur', 'select', 'click', 'scrollIntoView', 'showModal', 'show', 'close', 'showPopover', 'hidePopover', 'togglePopover']
26
+
27
+ const core = (): any => (globalThis as any).__SYGNAL_DIAGNOSTICS__
28
+
29
+ const send = (code: string, details: any): void => {
30
+ try {
31
+ core()?.report(code, {severity: DEV_CODE_SEVERITY[code], ...details})
32
+ } catch (err) {
33
+ // 'error' mode: rethrown asynchronously (never into the caller)
34
+ setTimeout(() => { throw err })
35
+ }
36
+ }
37
+
38
+ const isControl = (x: any): boolean => typeof x == 'function' && !!x.__sygnalControl
39
+ const keyOf = (control: any): string => String(control).slice(15, -2)
40
+
41
+ /** a command as written: `{ focus: Email, preventScroll: true }` */
42
+ export const describeCommand = (cmd: any): string => {
43
+ if (!cmd || typeof cmd != 'object') return JSON.stringify(cmd) ?? String(cmd)
44
+ const parts = Object.keys(cmd).map(k => {
45
+ const v = cmd[k]
46
+ let text: string
47
+ if (isControl(v)) text = keyOf(v)
48
+ else try { text = JSON.stringify(v) ?? String(v) } catch (_) { text = String(v) }
49
+ return `${k}: ${text}`
50
+ })
51
+ return `{ ${parts.join(', ')} }`
52
+ }
53
+
54
+ const distance = (a: string, b: string): number => {
55
+ const row = Array.from({length: b.length + 1}, (_, i) => i)
56
+ for (let i = 1; i <= a.length; i++) {
57
+ let prev = row[0]
58
+ row[0] = i
59
+ for (let j = 1; j <= b.length; j++) {
60
+ const cur = row[j]
61
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] == b[j - 1] ? 0 : 1))
62
+ prev = cur
63
+ }
64
+ }
65
+ return row[b.length]
66
+ }
67
+
68
+ const closest = (name: string, names: string[]): string | undefined => {
69
+ let best: string | undefined, d = 3
70
+ for (const n of names) {
71
+ const x = n.toLowerCase() == name.toLowerCase() ? 0 : distance(name, n)
72
+ if (x < d) { d = x; best = n }
73
+ }
74
+ return best
75
+ }
76
+
77
+ const tagOf = (el: any): string => el && typeof el.tagName == 'string' ? `<${el.tagName.toLowerCase()}>` : 'element'
78
+
79
+ const notACommand = (component: any, name: string, cmd: any) => send('SYG641', {
80
+ component,
81
+ message: `${name} sent ${describeCommand(cmd)} to the ELEMENT sink, which is not a command`,
82
+ fix: `Send { <method>: control or selector, ...options }, e.g. { focus: Email }, or an array of them`,
83
+ data: {command: cmd},
84
+ })
85
+
86
+ /** SYG640 (no `el`) or SYG641 (an element, but no command of that name) for `cmd` sent by `component` */
87
+ export function reportElementCommand(component: any, cmd: any, el?: any): void {
88
+ // a disposed instance's pending commands are dropped silently
89
+ if (component && component._disposed) return
90
+ const name = (component && component.name) || 'component'
91
+ const method = cmd && typeof cmd == 'object' ? Object.keys(cmd)[0] : undefined
92
+ if (method === undefined) return notACommand(component, name, cmd)
93
+ const target = cmd[method]
94
+ const what = `ELEMENT ${describeCommand(cmd)}`
95
+ const control = isControl(target) ? keyOf(target) : undefined
96
+ const data: any = {method, command: cmd, ...(control ? {control} : {selector: target == null ? target : String(target)})}
97
+ if (!el) {
98
+ const missing = target == null || target === ''
99
+ return send('SYG640', {
100
+ component,
101
+ message: missing
102
+ ? `${what} in ${name} has no target (${String(target)}), so the command was dropped`
103
+ : `${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`,
104
+ fix: missing
105
+ ? `Give the method a control or a selector: { ${method}: Email }`
106
+ : `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)`,
107
+ data,
108
+ })
109
+ }
110
+ // PLAN-5 W-1: the matched element is a widget's host (selector or control target): SYG142 names
111
+ // the widget's declared commands
112
+ const widget = el.__sw?.w
113
+ if (widget) {
114
+ const declared = Object.keys(widget.commands)
115
+ const hint = closest(method, [...declared, ...NATIVE_COMMAND_NAMES])
116
+ return send('SYG142', {
117
+ component,
118
+ message: `${what} in ${name}: the matched ${tagOf(el)} is a widget's host, and the widget declares no '${method}' command` +
119
+ (hint ? ` (did you mean '${hint}'?)` : '') + (declared.length ? `; it declares: ${declared.join(', ')}` : '; it declares none'),
120
+ fix: `Add '${method}' to the widget's commands (defineWidget({ commands: { ${method}: (instance, options) => … } })), or send one it declares`,
121
+ data: {...data, element: tagOf(el), commands: declared},
122
+ })
123
+ }
124
+ const commands = control && target.spec && typeof target.spec == 'object' && target.spec.commands
125
+ ? Object.keys(target.spec.commands) : []
126
+ data.element = tagOf(el)
127
+ if (commands.length) data.commands = commands
128
+ let message: string, fix: string
129
+ if (NATIVE_COMMAND_NAMES.includes(method)) {
130
+ 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(', ')})` : ''}`
131
+ fix = /^(show|showModal|close)$/.test(method)
132
+ ? `${method}() works on a <dialog> element: render the target as a <dialog>`
133
+ : /Popover$/.test(method)
134
+ ? `The popover methods need an element with the popover attribute (attrs: { popover: 'auto' }) in a browser with the Popover API`
135
+ : `Target an element that has ${method}()`
136
+ } else {
137
+ const hint = closest(method, [...commands, ...NATIVE_COMMAND_NAMES])
138
+ message = `${what} in ${name}: '${method}' is not an element command` + (hint ? ` (did you mean '${hint}'?)` : '') +
139
+ (el.tagName ? `: the matched ${tagOf(el)} has no such method` : '') +
140
+ (commands.length ? `, and the control ${control}'s spec declares only: ${commands.join(', ')}` : '')
141
+ 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' }`
142
+ }
143
+ send('SYG641', {component, message, fix, data})
144
+ }
145
+
146
+ // methods that change the DOM snabbdom owns (the next patch undoes them or trips over them)
147
+ 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)$/
148
+
149
+ /** SYG641 when it is sent: not a command object, or a method that changes the DOM Sygnal renders */
150
+ export function checkSentCommand(component: any, cmd: any): void {
151
+ if (cmd == null) return
152
+ const name = (component && component.name) || 'component'
153
+ if (typeof cmd != 'object' || Array.isArray(cmd) || !Object.keys(cmd).length) return notACommand(component, name, cmd)
154
+ const method = Object.keys(cmd)[0], target = cmd[method]
155
+ if (!DOM_MUTATORS.test(method) || (isControl(target) && target.spec?.commands?.[method])) return
156
+ send('SYG641', {
157
+ component,
158
+ message: `ELEMENT ${describeCommand(cmd)} in ${name}: ${method}() changes the DOM that ${name}'s view renders, so the next render undoes it or fails`,
159
+ fix: `Change the state and render the result instead; element commands are for what a view can't express (focus, scrolling, dialogs, popovers, media)`,
160
+ data: {method, command: cmd},
161
+ })
162
+ }
163
+
164
+ /** The dev entry check: checkSentCommand runs on every ELEMENT value through the core's onElementCommand hook (checks/next.ts) */
165
+ export const elementCommandsCheck: DiagnosticCheck = {
166
+ id: 'elementCommands',
167
+ }
168
+
169
+ /** Publish the element command hook on the core bridge. Returns an uninstall function. */
170
+ export function installElementCommandHooks(): () => void {
171
+ const c = core()
172
+ if (!c) return () => {}
173
+ c.elementCommand = reportElementCommand
174
+ return () => { if (c.elementCommand === reportElementCommand) c.elementCommand = undefined }
175
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * SYG105 — EVENTS type emitted but never selected, or selected but never
3
3
  * emitted (info).
4
+ * SYG116 — a value with no string `type` (typically a function) reached the
5
+ * EVENTS bus (error, G-143).
4
6
  *
5
7
  * Mechanism: the EVENTS driver (src/extra/eventDriver.ts) calls onBusSelect
6
8
  * for every EVENTS.select(type) and onBusEmit for every bus event.
@@ -12,7 +14,7 @@
12
14
  * EVENTS.select() with no type (all events) counts as selecting everything.
13
15
  */
14
16
  import type {DiagnosticCheck} from '../index'
15
- import {report, once, onReset, didYouMean} from './shared'
17
+ import {report, devReport, once, onReset, didYouMean} from './shared'
16
18
 
17
19
  let selected = new Set<string>()
18
20
  let emitted = new Set<string>()
@@ -29,7 +31,20 @@ export const eventsCheck: DiagnosticCheck = {
29
31
  },
30
32
 
31
33
  onBusEmit(type, emitterName) {
32
- if (typeof type !== 'string') return
34
+ if (typeof type !== 'string') {
35
+ // SYG116 (G-143): no EVENTS.select() can ever receive it. The core copies each emitted
36
+ // value into a new object (devtools stamp), so a function arrives here as {}.
37
+ if (once(`SYG116:${emitterName}`)) {
38
+ devReport('SYG116', {
39
+ component: emitterName,
40
+ message: `${emitterName ? emitterName + "'s model" : 'A model'} sent a value with no string 'type' to the EVENTS sink, so nothing receives it. ` +
41
+ `The value was probably a function (a model entry that returns event(...) instead of being event(...)), or undefined/null`,
42
+ fix: `Make event() the sink entry itself: ACTION: { EVENTS: event('TYPE', (state, data) => payload) }; return ABORT to send nothing`,
43
+ data: {type, emitter: emitterName},
44
+ })
45
+ }
46
+ return
47
+ }
33
48
  emitted.add(type)
34
49
  if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`)) return
35
50
  report('SYG105', {