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,135 @@
1
+ /**
2
+ * makeFetchDriver requests (PLAN-3 5-3: the query cache, D79/D80; G-175). Dev-entry codes
3
+ * (DEV_CODE_SEVERITY):
4
+ *
5
+ * SYG630 — a cached request that is not idempotent: `cache: true` or `staleTime` on a
6
+ * POST/PUT/PATCH/DELETE (a request with a body defaults to POST) (warn)
7
+ * SYG631 — `validate` is not a Standard Schema (no `~standard.validate`): the request
8
+ * fails (error)
9
+ * SYG632 — `{ invalidate }` matched no cache entry and no mounted resource (info)
10
+ * SYG633 — `{ abort: 'X' }` cancels the lane 'X', but this instance sends its `ok: 'X'`
11
+ * requests under another `key`, so nothing is cancelled (warn)
12
+ * SYG635 — `cache: true` / `staleTime` / `{ prefetch }` sent to a driver without a
13
+ * `queryCache()` (D88): nothing is cached (warn). The source's `__inspect().cache`
14
+ * is undefined without one
15
+ *
16
+ * Mechanism: onModel wraps the component's own model stream for every sink whose source
17
+ * takes the `resources` static (makeFetchDriver and renderComponent's HTTP fake) and checks
18
+ * each value as it is sent: requests, and each entry of a `{ resources }` declaration.
19
+ * SYG632 asks the source how many entries/resources the value matches (`__matches`).
20
+ * Each is reported once per component name and request URL / name.
21
+ */
22
+ import type {DiagnosticCheck} from '../index'
23
+ import {devReport, once, nameOf} from './shared'
24
+ import {isStandardSchema} from '../../standardSchema'
25
+
26
+ const IDEMPOTENT = /^(GET|HEAD)$/i
27
+
28
+ /** instance → reply key lanes it has sent, and ok name → the other key it was sent under */
29
+ const lanes = new WeakMap<object, {used: Set<string>; keyed: Map<string, string>}>()
30
+
31
+ const fetchSink = (component: any, name: string) => component?.sources?.[name]?.__sygnalStatic === 'resources'
32
+ /** 5-5: the sink's driver has no queryCache() */
33
+ const noCache = (component: any, sink: string) => {
34
+ const i = component?.sources?.[sink]?.__inspect?.()
35
+ return !!i && i.cache === undefined
36
+ }
37
+ const reportNoCache = (component: any, sink: string, what: string, url: string) => {
38
+ if (!noCache(component, sink) || !once(`SYG635:${nameOf(component)}:${what}:${url}`)) return
39
+ devReport('SYG635', {
40
+ component,
41
+ message: `${sink} ${url ? url + ' ' : ''}uses ${what}, but the ${sink} driver has no queryCache(), so nothing is cached`,
42
+ fix: `Give the driver a cache: makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) }) (import { queryCache } from 'sygnal'); in tests renderComponent(C, { http: { cache: queryCache() } }). Or remove ${what}`,
43
+ data: {sink, url, what},
44
+ })
45
+ }
46
+
47
+ function checkOne(component: any, sink: string, req: any, where: string) {
48
+ if (!req || typeof req !== 'object') return
49
+ const name = nameOf(component)
50
+ const url = typeof req.url === 'string' ? req.url : ''
51
+ const method = String(req.method || (req.json !== undefined || req.body !== undefined ? 'POST' : 'GET')).toUpperCase()
52
+ if ((req.cache === true || req.staleTime != null) && !IDEMPOTENT.test(method) && once(`SYG630:${name}:${method}:${url}`)) {
53
+ devReport('SYG630', {
54
+ component,
55
+ message: `${where} ${method} ${url} sets ${req.cache === true ? 'cache: true' : 'staleTime'}, so a cached reply answers it instead of sending it again`,
56
+ fix: `Cache reads only: remove ${req.cache === true ? 'cache' : 'staleTime'} from the ${method}, and refresh the cached reads after it succeeds with invalidates: ['tag']`,
57
+ data: {sink, method, url},
58
+ })
59
+ }
60
+ if (req.cache === true || req.staleTime != null) reportNoCache(component, sink, req.cache === true ? 'cache: true' : 'staleTime', url)
61
+ if ('validate' in req && !isStandardSchema(req.validate) && once(`SYG631:${name}:${url}`)) {
62
+ devReport('SYG631', {
63
+ component,
64
+ message: `${where} ${url} has validate: ${typeof req.validate}, which is not a Standard Schema, so the request fails`,
65
+ fix: "Pass a Standard Schema object (zod, valibot, arktype, ...: anything with a '~standard'.validate function), e.g. validate: QuoteSchema",
66
+ data: {sink, url},
67
+ })
68
+ }
69
+ }
70
+
71
+ export function check(component: any, sink: string, req: any) {
72
+ if (!req || typeof req !== 'object') return
73
+ const name = nameOf(component)
74
+ const res = req.resources
75
+ if (res && typeof res === 'object') {
76
+ for (const k of Object.keys(res)) checkOne(component, sink, typeof res[k] === 'string' ? {url: res[k]} : res[k], `${name}.resources.${k}:`)
77
+ return
78
+ }
79
+ if (req.prefetch) {
80
+ const p = req.prefetch
81
+ reportNoCache(component, sink, '{ prefetch }', typeof p === 'string' ? p : String(p?.url ?? ''))
82
+ return
83
+ }
84
+ if ('invalidate' in req) {
85
+ const count = component.sources[sink]?.__matches?.(req.invalidate)
86
+ const what = typeof req.invalidate === 'function' ? 'the predicate' : JSON.stringify(req.invalidate)
87
+ if (count === 0 && once(`SYG632:${name}:${what}`)) {
88
+ devReport('SYG632', {
89
+ component,
90
+ message: `${sink} { invalidate: ${what} } matched no cache entry and no mounted resource`,
91
+ fix: "Give the resources (or cached requests) to refresh tags: ['quotes'] and invalidate that tag, or a URL prefix starting with '/' ('/api/quotes')",
92
+ data: {sink, invalidate: what},
93
+ })
94
+ }
95
+ return
96
+ }
97
+ let l = lanes.get(component)
98
+ if (!l) lanes.set(component, (l = {used: new Set(), keyed: new Map()}))
99
+ const {ok, error, key, abort} = req
100
+ if (typeof abort === 'string' && key === undefined) {
101
+ const other = l.keyed.get(abort)
102
+ if (other !== undefined && !l.used.has(abort) && once(`SYG633:${name}:${abort}`)) {
103
+ devReport('SYG633', {
104
+ component,
105
+ message: `${sink} { abort: '${abort}' } cancels the requests with key '${abort}', but ${name} sends its ok: '${abort}' requests with key: '${other}', so nothing is cancelled`,
106
+ fix: `Cancel by that key: { abort: true, key: '${other}' } (or drop the key, so the lane is the ok action)`,
107
+ data: {sink, abort, key: other},
108
+ })
109
+ }
110
+ return
111
+ }
112
+ if (typeof ok === 'string' || typeof error === 'string') {
113
+ const lane = key ?? ok ?? error
114
+ l.used.add(lane)
115
+ if (typeof ok === 'string' && key !== undefined && key !== ok) l.keyed.set(ok, String(key))
116
+ }
117
+ checkOne(component, sink, req, `A ${sink} request`)
118
+ }
119
+
120
+ export const fetchCheck: DiagnosticCheck = {
121
+ id: 'fetch',
122
+
123
+ onModel(component) {
124
+ const model$ = component?.model$
125
+ if (!model$ || typeof model$ !== 'object') return
126
+ for (const sink of Object.keys(model$)) {
127
+ const s$ = model$[sink]
128
+ if (!fetchSink(component, sink) || !s$ || typeof s$.map !== 'function') continue
129
+ model$[sink] = s$.map((req: any) => {
130
+ try { check(component, sink, req) } catch (_) {}
131
+ return req
132
+ })
133
+ }
134
+ },
135
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * PLAN-5 F-1: `form` behavior diagnostics (dev entry; src/extra/form.ts carries no text).
3
+ *
4
+ * The form module calls `__SYGNAL_DIAGNOSTICS__.form(code, ...args)`:
5
+ * 231 form(schema) with a schema that isn't a Standard Schema: throws the SYG231 Error (in
6
+ * production the first validation throws a plain TypeError)
7
+ * 230 (name, values) an input / focusout from a named field whose name isn't in `values`:
8
+ * SYG230 (warn, once per name); the event is ignored
9
+ * 232 (slice) a submit while submitting or while a queued submit waits for a check: SYG232
10
+ * (info, every time); the submit is dropped
11
+ * 233 (values, output) a valid submit: each top-level `values` key missing from the schema's
12
+ * output, SYG233 (warn, once per key): the schema strips it (zod's z.object does). G-377:
13
+ * only when every output key is a values key (an output with other keys reshapes: a rename)
14
+ * 235 (field, request) a check's request with `ok` / `error` / `latest` keys, which the form
15
+ * overwrites: SYG235 (warn, once per field)
16
+ * 234 (submit, key) G-577: a valid submit whose `submit` names one of the form's own actions,
17
+ * so it dispatched '<key>.<submit>' instead of the host's action: SYG234 (warn, once per
18
+ * key and name), at the submit (the form's own action then often throws: SYG214 / SYG216)
19
+ * Host checks (onModel, once per component name), for each `uses` entry made by form():
20
+ * SYG234 (warn) `submit` missing, not a host model entry, or one of the form's own action names
21
+ * (G-577: every action in that form's model, CHECKED_<field> included)
22
+ * SYG235 (warn) a `check` key that isn't a field of `values`
23
+ * SYG236 (warn) an array of objects in `values` with a row without an `id`
24
+ * SYG237 (warn) two form uses of one host with the same form selector (G-374)
25
+ */
26
+ import type {DiagnosticCheck} from '../index'
27
+ import {devReport, once, nameOf} from './shared'
28
+ import {docsUrlFor} from '../codes'
29
+ import {hasField} from '../../formHelpers'
30
+
31
+ // the form's own action names (G-577: read from the form's model, so CHECKED_<field> counts)
32
+ const ownActions = (b: any): string[] => Object.keys(b?.model || {})
33
+ const SUBMIT_FIX = (key: string, a: string, own: string[]) =>
34
+ `Name the submit after a host action the form doesn't define (not ${own.join(', ')}): submit: 'SAVE', and rename the model entry '${a}' to SAVE: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) }`
35
+ const RESERVED = ['ok', 'error', 'latest']
36
+
37
+ const what = (v: any): string =>
38
+ v === null || v === undefined ? String(v)
39
+ : typeof v == 'function' ? 'a function'
40
+ : Array.isArray(v) ? 'an array'
41
+ : typeof v == 'object' ? `an object without ~standard.validate${typeof v.parse == 'function' ? ' (a validator from before Standard Schema?)' : ''}`
42
+ : `a ${typeof v}`
43
+
44
+ export function reportForm(code: number, a?: any, b?: any, c?: any): any {
45
+ if (code == 231) {
46
+ const err: any = new TypeError(`[Sygnal SYG231] form(schema, options): the schema is ${what(a)}, not a Standard Schema. ` +
47
+ `Pass a Standard Schema object: a zod, valibot or arktype schema, or an object with '~standard': { version: 1, vendor, validate }. ${docsUrlFor('SYG231')}`)
48
+ err.code = 'SYG231'
49
+ throw err
50
+ }
51
+ if (code == 230) {
52
+ if (!once(`SYG230:${a}`)) return
53
+ devReport('SYG230', {
54
+ component: 'form',
55
+ message: `form: a field named '${a}' changed, but '${a}' isn't a field of the form's values (${Object.keys(b || {}).join(', ') || 'none'}); the change is ignored`,
56
+ fix: `Name the input after a path in values (name="email", name="addresses.<row id>.city"), add '${a}' to values, or move the input out of the form element`,
57
+ data: {name: a},
58
+ })
59
+ } else if (code == 232) {
60
+ devReport('SYG232', {
61
+ component: 'form',
62
+ message: `form: a submit was dropped: ${a?.submitting ? 'the previous submit is still being sent (no form.DONE / form.ERRORS yet)' : 'a submit is already queued, waiting for an async check or an async schema'}`,
63
+ fix: `Disable the submit button while state.form.submitting (or queued) is true; answer the submit with ok: 'form.DONE', error: 'form.ERRORS'`,
64
+ })
65
+ } else if (code == 233) {
66
+ if (!b || typeof b != 'object' || Object.keys(b).some(k => !(k in (a || {})))) return
67
+ for (const k of Object.keys(a || {})) {
68
+ if (!(k in b) && once(`SYG233:${k}`)) devReport('SYG233', {
69
+ component: 'form',
70
+ message: `form: values.${k} is not in the schema's output (stripped or renamed), so the submit action doesn't get it under that name (a schema strips keys it doesn't declare)`,
71
+ fix: `Declare '${k}' in the schema (or make the schema keep unknown keys), or remove it from values; if the schema renames it on purpose, ignore this`,
72
+ data: {key: k},
73
+ })
74
+ }
75
+ } else if (code == 234) {
76
+ if (!once(`SYG234:run:${b}:${a}`)) return
77
+ devReport('SYG234', {
78
+ component: 'form',
79
+ message: `form '${b}': a valid submit dispatched '${b}.${a}' (the form's own ${a} action), not the host's '${a}': submit: '${a}' names one of the form's own actions, which next() prefers`,
80
+ fix: SUBMIT_FIX(b, a, ownActions(c)),
81
+ data: {key: b, submit: a},
82
+ })
83
+ } else if (code == 235) {
84
+ const set = RESERVED.filter(k => b && k in b)
85
+ if (set.length && once(`SYG235:req:${a}`)) devReport('SYG235', {
86
+ component: 'form',
87
+ message: `form: the check for '${a}' returns a request with ${set.map(k => `'${k}'`).join(', ')}; the form sets them itself (its reply action and latest: true), so they are replaced`,
88
+ fix: `Return only the request: check: { ${a}: { request: (v) => ({ url, query }), error: (body) => message } }`,
89
+ data: {field: a, keys: set},
90
+ })
91
+ }
92
+ }
93
+
94
+ /** row ids missing in `values`: the names of the arrays of objects that have a row without an id */
95
+ const missingIds = (v: any, at = '', out: string[] = []): string[] => {
96
+ if (v && typeof v == 'object') {
97
+ if (Array.isArray(v) && v.some(r => r && typeof r == 'object' && !Array.isArray(r) && r.id == null)) out.push(at)
98
+ for (const k in v) missingIds(v[k], at ? `${at}.${k}` : k, out)
99
+ }
100
+ return out
101
+ }
102
+
103
+ export const formsCheck: DiagnosticCheck = {
104
+ id: 'forms',
105
+
106
+ onModel(component) {
107
+ const view = component?.view, uses = view?.uses
108
+ if (!uses || typeof uses != 'object') return
109
+ const name = nameOf(component), sels: Record<string, string> = {}
110
+ for (const key of Object.keys(uses)) {
111
+ const b = uses[key]
112
+ if (!b?.form) continue
113
+ // G-374: each form listens on its selector inside the host, so two with one selector hear
114
+ // each other's fields and submits
115
+ const sel = b.options?.form || 'form'
116
+ if (sel in sels) {
117
+ if (once(`SYG237:${name}:${sel}`)) devReport('SYG237', {
118
+ component,
119
+ message: `${name}'s forms '${sels[sel]}' and '${key}' both listen on '${sel}', so each hears the other's fields and submits`,
120
+ fix: `Give each form element its own class and pass it: ${sels[sel]}: form(schema, { ..., form: '.${sels[sel]}' }), ${key}: form(schema, { ..., form: '.${key}' })`,
121
+ data: {keys: [sels[sel], key], form: sel},
122
+ })
123
+ } else sels[sel] = key
124
+ if (!once(`SYG23x:${name}:${key}`)) continue
125
+ const o = b.options || {}, values = o.values || {}
126
+ const model = Object.keys(view.model || {}).map(a => a.split('|')[0].trim()), own = ownActions(b)
127
+ const clash = own.includes(o.submit)
128
+ if (typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
129
+ devReport('SYG234', {
130
+ component,
131
+ message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action, so a valid submit dispatches nothing`
132
+ : clash ? `${name}'s form '${key}' names submit: '${o.submit}', one of the form's own actions; a valid submit dispatches the form's '${key}.${o.submit}'${model.includes(o.submit) ? `, never ${name}'s '${o.submit}' entry` : ''}`
133
+ : `${name}'s form '${key}' dispatches '${o.submit}' on a valid submit, but ${name} has no model entry '${o.submit}'`,
134
+ fix: clash ? SUBMIT_FIX(key, o.submit, own)
135
+ : `Name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
136
+ data: {key, submit: o.submit},
137
+ })
138
+ }
139
+ for (const f of Object.keys(o.check || {})) {
140
+ if (!hasField(values, f)) devReport('SYG235', {
141
+ component,
142
+ message: `${name}'s form '${key}' has a check for '${f}', which isn't a field of its values; it never runs`,
143
+ fix: `Use a field name from values (${Object.keys(values).join(', ')})`,
144
+ data: {key, field: f},
145
+ })
146
+ }
147
+ for (const at of missingIds(values)) devReport('SYG236', {
148
+ component,
149
+ message: `${name}'s form '${key}': values.${at} has rows without an id, so its fields are named by index and errors, touched state and focus don't follow a row when another is removed`,
150
+ fix: `Give each row a unique id: ${at}: [{ id: 1, ... }] (form.ADD numbers new rows itself)`,
151
+ data: {key, field: at},
152
+ })
153
+ }
154
+ },
155
+ }
156
+
157
+ /** Install the `form` hook on the core bridge (form.ts calls it). */
158
+ export function installFormHooks(): () => void {
159
+ const c = (globalThis as any).__SYGNAL_DIAGNOSTICS__
160
+ if (!c) return () => {}
161
+ c.form = reportForm
162
+ return () => { if (c.form === reportForm) c.form = undefined }
163
+ }
@@ -17,15 +17,56 @@
17
17
  * | SYG104 | intent selector only matches inside a child component | dom.ts |
18
18
  * | SYG105 | EVENTS type emitted-not-selected / selected-not-emitted| events.ts |
19
19
  * | SYG106 | parent prop overwritten by a reserved view argument | props.ts |
20
+ * | SYG112 | reply action has no model entry | replies.ts |
21
+ * | SYG630 | cached request that isn't idempotent (POST + cache) | fetch.ts |
22
+ * | SYG631 | validate is not a Standard Schema | fetch.ts |
23
+ * | SYG632 | invalidate matched nothing (info) | fetch.ts |
24
+ * | SYG633 | abort names a lane its requests don't use | fetch.ts |
25
+ * | SYG635 | cache: true / staleTime / prefetch without queryCache() | fetch.ts |
26
+ * | SYG115 | unknown DOM event shorthand (DOM.key(...)) | shorthand.ts |
27
+ * | SYG124 | component where a control or selector is expected | controls.ts |
28
+ * | SYG140 | widget emitted an event it doesn't declare | widgets.ts |
29
+ * | SYG142 | widget command not declared (also a reserved name) | elementCommands.ts, widgets.ts |
30
+ * | SYG143 | widget tag used as a selector | widgets.ts |
31
+ * | SYG144 | declared widget event the host also fires (info) | widgets.ts |
32
+ * | SYG660 | widget mount / update / unmount threw (660/661/662) | widgets.ts |
33
+ * | SYG430 | VirtualCollection: no bounded height; SYG431 items without ids; SYG432 item not one element; SYG433 scroll target not in the list; SYG434 bad estimateSize / overscan | virtual.ts |
34
+ * | SYG125 | control given component statics / bad spec vnode() | controls.ts |
35
+ * | SYG127 | behavior key in initialState / unresolvable uses entry | behaviors.ts |
36
+ * | SYG230 | form: field name not in values; SYG231 not a Standard Schema; SYG232 submit dropped (info); SYG233 value missing from the schema output | forms.ts |
37
+ * | SYG234 | form submit action has no model entry; SYG235 check names an unknown field / sets reply fields; SYG236 rows without an id | forms.ts |
38
+ * | SYG145 | sortable: item without its id attribute; SYG146 item / handle selector matches nothing; SYG147 from is not an array | sortable.ts |
39
+ * | SYG116 | EVENTS value with no string type (a function) | events.ts |
40
+ * | SYG130 | href() names no route / leaves out a param | router.ts |
41
+ * | SYG131 | route params the pattern doesn't use | router.ts |
42
+ * | SYG132 | declaration static never sent: a root without initialState | router.ts |
43
+ * | SYG133 | SPA router inside a Vike app | router.ts |
44
+ * | SYG422 | timer spec makeTimerDriver() can't run | timers.ts |
45
+ * | SYG643 | timers/connections/resources/browser with no driver | timers.ts |
46
+ * | SYG663 | browser entry / BROWSER command that can't run | browserSources.ts |
47
+ * | SYG664 | browser source the driver wasn't made with | browserSources.ts |
48
+ * | SYG665 | browser source failed with no error action | browserSources.ts |
49
+ * | SYG668 | intersection / resize with no element to observe | browserSources.ts |
50
+ * | SYG238 | a root with a model but no initialState: renders nothing | state.ts |
51
+ * | SYG223 | persist pick/omit key not in initialState | persist.ts |
52
+ * | SYG224 | persist on a component that isn't the root | persist.ts |
53
+ * | SYG645 | viewTransitions without makeViewTransitionDOMDriver(), or not an array | viewTransitions.ts |
20
54
  * | SYG201 | STATE reducer dropped keys | state.ts |
21
55
  * | SYG202 | STATE reducer returned undefined | state.ts |
56
+ * | SYG221 | set() called with a string | state.ts |
57
+ * | SYG222 | same object returned after an in-place mutation | state.ts |
22
58
  * | SYG301 | RxJS operator used on an xstream stream | rxjsHints.ts |
23
59
  * | SYG401 | Collection `from` missing or not an array | collections.ts |
24
- * | SYG501 | strict: view uses positional (props, state, ...) args | strict.ts |
25
- * | SYG502 | strict: STATE reducer returned the unchanged state | strict.ts |
26
- * | SYG504 | strict: 'ACTION | SINK' shorthand model key | strict.ts |
60
+ * | SYG421 | invalid data (dataset) key in a view | dataset.ts |
61
+ * | SYG612 | a form 6.0 removed (also SYG501/504 statically) | next.ts |
62
+ * | SYG502 | retired in 6.0 (never reported; GS-4) | — |
63
+ * | SYG508 | strict: select()/errors() round trip on a reply source | replies.ts |
64
+ * | — | D152: shared statics frozen (initialState deep) | statics.ts |
27
65
  * | — | inspect(): the runtime app graph (2B) | inspect.ts |
28
66
  *
67
+ * SYG112, SYG130-133 (PLAN-3), SYG115/116/221/421 (G-143) SYG124/125 (PLAN-4 CT-1) and SYG127 (GS-1) are dev-entry-only codes: their severities live in
68
+ * DEV_CODE_SEVERITY (codes.ts), registered by ./shared, not in the main bundle.
69
+ *
29
70
  * Strict checks (SYG5xx) only report after configureStrict(true) (or
30
71
  * renderComponent(C, { strict: true }), or globalThis.__SYGNAL_STRICT__).
31
72
  */
@@ -38,16 +79,34 @@ import {eventsCheck} from './events'
38
79
  import {propsCheck} from './props'
39
80
  import {collectionsCheck} from './collections'
40
81
  import {domCheck} from './dom'
82
+ import {shorthandCheck} from './shorthand'
83
+ import {controlsCheck, installControlHooks} from './controls'
84
+ import {elementCommandsCheck, installElementCommandHooks} from './elementCommands'
85
+ import {installWidgetHooks} from './widgets'
86
+ import {installVirtualHooks} from './virtual'
87
+ import {behaviorsCheck} from './behaviors'
88
+ import {formsCheck, installFormHooks} from './forms'
89
+ import {installSortableHooks} from './sortable'
90
+ import {datasetCheck} from './dataset'
41
91
  import {strictCheck} from './strict'
92
+ import {repliesCheck} from './replies'
93
+ import {routerCheck, installRouterHooks} from './router'
94
+ import {fetchCheck} from './fetch'
95
+ import {timersCheck, installTimerHooks} from './timers'
96
+ import {installBrowserSourceHooks} from './browserSources'
97
+ import {persistCheck} from './persist'
98
+ import {viewTransitionsCheck} from './viewTransitions'
99
+ import {staticsCheck} from './statics'
42
100
  import {installRxjsHints} from './rxjsHints'
43
101
  import {inspectCheck, installInspect} from './inspect'
102
+ import {nextHooks} from './next'
44
103
 
45
104
  export {checkEventBus} from './events'
46
105
  export {configureStrict, isStrictEnabled} from './strict'
47
106
  export type {EventBusSummary} from './events'
48
107
  export {RXJS_HINTS} from './rxjsHints'
49
108
  export {inspect} from './inspect'
50
- export type {InspectGraph, InspectComponent, InspectAction, InspectActionTrigger, InspectChild, InspectSelector, InspectDiagnostic, InspectOptions} from './public'
109
+ export type {InspectResource, InspectCacheEntry, InspectGraph, InspectComponent, InspectAction, InspectActionTrigger, InspectChild, InspectSelector, InspectControl, InspectDiagnostic, InspectOptions, InspectRecentAction} from './public'
51
110
  export {listCodes, getCodeInfo} from '../codes'
52
111
  export type {DiagnosticCodeInfo} from '../codes'
53
112
 
@@ -59,7 +118,20 @@ export const checks: DiagnosticCheck[] = [
59
118
  propsCheck,
60
119
  collectionsCheck,
61
120
  domCheck,
121
+ shorthandCheck,
122
+ controlsCheck,
123
+ elementCommandsCheck,
124
+ behaviorsCheck,
125
+ formsCheck,
126
+ datasetCheck,
62
127
  strictCheck,
128
+ repliesCheck,
129
+ routerCheck,
130
+ fetchCheck,
131
+ timersCheck,
132
+ persistCheck,
133
+ viewTransitionsCheck,
134
+ staticsCheck,
63
135
  inspectCheck,
64
136
  ]
65
137
 
@@ -75,10 +147,31 @@ export function installChecks(): () => void {
75
147
  const unregister = checks.map(check => core.registerCheck(check))
76
148
  const uninstallHints = installRxjsHints(Stream && Stream.prototype)
77
149
  const uninstallInspect = installInspect()
150
+ const uninstallRouter = installRouterHooks()
151
+ const uninstallControls = installControlHooks()
152
+ const uninstallTimers = installTimerHooks()
153
+ const uninstallBrowser = installBrowserSourceHooks()
154
+ const uninstallElementCommands = installElementCommandHooks()
155
+ const uninstallWidgets = installWidgetHooks()
156
+ const uninstallVirtual = installVirtualHooks()
157
+ const uninstallForms = installFormHooks()
158
+ const uninstallSortable = installSortableHooks()
159
+ // PLAN-4.6 R4: the core reads its hooks from the bridge once per app (checks/next.ts)
160
+ ;(core.layers ||= new Set()).add(nextHooks)
78
161
  const uninstall = () => {
162
+ core.layers?.delete(nextHooks)
79
163
  unregister.forEach(fn => fn())
80
164
  uninstallHints()
81
165
  uninstallInspect()
166
+ uninstallRouter()
167
+ uninstallControls()
168
+ uninstallTimers()
169
+ uninstallBrowser()
170
+ uninstallElementCommands()
171
+ uninstallWidgets()
172
+ uninstallVirtual()
173
+ uninstallForms()
174
+ uninstallSortable()
82
175
  if (core.__uninstallChecks === uninstall) core.__uninstallChecks = undefined
83
176
  }
84
177
  core.__uninstallChecks = uninstall