sygnal 5.3.6 → 5.4.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 (226) hide show
  1. package/README.md +53 -38
  2. package/dist/astro/client.cjs.js +18 -8106
  3. package/dist/astro/client.cjs.js.map +1 -0
  4. package/dist/astro/client.mjs +17 -8105
  5. package/dist/astro/client.mjs.map +1 -0
  6. package/dist/astro/index.cjs.js +768 -9
  7. package/dist/astro/index.cjs.js.map +1 -0
  8. package/dist/astro/index.mjs +768 -9
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/server.cjs.js +4 -1
  11. package/dist/astro/server.cjs.js.map +1 -0
  12. package/dist/astro/server.mjs +4 -1
  13. package/dist/astro/server.mjs.map +1 -0
  14. package/dist/diagnostics.cjs.js +1297 -0
  15. package/dist/diagnostics.cjs.js.map +1 -0
  16. package/dist/diagnostics.esm.js +1267 -0
  17. package/dist/diagnostics.esm.js.map +1 -0
  18. package/dist/index.cjs.js +2981 -1386
  19. package/dist/index.cjs.js.map +1 -0
  20. package/dist/index.d.ts +848 -111
  21. package/dist/index.esm.js +2997 -1411
  22. package/dist/index.esm.js.map +1 -0
  23. package/dist/jsx-dev-runtime.cjs.js +15 -9
  24. package/dist/jsx-dev-runtime.cjs.js.map +1 -0
  25. package/dist/jsx-dev-runtime.esm.js +15 -9
  26. package/dist/jsx-dev-runtime.esm.js.map +1 -0
  27. package/dist/jsx-runtime.cjs.js +15 -9
  28. package/dist/jsx-runtime.cjs.js.map +1 -0
  29. package/dist/jsx-runtime.esm.js +15 -9
  30. package/dist/jsx-runtime.esm.js.map +1 -0
  31. package/dist/jsx.cjs.js +15 -9
  32. package/dist/jsx.cjs.js.map +1 -0
  33. package/dist/jsx.esm.js +15 -9
  34. package/dist/jsx.esm.js.map +1 -0
  35. package/dist/sygnal.min.js +2 -1
  36. package/dist/sygnal.min.js.map +1 -0
  37. package/dist/vike/+config.cjs.js +1 -0
  38. package/dist/vike/+config.cjs.js.map +1 -0
  39. package/dist/vike/+config.js +1 -0
  40. package/dist/vike/+config.js.map +1 -0
  41. package/dist/vike/ClientOnly.cjs.js +1 -0
  42. package/dist/vike/ClientOnly.cjs.js.map +1 -0
  43. package/dist/vike/ClientOnly.mjs +1 -0
  44. package/dist/vike/ClientOnly.mjs.map +1 -0
  45. package/dist/vike/onRenderClient.cjs.js +1 -0
  46. package/dist/vike/onRenderClient.cjs.js.map +1 -0
  47. package/dist/vike/onRenderClient.mjs +1 -0
  48. package/dist/vike/onRenderClient.mjs.map +1 -0
  49. package/dist/vike/onRenderHtml.cjs.js +1 -0
  50. package/dist/vike/onRenderHtml.cjs.js.map +1 -0
  51. package/dist/vike/onRenderHtml.mjs +1 -0
  52. package/dist/vike/onRenderHtml.mjs.map +1 -0
  53. package/dist/vite/plugin.cjs.js +680 -47
  54. package/dist/vite/plugin.cjs.js.map +1 -0
  55. package/dist/vite/plugin.mjs +680 -47
  56. package/dist/vite/plugin.mjs.map +1 -0
  57. package/llms.txt +250 -0
  58. package/package.json +15 -6
  59. package/src/astro/client.ts +13 -2
  60. package/src/astro/index.d.ts +24 -1
  61. package/src/astro/index.ts +40 -9
  62. package/src/astro/server.ts +3 -1
  63. package/src/collection.ts +5 -2
  64. package/src/component.ts +375 -330
  65. package/src/cycle/dom/MainDOMSource.ts +4 -0
  66. package/src/cycle/dom/classNameModule.ts +74 -0
  67. package/src/cycle/dom/controlledInputModule.ts +33 -0
  68. package/src/cycle/dom/enrichEventStream.ts +9 -3
  69. package/src/cycle/dom/makeDOMDriver.ts +1 -2
  70. package/src/cycle/dom/mockDOMSource.ts +62 -8
  71. package/src/cycle/dom/modules.ts +5 -1
  72. package/src/cycle/state/Collection.ts +13 -3
  73. package/src/cycle/state/StateSource.ts +1 -1
  74. package/src/cycle/state/pickCombine.ts +35 -1
  75. package/src/cycle/state/withState.ts +1 -1
  76. package/src/extra/devtools.ts +8 -0
  77. package/src/extra/diagnostics/checks/collections.ts +67 -0
  78. package/src/extra/diagnostics/checks/dom.ts +241 -0
  79. package/src/extra/diagnostics/checks/events.ts +68 -0
  80. package/src/extra/diagnostics/checks/index.ts +109 -0
  81. package/src/extra/diagnostics/checks/inspect.ts +319 -0
  82. package/src/extra/diagnostics/checks/props.ts +51 -0
  83. package/src/extra/diagnostics/checks/public.d.ts +211 -0
  84. package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
  85. package/src/extra/diagnostics/checks/shared.ts +119 -0
  86. package/src/extra/diagnostics/checks/state.ts +55 -0
  87. package/src/extra/diagnostics/checks/strict.ts +112 -0
  88. package/src/extra/diagnostics/checks/wiring.ts +65 -0
  89. package/src/extra/diagnostics/codes.ts +232 -0
  90. package/src/extra/diagnostics/index.ts +328 -0
  91. package/src/extra/diagnostics/legacy.ts +59 -0
  92. package/src/extra/driverFactories.ts +106 -58
  93. package/src/extra/eventDriver.ts +4 -0
  94. package/src/extra/flatten.ts +75 -0
  95. package/src/extra/reducers.ts +12 -5
  96. package/src/extra/run.ts +11 -0
  97. package/src/extra/testing.ts +890 -180
  98. package/src/extra/xstreamExtras.ts +269 -0
  99. package/src/index.d.ts +428 -17
  100. package/src/index.ts +7 -6
  101. package/src/pragma/index.ts +14 -9
  102. package/src/switchable.ts +10 -8
  103. package/src/vike/types.ts +21 -4
  104. package/src/vite/plugin.d.ts +95 -2
  105. package/src/vite/plugin.ts +730 -55
  106. package/dist/astro/astro/client.d.ts +0 -5
  107. package/dist/astro/astro/index.d.ts +0 -16
  108. package/dist/astro/astro/server.d.ts +0 -12
  109. package/dist/astro/client.d.ts +0 -5
  110. package/dist/astro/client.esm.js +0 -4665
  111. package/dist/astro/collection.d.ts +0 -12
  112. package/dist/astro/component.d.ts +0 -22
  113. package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
  114. package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
  115. package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
  116. package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
  117. package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
  118. package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
  119. package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
  120. package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
  121. package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
  122. package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
  123. package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
  124. package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
  125. package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
  126. package/dist/astro/cycle/dom/index.d.ts +0 -8
  127. package/dist/astro/cycle/dom/isolate.d.ts +0 -9
  128. package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
  129. package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
  130. package/dist/astro/cycle/dom/modules.d.ts +0 -5
  131. package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
  132. package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
  133. package/dist/astro/cycle/dom/thunk.d.ts +0 -11
  134. package/dist/astro/cycle/dom/utils.d.ts +0 -8
  135. package/dist/astro/cycle/isolate/index.d.ts +0 -42
  136. package/dist/astro/cycle/run/adapt.d.ts +0 -6
  137. package/dist/astro/cycle/run/index.d.ts +0 -36
  138. package/dist/astro/cycle/run/internals.d.ts +0 -8
  139. package/dist/astro/cycle/run/types.d.ts +0 -49
  140. package/dist/astro/cycle/state/Collection.d.ts +0 -29
  141. package/dist/astro/cycle/state/StateSource.d.ts +0 -20
  142. package/dist/astro/cycle/state/index.d.ts +0 -5
  143. package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
  144. package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
  145. package/dist/astro/cycle/state/types.d.ts +0 -18
  146. package/dist/astro/cycle/state/withState.d.ts +0 -17
  147. package/dist/astro/extra/classes.d.ts +0 -7
  148. package/dist/astro/extra/devtools.d.ts +0 -78
  149. package/dist/astro/extra/dragDriver.d.ts +0 -20
  150. package/dist/astro/extra/driverFactories.d.ts +0 -12
  151. package/dist/astro/extra/eventDriver.d.ts +0 -9
  152. package/dist/astro/extra/exactState.d.ts +0 -1
  153. package/dist/astro/extra/hmr.d.ts +0 -17
  154. package/dist/astro/extra/logDriver.d.ts +0 -2
  155. package/dist/astro/extra/processDrag.d.ts +0 -19
  156. package/dist/astro/extra/processForm.d.ts +0 -15
  157. package/dist/astro/extra/run.d.ts +0 -13
  158. package/dist/astro/extra/xstreamCompat.d.ts +0 -5
  159. package/dist/astro/index.d.ts +0 -19
  160. package/dist/astro/index.esm.js +0 -27
  161. package/dist/astro/jsx-dev-runtime.d.ts +0 -1
  162. package/dist/astro/jsx-runtime.d.ts +0 -4
  163. package/dist/astro/jsx.d.ts +0 -2
  164. package/dist/astro/pragma/fn.d.ts +0 -7
  165. package/dist/astro/pragma/index.d.ts +0 -7
  166. package/dist/astro/pragma/is.d.ts +0 -10
  167. package/dist/astro/server.d.ts +0 -12
  168. package/dist/astro/server.esm.js +0 -25
  169. package/dist/astro/switchable.d.ts +0 -7
  170. package/dist/collection.d.ts +0 -12
  171. package/dist/component.d.ts +0 -22
  172. package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
  173. package/dist/cycle/dom/DOMSource.d.ts +0 -11
  174. package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
  175. package/dist/cycle/dom/ElementFinder.d.ts +0 -8
  176. package/dist/cycle/dom/EventDelegator.d.ts +0 -34
  177. package/dist/cycle/dom/IsolateModule.d.ts +0 -23
  178. package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
  179. package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
  180. package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
  181. package/dist/cycle/dom/SymbolTree.d.ts +0 -9
  182. package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
  183. package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
  184. package/dist/cycle/dom/fromEvent.d.ts +0 -6
  185. package/dist/cycle/dom/index.d.ts +0 -8
  186. package/dist/cycle/dom/isolate.d.ts +0 -9
  187. package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
  188. package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
  189. package/dist/cycle/dom/modules.d.ts +0 -5
  190. package/dist/cycle/dom/snabbdom.d.ts +0 -22
  191. package/dist/cycle/dom/styleModule.d.ts +0 -13
  192. package/dist/cycle/dom/thunk.d.ts +0 -11
  193. package/dist/cycle/dom/utils.d.ts +0 -8
  194. package/dist/cycle/isolate/index.d.ts +0 -42
  195. package/dist/cycle/run/adapt.d.ts +0 -6
  196. package/dist/cycle/run/index.d.ts +0 -36
  197. package/dist/cycle/run/internals.d.ts +0 -8
  198. package/dist/cycle/run/types.d.ts +0 -49
  199. package/dist/cycle/state/Collection.d.ts +0 -29
  200. package/dist/cycle/state/StateSource.d.ts +0 -20
  201. package/dist/cycle/state/index.d.ts +0 -5
  202. package/dist/cycle/state/pickCombine.d.ts +0 -3
  203. package/dist/cycle/state/pickMerge.d.ts +0 -3
  204. package/dist/cycle/state/types.d.ts +0 -18
  205. package/dist/cycle/state/withState.d.ts +0 -17
  206. package/dist/extra/classes.d.ts +0 -7
  207. package/dist/extra/devtools.d.ts +0 -78
  208. package/dist/extra/dragDriver.d.ts +0 -20
  209. package/dist/extra/driverFactories.d.ts +0 -12
  210. package/dist/extra/eventDriver.d.ts +0 -9
  211. package/dist/extra/exactState.d.ts +0 -1
  212. package/dist/extra/hmr.d.ts +0 -17
  213. package/dist/extra/logDriver.d.ts +0 -2
  214. package/dist/extra/processDrag.d.ts +0 -19
  215. package/dist/extra/processForm.d.ts +0 -15
  216. package/dist/extra/run.d.ts +0 -13
  217. package/dist/extra/xstreamCompat.d.ts +0 -5
  218. package/dist/jsx-dev-runtime.d.ts +0 -1
  219. package/dist/jsx-dev-runtime.js +0 -224
  220. package/dist/jsx-runtime.d.ts +0 -4
  221. package/dist/jsx-runtime.js +0 -224
  222. package/dist/jsx.d.ts +0 -2
  223. package/dist/pragma/fn.d.ts +0 -7
  224. package/dist/pragma/index.d.ts +0 -7
  225. package/dist/pragma/is.d.ts +0 -10
  226. package/dist/switchable.d.ts +0 -7
@@ -0,0 +1,241 @@
1
+ /**
2
+ * SYG104 — isolation boundary: an intent selector matches nothing in the
3
+ * component's own scope, but matches elements inside a child
4
+ * component / Collection item (warn).
5
+ * SYG103 — an intent selector has never matched a rendered element (info,
6
+ * escalated to warn after >= 3 renders AND 2 s of render-idle time
7
+ * without a match).
8
+ *
9
+ * Mechanism:
10
+ * 1. MainDOMSource.select() calls the onSelector hook with the source it was
11
+ * called on. Its namespace = the component's isolation scopes + any
12
+ * earlier select() selectors. Selectors are queued as "pending".
13
+ * 2. onIntent (right after the intent function ran, still inside the
14
+ * component's constructor) claims the pending selectors whose isolation
15
+ * scopes equal the component's DOM source scopes, and records
16
+ * scopes -> component name (used to name the child for SYG104).
17
+ * 3. onRender schedules a sweep after `settleMs` (onRender sees the vnode
18
+ * before the DOM patch; nested components patch a few ms later). The
19
+ * sweep finds the component's root element through the DOM driver's
20
+ * IsolateModule and runs each selector with querySelectorAll, splitting
21
+ * the matches with the DOM driver's own ScopeChecker (the same test the
22
+ * EventDelegator uses to deliver events) into "in scope" and "inside a
23
+ * total-isolation boundary" (child component).
24
+ * 4. "Ever matched" is tracked per component NAME + selector, so a selector
25
+ * that matches in one Collection item is fine for all of them.
26
+ *
27
+ * SYG103 noise control (conditional rendering is normal): a selector whose
28
+ * class/id/tag tokens all appear in the component's view source is assumed
29
+ * to be conditionally rendered and is not reported. A selector that does not
30
+ * appear in the view (typo, or an element rendered elsewhere) is reported as
31
+ * info on the first sweep, and escalated per the rule above.
32
+ *
33
+ * Only real DOM sources (with an IsolateModule) are checked; renderComponent's
34
+ * mock DOM never calls onSelector.
35
+ */
36
+ import type {DiagnosticCheck} from '../index'
37
+ import {ScopeChecker} from '../../../cycle/dom/ScopeChecker'
38
+ import type {Scope} from '../../../cycle/dom/isolate'
39
+ import {reportSafely, once, onReset, timing, nameOf} from './shared'
40
+
41
+ interface Tracked {
42
+ name: string
43
+ source: any
44
+ selectors: string[]
45
+ renders: number
46
+ settleTimer?: any
47
+ idleTimer?: any
48
+ /** selectors already explained by SYG104 */
49
+ crossed: Set<string>
50
+ disposed?: boolean
51
+ }
52
+
53
+ const scopesOf = (ns: Scope[]): Scope[] => ns.filter(s => s.type !== 'selector')
54
+ const keyOf = (scopes: Scope[]): string => scopes.map(s => `${s.type[0]}:${s.scope}`).join('/')
55
+
56
+ let pending: Array<{key: string; selector: string}> = []
57
+ let tracked = new WeakMap<object, Tracked>()
58
+ /** isolation-scope key -> the component instance that owns it (latest wins) */
59
+ let owners = new Map<string, any>()
60
+ const ownerKeys = new WeakMap<object, string>()
61
+ let matched = new Set<string>()
62
+ let viewTokens = new WeakMap<object, string>()
63
+
64
+ onReset(() => {
65
+ pending = []
66
+ tracked = new WeakMap()
67
+ owners = new Map()
68
+ matched = new Set()
69
+ })
70
+
71
+ const unref = (t: any) => { if (t && typeof t.unref === 'function') t.unref(); return t }
72
+
73
+ function viewSource(component: any): string {
74
+ const view = component && component.view
75
+ if (typeof view !== 'function') return ''
76
+ let src = viewTokens.get(view)
77
+ if (src === undefined) {
78
+ try { src = Function.prototype.toString.call(view) } catch (_) { src = '' }
79
+ viewTokens.set(view, src as string)
80
+ }
81
+ return src as string
82
+ }
83
+
84
+ /** class / id / tag tokens of a selector: '.a .b-c > #d' -> ['a', 'b-c', 'd'] */
85
+ const tokensOf = (selector: string): string[] =>
86
+ (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [])
87
+
88
+ function appearsInView(component: any, selector: string): boolean {
89
+ const src = viewSource(component)
90
+ if (!src) return false
91
+ const tokens = tokensOf(selector)
92
+ if (!tokens.length) return false
93
+ return tokens.every(t => new RegExp(`(^|[^\\w-])${t.replace(/[-]/g, '\\-')}($|[^\\w-])`).test(src))
94
+ }
95
+
96
+ function rootElementOf(t: Tracked): Element | undefined {
97
+ try {
98
+ const im = t.source._isolateModule
99
+ return im && im.getElement(scopesOf(t.source.namespace))
100
+ } catch (_) {
101
+ return undefined
102
+ }
103
+ }
104
+
105
+ function childNameFor(t: Tracked, elements: Element[]): string | undefined {
106
+ const im = t.source._isolateModule
107
+ const depth = scopesOf(t.source.namespace).length
108
+ for (const el of elements) {
109
+ let ns: Scope[] | undefined
110
+ try { ns = im.getNamespace(el) } catch (_) { ns = undefined }
111
+ if (!ns) continue
112
+ for (let i = depth; i < ns.length; i++) {
113
+ if (ns[i].type !== 'total') continue
114
+ const owner = owners.get(keyOf(ns.slice(0, i + 1)))
115
+ if (owner) return nameOf(owner)
116
+ break
117
+ }
118
+ }
119
+ return undefined
120
+ }
121
+
122
+ function report103(component: any, t: Tracked, selector: string, severity: 'info' | 'warn'): void {
123
+ if (!once(`SYG103:${severity}:${t.name}:${selector}`)) return
124
+ reportSafely('SYG103', {
125
+ component,
126
+ severity,
127
+ message: `DOM.select('${selector}') in ${t.name} has not matched any element ${t.name} rendered` +
128
+ (severity === 'warn' ? ` (after ${t.renders} renders)` : ''),
129
+ fix: `Check the selector against the view's className/id. If the element is rendered by a child component, handle the event inside that child`,
130
+ data: {selector, renders: t.renders},
131
+ })
132
+ }
133
+
134
+ function sweep(component: any, t: Tracked, escalate: boolean): void {
135
+ if (t.disposed) return
136
+ const root = rootElementOf(t)
137
+ if (!root || typeof root.querySelectorAll !== 'function') return
138
+ const im = t.source._isolateModule
139
+ const checker = new ScopeChecker(t.source.namespace, im)
140
+ let unmatched = false
141
+
142
+ for (const selector of t.selectors) {
143
+ const matchKey = `${t.name}\u0000${selector}`
144
+ if (matched.has(matchKey) || t.crossed.has(selector)) continue
145
+ let all: Element[]
146
+ try {
147
+ all = Array.prototype.slice.call(root.querySelectorAll(selector))
148
+ if (root.matches(selector)) all.push(root)
149
+ } catch (_) {
150
+ continue // invalid selector: the DOM driver reports that itself
151
+ }
152
+ const own = all.filter(el => { try { return checker.isDirectlyInScope(el) } catch (_) { return false } })
153
+ if (own.length) { matched.add(matchKey); continue }
154
+
155
+ if (all.length) {
156
+ t.crossed.add(selector)
157
+ if (!once(`SYG104:${t.name}:${selector}`)) continue
158
+ const child = childNameFor(t, all) || 'a child component'
159
+ reportSafely('SYG104', {
160
+ 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},
164
+ })
165
+ continue
166
+ }
167
+
168
+ if (appearsInView(component, selector)) continue
169
+ unmatched = true
170
+ if (escalate && t.renders >= timing.minRenders) report103(component, t, selector, 'warn')
171
+ else report103(component, t, selector, 'info')
172
+ }
173
+
174
+ if (unmatched && !escalate) {
175
+ clearTimeout(t.idleTimer)
176
+ t.idleTimer = unref(setTimeout(() => { t.idleTimer = undefined; sweep(component, t, true) }, timing.idleMs))
177
+ }
178
+ }
179
+
180
+ /**
181
+ * inspect (2B, read-only): a component's intent selectors on a real DOM source
182
+ * and whether a sweep has seen them match (true), only match inside a child
183
+ * (crossed), or neither yet. undefined when the component isn't tracked.
184
+ */
185
+ export function selectorStatus(component: any): Array<{selector: string; matched: boolean | null; crossed: boolean}> | undefined {
186
+ const t = tracked.get(component)
187
+ return t && t.selectors.map(selector => ({
188
+ selector,
189
+ matched: matched.has(`${t.name}\u0000${selector}`) ? true : t.crossed.has(selector) ? false : null,
190
+ crossed: t.crossed.has(selector),
191
+ }))
192
+ }
193
+
194
+ export const domCheck: DiagnosticCheck = {
195
+ id: 'dom',
196
+
197
+ onSelector(source, selector) {
198
+ const ns: Scope[] | undefined = source && source.namespace
199
+ if (!Array.isArray(ns) || typeof selector !== 'string' || !selector) return
200
+ const prefix = ns.filter(s => s.type === 'selector').map(s => s.scope).join(' ')
201
+ pending.push({key: keyOf(scopesOf(ns)), selector: prefix ? `${prefix} ${selector}` : selector})
202
+ if (pending.length > 1000) pending.shift()
203
+ },
204
+
205
+ onIntent(component) {
206
+ const claimed = pending
207
+ pending = []
208
+ const source = component && component.sources && component.sources[component.DOMSourceName || 'DOM']
209
+ if (!source || !source._isolateModule || !Array.isArray(source.namespace)) return
210
+ const key = keyOf(scopesOf(source.namespace))
211
+ const name = nameOf(component)
212
+ owners.set(key, component)
213
+ ownerKeys.set(component, key)
214
+ const selectors = [...new Set(claimed.filter(p => p.key === key).map(p => p.selector))]
215
+ if (selectors.length) tracked.set(component, {name, source, selectors, renders: 0, crossed: new Set()})
216
+ },
217
+
218
+ onRender(component) {
219
+ const t = tracked.get(component)
220
+ if (!t || t.disposed) return
221
+ t.renders++
222
+ clearTimeout(t.idleTimer)
223
+ t.idleTimer = undefined
224
+ if (t.settleTimer) return
225
+ t.settleTimer = unref(setTimeout(() => {
226
+ t.settleTimer = undefined
227
+ sweep(component, t, false)
228
+ }, timing.settleMs))
229
+ },
230
+
231
+ onDispose(component) {
232
+ const key = ownerKeys.get(component)
233
+ if (key !== undefined && owners.get(key) === component) owners.delete(key)
234
+ const t = tracked.get(component)
235
+ if (!t) return
236
+ t.disposed = true
237
+ clearTimeout(t.settleTimer)
238
+ clearTimeout(t.idleTimer)
239
+ tracked.delete(component)
240
+ },
241
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * SYG105 — EVENTS type emitted but never selected, or selected but never
3
+ * emitted (info).
4
+ *
5
+ * Mechanism: the EVENTS driver (src/extra/eventDriver.ts) calls onBusSelect
6
+ * for every EVENTS.select(type) and onBusEmit for every bus event.
7
+ * - emitted, nothing selects it: reported when the event is emitted. The bus
8
+ * is a broadcast, so an event nobody has subscribed to by then is dropped.
9
+ * - selected, never emitted: only knowable at the end of a session, so it is
10
+ * reported on demand by checkEventBus() (inspect / test helpers), never
11
+ * automatically.
12
+ * EVENTS.select() with no type (all events) counts as selecting everything.
13
+ */
14
+ import type {DiagnosticCheck} from '../index'
15
+ import {report, once, onReset, didYouMean} from './shared'
16
+
17
+ let selected = new Set<string>()
18
+ let emitted = new Set<string>()
19
+ let selectsAll = false
20
+
21
+ onReset(() => { selected = new Set(); emitted = new Set(); selectsAll = false })
22
+
23
+ export const eventsCheck: DiagnosticCheck = {
24
+ id: 'events',
25
+
26
+ onBusSelect(type) {
27
+ if (type === undefined || type === null || type === '') { selectsAll = true; return }
28
+ for (const t of ([] as any[]).concat(type)) if (typeof t === 'string') selected.add(t)
29
+ },
30
+
31
+ onBusEmit(type, emitterName) {
32
+ if (typeof type !== 'string') return
33
+ emitted.add(type)
34
+ if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`)) return
35
+ report('SYG105', {
36
+ component: emitterName,
37
+ message: `EVENTS event '${type}' was emitted, but nothing selects it, so it was dropped${didYouMean(type, selected)}`,
38
+ fix: `Subscribe with EVENTS.select('${type}') in the component that should react, or check the event name`,
39
+ data: {type, direction: 'emitted-not-selected', emitter: emitterName},
40
+ })
41
+ },
42
+ }
43
+
44
+ export interface EventBusSummary {
45
+ selected: string[]
46
+ emitted: string[]
47
+ selectedNeverEmitted: string[]
48
+ emittedNeverSelected: string[]
49
+ }
50
+
51
+ /**
52
+ * Report SYG105 (info) for every EVENTS type that has been selected but never
53
+ * emitted so far, and return the bus registry. Intended for inspect tooling and
54
+ * tests, at a point where the session is "done".
55
+ */
56
+ export function checkEventBus(): EventBusSummary {
57
+ const selectedNeverEmitted = [...selected].filter(t => !emitted.has(t))
58
+ const emittedNeverSelected = selectsAll ? [] : [...emitted].filter(t => !selected.has(t))
59
+ for (const type of selectedNeverEmitted) {
60
+ if (!once(`SYG105:select:${type}`)) continue
61
+ report('SYG105', {
62
+ message: `EVENTS.select('${type}') is used, but no component has emitted '${type}'${didYouMean(type, emitted)}`,
63
+ fix: `Emit it from a model entry, e.g. { EVENTS: event('${type}', ...) }, or check the event name`,
64
+ data: {type, direction: 'selected-not-emitted'},
65
+ })
66
+ }
67
+ return {selected: [...selected], emitted: [...emitted], selectedNeverEmitted, emittedNeverSelected}
68
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 'sygnal/diagnostics' — runtime consistency checks (PLAN-1 workstream 1A).
3
+ *
4
+ * import 'sygnal/diagnostics'
5
+ *
6
+ * Importing this entry registers every check with the diagnostics core of the
7
+ * 'sygnal' package (a side-effect import) and installs the RxJS-operator hints.
8
+ * Checks only run while diagnostics are on (run(App, drivers, { diagnostics }),
9
+ * or globalThis.__SYGNAL_DEV__ from the Vite plugin in dev). None of this code
10
+ * is in the main 'sygnal' bundle.
11
+ *
12
+ * | Code | Check | Module |
13
+ * |--------|--------------------------------------------------------|----------------|
14
+ * | SYG101 | intent action has no model entry | wiring.ts |
15
+ * | SYG102 | model entry has no intent trigger (info) | wiring.ts |
16
+ * | SYG103 | intent selector never matched (info -> warn) | dom.ts |
17
+ * | SYG104 | intent selector only matches inside a child component | dom.ts |
18
+ * | SYG105 | EVENTS type emitted-not-selected / selected-not-emitted| events.ts |
19
+ * | SYG106 | parent prop overwritten by a reserved view argument | props.ts |
20
+ * | SYG201 | STATE reducer dropped keys | state.ts |
21
+ * | SYG202 | STATE reducer returned undefined | state.ts |
22
+ * | SYG301 | RxJS operator used on an xstream stream | rxjsHints.ts |
23
+ * | 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 |
27
+ * | — | inspect(): the runtime app graph (2B) | inspect.ts |
28
+ *
29
+ * Strict checks (SYG5xx) only report after configureStrict(true) (or
30
+ * renderComponent(C, { strict: true }), or globalThis.__SYGNAL_STRICT__).
31
+ */
32
+ import type {DiagnosticCheck} from '../index'
33
+ import {Stream} from '../../xstreamCompat'
34
+ import {bridge, resetCheckState, timing} from './shared'
35
+ import {wiringCheck} from './wiring'
36
+ import {stateCheck} from './state'
37
+ import {eventsCheck} from './events'
38
+ import {propsCheck} from './props'
39
+ import {collectionsCheck} from './collections'
40
+ import {domCheck} from './dom'
41
+ import {strictCheck} from './strict'
42
+ import {installRxjsHints} from './rxjsHints'
43
+ import {inspectCheck, installInspect} from './inspect'
44
+
45
+ export {checkEventBus} from './events'
46
+ export {configureStrict, isStrictEnabled} from './strict'
47
+ export type {EventBusSummary} from './events'
48
+ export {RXJS_HINTS} from './rxjsHints'
49
+ export {inspect} from './inspect'
50
+ export type {InspectGraph, InspectComponent, InspectAction, InspectActionTrigger, InspectChild, InspectSelector, InspectDiagnostic, InspectOptions} from './public'
51
+ export {listCodes, getCodeInfo} from '../codes'
52
+ export type {DiagnosticCodeInfo} from '../codes'
53
+
54
+ /** Every runtime check in this entry. */
55
+ export const checks: DiagnosticCheck[] = [
56
+ wiringCheck,
57
+ stateCheck,
58
+ eventsCheck,
59
+ propsCheck,
60
+ collectionsCheck,
61
+ domCheck,
62
+ strictCheck,
63
+ inspectCheck,
64
+ ]
65
+
66
+ /**
67
+ * (Re-)register all checks with the core and install the RxJS hints. Called
68
+ * on import. Idempotent across duplicate copies of this entry: the previous
69
+ * installation (stored on the core bridge) is removed first.
70
+ */
71
+ export function installChecks(): () => void {
72
+ const core = bridge()
73
+ if (!core) throw new Error(`[Sygnal] 'sygnal/diagnostics' could not find the Sygnal diagnostics core`)
74
+ if (typeof core.__uninstallChecks === 'function') core.__uninstallChecks()
75
+ const unregister = checks.map(check => core.registerCheck(check))
76
+ const uninstallHints = installRxjsHints(Stream && Stream.prototype)
77
+ const uninstallInspect = installInspect()
78
+ const uninstall = () => {
79
+ unregister.forEach(fn => fn())
80
+ uninstallHints()
81
+ uninstallInspect()
82
+ if (core.__uninstallChecks === uninstall) core.__uninstallChecks = undefined
83
+ }
84
+ core.__uninstallChecks = uninstall
85
+ return uninstall
86
+ }
87
+
88
+ export interface ChecksOptions {
89
+ /** delay after a render before the DOM checks (SYG103/104) run; default 50 ms */
90
+ settleMs?: number
91
+ /** render-idle time before SYG103 escalates to warn; default 2000 ms */
92
+ idleMs?: number
93
+ /** renders required before SYG103 escalates to warn; default 3 */
94
+ minRenders?: number
95
+ }
96
+
97
+ /** Tune the timing of the DOM checks (mostly for tests). */
98
+ export function configureChecks(options: ChecksOptions = {}): void {
99
+ for (const key of Object.keys(timing) as Array<keyof typeof timing>) {
100
+ if (typeof options[key] === 'number') timing[key] = options[key] as number
101
+ }
102
+ }
103
+
104
+ /** Forget what the checks have seen and reported (dedupe state, registries). For tests. */
105
+ export function resetChecks(): void {
106
+ resetCheckState()
107
+ }
108
+
109
+ installChecks()