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,119 @@
1
+ /**
2
+ * Shared plumbing for the runtime checks (PLAN-1 workstream 1A).
3
+ *
4
+ * The checks are bundled separately from the main 'sygnal' entry
5
+ * ('sygnal/diagnostics'). They must talk to the SAME diagnostics core
6
+ * instance the components use, so they never import the core's functions
7
+ * directly: `import '../index'` only makes sure the core is loaded (in the
8
+ * dist build that import is rewritten to the external 'sygnal' package, see
9
+ * rollup.config.mjs), and the functions are read from the bridge object the
10
+ * core publishes on globalThis.__SYGNAL_DIAGNOSTICS__.
11
+ */
12
+ import '../index'
13
+ import type {DiagnosticCheck, DiagnosticDetails, Diagnostic} from '../index'
14
+
15
+ interface CoreBridge {
16
+ registerCheck(check: DiagnosticCheck): () => void
17
+ report(code: string, details: DiagnosticDetails): Diagnostic | undefined
18
+ [key: string]: any
19
+ }
20
+
21
+ export const bridge = (): CoreBridge => (globalThis as any).__SYGNAL_DIAGNOSTICS__
22
+
23
+ export const report = (code: string, details: DiagnosticDetails): Diagnostic | undefined =>
24
+ bridge().report(code, details)
25
+
26
+ /**
27
+ * report() for code that is NOT running inside a core hook (timers, wrapped
28
+ * methods): never throws synchronously; an 'error'-mode DiagnosticError is
29
+ * rethrown asynchronously, like the core does for hooks.
30
+ */
31
+ export const reportSafely = (code: string, details: DiagnosticDetails): Diagnostic | undefined => {
32
+ try {
33
+ return report(code, details)
34
+ } catch (err) {
35
+ const raise = () => { throw err }
36
+ typeof queueMicrotask === 'function' ? queueMicrotask(raise) : setTimeout(raise)
37
+ return undefined
38
+ }
39
+ }
40
+
41
+ /** Tunables (test seam: configureChecks()). */
42
+ export const timing = {
43
+ /** delay after a render before DOM checks run (lets nested renders patch) */
44
+ settleMs: 50,
45
+ /** SYG103 escalates to warn after this much render-idle time ... */
46
+ idleMs: 2000,
47
+ /** ... and only once the component has rendered at least this often */
48
+ minRenders: 3,
49
+ }
50
+
51
+ /** Built-in actions Sygnal dispatches itself; never "missing" or "unreachable". */
52
+ export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'HYDRATE', 'DISPOSE', 'READY'])
53
+
54
+ /** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
55
+ export const isInternalAction = (action: string): boolean =>
56
+ typeof action !== 'string' || action.startsWith('__') || BUILTIN_ACTIONS.has(action)
57
+
58
+ let seen = new Set<string>()
59
+
60
+ /** true the first time `key` is seen (dedupes reports, e.g. per component name). */
61
+ export const once = (key: string): boolean => (seen.has(key) ? false : (seen.add(key), true))
62
+
63
+ /**
64
+ * Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
65
+ * when an outermost instance starts, so findings are reported again in every test). Unlike
66
+ * resetCheckState() it keeps the checks' registries, e.g. inspect()'s live records.
67
+ */
68
+ export const resetOnce = (): void => { seen = new Set() }
69
+ {
70
+ const core = bridge()
71
+ if (core) core.resetOnce = resetOnce
72
+ }
73
+
74
+ const resetters: Array<() => void> = []
75
+ export const onReset = (fn: () => void): void => { resetters.push(fn) }
76
+
77
+ /** Clear every check's dedupe/registry state (tests). */
78
+ export function resetCheckState(): void {
79
+ seen = new Set()
80
+ resetters.forEach(fn => fn())
81
+ }
82
+
83
+ export const nameOf = (component: any): string => (component && component.name) || 'component'
84
+
85
+ export const isPlainObject = (value: any): boolean =>
86
+ !!value && typeof value === 'object' && !Array.isArray(value) &&
87
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
88
+
89
+ /** Levenshtein distance (small strings only). */
90
+ function distance(a: string, b: string): number {
91
+ const row = Array.from({length: b.length + 1}, (_, i) => i)
92
+ for (let i = 1; i <= a.length; i++) {
93
+ let prev = row[0]
94
+ row[0] = i
95
+ for (let j = 1; j <= b.length; j++) {
96
+ const tmp = row[j]
97
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1))
98
+ prev = tmp
99
+ }
100
+ }
101
+ return row[b.length]
102
+ }
103
+
104
+ /** Closest candidate to `name` (case-insensitive match or edit distance <= 2), if any. */
105
+ export function suggest(name: string, candidates: Iterable<string>): string | undefined {
106
+ let best: string | undefined
107
+ let bestScore = Infinity
108
+ for (const c of candidates) {
109
+ if (c === name) continue
110
+ const score = c.toLowerCase() === name.toLowerCase() ? 0 : distance(c, name)
111
+ if (score < bestScore) { best = c; bestScore = score }
112
+ }
113
+ return bestScore <= Math.min(2, Math.floor(name.length / 3)) ? best : undefined
114
+ }
115
+
116
+ export const didYouMean = (name: string, candidates: Iterable<string>): string => {
117
+ const s = suggest(name, candidates)
118
+ return s ? ` (did you mean '${s}'?)` : ''
119
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SYG201 — STATE reducer dropped keys that existed in the previous state (warn)
3
+ * SYG202 — STATE reducer returned undefined (warn)
4
+ *
5
+ * Mechanism: onReducer(component, action, prevState, nextState). Reported once
6
+ * per action per component name. Not reported for:
7
+ * - synthetic actions (`__*`) and INITIALIZE/HYDRATE (whole-state replacement)
8
+ * - keys that are calculated fields (they are re-derived after the reducer)
9
+ * - keys whose previous value was undefined
10
+ * The set()/toggle() helpers always spread the previous state, so they can't
11
+ * drop keys and never trigger SYG201.
12
+ */
13
+ import type {DiagnosticCheck} from '../index'
14
+ import {report, once, nameOf, isPlainObject} from './shared'
15
+
16
+ const SKIP = new Set(['INITIALIZE', 'HYDRATE'])
17
+
18
+ export const stateCheck: DiagnosticCheck = {
19
+ id: 'state',
20
+
21
+ onReducer(component, action, prevState, nextState) {
22
+ if (typeof action !== 'string' || action.startsWith('__') || SKIP.has(action)) return
23
+ const name = nameOf(component)
24
+
25
+ if (nextState === undefined) {
26
+ if (!once(`SYG202:${name}:${action}`)) return
27
+ // Returning undefined from a Collection item's reducer is the documented way to remove
28
+ // the item, so only the root (where it wipes the whole app state) is a warning.
29
+ const sub = !!component?.isSubComponent
30
+ report('SYG202', {
31
+ component,
32
+ severity: sub ? 'info' : 'warn',
33
+ message: `The STATE reducer for '${action}' returned undefined` +
34
+ (sub ? ' (this removes the item if the component is a Collection item)' : ''),
35
+ fix: `Return the new state, e.g. (state, data) => ({ ...state, ... }), or return ABORT to leave the state unchanged`,
36
+ data: {action},
37
+ })
38
+ return
39
+ }
40
+
41
+ if (!isPlainObject(prevState) || !isPlainObject(nextState)) return
42
+ const calculated: Set<string> | null = component?._calculatedFieldNames || null
43
+ const dropped = Object.keys(prevState).filter(key =>
44
+ !(key in nextState) && prevState[key] !== undefined && !(calculated && calculated.has(key)))
45
+ if (dropped.length === 0 || !once(`SYG201:${name}:${action}`)) return
46
+
47
+ const list = dropped.map(k => `'${k}'`).join(', ')
48
+ report('SYG201', {
49
+ component,
50
+ message: `The STATE reducer for '${action}' returned a state without ${list}, which ${dropped.length === 1 ? 'was' : 'were'} in the previous state (likely a missing ...state spread)`,
51
+ fix: `Spread the previous state: (state, data) => ({ ...state, ... }). If removing ${dropped.length === 1 ? 'the key' : 'these keys'} is intended, ignore this`,
52
+ data: {action, droppedKeys: dropped},
53
+ })
54
+ },
55
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Strict mode: canonical-form checks (PLAN-1 workstream 2A,
3
+ * dev-plans/PLAN-1-canonical-forms.md). Runtime half; sygnal-check --strict
4
+ * implements every rule statically.
5
+ *
6
+ * Off by default. Turn it on with any of:
7
+ * - configureStrict(true) (this entry)
8
+ * - renderComponent(C, { strict: true }) (tests)
9
+ * - globalThis.__SYGNAL_STRICT__ = true (e.g. injected by a dev tool)
10
+ * and diagnostics must be on (strict reports go through the same core).
11
+ * The flag lives on the core bridge object, so it is shared by every copy of
12
+ * this entry and by renderComponent.
13
+ *
14
+ * | Code | Rule (canonical form) | Runtime mechanism |
15
+ * |--------|------------------------------------------|-----------------------------------------|
16
+ * | SYG501 | C1 destructure the view's first argument | onModel: component.view.length > 1 |
17
+ * | SYG502 | C3 return ABORT for "no change" | onReducer: returned the previous state |
18
+ * | | | object (undefined stays SYG202) |
19
+ * | SYG504 | C5 object form, no 'ACTION | SINK' keys | onModel: raw model keys containing '|' |
20
+ * | SYG503, SYG505, SYG506, SYG507: static only (sygnal-check --strict); the
21
+ * runtime can't tell emit() from { EVENTS }, a side effect from a pure
22
+ * reducer, or see CHILD.select() arguments without a core hook.
23
+ *
24
+ * Every finding is reported once per component name (+ action / key).
25
+ * The SYG5xx severities are registered here (registerCodes) and passed
26
+ * explicitly, so the main bundle doesn't carry them (D29).
27
+ */
28
+ import type {DiagnosticCheck} from '../index'
29
+ import {CODE_TITLES, STRICT_CODE_SEVERITY, registerCodes} from '../codes'
30
+ import type {DiagnosticSeverity} from '../codes'
31
+ import {bridge, report, once, nameOf, isInternalAction} from './shared'
32
+
33
+ registerCodes(Object.keys(STRICT_CODE_SEVERITY).map(code =>
34
+ [code, STRICT_CODE_SEVERITY[code], CODE_TITLES[code]] as [string, DiagnosticSeverity, string]))
35
+
36
+ /**
37
+ * Turn runtime strict checks on (true), off (false), or back to the default
38
+ * (undefined → globalThis.__SYGNAL_STRICT__ === true).
39
+ */
40
+ export function configureStrict(on?: boolean): void {
41
+ const core = bridge()
42
+ if (core) core.strict = on
43
+ }
44
+
45
+ /** Whether runtime strict checks are on. */
46
+ export function isStrictEnabled(): boolean {
47
+ const flag = bridge()?.strict
48
+ return flag === undefined ? (globalThis as any).__SYGNAL_STRICT__ === true : flag === true
49
+ }
50
+
51
+ const strictReport = (code: string, details: Parameters<typeof report>[1]) =>
52
+ report(code, {severity: STRICT_CODE_SEVERITY[code], ...details})
53
+
54
+ export const strictCheck: DiagnosticCheck = {
55
+ id: 'strict',
56
+
57
+ onModel(component) {
58
+ if (!isStrictEnabled() || !component) return
59
+ const name = nameOf(component)
60
+
61
+ // SYG501 — view(props, state, context, peers): positional use of the 2nd+ args.
62
+ // (A default value or a rest parameter stops Function.length, so this can
63
+ // miss `(props, state = {})`, but it never flags a single-argument view.)
64
+ const view = component.view
65
+ if (typeof view === 'function' && view.length > 1 && !view.__sygnalLazy && once(`SYG501:${name}`)) {
66
+ const extra = ['state', 'context', 'peers'].slice(0, view.length - 1)
67
+ strictReport('SYG501', {
68
+ component,
69
+ message: `The view takes ${view.length} positional arguments (props, ${extra.join(', ')})`,
70
+ fix: `Destructure the first argument instead: function ${name}({ ${extra.join(', ')}, ...props })`,
71
+ data: {arity: view.length},
72
+ })
73
+ }
74
+
75
+ // SYG504 — 'ACTION | SINK' shorthand keys
76
+ const model = component.model
77
+ if (!model || typeof model !== 'object') return
78
+ for (const key of Object.keys(model)) {
79
+ if (!key.includes('|')) continue
80
+ const [action, sink] = key.split('|').map(s => s.trim())
81
+ if (!action || !sink || !once(`SYG504:${name}:${key}`)) continue
82
+ const rewrite = sink === 'EVENTS'
83
+ ? `${action}: { EVENTS: event('TYPE', (state, data) => payload) }`
84
+ : `${action}: { ${sink}: (state, data, next) => ... }`
85
+ strictReport('SYG504', {
86
+ component,
87
+ message: `Model key '${key}' uses the 'ACTION | SINK' shorthand`,
88
+ fix: `Use the object form: ${rewrite} (merge it into an existing '${action}' entry if there is one)`,
89
+ data: {key, action, sink},
90
+ })
91
+ }
92
+ },
93
+
94
+ onReducer(component, action, prevState, nextState) {
95
+ if (!isStrictEnabled() || isInternalAction(action)) return // incl. INITIALIZE/HYDRATE/DISPOSE
96
+ // undefined is SYG202 (state check, every mode); only the identical object here.
97
+ if (!prevState || typeof prevState !== 'object' || nextState === undefined) return
98
+ let same = nextState === prevState
99
+ if (!same && typeof component?.addCalculated === 'function') {
100
+ // with calculated fields the reducer saw addCalculated(prev) (memoized: same object)
101
+ try { same = component.addCalculated(prevState) === nextState } catch (_) {}
102
+ }
103
+ const name = nameOf(component)
104
+ if (!same || !once(`SYG502:${name}:${action}`)) return
105
+ strictReport('SYG502', {
106
+ component,
107
+ message: `The STATE reducer for '${action}' returned the unchanged state object`,
108
+ fix: `Return ABORT for "no change": ${action}: (state, data) => cond ? { ...state, ... } : ABORT`,
109
+ data: {action},
110
+ })
111
+ },
112
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * SYG101 — intent action has no model entry (warn)
3
+ * SYG102 — model entry has no intent trigger (info; it may still be reached
4
+ * with next(), which is only known at call time)
5
+ *
6
+ * Mechanism: onIntent records the intent's action names per component;
7
+ * onModel (always called right after, once per instance) compares them with
8
+ * the normalized model map (shorthand already expanded by the core).
9
+ * Synthetic actions (`__*`, e.g. renderComponent's `__TEST_ACTION__`) and the
10
+ * built-ins BOOTSTRAP/INITIALIZE/HYDRATE/DISPOSE/READY are never reported.
11
+ * Under renderComponent, intent streams injected for model actions (so that
12
+ * simulateAction can dispatch them by their real names) are listed on the
13
+ * intent object's non-enumerable `__sygnalTestActions` property. They are not
14
+ * user intent actions (never reported as SYG101), and SYG102 is not reported
15
+ * for those model actions (the test dispatches them with simulateAction).
16
+ */
17
+ import type {DiagnosticCheck} from '../index'
18
+ import {report, once, isInternalAction, nameOf, didYouMean} from './shared'
19
+
20
+ const intentActions = new WeakMap<object, string[]>()
21
+
22
+ export const wiringCheck: DiagnosticCheck = {
23
+ id: 'wiring',
24
+
25
+ onIntent(component, actionNames) {
26
+ intentActions.set(component, actionNames || [])
27
+ },
28
+
29
+ onModel(component, modelMap) {
30
+ const name = nameOf(component)
31
+ const intent$ = component && component.intent$
32
+ const injected = new Set<string>(
33
+ (intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || [])
34
+ const actions = (intentActions.get(component) || []).filter(a => !injected.has(a))
35
+ intentActions.delete(component)
36
+ const modelActions = Object.keys(modelMap || {})
37
+
38
+ for (const action of actions) {
39
+ if (isInternalAction(action) || modelActions.includes(action)) continue
40
+ if (!once(`SYG101:${name}:${action}`)) continue
41
+ report('SYG101', {
42
+ component,
43
+ message: `Intent action '${action}' has no model entry, so it never does anything${didYouMean(action, modelActions)}`,
44
+ fix: `Add '${action}' to ${name}.model, or remove it from ${name}.intent`,
45
+ data: {action, modelActions},
46
+ })
47
+ }
48
+
49
+ // A single-stream intent (intent returns one action$ stream) has no
50
+ // action names to compare against.
51
+ if (intent$ && typeof intent$.addListener === 'function') return
52
+
53
+ const hmr = ([] as string[]).concat(component?.hmrActions || [])
54
+ for (const action of modelActions) {
55
+ if (isInternalAction(action) || actions.includes(action) || hmr.includes(action) || injected.has(action)) continue
56
+ if (!once(`SYG102:${name}:${action}`)) continue
57
+ report('SYG102', {
58
+ component,
59
+ message: `Model entry '${action}' has no intent action with that name${didYouMean(action, actions)}`,
60
+ fix: `Add '${action}' to ${name}.intent, or remove the model entry. If it is only dispatched with next('${action}'), ignore this`,
61
+ data: {action, intentActions: actions},
62
+ })
63
+ }
64
+ },
65
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Sygnal diagnostic code registry.
3
+ *
4
+ * Every diagnostic Sygnal reports has a stable code (`SYG123`), a default
5
+ * severity, a short title, and a docs anchor (`docsSlug`) on
6
+ * https://sygnal.js.org/reference/errors (base URL = the `site` field of
7
+ * docs/astro.config.mjs).
8
+ *
9
+ * The registry is stored as two parallel tables so the published bundle only
10
+ * carries what the runtime needs:
11
+ * - CODE_SEVERITY: code → default severity (used by report(); in the bundle)
12
+ * - CODE_TITLES: code → title (docs/inspect/static-checker metadata; pure
13
+ * object literal, tree-shaken unless something imports it)
14
+ * getCodeInfo(code) / listCodes() assemble the full
15
+ * { code, severity, title, docsSlug } records. To add a code, add it to BOTH
16
+ * tables below (inside your reserved range), or call registerCodes() from a
17
+ * workstream's own module. docsSlug is always the lower-cased code
18
+ * ('SYG101' → https://sygnal.js.org/reference/errors#syg101).
19
+ *
20
+ * (Comments are attached to type-only declarations so the TypeScript emit
21
+ * drops them — keeps them out of the published bundle and the size gate.)
22
+ *
23
+ * Severity:
24
+ * - 'error' — throws (or is rethrown, enriched) at the call site
25
+ * - 'warn' — console + collected (when diagnostics mode is 'warn')
26
+ * - 'info' — collected only; surfaced through inspect / test helpers
27
+ *
28
+ * Ranges (see dev-plans/PLAN-1.md §2):
29
+ * SYG1xx Wiring: intent <-> model <-> view <-> events
30
+ * SYG2xx State & reducers
31
+ * SYG3xx Streams & intent construction (incl. xstream hints)
32
+ * SYG4xx Collections, Switchable, sub-components, context
33
+ * SYG5xx Strict mode / canonical-form violations
34
+ * SYG6xx Drivers & run configuration
35
+ * SYG9xx Internal invariants
36
+ *
37
+ * ---------------------------------------------------------------------------
38
+ * RESERVED RANGES (PLAN-1 workstream allocation — add entries only inside the
39
+ * range your workstream owns; changes outside go through the coordinator):
40
+ *
41
+ * 0B (diagnostics core) SYG900 (check-failure invariant)
42
+ * 1A (runtime checks) SYG101-105, SYG110, SYG201, SYG202, SYG301,
43
+ * SYG401 (pre-seeded below), plus SYG106-199
44
+ * and SYG302-399 for new checks
45
+ * 1E (message retrofit) SYG206-299, SYG402-499, SYG601-699, SYG901-999
46
+ * 2A (strict mode) SYG500-599
47
+ * unallocated SYG100, SYG200, SYG203-205, SYG300, SYG400,
48
+ * SYG600 (coordinator assigns on request)
49
+ * ---------------------------------------------------------------------------
50
+ */
51
+ export type DiagnosticSeverity = 'error' | 'warn' | 'info'
52
+
53
+ export interface DiagnosticCodeInfo {
54
+ code: string
55
+ severity: DiagnosticSeverity
56
+ title: string
57
+ // anchor on the errors reference page, e.g. 'syg101' → /reference/errors#syg101
58
+ docsSlug: string
59
+ }
60
+
61
+ export const DOCS_BASE_URL = 'https://sygnal.js.org'
62
+
63
+ export const CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
64
+ SYG101: 'warn',
65
+ SYG102: 'info',
66
+ SYG103: 'info',
67
+ SYG104: 'warn',
68
+ SYG105: 'info',
69
+ SYG106: 'warn',
70
+ SYG110: 'warn',
71
+ SYG111: 'warn',
72
+ SYG201: 'warn',
73
+ SYG202: 'warn',
74
+ SYG301: 'error',
75
+ SYG401: 'warn',
76
+ SYG900: 'warn',
77
+ // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
78
+ SYG206: 'error',
79
+ SYG207: 'warn',
80
+ SYG208: 'warn',
81
+ SYG209: 'error',
82
+ SYG210: 'warn',
83
+ SYG211: 'error',
84
+ SYG212: 'error',
85
+ SYG213: 'warn',
86
+ SYG214: 'error',
87
+ SYG215: 'error',
88
+ SYG216: 'error',
89
+ SYG217: 'warn',
90
+ SYG218: 'error',
91
+ SYG219: 'warn',
92
+ SYG220: 'warn',
93
+ SYG402: 'error',
94
+ SYG403: 'error',
95
+ SYG404: 'error',
96
+ SYG405: 'warn',
97
+ SYG406: 'error',
98
+ SYG407: 'error',
99
+ SYG408: 'error',
100
+ SYG409: 'warn',
101
+ SYG410: 'error',
102
+ SYG411: 'error',
103
+ SYG412: 'error',
104
+ SYG413: 'error',
105
+ SYG414: 'error',
106
+ SYG415: 'error',
107
+ SYG416: 'error',
108
+ SYG417: 'warn',
109
+ SYG418: 'error',
110
+ SYG419: 'error',
111
+ SYG420: 'error',
112
+ SYG601: 'error',
113
+ SYG602: 'error',
114
+ SYG603: 'error',
115
+ SYG604: 'error',
116
+ SYG605: 'error',
117
+ SYG606: 'error',
118
+ SYG607: 'error',
119
+ SYG901: 'error',
120
+ SYG902: 'error',
121
+ SYG903: 'error',
122
+ // ---- end 1E retrofit codes ----
123
+ }
124
+
125
+ export const CODE_TITLES: Record<string, string> = {
126
+ // SYG1xx wiring (1A)
127
+ SYG101: 'Intent action has no model entry',
128
+ SYG102: 'Model entry is unreachable',
129
+ SYG103: 'Intent selector matched no rendered element',
130
+ SYG104: 'Intent selector crosses an isolation boundary',
131
+ SYG105: 'EVENTS type selected but never emitted, or emitted but never selected',
132
+ SYG106: 'Parent prop is overwritten by a reserved view argument',
133
+ SYG110: 'Intent selector not present in the component view',
134
+ SYG111: 'Controlled input has no input listener',
135
+ // SYG2xx state & reducers (1A)
136
+ SYG201: 'STATE reducer dropped keys from the previous state',
137
+ SYG202: 'STATE reducer returned undefined',
138
+ // SYG3xx streams & intent construction (1A)
139
+ SYG301: 'RxJS operator used on an xstream stream',
140
+ // SYG4xx collections / switchable / sub-components / context (1A)
141
+ SYG401: "Collection 'from' field is missing or not an array",
142
+ // SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
143
+ SYG501: 'View uses positional arguments',
144
+ SYG502: 'STATE reducer signals no change without ABORT',
145
+ SYG503: 'Side effect in a STATE reducer that returns ABORT',
146
+ SYG504: "'ACTION | SINK' shorthand model key",
147
+ SYG505: 'Non-canonical EVENTS emit',
148
+ SYG506: 'CHILD.select() with a string component name',
149
+ SYG507: 'Prop drilled through more than 2 component levels',
150
+ // SYG9xx internal (0B)
151
+ SYG900: 'A diagnostics check threw',
152
+ // ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
153
+ SYG206: 'Invalid calculated field definition',
154
+ SYG207: 'Calculated field shadows an initialState key',
155
+ SYG208: 'Calculated field depends on an unknown key',
156
+ SYG209: 'Circular calculated field dependency',
157
+ SYG210: 'INITIALIZE used with a sink other than STATE',
158
+ SYG211: 'Invalid model shorthand entry',
159
+ SYG212: 'Model entry is not a function or an object of sinks',
160
+ SYG213: 'Duplicate model entry for the same action and sink',
161
+ SYG214: 'EFFECT handler threw',
162
+ SYG215: 'next() delay is not a number',
163
+ SYG216: 'Reducer threw',
164
+ SYG217: 'Driver-sink reducer returned undefined',
165
+ SYG218: 'Reducer returned an unsupported type',
166
+ SYG219: 'EFFECT handler returned a value',
167
+ SYG220: 'Calculated field threw',
168
+ SYG402: 'Context is not an object',
169
+ SYG403: 'Invalid context entry',
170
+ SYG404: 'Context stream errored',
171
+ SYG405: 'Sub-component has initialState without isolatedState',
172
+ SYG406: 'View threw',
173
+ SYG407: 'onError handler threw',
174
+ SYG408: 'Sub-component failed to instantiate',
175
+ SYG409: 'Child tried to update a calculated field',
176
+ SYG410: "Invalid sub-component 'state' prop",
177
+ SYG411: "Collection 'of' is missing or not a component",
178
+ SYG412: "Invalid Collection 'from' prop",
179
+ SYG413: 'Unnamed component factory not found',
180
+ SYG414: 'Component not found',
181
+ SYG415: "Switchable 'of' is missing or invalid",
182
+ SYG416: "Switchable 'current' is missing or invalid",
183
+ SYG417: 'Portal target not found',
184
+ SYG418: 'Invalid Collection sort prop',
185
+ SYG419: "Invalid switchable() 'name$' parameter",
186
+ SYG420: 'JSX tag is undefined',
187
+ SYG601: 'Missing or invalid sources',
188
+ SYG602: 'Intent is not a function',
189
+ SYG603: 'Intent returned an invalid value',
190
+ SYG604: 'Invalid hmrActions',
191
+ SYG605: "Intent action name contains '|'",
192
+ SYG606: 'calculated is not an object',
193
+ SYG607: 'State source not found',
194
+ SYG901: 'Sub-component sink stream errored',
195
+ SYG902: 'EFFECT stream errored',
196
+ SYG903: 'Component factory returned invalid sinks',
197
+ // ---- end 1E retrofit codes ----
198
+ }
199
+
200
+ // 2A: strict-mode (SYG5xx) default severities. Kept OUT of CODE_SEVERITY so the
201
+ // main bundle doesn't carry them (D29: zero bytes); the 'sygnal/diagnostics'
202
+ // entry registers them (registerCodes) and its reports pass the severity
203
+ // explicitly. sygnal-check keeps the same table in sygnal-check/src/codes.js.
204
+ export const STRICT_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
205
+ SYG501: 'warn',
206
+ SYG502: 'warn',
207
+ SYG503: 'warn',
208
+ SYG504: 'warn',
209
+ SYG505: 'warn',
210
+ SYG506: 'warn',
211
+ SYG507: 'info',
212
+ }
213
+
214
+ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined {
215
+ const severity = CODE_SEVERITY[code]
216
+ return severity && { code, severity, title: CODE_TITLES[code] || '', docsSlug: code.toLowerCase() }
217
+ }
218
+
219
+ export function listCodes(): DiagnosticCodeInfo[] {
220
+ return Object.keys(CODE_SEVERITY).map(c => getCodeInfo(c)!)
221
+ }
222
+
223
+ export function registerCodes(entries: Array<[string, DiagnosticSeverity, string]>): void {
224
+ for (const [code, severity, title] of entries) {
225
+ CODE_SEVERITY[code] = severity
226
+ CODE_TITLES[code] = title
227
+ }
228
+ }
229
+
230
+ export function docsUrlFor(code: string): string {
231
+ return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`
232
+ }