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
@@ -34,9 +34,8 @@ export interface DiagnosticCheck {
34
34
  * Strict mode (canonical forms, SYG5xx): turn the runtime strict checks on
35
35
  * (true), off (false), or back to the default (undefined → on only when
36
36
  * globalThis.__SYGNAL_STRICT__ === true). Off by default. Diagnostics must
37
- * also be on. Runtime rules: SYG501 (positional view args), SYG502 (reducer
38
- * returned the unchanged state instead of ABORT), SYG504 ('ACTION | SINK'
39
- * keys); `sygnal-check --strict` checks all of SYG501-507 statically.
37
+ * also be on. Runtime rules: SYG501 (positional view args), SYG504 ('ACTION | SINK'
38
+ * keys); `sygnal-check --strict` checks SYG501-508 statically (SYG502 is retired in 6.0).
40
39
  * In tests: renderComponent(C, { strict: true }).
41
40
  */
42
41
  export function configureStrict(on?: boolean): void
@@ -96,15 +95,18 @@ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
96
95
  // ---------------------------------------------------------------------------
97
96
 
98
97
  /** How an action is dispatched. */
99
- export type InspectActionTrigger = 'intent' | 'next' | 'builtin' | 'unknown'
98
+ export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'unknown'
100
99
 
101
100
  export interface InspectAction {
102
101
  name: string
103
102
  /**
104
103
  * 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
105
- * INITIALIZE, HYDRATE, DISPOSE or READY. 'next': dispatched with next()
106
- * (statically: a next('NAME') literal; at runtime: a model-only action whose
107
- * STATE reducer was seen running). 'unknown': none of these is known.
104
+ * INITIALIZE, DISPOSE or READY. 'reply': named as a reply action by a request
105
+ * (`ok: 'NAME'` / `error: 'NAME'`) or a `connections` entry (statically: a
106
+ * string literal; at runtime: a request the instance was seen sending).
107
+ * 'next': dispatched with next() (statically: a next('NAME') literal; at
108
+ * runtime: a model-only action whose STATE reducer was seen running).
109
+ * 'unknown': none of these is known.
108
110
  */
109
111
  trigger: InspectActionTrigger
110
112
  /** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
@@ -134,6 +136,20 @@ export interface InspectSelector {
134
136
  matched: boolean | null
135
137
  /** the child component whose (isolated) elements it matches instead, if any */
136
138
  isolationHit: string | null
139
+ /** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
140
+ control?: string
141
+ }
142
+
143
+ /** PLAN-4 CT-1: a control (controls()) a component renders */
144
+ export interface InspectControl {
145
+ /** the control's key (rendered as data-control="<name>") */
146
+ name: string
147
+ /** the intrinsic tag of a tag spec; null for a spec object */
148
+ element: string | null
149
+ /** a spec object's kind (e.g. 'widget') */
150
+ kind?: string
151
+ /** whether the component's intent listens to it; null when unknown */
152
+ listened: boolean | null
137
153
  }
138
154
 
139
155
  export interface InspectDiagnostic {
@@ -178,7 +194,60 @@ export interface InspectComponent {
178
194
  eventsSelected: string[]
179
195
  children: InspectChild[]
180
196
  selectors: InspectSelector[]
197
+ /** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
198
+ controls?: InspectControl[]
199
+ /** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
200
+ commands?: InspectCommand[]
201
+ /** static only, PLAN-4 GS-7 (G-224): the literal timer specs of its `timers` static; omitted when none */
202
+ timers?: InspectTimer[]
181
203
  diagnostics: InspectDiagnostic[]
204
+ /** runtime, PLAN-3 5-3: the instance's `resources` and their state */
205
+ resources?: InspectResource[]
206
+ }
207
+
208
+ /** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
209
+ export interface InspectCommand {
210
+ /** the model entry that sends it */
211
+ action: string
212
+ /** the command's method (its first key) */
213
+ method: string
214
+ /** the control's key or the selector; null when not static */
215
+ target: string | null
216
+ /** the control it targets (its key) */
217
+ control?: string
218
+ /** intent actions listening on the target for a native event the command causes (close, toggle...) */
219
+ triggers?: string[]
220
+ }
221
+
222
+ /** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
223
+ export interface InspectTimer {
224
+ name: string
225
+ every?: number | null
226
+ after?: number | null
227
+ /** the action a frame timer dispatches */
228
+ frame?: string | null
229
+ action?: string | null
230
+ background?: true
231
+ }
232
+
233
+ /** PLAN-3 5-3: a resource of a component instance (runtime inspect()) */
234
+ export interface InspectResource {
235
+ name: string
236
+ status: 'idle' | 'loading' | 'success' | 'error'
237
+ refreshing: boolean
238
+ /** `data` is set (a success, or kept through a refetch) */
239
+ hasData: boolean
240
+ /** the error message while 'error' */
241
+ error?: string
242
+ }
243
+
244
+ /** PLAN-3 5-3: a makeFetchDriver cache entry (runtime inspect(), t.cache) */
245
+ export interface InspectCacheEntry {
246
+ key: string
247
+ age?: number
248
+ stale: boolean
249
+ subscribers: number
250
+ data: any
182
251
  }
183
252
 
184
253
  export interface InspectGraph {
@@ -190,6 +259,26 @@ export interface InspectGraph {
190
259
  events: Record<string, { emitters: string[]; selectors: string[] }>
191
260
  /** diagnostics not tied to a listed component */
192
261
  diagnostics: InspectDiagnostic[]
262
+ /** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
263
+ cache?: Record<string, InspectCacheEntry[]>
264
+ /** runtime, PLAN-4 2-C (GS-10): with `inspect({ actions })`, the most recent actions, oldest first */
265
+ recentActions?: InspectRecentAction[]
266
+ }
267
+
268
+ /** PLAN-4 2-C (GS-10): one recent action (inspect({ actions })) */
269
+ export interface InspectRecentAction {
270
+ type: string
271
+ /** the action's data, when it is JSON-safe and small (a DOM event is left out) */
272
+ data?: any
273
+ /** the component's name */
274
+ component: string
275
+ /** the instance's id (InspectComponent.id) */
276
+ instance: string
277
+ /** the sinks that produced a value for it (not ABORT) */
278
+ sinks: string[]
279
+ cause: 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
280
+ /** ms since the dev entry started recording (or the last resetChecks()) */
281
+ at: number
193
282
  }
194
283
 
195
284
  export interface InspectOptions {
@@ -199,6 +288,8 @@ export interface InspectOptions {
199
288
  selectors?: Record<string, InspectSelector[]>
200
289
  /** diagnostics to attach (default: the devtools' collected diagnostics, when available) */
201
290
  diagnostics?: Array<{ code: string; severity: DiagnosticSeverity; component?: string; message: string; [key: string]: any }>
291
+ /** PLAN-4 2-C: add `recentActions`: true for every kept one (the last 200), a number for the last n */
292
+ actions?: boolean | number
202
293
  }
203
294
 
204
295
  /**
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Reply actions (PLAN-3 §1.1, workstream 1-D).
3
+ *
4
+ * SYG112 — a request names a reply action the sending component has no
5
+ * model entry for (error: the reply is dropped). Dev-entry code
6
+ * (DEV_CODE_SEVERITY).
7
+ * SYG508 — strict: the component reads the replies to its own request with
8
+ * X.select('cat') / X.errors('cat') where reply actions would do.
9
+ *
10
+ * Mechanism:
11
+ * onSink (checks/next.ts: checkRequest) every value an instance sends to a
12
+ * reply-capable sink (`__sygnalReplies === true`: makeFetchDriver,
13
+ * driverFromAsync, the socket driver) is checked as it is sent.
14
+ * The names it sees feed inspect()'s 'reply' trigger.
15
+ * sources (strict only) wraps those sources for the intent, recording the
16
+ * select()/errors() categories the instance reads.
17
+ * A request's `category` is compared for SYG508 (driverFromAsync's custom
18
+ * `selector` property is not known at run time; sygnal-check covers it).
19
+ * Reported once per component name and action / category.
20
+ *
21
+ * `{ connections }` values (the `connections` static, which the core sends to
22
+ * the same sink, or one a model entry sends) are checked the same way:
23
+ * each connection's message / open / close / error and its SSE `events` names.
24
+ */
25
+ import type {DiagnosticCheck} from '../index'
26
+ import {STRICT_CODE_SEVERITY} from '../codes'
27
+ import {devReport, reportSafely, once, nameOf, suggest, replySeen} from './shared'
28
+ import {isStrictEnabled} from './strict'
29
+
30
+ const REPLY_KEYS = ['ok', 'error']
31
+ // PLAN-3 5-4b: on the router's sink, `{ route }` (the route static) and `{ block }` name reply actions
32
+ const ROUTER_KEYS = ['route', 'block']
33
+ const CONNECTION_KEYS = ['message', 'open', 'close', 'error']
34
+
35
+ /** [action, key, connection name?] for every reply action name a sink value names */
36
+ function replyNames(req: any, router?: boolean): Array<[string, string, string?]> {
37
+ const out: Array<[string, string, string?]> = []
38
+ for (const key of router ? REPLY_KEYS.concat(ROUTER_KEYS) : REPLY_KEYS) if (typeof req[key] === 'string') out.push([req[key], key])
39
+ const conns = req.connections
40
+ if (conns && typeof conns === 'object') {
41
+ for (const name of Object.keys(conns)) {
42
+ const spec = conns[name]
43
+ if (!spec || typeof spec !== 'object') continue
44
+ for (const key of CONNECTION_KEYS) if (typeof spec[key] === 'string') out.push([spec[key], key, name])
45
+ const events = spec.events
46
+ if (events && typeof events === 'object') {
47
+ for (const ev of Object.keys(events)) if (typeof events[ev] === 'string') out.push([events[ev], `events.${ev}`, name])
48
+ }
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ /** instance → 'SINK\0category' → the select/errors call that read it */
55
+ const selected = new WeakMap<object, Map<string, string>>()
56
+
57
+
58
+ function readingSource(component: any, sink: string, source: any) {
59
+ const wrapped = Object.create(source)
60
+ for (const method of ['select', 'errors']) {
61
+ if (typeof source[method] !== 'function') continue
62
+ wrapped[method] = (category?: any, ...rest: any[]) => {
63
+ if (typeof category === 'string') {
64
+ let reads = selected.get(component)
65
+ if (!reads) selected.set(component, reads = new Map())
66
+ reads.set(`${sink}\0${category}`, `${sink}.${method}('${category}')`)
67
+ }
68
+ return source[method](category, ...rest)
69
+ }
70
+ }
71
+ return wrapped
72
+ }
73
+
74
+ export function checkRequest(component: any, sink: string, req: any, modelActions: string[]) {
75
+ if (!req || typeof req !== 'object') return
76
+ const name = nameOf(component)
77
+ let replies = false
78
+ for (const [action, key, connection] of replyNames(req, !!component?.sources?.[sink]?.__sygnalRouter)) {
79
+ replies = true
80
+ let seen = replySeen.get(component)
81
+ if (!seen) replySeen.set(component, seen = new Set())
82
+ seen.add(action)
83
+ if (modelActions.includes(action) || !once(`SYG112:${name}:${action}`)) continue
84
+ const near = suggest(action, modelActions.filter(a => !a.startsWith('__')))
85
+ devReport('SYG112', {
86
+ component,
87
+ message: (connection
88
+ ? `${sink} connection '${connection}' names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so those events are dropped`
89
+ : `A ${sink} request names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so the reply is dropped`) +
90
+ (near ? ` (did you mean '${near}'?)` : ''),
91
+ fix: near
92
+ ? `Use the existing entry: ${key.startsWith('events.') ? `events: { '${key.slice(7)}': '${near}' }` : `${key}: '${near}'`}`
93
+ : `Add '${action}' to ${name}.model, e.g. ${action}: (state, data) => ({ ...state, ... }), or fix the name`,
94
+ data: {action, key, sink, ...(connection ? {connection} : {}), ...(near ? {suggestion: near} : {}), modelActions},
95
+ })
96
+ }
97
+
98
+ // SYG508 (strict): a request without reply actions whose replies this instance reads back with select()/errors()
99
+ if (replies || 'abort' in req || typeof req.category !== 'string' || !isStrictEnabled()) return
100
+ const read = selected.get(component)?.get(`${sink}\0${req.category}`)
101
+ if (!read || !once(`SYG508:${name}:${sink}:${req.category}`)) return
102
+ // (inside a sink stream: reportSafely, so 'error' mode can't break the stream)
103
+ reportSafely('SYG508', {
104
+ severity: STRICT_CODE_SEVERITY.SYG508,
105
+ component,
106
+ message: `${name} sends a ${sink} request with category: '${req.category}' and reads the reply back with ${read}`,
107
+ fix: `Use reply actions instead: name them in the request, ${sink}: (state) => ({ url, ok: 'LOADED', error: 'FAILED' }), ` +
108
+ `handle LOADED (the parsed body) and FAILED ({ error, status, body, request }) in the model, and remove ${read} from the intent`,
109
+ data: {sink, category: req.category, read},
110
+ })
111
+ }
112
+
113
+ export const repliesCheck: DiagnosticCheck = {
114
+ id: 'replies',
115
+
116
+ sources(component, sources) {
117
+ if (!isStrictEnabled() || typeof Proxy != 'function') return
118
+ const cache = new Map<string, any>()
119
+ return new Proxy(sources, {
120
+ get(t: any, k: any) {
121
+ const v = t[k]
122
+ if (typeof k != 'string' || !v || v.__sygnalReplies !== true) return v
123
+ if (!cache.has(k)) cache.set(k, readingSource(component, k, v))
124
+ return cache.get(k)
125
+ },
126
+ })
127
+ },
128
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Router and declaration statics (PLAN-3 5-4b). Dev-entry-only codes (DEV_CODE_SEVERITY):
3
+ *
4
+ * SYG130 — href() names no route, or leaves out a param its pattern needs (the link is broken).
5
+ * (A `{ to }` command with the same problem is SYG620, reported by the router itself
6
+ * in production too, and not navigated.)
7
+ * SYG131 — href() or a `{ to, params }` command passes params the route's pattern doesn't use.
8
+ * SYG132 — G-167: a component declares a static the core sends to a driver (`route`,
9
+ * `resources`, `connections`, `head`) but never sends it: it has no model (the core
10
+ * wires statics in its model setup), or it is a root without initialState (a static is
11
+ * computed from the state stream, which a root without state never emits).
12
+ * SYG133 — the SPA router (makeRouter().driver without `navigate`) runs inside a Vike app,
13
+ * where Vike owns links and history.
14
+ *
15
+ * Mechanism: the router's href() calls `__SYGNAL_DIAGNOSTICS__.routerHref(routes, name, params)`
16
+ * when it exists (installRouterHooks() sets it); onModel looks at the component's sources for
17
+ * `__sygnalStatic` / `__sygnalRouter` markers.
18
+ */
19
+ import type {DiagnosticCheck} from '../index'
20
+ import {bridge, devReport, once, nameOf, suggest} from './shared'
21
+
22
+ const PARAM = /:(\w+)/g
23
+ const paramsOf = (pat: string) => (pat.match(PARAM) || []).map(p => p.slice(1))
24
+
25
+ function checkHref(routes: Record<string, string>, name: string, params: any) {
26
+ const names = Object.keys(routes).filter(k => routes[k] != '*')
27
+ const pat = routes[name]
28
+ if (pat == null || pat == '*') {
29
+ if (!once(`SYG130:${name}`)) return
30
+ const near = suggest(String(name), names)
31
+ devReport('SYG130', {
32
+ message: `href('${name}') names no route, so the link points to '/'` + (near ? ` (did you mean '${near}'?)` : ''),
33
+ fix: `Use one of the route names: ${names.join(', ')}`,
34
+ data: {name, routes: names, ...(near ? {suggestion: near} : {})},
35
+ })
36
+ return
37
+ }
38
+ const wanted = paramsOf(pat)
39
+ const given = params && typeof params == 'object' ? params : {}
40
+ const missing = wanted.filter(k => given[k] == null)
41
+ const extra = Object.keys(given).filter(k => !wanted.includes(k))
42
+ if (missing.length && once(`SYG130:${name}:${missing}`)) {
43
+ devReport('SYG130', {
44
+ message: `href('${name}') leaves out ${missing.map(k => `'${k}'`).join(', ')} (${pat}), so that segment is empty`,
45
+ fix: `href('${name}', { ${wanted.join(', ')} })`,
46
+ data: {name, pattern: pat, missing},
47
+ })
48
+ }
49
+ if (extra.length && once(`SYG131:${name}:${extra}`)) {
50
+ devReport('SYG131', {
51
+ message: `Route '${name}' (${pat}) has no param ${extra.map(k => `'${k}'`).join(', ')}; it is ignored`,
52
+ 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]} })`,
53
+ data: {name, pattern: pat, extra},
54
+ })
55
+ }
56
+ }
57
+
58
+ /** install the hook the router's href() calls (removed by the returned function) */
59
+ export function installRouterHooks(): () => void {
60
+ const core = bridge()
61
+ if (!core) return () => {}
62
+ core.routerHref = checkHref
63
+ return () => { if (core.routerHref === checkHref) core.routerHref = undefined }
64
+ }
65
+
66
+ export const routerCheck: DiagnosticCheck = {
67
+ id: 'router',
68
+
69
+ onModel(component) {
70
+ const sources = component?.sources
71
+ if (!sources || typeof sources != 'object') return
72
+ const name = nameOf(component)
73
+ for (const n of component.sourceNames || Object.keys(sources)) {
74
+ const src = sources[n]
75
+ const k = src?.__sygnalStatic
76
+ if (typeof k == 'string' && component.view?.[k] != null) {
77
+ // statics are computed from state (G-167: with or without a model): a root whose state
78
+ // never emits (a model but no initialState) never sends one
79
+ if (!component.isSubComponent && component.initialState === undefined && once(`SYG132:${name}:${k}`)) {
80
+ devReport('SYG132', {
81
+ component,
82
+ message: `${name} declares ${name}.${k}, but a root without initialState has no state, so ${k} is never sent to ${n}`,
83
+ fix: `Add ${name}.initialState (e.g. { }${k == 'route' ? ', or { route: router.current() }' : ''})`,
84
+ data: {static: k, source: n, reason: 'state'},
85
+ })
86
+ }
87
+ }
88
+ const opts = src?.__sygnalRouter
89
+ const g: any = globalThis
90
+ if (opts && typeof opts == 'object' && !opts.navigate && g.document?.getElementById?.('vike_pageContext') && once('SYG133')) {
91
+ devReport('SYG133', {
92
+ component,
93
+ message: `The SPA router (${n}) runs inside a Vike app: Vike owns links and history, so both would handle a click`,
94
+ fix: "Pass Vike's navigate: makeRouter({ routes, navigate }) with import { navigate } from 'vike/client/router', or use Vike's routing",
95
+ data: {source: n},
96
+ })
97
+ }
98
+ }
99
+ },
100
+ }
@@ -9,7 +9,7 @@
9
9
  * xstream equivalent and the SYG301 docs link. The error is also reported
10
10
  * (collected / printed per the diagnostics mode; in 'error' mode report()
11
11
  * throws the DiagnosticError itself). It works anywhere a stream is used
12
- * (intent, peers, drivers), with no hook in component.ts.
12
+ * (intent, peers, drivers), with no hook in the core.
13
13
  *
14
14
  * Caveat: `typeof stream.pipe === 'function'` is true while this entry is
15
15
  * loaded (dev only).
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import '../index'
13
13
  import type {DiagnosticCheck, DiagnosticDetails, Diagnostic} from '../index'
14
+ import {CODE_TITLES, DEV_CODE_SEVERITY, registerCodes} from '../codes'
15
+ import type {DiagnosticSeverity} from '../codes'
14
16
 
15
17
  interface CoreBridge {
16
18
  registerCheck(check: DiagnosticCheck): () => void
@@ -38,6 +40,16 @@ export const reportSafely = (code: string, details: DiagnosticDetails): Diagnost
38
40
  }
39
41
  }
40
42
 
43
+ registerCodes(Object.keys(DEV_CODE_SEVERITY).map(code =>
44
+ [code, DEV_CODE_SEVERITY[code], CODE_TITLES[code]] as [string, DiagnosticSeverity, string]))
45
+
46
+ /**
47
+ * reportSafely() for the dev-entry-only codes (DEV_CODE_SEVERITY, G-143): the main bundle's
48
+ * core doesn't know their severity, so it is passed explicitly.
49
+ */
50
+ export const devReport = (code: string, details: DiagnosticDetails): Diagnostic | undefined =>
51
+ reportSafely(code, {severity: DEV_CODE_SEVERITY[code], ...details})
52
+
41
53
  /** Tunables (test seam: configureChecks()). */
42
54
  export const timing = {
43
55
  /** delay after a render before DOM checks run (lets nested renders patch) */
@@ -48,8 +60,76 @@ export const timing = {
48
60
  minRenders: 3,
49
61
  }
50
62
 
51
- /** Built-in actions Sygnal dispatches itself; never "missing" or "unreachable". */
52
- export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'HYDRATE', 'DISPOSE', 'READY'])
63
+ /**
64
+ * Built-in actions Sygnal dispatches itself; never "missing" or "unreachable".
65
+ * (HYDRATE is not one since 6.0, D66: nothing dispatches it, so it is an ordinary action.)
66
+ */
67
+ export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'DISPOSE', 'READY', 'RESOURCE'])
68
+
69
+ /** Sinks the core handles itself: their values never go to a reply-action driver. */
70
+ // (PLAN-4 GS-2: ELEMENT too; `{ scrollIntoView: Row, block: 'center' }` names no action)
71
+ const NON_REPLY_SINK = /^(STATE|EFFECT|EVENTS|PARENT|READY|DOM|CHILD|ELEMENT|PERSIST)$/
72
+
73
+ // `ok: 'X'` / `"error": "X"` in function source (minified code keeps string literals and keys)
74
+ const keyedNames = (keys: string) => new RegExp(`(?:^|[{,\\s])["']?(?:${keys})["']?\\s*:\\s*(["'\`])([\\w$.:/-]+)\\1`, 'g')
75
+ // (PLAN-3 5-4b: and a router `{ block: 'ACTION' }`)
76
+ const REPLY_IN_SOURCE = keyedNames('ok|error|block')
77
+ const CONNECTION_IN_SOURCE = keyedNames('message|open|close|error')
78
+ // G-224: a `timers` static's `action: 'TICK'` / `frame: 'FRAME'`
79
+ const TIMER_IN_SOURCE = keyedNames('action|frame')
80
+ // PLAN-5 B-3: a `browser` static's `action: 'SEEN'` / `error: 'GEO_FAILED'`
81
+ const BROWSER_IN_SOURCE = keyedNames('action|error')
82
+
83
+ const namesIn = (fn: any, re: RegExp, out: Set<string>) => {
84
+ if (typeof fn !== 'function') return
85
+ let src = ''
86
+ try { src = Function.prototype.toString.call(fn) } catch (_) { return }
87
+ re.lastIndex = 0
88
+ for (let m = re.exec(src); m; m = re.exec(src)) out.add(m[2])
89
+ }
90
+
91
+ /**
92
+ * Reply actions a component's requests and `connections` static can name, read from the
93
+ * source of its non-STATE sink functions (`HTTP: (s) => ({ url, ok: 'LOADED', error: 'FAILED' })`)
94
+ * and of `connections` (message/open/close/error). A heuristic for SYG102 (PLAN-3): a name
95
+ * built at run time is missed (SYG102 stays info, so that only costs a false hint).
96
+ */
97
+ export function replyNamesOf(component: any): Set<string> {
98
+ const out = new Set<string>()
99
+ const model = component && component.model
100
+ if (model && typeof model === 'object') {
101
+ for (const key of Object.keys(model)) {
102
+ const value = model[key]
103
+ const bar = key.indexOf('|')
104
+ if (bar >= 0) {
105
+ if (!NON_REPLY_SINK.test(key.slice(bar + 1).trim())) namesIn(value, REPLY_IN_SOURCE, out)
106
+ } else if (value && typeof value === 'object') {
107
+ for (const sink of Object.keys(value)) {
108
+ if (!NON_REPLY_SINK.test(sink)) namesIn(value[sink], REPLY_IN_SOURCE, out)
109
+ }
110
+ }
111
+ }
112
+ }
113
+ namesIn(component?.view?.connections, CONNECTION_IN_SOURCE, out)
114
+ // G-224: makeTimerDriver() replies the actions the `timers` static names (a function of the
115
+ // state, or an object of them)
116
+ const timers = component?.view?.timers
117
+ if (timers && typeof timers === 'object') for (const k in timers) namesIn(timers[k], TIMER_IN_SOURCE, out)
118
+ else namesIn(timers, TIMER_IN_SOURCE, out)
119
+ // PLAN-5 B-3: the browser driver replies the actions the `browser` static names
120
+ const browser = component?.view?.browser
121
+ if (browser && typeof browser === 'object') for (const k in browser) namesIn(browser[k], BROWSER_IN_SOURCE, out)
122
+ else namesIn(browser, BROWSER_IN_SOURCE, out)
123
+ // PLAN-3 5-4b: the router replies the action a `route` static names
124
+ const route = component?.view?.route
125
+ if (typeof route === 'string') out.add(route)
126
+ // PLAN-4 GS-5: persist() adds RESTORE (sent after hydration and by sync)
127
+ if (component?.view?.persist) out.add('RESTORE')
128
+ return out
129
+ }
130
+
131
+ /** Reply actions a component instance's requests were seen naming (replies.ts → inspect). */
132
+ export const replySeen = new WeakMap<object, Set<string>>()
53
133
 
54
134
  /** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
55
135
  export const isInternalAction = (action: string): boolean =>
@@ -59,6 +139,10 @@ let seen = new Set<string>()
59
139
 
60
140
  /** true the first time `key` is seen (dedupes reports, e.g. per component name). */
61
141
  export const once = (key: string): boolean => (seen.has(key) ? false : (seen.add(key), true))
142
+ /** undo once(key): the report it guarded wasn't made (G-470: diagnostics off) */
143
+ export const unsee = (key: string): void => void seen.delete(key)
144
+ /** whether once(`key`) has reported already (a check can skip its work: SYG435) */
145
+ export const onced = (key: string): boolean => seen.has(key)
62
146
 
63
147
  /**
64
148
  * Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SYG115 — unknown DOM event shorthand (warn, G-143).
3
+ *
4
+ * The core's DOM source Proxy (the DOM source wrapper) turns ANY unknown property
5
+ * into an event shorthand: `DOM.<name>(sel)` = `DOM.select(sel).events('<name>')`. A made-up
6
+ * name such as `DOM.key('.x')` or `DOM.enter('.x')` therefore listens for an event the browser
7
+ * never fires, and the action silently never happens.
8
+ *
9
+ * Mechanism: the `sources` hook (called before each component's intent) wraps the DOM source
10
+ * in a second Proxy. Calling a shorthand whose name is not a known DOM event reports SYG115
11
+ * (once per component name and event), then behaves exactly like the core shorthand. Only the
12
+ * call is reported, not the property read, so tools that inspect the source (pretty-printers)
13
+ * don't trigger it. `DOM.select(sel).events(name)` is never checked: it is the explicit form
14
+ * for custom events.
15
+ */
16
+ import type {DiagnosticCheck} from '../index'
17
+ import {devReport, once, nameOf, suggest} from './shared'
18
+
19
+ // Standard DOM / UI event names (lower case). In a browser, `on<name>` on an element or the
20
+ // window also counts, so newer events are accepted there too.
21
+ const KNOWN = new Set((
22
+ 'abort afterprint animationcancel animationend animationiteration animationstart auxclick ' +
23
+ 'beforeinput beforeprint beforetoggle beforeunload blur cancel canplay canplaythrough change click close ' +
24
+ 'compositionend compositionstart compositionupdate contextlost contextmenu contextrestored copy cuechange cut ' +
25
+ 'dblclick drag dragend dragenter dragleave dragover dragstart drop durationchange emptied ended error ' +
26
+ 'focus focusin focusout formdata fullscreenchange fullscreenerror gotpointercapture hashchange input invalid ' +
27
+ 'keydown keypress keyup languagechange load loadeddata loadedmetadata loadstart lostpointercapture message ' +
28
+ 'messageerror mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup offline online pagehide ' +
29
+ 'pageshow paste pause play playing pointercancel pointerdown pointerenter pointerleave pointermove pointerout ' +
30
+ 'pointerover pointerrawupdate pointerup popstate progress ratechange rejectionhandled reset resize scroll ' +
31
+ 'scrollend search securitypolicyviolation seeked seeking select selectionchange selectstart slotchange stalled ' +
32
+ 'storage submit suspend timeupdate toggle touchcancel touchend touchmove touchstart transitioncancel ' +
33
+ 'transitionend transitionrun transitionstart unhandledrejection unload visibilitychange volumechange waiting wheel'
34
+ ).split(' '))
35
+
36
+ const isKnown = (name: string): boolean => {
37
+ // PLAN-5 0-S6: a hyphenated name is a custom element's event (DOM['wa-hover']('.food')): no
38
+ // standard DOM event has a hyphen, and a bracketed name is no typo for one
39
+ if (KNOWN.has(name) || name.includes('-')) return true
40
+ const on = 'on' + name
41
+ const g: any = globalThis
42
+ try {
43
+ return !!((g.HTMLElement && on in g.HTMLElement.prototype) || (g.window && on in g.window) ||
44
+ (g.Document && on in g.Document.prototype))
45
+ } catch (_) { return false }
46
+ }
47
+
48
+ // Names agents reach for that are enriched-stream methods or key names, not events
49
+ const KEY_FIX = (sel: string, key?: string) =>
50
+ `Listen for the real event and read the key: DOM.keydown(${sel}).key()` +
51
+ (key ? `.filter(k => k === '${key}')` : '')
52
+ const SPECIAL: Record<string, (sel: string) => string> = {
53
+ key: sel => KEY_FIX(sel),
54
+ keys: sel => KEY_FIX(sel),
55
+ enter: sel => KEY_FIX(sel, 'Enter'),
56
+ escape: sel => KEY_FIX(sel, 'Escape'),
57
+ esc: sel => KEY_FIX(sel, 'Escape'),
58
+ value: sel => `Listen for the real event and read the value: DOM.input(${sel}).value()`,
59
+ checked: sel => `Listen for the real event and read the box: DOM.change(${sel}).checked()`,
60
+ data: sel => `Listen for the real event and read the attribute: DOM.click(${sel}).data('name')`,
61
+ doubleclick: sel => `Use DOM.dblclick(${sel})`,
62
+ }
63
+
64
+ const fixFor = (name: string, selector: any): string => {
65
+ const sel = typeof selector === 'string' ? `'${selector}'` : 'sel'
66
+ const special = SPECIAL[name.toLowerCase()]
67
+ if (special) return special(sel)
68
+ const s = suggest(name, KNOWN)
69
+ return (s ? `Did you mean DOM.${s}(${sel})? ` : '') +
70
+ `If '${name}' is a custom event you dispatch yourself, use the explicit form DOM.select(${sel}).events('${name}')`
71
+ }
72
+
73
+ const wrapped = new WeakMap<object, any>()
74
+
75
+ function checkedDOM(component: any, dom: any): any {
76
+ if (!dom || typeof dom != 'object') return dom
77
+ let proxy = wrapped.get(dom)
78
+ if (proxy) return proxy
79
+ const name = nameOf(component)
80
+ proxy = new Proxy(dom, {
81
+ get(t: any, k: any) {
82
+ const v = t[k]
83
+ if (typeof k != 'string' || k in t || typeof v != 'function' || isKnown(k)) return v
84
+ return function (this: any, selector: any, ...rest: any[]) {
85
+ if (once(`SYG115:${name}:${k}`)) {
86
+ devReport('SYG115', {
87
+ component,
88
+ message: `${name}.intent uses DOM.${k}(${typeof selector == 'string' ? `'${selector}'` : '…'}), but '${k}' is not a DOM event, so it never fires`,
89
+ fix: fixFor(k, selector),
90
+ data: {event: k, selector},
91
+ })
92
+ }
93
+ return v.call(this, selector, ...rest)
94
+ }
95
+ },
96
+ })
97
+ wrapped.set(dom, proxy)
98
+ return proxy
99
+ }
100
+
101
+ export const shorthandCheck: DiagnosticCheck = {
102
+ id: 'shorthand',
103
+
104
+ sources(component, sources) {
105
+ if (!sources || typeof Proxy != 'function') return
106
+ const domName = component?.DOMSourceName || 'DOM'
107
+ if (!sources[domName]) return
108
+ return new Proxy(sources, {
109
+ get(t: any, k: any) {
110
+ const v = t[k]
111
+ return k === domName ? checkedDOM(component, v) : v
112
+ },
113
+ })
114
+ },
115
+ }