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
@@ -11,6 +11,8 @@ import {ElementFinder} from './ElementFinder';
11
11
  import {makeIsolateSink, getScopeObj, Scope, IsolateSink} from './isolate';
12
12
  import {IsolateModule} from './IsolateModule';
13
13
  import {EventDelegator} from './EventDelegator';
14
+ // [diagnostics hook] no-op when diagnostics are off
15
+ import {onSelector} from '../../extra/diagnostics/index';
14
16
 
15
17
  export interface SpecialSelector {
16
18
  body: BodyDOMSource;
@@ -91,6 +93,8 @@ export class MainDOMSource {
91
93
  return new BodyDOMSource(this._name);
92
94
  }
93
95
 
96
+ if (selector !== ':root') onSelector(this, selector.trim());
97
+
94
98
  const namespace =
95
99
  selector === ':root'
96
100
  ? []
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Snabbdom module that unsets a removed `className` prop (B-012).
3
+ *
4
+ * Problem: `className` goes through snabbdom's propsModule, which writes new
5
+ * and changed props but never unsets a prop that disappears. So when snabbdom
6
+ * reuses an element (`<p className="placeholder">` -> `<p>`), the old class
7
+ * stays. A nullish `className` is worse: propsModule writes the string
8
+ * "null"/"false".
9
+ *
10
+ * Fix: runs after propsModule. When `className` is gone (or nullish/false),
11
+ * drop the classes it had set, keeping the classes from the selector
12
+ * (`p.foo`) and the truthy `class={{...}}` entries.
13
+ */
14
+
15
+ import type {VNode} from 'snabbdom/build/vnode.js';
16
+
17
+ function classObject(vnode: VNode): Array<string> {
18
+ const out: Array<string> = [];
19
+ const klass: any = vnode.data?.class;
20
+ if (klass && typeof klass === 'object') {
21
+ for (const name in klass) if (klass[name]) out.push(name);
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function baseClasses(vnode: VNode): Array<string> {
27
+ const sel = vnode.sel || '';
28
+ const hashIdx = sel.indexOf('#');
29
+ const dotIdx = sel.indexOf('.', hashIdx);
30
+ const fromSel = dotIdx > 0 ? sel.slice(dotIdx + 1).split('.') : [];
31
+ return fromSel.concat(classObject(vnode));
32
+ }
33
+
34
+ function addAll(elm: any, names: Array<string>): void {
35
+ for (const name of names) if (name) elm.classList.add(name);
36
+ }
37
+
38
+ const isSet = (cn: any) => cn != null && cn !== false;
39
+ const tokens = (cn: any) => (typeof cn === 'string' ? cn.split(/\s+/).filter(Boolean) : []);
40
+
41
+ function syncClassName(oldVnode: VNode, vnode: VNode): void {
42
+ const elm = vnode.elm as any;
43
+ if (!elm || !elm.classList) return;
44
+ const props: any = vnode.data?.props;
45
+ const oldProps: any = oldVnode.data?.props;
46
+ const hasKey = !!props && 'className' in props;
47
+ const cn = hasKey ? props.className : undefined;
48
+ if (isSet(cn)) {
49
+ // propsModule replaced the whole attribute: put the class={{...}} entries back
50
+ if (!oldProps || oldProps.className !== cn) addAll(elm, classObject(vnode));
51
+ return;
52
+ }
53
+ if (hasKey) {
54
+ // propsModule wrote String(cn) over the whole attribute: rebuild it
55
+ if (!oldProps || oldProps.className !== cn) {
56
+ elm.className = baseClasses(vnode).join(' ');
57
+ if (elm.className === '') elm.removeAttribute('class');
58
+ }
59
+ return;
60
+ }
61
+ const oldCn = oldProps ? oldProps.className : undefined;
62
+ if (!isSet(oldCn)) return;
63
+ const keep = baseClasses(vnode);
64
+ for (const name of tokens(oldCn)) {
65
+ if (keep.indexOf(name) === -1) elm.classList.remove(name);
66
+ }
67
+ addAll(elm, keep);
68
+ if (elm.getAttribute('class') === '') elm.removeAttribute('class');
69
+ }
70
+
71
+ export const classNameModule = {
72
+ create: (emptyVnode: VNode, vnode: VNode) => syncClassName(emptyVnode, vnode),
73
+ update: syncClassName,
74
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Snabbdom module that keeps controlled form fields in sync with the vnode (B-004).
3
+ *
4
+ * Problem: snabbdom's propsModule only writes `value`/`checked` when the prop
5
+ * differs from the previous vnode's prop. If the user types 'x' and an action
6
+ * resets the state to '' within one render window, the 'x' render is
7
+ * coalesced away: the vnodes go '' -> '' and the typed text stays in the DOM.
8
+ *
9
+ * Fix: after propsModule has run, compare `value`/`checked` props against the
10
+ * live element and write them when they differ. Only form fields (INPUT,
11
+ * TEXTAREA, SELECT; not input type=file) are controlled: `value` compares as a
12
+ * string, `checked` as a boolean (1H-7). Other elements with a value prop
13
+ * (<progress>, <meter>, custom elements with object values) are left alone.
14
+ */
15
+
16
+ import type {VNode} from 'snabbdom/build/vnode.js';
17
+
18
+ function syncControlled(oldVnode: VNode, vnode: VNode): void {
19
+ const props = vnode.data?.props;
20
+ const elm = vnode.elm as any;
21
+ if (!props || !elm || !/^(INPUT|TEXTAREA|SELECT)$/.test(elm.tagName) || elm.type == 'file') return;
22
+ const oldProps: any = oldVnode.data?.props || {};
23
+ const {value, checked} = props;
24
+ // Only the case propsModule skipped (prop unchanged); it handles real changes.
25
+ if (value === oldProps.value && value != null && elm.value !== String(value)) {
26
+ elm.value = String(value);
27
+ }
28
+ if (checked === oldProps.checked && checked != null && elm.checked !== !!checked) {
29
+ elm.checked = !!checked;
30
+ }
31
+ }
32
+
33
+ export const controlledInputModule = {update: syncControlled};
@@ -46,11 +46,17 @@ export function enrichEventStream(stream$: any): any {
46
46
  return enrichEventStream(mapped);
47
47
  }
48
48
 
49
- // .data(name, fn?) — extract e.target.dataset[name]
49
+ // .data(name, fn?) — extract dataset[name] from e.target or its nearest ancestor that has
50
+ // it. dataset keys are camelCase but attributes are kebab-case (`taskId` is
51
+ // `data-task-id`), so the closest() selector converts (B-028). The name may be given
52
+ // either way: `.data('taskId')` and `.data('task-id')` read the same attribute (3E/R3).
50
53
  stream$.data = function data(name: string, fn?: (val: any) => any): any {
54
+ const key = String(name).replace(/-([a-z])/g, (_, c) => c.toUpperCase())
55
+ const attr = `[data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}]`
51
56
  const mapped = stream$.map((e: any) => {
52
- const el = e?.target instanceof Element ? e.target.closest(`[data-${name}]`) || e.target : e?.target
53
- const val = el?.dataset?.[name]
57
+ const t = e?.target
58
+ const el = typeof t?.closest == 'function' ? t.closest(attr) || t : t
59
+ const val = el?.dataset?.[key]
54
60
  return fn ? fn(val) : val
55
61
  });
56
62
  return enrichEventStream(mapped);
@@ -1,8 +1,7 @@
1
1
  import {Driver} from '../run/types';
2
2
  import {init, Module, Options as SnabbdomOptions, VNode, toVNode} from './snabbdom';
3
3
  import xs, {Stream, Listener} from 'xstream';
4
- import concat from 'xstream/extra/concat.js';
5
- import sampleCombine from 'xstream/extra/sampleCombine.js';
4
+ import {concat, sampleCombine} from '../../extra/xstreamExtras';
6
5
  import {MainDOMSource} from './MainDOMSource';
7
6
  import {VNodeWrapper} from './VNodeWrapper';
8
7
  import {getValidNode, checkValidContainer} from './utils';
@@ -3,6 +3,22 @@ import {DevToolEnabledSource, FantasyObservable} from '../run/types';
3
3
  import {VNode} from './snabbdom';
4
4
  import {EventsFnOptions} from './DOMSource';
5
5
  import {adapt} from '../run/adapt';
6
+ import {enrichEventStream} from './enrichEventStream';
7
+
8
+ /**
9
+ * Optional simulated-event hub (used by renderComponent's simulateEvent): a
10
+ * stream of `{type, event, match(path)}`; each source's events(type) also emits
11
+ * the hub events whose `match` accepts its selector path (isolation scopes
12
+ * included as '.___scope' segments).
13
+ */
14
+ export type MockEventHub = Stream<{type: string; event: any; match: (path: string[]) => boolean}>;
15
+
16
+ /**
17
+ * Optional listener callback (renderComponent): called with `live` undefined when events() is
18
+ * called (SYG103/104 checks), then with true / false when that hub listener is subscribed /
19
+ * unsubscribed (simulateEvent waits for a just-mounted child's listeners, G-039).
20
+ */
21
+ export type MockOnEvents = (path: string[], eventType: string, live?: boolean) => void;
6
22
 
7
23
  export type MockConfig = {
8
24
  [name: string]: FantasyObservable<any> | MockConfig;
@@ -13,7 +29,12 @@ const SCOPE_PREFIX = '___';
13
29
  export class MockedDOMSource {
14
30
  private _elements: FantasyObservable<any>;
15
31
 
16
- constructor(private _mockConfig: MockConfig) {
32
+ constructor(
33
+ private _mockConfig: MockConfig,
34
+ private _hub?: MockEventHub,
35
+ public _path: string[] = [],
36
+ private _onEvents?: MockOnEvents
37
+ ) {
17
38
  if (_mockConfig.elements) {
18
39
  this._elements = _mockConfig.elements as FantasyObservable<any>;
19
40
  } else {
@@ -43,10 +64,31 @@ export class MockedDOMSource {
43
64
  options?: EventsFnOptions,
44
65
  bubbles?: boolean
45
66
  ): any {
46
- const streamForEventType = this._mockConfig[eventType] as any;
47
- const out: DevToolEnabledSource & FantasyObservable<any> = adapt(
48
- streamForEventType || xs.empty()
49
- );
67
+ const configured = this._mockConfig[eventType] as any;
68
+ const {_hub: hub, _path: path, _onEvents: on} = this;
69
+ if (on) on(path, eventType);
70
+ let hub$: Stream<any> | undefined;
71
+ if (hub) {
72
+ const ev$ = hub.filter(e => e.type === eventType && e.match(path)).map(e => e.event);
73
+ let l: any;
74
+ hub$ = on
75
+ ? xs.create({
76
+ start: (x: any) => {
77
+ ev$.addListener(l = {next: (v: any) => x.next(v), error: (e: any) => x.error(e), complete: () => x.complete()});
78
+ on(path, eventType, true);
79
+ },
80
+ stop: () => {
81
+ ev$.removeListener(l);
82
+ on(path, eventType, false);
83
+ },
84
+ })
85
+ : ev$;
86
+ }
87
+ const out: DevToolEnabledSource & FantasyObservable<any> = enrichEventStream(adapt(
88
+ hub$
89
+ ? xs.merge(configured ? xs.fromObservable(configured) : xs.empty(), hub$)
90
+ : configured || xs.empty()
91
+ ));
50
92
 
51
93
  out._isCycleSource = 'MockedDOM';
52
94
 
@@ -56,7 +98,12 @@ export class MockedDOMSource {
56
98
  public select(selector: string): MockedDOMSource {
57
99
  const mockConfigForSelector = this._mockConfig[selector] || {};
58
100
 
59
- return new MockedDOMSource(mockConfigForSelector as MockConfig);
101
+ return new MockedDOMSource(
102
+ mockConfigForSelector as MockConfig,
103
+ this._hub,
104
+ this._path.concat(selector),
105
+ this._onEvents
106
+ );
60
107
  }
61
108
 
62
109
  public isolateSource(
@@ -69,6 +116,9 @@ export class MockedDOMSource {
69
116
  public isolateSink(sink: any, scope: string): any {
70
117
  return adapt(
71
118
  xs.fromObservable<any>(sink).map((vnode: VNode) => {
119
+ // B-025: a Switchable's DOM sink starts with undefined (nothing rendered yet); the
120
+ // real DOM driver's isolateSink passes it through too
121
+ if (!vnode) return vnode;
72
122
  if (vnode.sel && vnode.sel.indexOf(SCOPE_PREFIX + scope) !== -1) {
73
123
  return vnode;
74
124
  } else {
@@ -80,6 +130,10 @@ export class MockedDOMSource {
80
130
  }
81
131
  }
82
132
 
83
- export function mockDOMSource(mockConfig: MockConfig): MockedDOMSource {
84
- return new MockedDOMSource(mockConfig);
133
+ export function mockDOMSource(
134
+ mockConfig: MockConfig,
135
+ hub?: MockEventHub,
136
+ onEvents?: MockOnEvents
137
+ ): MockedDOMSource {
138
+ return new MockedDOMSource(mockConfig, hub, [], onEvents);
85
139
  }
@@ -1,16 +1,20 @@
1
1
  import {Module, classModule, propsModule, attributesModule, datasetModule} from './snabbdom';
2
2
  import {styleModule} from './styleModule';
3
3
  import {selectModule} from './selectModule';
4
+ import {controlledInputModule} from './controlledInputModule';
5
+ import {classNameModule} from './classNameModule';
4
6
 
5
7
  const modules: Array<Module> = [
6
8
  styleModule,
7
9
  classModule,
8
10
  propsModule,
11
+ classNameModule, // after propsModule (B-012)
9
12
  attributesModule,
10
13
  datasetModule,
11
14
  selectModule,
15
+ controlledInputModule,
12
16
  ];
13
17
 
14
- export {styleModule, classModule, propsModule, attributesModule, datasetModule, selectModule};
18
+ export {styleModule, classModule, propsModule, attributesModule, datasetModule, selectModule, controlledInputModule, classNameModule};
15
19
 
16
20
  export default modules;
@@ -106,9 +106,11 @@ export function makeCollection<S, So = any, Si = any>(
106
106
  const itemKey = opts.itemKey;
107
107
  const itemScope = opts.itemScope || defaultItemScope;
108
108
  const state$ = xs.fromObservable((sources[name] as StateSource<S>).stream);
109
+ const dict = new Map();
110
+ let disposed = false;
109
111
  const instances$ = state$.fold(
110
112
  (acc: InternalInstances<Si>, nextState: Array<any> | any) => {
111
- const dict = acc.dict;
113
+ if (disposed) return acc;
112
114
  if (Array.isArray(nextState)) {
113
115
  const nextInstArray = Array(nextState.length) as Array<
114
116
  Si & {_key: string}
@@ -167,8 +169,16 @@ export function makeCollection<S, So = any, Si = any>(
167
169
  return {dict: dict, arr: [sinks]};
168
170
  }
169
171
  },
170
- {dict: new Map(), arr: []} as InternalInstances<Si>
172
+ {dict, arr: []} as InternalInstances<Si>
171
173
  );
172
- return opts.collectSinks(new Instances<Si>(instances$));
174
+ const sinks = opts.collectSinks(new Instances<Si>(instances$));
175
+ // B-024: disposing the collection (its owner was disposed or stopped rendering it)
176
+ // disposes every live item, and so their subtrees
177
+ sinks.__dispose = () => {
178
+ disposed = true;
179
+ dict.forEach((s: any) => s.__dispose?.());
180
+ dict.clear();
181
+ };
182
+ return sinks;
173
183
  };
174
184
  }
@@ -1,5 +1,5 @@
1
1
  import xs, {Stream, MemoryStream} from 'xstream';
2
- import dropRepeats from 'xstream/extra/dropRepeats.js';
2
+ import {dropRepeats} from '../../extra/xstreamExtras';
3
3
  import {DevToolEnabledSource} from '../run/types';
4
4
  import {adapt} from '../run/adapt';
5
5
  import {Getter, Setter, Scope, Reducer} from './types';
@@ -43,6 +43,28 @@ class PickCombineListener<Si, T>
43
43
  public _c(): void {}
44
44
  }
45
45
 
46
+ // True when the items present both before and after appear in a different relative
47
+ // order. Additions emit through the new item's sink and removals are handled
48
+ // separately, so only a permutation of the surviving items counts here.
49
+ function isReordered(prev: Array<string>, next: Array<string>, dict: Map<string, any>): boolean {
50
+ const m = prev.length;
51
+ if (m === next.length) {
52
+ let same = true;
53
+ for (let i = 0; i < m && same; ++i) same = prev[i] === next[i];
54
+ if (same) return false;
55
+ }
56
+ let j = 0;
57
+ const seen = new Set(prev);
58
+ for (let i = 0; i < next.length; ++i) {
59
+ const key = next[i];
60
+ if (!seen.has(key)) continue;
61
+ while (j < m && !dict.has(prev[j])) ++j;
62
+ if (prev[j] !== key) return true;
63
+ ++j;
64
+ }
65
+ return false;
66
+ }
67
+
46
68
  class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
47
69
  public type = 'combine';
48
70
  public ins: Stream<InternalInstances<Si>>;
@@ -50,6 +72,8 @@ class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
50
72
  public sel: string;
51
73
  public ils: Map<string, PickCombineListener<Si, R>>;
52
74
  public inst: InternalInstances<Si>;
75
+ // item keys in the order of the last instances (B-010: detect pure reorders)
76
+ public keys: Array<string>;
53
77
 
54
78
  constructor(sel: string, ins: Stream<InternalInstances<Si>>) {
55
79
  this.ins = ins;
@@ -57,6 +81,7 @@ class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
57
81
  this.out = null as any;
58
82
  this.ils = new Map();
59
83
  this.inst = null as any;
84
+ this.keys = [];
60
85
  }
61
86
 
62
87
  public _start(out: Stream<Array<R>>): void {
@@ -77,6 +102,7 @@ class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
77
102
  this.out = null as any;
78
103
  this.ils = new Map();
79
104
  this.inst = null as any;
105
+ this.keys = [];
80
106
  }
81
107
 
82
108
  public up(): void {
@@ -120,13 +146,16 @@ class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
120
146
  }
121
147
  });
122
148
  if (n === 0) {
149
+ this.keys = [];
123
150
  out._n([]);
124
151
  return;
125
152
  }
126
153
  // add
154
+ const keys: Array<string> = Array(n);
127
155
  for (let i = 0; i < n; ++i) {
128
156
  const sinks = arrSinks[i];
129
157
  const key = (sinks as any)._key as string;
158
+ keys[i] = key;
130
159
  if (!(sinks as any)[sel]) {
131
160
  throw new Error('pickCombine found an undefined child sink stream');
132
161
  }
@@ -136,7 +165,12 @@ class PickCombine<Si, R> implements Operator<InternalInstances<Si>, Array<R>> {
136
165
  sink._add(ils.get(key) as PickCombineListener<Si, R>);
137
166
  }
138
167
  }
139
- if (removed) {
168
+ const reordered = isReordered(this.keys, keys, dict);
169
+ this.keys = keys;
170
+ // B-010: a permutation (swap, reverse, move) neither removes an item nor makes an
171
+ // item sink emit, so re-emit whenever the order of keys changed. up() waits until
172
+ // every item has emitted, so a brand new item still triggers its own emission.
173
+ if (removed || reordered) {
140
174
  this.up();
141
175
  }
142
176
  }
@@ -1,5 +1,5 @@
1
1
  import xs, {Stream} from 'xstream';
2
- import concat from 'xstream/extra/concat.js';
2
+ import {concat} from '../../extra/xstreamExtras';
3
3
  import {MainFn, Reducer} from './types';
4
4
  import {StateSource} from './StateSource';
5
5
 
@@ -1,3 +1,6 @@
1
+ import {getDiagnostics} from './diagnostics/index';
2
+ import type {Diagnostic} from './diagnostics/index';
3
+
1
4
  const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
2
5
  const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
3
6
  const DEFAULT_MAX_HISTORY = 200;
@@ -68,6 +71,11 @@ class SygnalDevTools {
68
71
  this._maxHistory = DEFAULT_MAX_HISTORY;
69
72
  }
70
73
 
74
+ /** Diagnostics collected by the shared diagnostics core (see src/extra/diagnostics). */
75
+ getDiagnostics(): Diagnostic[] {
76
+ return getDiagnostics();
77
+ }
78
+
71
79
  get connected(): boolean {
72
80
  return this._connected && typeof window !== 'undefined';
73
81
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SYG401 — <Collection> has no `from` and the whole state is not an array (warn).
3
+ * A defined `from` is checked by the core (D22).
4
+ *
5
+ * Reported through diagnostics in addition to the existing console output.
6
+ *
7
+ * Mechanism: the core has no hook at Collection instantiation, and the
8
+ * Collection marker vnode is gone by the time onRender sees the tree. So, at
9
+ * onModel (construction time, before the first render), this check wraps the
10
+ * component INSTANCE's instantiateCollection method (an own property that
11
+ * shadows the prototype method; Component calls `this.instantiateCollection`
12
+ * for every new Collection). The wrapper inspects the marker's props and the
13
+ * component's current state, then always calls the original. This only
14
+ * happens when the dev-only 'sygnal/diagnostics' entry is loaded and
15
+ * diagnostics are on. Reported once per component name and `from` value.
16
+ */
17
+ import type {DiagnosticCheck} from '../index'
18
+ import {reportSafely, once, nameOf, isPlainObject} from './shared'
19
+
20
+ const describe = (v: any): string =>
21
+ v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`
22
+
23
+ function checkCollection(component: any, el: any): void {
24
+ const props = (el && el.data && el.data.props) || {}
25
+ const from = props.from
26
+ const of = props.of
27
+ const ofName = typeof of === 'function' ? (of.componentName || of.label || of.name || 'component') : String(of)
28
+ const name = nameOf(component)
29
+
30
+ let state = component.currentState
31
+ try { if (typeof component.addCalculated === 'function') state = component.addCalculated(state) } catch (_) {}
32
+
33
+ let problem: string | undefined
34
+ let fix = `Pass the name of an array field in ${name}'s state, e.g. <Collection of={${ofName}} from="items" />, and initialize that field to [] in initialState`
35
+ // Every defined `from` (string field, lens object, or an invalid value) is reported by
36
+ // the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
37
+ // this check only covers a missing `from`.
38
+ if (from !== undefined) return
39
+ if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
40
+ problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe(state)}, not an array`
41
+ }
42
+
43
+ if (!problem || !once(`SYG401:${name}:undefined`)) return
44
+ reportSafely('SYG401', {
45
+ component,
46
+ message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
47
+ fix,
48
+ data: {from: 'undefined', of: ofName},
49
+ })
50
+ }
51
+
52
+ const WRAPPED = '__sygnalDiagWrapped'
53
+
54
+ export const collectionsCheck: DiagnosticCheck = {
55
+ id: 'collections',
56
+
57
+ onModel(component) {
58
+ const original = component && component.instantiateCollection
59
+ if (typeof original !== 'function' || original[WRAPPED]) return
60
+ const wrapped = function (this: any, el: any, ...rest: any[]) {
61
+ try { checkCollection(this, el) } catch (_) {}
62
+ return original.call(this, el, ...rest)
63
+ }
64
+ ;(wrapped as any)[WRAPPED] = true
65
+ component.instantiateCollection = wrapped
66
+ },
67
+ }